Spring Boot 환경에서 Exposed를 안정적으로 운영하기 위한 통합 패턴을 다루는 챕터입니다. 자동 설정 기반 연결부터 선언적 트랜잭션, Repository 패턴(동기/코루틴), Spring Cache 통합까지 단계적으로 학습할 수 있는 예제를 제공합니다.
- Spring 트랜잭션과 Exposed 트랜잭션 경계를 정렬해 일관된 데이터 흐름을 설계한다.
- 동기(
Spring MVC)와 비동기(Spring WebFlux + 코루틴) Repository 레이어 표준 패턴을 정립한다. - 캐시 통합 시 일관성과 성능의 균형을 맞추는 전략을 확인한다.
- Spring Boot 기본 (
DataSource,@Transactional,@Cacheable) 05-exposed-dml챕터 내용 (DSL/DAO 기초)08-coroutines챕터 내용 (코루틴 모듈 학습 시)
| 모듈 | 설명 | 핵심 기술 |
|---|---|---|
01-springboot-autoconfigure |
Spring Boot 자동 설정 기반 Exposed 통합 | ExposedAutoConfiguration, DatabaseInitializer |
02-transactiontemplate |
TransactionTemplate 프로그래밍 트랜잭션 |
TransactionTemplate, TransactionOperations |
03-spring-transaction |
@Transactional 선언적 트랜잭션 |
SpringTransactionManager, @Transactional |
04-exposed-repository |
동기 Repository 패턴 (Spring MVC) | JdbcRepository, DSL/DAO 혼용 |
05-exposed-repository-coroutines |
코루틴 Repository 패턴 (Spring WebFlux) | newSuspendedTransaction, suspend fun |
06-spring-cache |
Spring Cache + Redis 동기 캐시 | @Cacheable, @CacheEvict, RedisCacheManager |
07-spring-suspended-cache |
코루틴 기반 Redis 캐시 | LettuceSuspendedCache, 데코레이터 패턴 |
flowchart TD
subgraph AutoConfig["01 AutoConfiguration"]
AC[ExposedAutoConfiguration] --> STM[SpringTransactionManager]
AC --> DI[DatabaseInitializer]
end
subgraph TxControl["02-03 트랜잭션 제어"]
TT[TransactionTemplate] --> STM
AT["@Transactional"] --> STM
end
subgraph Repo["04-05 Repository 패턴"]
MVC[MovieExposedRepository\nActorExposedRepository\nSpring MVC] --> AT
WF[MovieExposedRepository\nActorExposedRepository\nWebFlux Coroutine] --> NST[newSuspendedTransaction]
end
subgraph Cache["06-07 캐시 통합"]
SC[CountryRepository\n@Cacheable/@CacheEvict] --> Redis[(Redis)]
CC[CachedCountrySuspendedRepository\nLettuceSuspendedCache] --> Redis
SC --> AT
CC --> NST
end
STM --> DB[(Database)]
NST --> DB
@Configuration
@EnableTransactionManagement
class DataSourceConfig: TransactionManagementConfigurer {
@Bean
override fun annotationDrivenTransactionManager(): TransactionManager =
SpringTransactionManager(dataSource(), DatabaseConfig {
useNestedTransactions = true
})
}@Repository
class MovieExposedRepository: JdbcRepository<Long, MovieTable, MovieRecord> {
override val table = MovieTable
override fun ResultRow.toEntity() = toMovieRecord()
@Transactional(readOnly = true)
fun searchMovies(params: Map<String, String?>): List<MovieRecord> { ... }
}@Repository
class MovieExposedRepository: JdbcRepository<Long, MovieTable, MovieRecord> {
suspend fun create(movie: MovieRecord): MovieRecord = /* newSuspendedTransaction 내부 */
MovieTable.insertAndGetId { ... }.let { movie.copy(id = it.value) }
}class CachedCountrySuspendedRepository(
private val delegate: CountrySuspendedRepository,
private val cacheManager: LettuceSuspendedCacheManager,
): CountrySuspendedRepository {
override suspend fun findByCode(code: String): CountryRecord? =
cache.get(code) ?: delegate.findByCode(code)?.apply { cache.put(code, this) }
override suspend fun update(countryRecord: CountryRecord): Int {
cache.evict(countryRecord.code)
return delegate.update(countryRecord)
}
}01-springboot-autoconfigure
↓
02-transactiontemplate
↓
03-spring-transaction
↓
04-exposed-repository ──────────────→ 05-exposed-repository-coroutines
↓ ↓
06-spring-cache ──────────────────→ 07-spring-suspended-cache
# 전체 챕터 테스트
./gradlew :09-spring:01-springboot-autoconfigure:test \
:09-spring:02-transactiontemplate:test \
:09-spring:03-spring-transaction:test \
:09-spring:04-exposed-repository:test \
:09-spring:05-exposed-repository-coroutines:test \
:09-spring:06-spring-cache:test \
:09-spring:07-spring-suspended-cache:test
# 개별 모듈 테스트
./gradlew :09-spring:04-exposed-repository:test
# 테스트 로그 요약
./bin/repo-test-summary -- ./gradlew :09-spring:04-exposed-repository:test- 트랜잭션 전파/롤백 규칙이 의도대로 작동하는지 검증한다.
- 캐시 적중/미스/무효화 시나리오를 각각 검증한다.
- 코루틴 경로와 동기 경로의 결과 일관성을 비교한다.
@Transactional이 suspend 함수에 적용되지 않음을 확인하고 대안이 올바르게 동작하는지 점검한다.
| 선택 | 장점 | 단점 |
|---|---|---|
@Transactional |
선언적, 간결 | suspend 함수 미지원 |
TransactionTemplate |
세밀한 경계 제어 | 코드 복잡도 증가 |
newSuspendedTransaction |
코루틴 네이티브 | 명시적 감싸기 필요 |
Spring Cache @Cacheable |
선언적, 간결 | suspend 함수 미지원 |
LettuceSuspendedCache |
suspend 지원, 논블로킹 | 수동 캐시 로직 작성 |
DataSourceTransactionManagerAutoConfiguration을 반드시 제외해 트랜잭션 매니저 충돌 방지- Exposed JDBC는 블로킹 드라이버이므로 WebFlux 이벤트 루프에서 직접 호출 금지 —
newSuspendedTransaction필수 - 캐시 무효화 누락으로 stale 데이터가 노출되지 않도록 갱신/삭제 시 반드시
@CacheEvict또는cache.evict()적용 - 커넥션 풀(
HikariCP) 크기를 동시 트랜잭션 수에 맞게 조정
@Transactional로 감싼 Spring 트랜잭션 내에서 Exposed DSL 쿼리를 실행할 때,
SpringTransactionManager가 두 트랜잭션 경계를 같은 커넥션으로 통합합니다.
useNestedTransactions = true 설정으로 SAVEPOINT 기반 중첩 트랜잭션도 지원합니다.
- 관련 모듈:
03-spring-transaction
CachedCountrySuspendedRepository는 DefaultCountrySuspendedRepository를 감싸는 데코레이터로, Redis LettuceSuspendedCache와
newSuspendedTransaction DB 접근을 조합해 캐시 히트 시 DB 트랜잭션을 열지 않는 최적화된 구조를 제공합니다.
- 관련 모듈:
07-spring-suspended-cache
../10-multi-tenant/README.md: 테넌트 분리 아키텍처를 실전 형태로 확장합니다.