대화형 아키텍처 해설

JPA에서 Exposed로: 트랜잭션 소유권을 드러내기

Exposed는 JPA/Hibernate가 제공하는 객체 그래프 자동화 일부를 명시적인 SQL 의도와 실행 경계로 바꾼다. 마이그레이션의 핵심 질문은 어느 저장소가 트랜잭션을 여느냐가 아니라, 어느 애플리케이션 경계가 전체 작업 단위를 소유하느냐이다.

01

불변 규칙

먼저 사고방식을 바꾼다

JPA / Hibernate

JPA / Hibernate의 기본 사고방식

관리 엔티티 → 변경 감지(dirty checking) → flush

장점: 객체 그래프 중심의 개발 생산성이 높고, 생태계와 영속성 컨텍스트 기능이 성숙하다.

비용: 프록시, N+1 관계 탐색, 플러시 시점, 엔티티 생명주기 뒤에 I/O가 숨을 수 있다.

JetBrains Exposed

Exposed DSL / 저장소

명시적 select / join / map → 명시적 insert / update / delete

장점: SQL 의도와 실행 지점이 애플리케이션 코드에 드러난다.

비용: 쿼리 구조, 매핑, 저장 연산, 트랜잭션 경계를 명시적으로 설계해야 한다.

02

구조 비교

호출 흐름을 보기 전에 책임 구조부터 비교한다

첫 번째 다이어그램은 전형적인 Spring Data JPA와 Hibernate 구조를 Exposed와 같은 책임 수준에서 비교한다. 두 번째 다이어그램은 저장소에서 승인한 Exposed 트랜잭션 소유권 구조를 상세하게 보여 준다.

JPA/Hibernate와 Exposed 책임 구조 비교 트랜잭션 경계, 영속성 상태, 저장소 역할, 데이터베이스 접근 방식을 같은 수준에서 비교한다.
JPA와 Exposed 아키텍처 비교. 전형적인 Spring Data JPA와 Hibernate 경로는 트랜잭션에 연결된 EntityManager와 영속성 컨텍스트를 거쳐 JDBC를 사용하고, Exposed는 호출자가 소유하는 JDBC 또는 R2DBC 트랜잭션 문맥에서 저장소와 SQL 연산을 명시한다.
JPA/Hibernate와 Exposed 책임 구조 비교
JPA와 Exposed 아키텍처 비교. 전형적인 Spring Data JPA와 Hibernate 경로는 트랜잭션에 연결된 EntityManager와 영속성 컨텍스트를 거쳐 JDBC를 사용하고, Exposed는 호출자가 소유하는 JDBC 또는 R2DBC 트랜잭션 문맥에서 저장소와 SQL 연산을 명시한다.
Exposed JDBC와 R2DBC 트랜잭션 소유권 Exposed 내부를 확대해 JDBC와 R2DBC가 호출자 소유 트랜잭션 원칙을 어떻게 구현하는지 확인한다.
서비스 또는 컨트롤러 경계가 JDBC transaction {} 블록과 R2DBC suspendTransaction {} 블록을 열고, 그 안에서 저장소 호출이 데이터베이스로 이어지는 트랜잭션 소유권 아키텍처
Exposed JDBC와 R2DBC 트랜잭션 소유권
서비스 또는 컨트롤러 경계가 JDBC transaction {} 블록과 R2DBC suspendTransaction {} 블록을 열고, 그 안에서 저장소 호출이 데이터베이스로 이어지는 트랜잭션 소유권 아키텍처
03

경계 시나리오

여섯 가지 트랜잭션 경계 시나리오 비교

시나리오를 선택하면 실행 순서, 경계 소유자, 종료 결과, 코드 대응 관계, 주의 사항이 함께 바뀐다.

04

호출 → 반환 → 종료

실행 시퀀스

COMMIT

JDBC · 단일 저장소

하나의 동기 경계가 조회, 매핑, 커밋을 감싼다.

경계 소유자
컨트롤러 / 서비스 transaction {}
클라이언트
컨트롤러 / 서비스
JdbcRepository
데이터베이스
  1. 1 CALL 유스케이스 실행
  2. 2 CALL transaction { findAll() }
  3. 3 RETURN 매핑된 레코드
  4. 4 COMMIT 트랜잭션 경계
COMMIT 블록이 정상적으로 종료된다.
05

책임

누가 무엇을 소유하는가?

주체소유 책임이유
애플리케이션 경계업무 트랜잭션을 시작하고 끝낸다여러 저장소 호출을 하나의 원자적 작업으로 묶는다
저장소쿼리, 매핑, 저장 연산을 표현한다유스케이스 경계를 몰래 넓히지 않는다
Exposed 런타임JDBC 스레드 또는 R2DBC 코루틴 컨텍스트에 트랜잭션 상태를 결합한다경계를 벗어난 작업은 트랜잭션 일관성을 깨뜨릴 수 있다
데이터베이스작업 단위를 커밋하거나 롤백한다커밋 또는 롤백 중 하나의 최종 결과만 반영한다
06

마이그레이션 대응

JPA 사용 방식을 Exposed 코드로 옮기기

JPA 습관Exposed 매핑
@Transactional 서비스 메서드애플리케이션 경계의 transaction { ... } 또는 suspendTransaction { ... }
관리 엔티티 변경명시적인 update, insert, delete 저장소 연산
지연 관계 순회동일 경계 안의 명시적 join 또는 후속 쿼리
flush 시점 SQLExposed DSL 또는 저장소 호출이 실행되는 지점의 SQL
영속성 컨텍스트 수명트랜잭션 블록 / 코루틴 컨텍스트 수명
07

선택 기준

비용까지 보고 선택하기

선택판단 신호
JPA/Hibernate가 유리한 경우복잡한 객체 그래프, 도메인 탐색, 생태계 통합의 가치가 숨은 I/O 위험보다 크다.
Exposed가 유리한 경우SQL 형태, 예측 가능한 I/O, Kotlin DSL 조합, 명시적 경계가 우선이다.
이름만 바꾸는 마이그레이션은 피한다사고방식을 바꾸지 않고 저장소 이름만 옮기면 경계와 N+1 문제도 그대로 남는다.
둘을 의도적으로 함께 쓴다바운디드 컨텍스트별로 선택하되 한 유스케이스 안에서 트랜잭션 소유권을 섞지 않는다.
08

검증 근거

소스·테스트 근거

검증 내용 구현 소스 테스트 또는 실행 예제 검증 방법
JDBC 저장소 호출은 호출자가 소유한 transaction {} 블록 안에서 실행된다. JdbcRepository ProductController rg -n "transaction \\{" examples/jdbc-demo/src/main/kotlin
실행 가능한 JDBC 데모는 컨트롤러 유스케이스에 경계를 둔다. ProductController transaction-ownership.md rg -n "transaction \\{" examples/jdbc-demo/src/main/kotlin
R2DBC 연산은 suspendTransaction이 제공하는 코루틴 컨텍스트에 의존한다. R2dbcRepository MovieR2dbcRepositoryTest ./gradlew :bluetape4k-exposed-r2dbc:test
R2DBC 데모는 저장소 작업 전에 suspendTransaction 경계를 연다. ProductController repository-patterns.md rg -n "suspendTransaction" examples/r2dbc-demo/src/main/kotlin
저장소 테스트는 일시 중단 가능한 쿼리와 트랜잭션 동작을 검증한다. MovieR2dbcRepositoryTest coroutine-transactions.md ./gradlew :bluetape4k-exposed-r2dbc:test --tests "*MovieR2dbcRepositoryTest"
매뉴얼은 코루틴 수명과 콜드 Flow의 collect 시점을 명시한다. coroutine-transactions.md repository-patterns.md ruby scripts/manual/validate_manuals.rb build/manual/module-inventory-1.11.0.json docs/manual/manifest.yaml
선택한 시나리오: