스택큐힙리스트

Kotlin 코루틴에서 트랜잭션 전파 이슈 잡기 본문

개발

Kotlin 코루틴에서 트랜잭션 전파 이슈 잡기

스택큐힙리스트 2025. 7. 18. 20:47
반응형

1. 코루틴과 트랜잭션이 충돌하는 이유

  • Spring @Transactional ↔ ThreadLocal
    JPA 트랜잭션은 현재 스레드에 커넥션·영속성 컨텍스트를 바인딩합니다. 하지만 suspend 함수는 언제든 다른 스레드로 재개(resume)될 수 있어 컨텍스트가 사라지면서 TransactionRequiredException 등이 터집니다.
  • 전파 옵션 무력화
    REQUIRES_NEW처럼 새 트랜잭션을 강제해도, 같은 빈 내부·동일 코루틴 컨텍스트라면 프록시가 건너뛰어 한 트랜잭션으로 묶이는 사례가 잦습니다.
  • 동시성 확산
    async {} · withContext(Dispatchers.Default) 등으로 트랜잭션을 복수 스레드에 퍼뜨리면 데드락·커넥션 고갈로 직행합니다.

2. Spring Boot 3.x 이후 지원 현황

  • Suspend 함수에 @Transactional — 프록시가 인터셉트하지만 스레드 이동 시 안전을 보장하지 않습니다.
  • 공식 권장: 블로킹 JDBC ⇨ 비동기화하려면 코루틴용 Reactive Stack(R2DBC) 또는 TransactionTemplate·TransactionalOperator를 사용한 프로그래밍 방식

3. 깨지는 패턴 Top 3 & 대응책

  1. withContext로 I/O 스레드 분리
  2. 해결: 트랜잭션 경계를 벗어나기 전까지 같은 Dispatcher 유지, 꼭 분리해야 한다면 TransactionTemplate로 감싸기.
  3. REQUIRES_NEW가 안 먹는 경우
  4. 해결: 새 트랜잭션이 필요한 메서드를 다른 빈으로 분리해 프록시를 강제로 타게 한다.
  5. 트랜잭션 안에서 async 병렬 호출
  6. 해결: 미리 데이터를 분할해 배치 API를 설계하거나, 테이블 Lock/Index를 재설계해 데드락을 예방.

4. 실전 코드 패턴 2가지

패턴 A – 트랜잭션을 non-suspend로 고정

@Service
class MessageService(
    private val repo: MessageRepository
) {
    @Transactional           // 스레드 이동 없음
    fun markReadTx(id: Long, userId: Long): Long {
        repo.markMessageAsRead(id, userId)
        return repo.countUnreadMessagesByUserId(userId)
    }

    suspend fun readMessage(id: Long, userId: Long): Long =
        withContext(Dispatchers.IO) { markReadTx(id, userId) } // 코루틴에서 호출
}
  • 장점: 전파(REQUIRED·REQUIRES_NEW) 규칙이 그대로 동작.
  • 단점: I/O 스레드를 블로킹하므로 대량 호출엔 부적합. Velog

패턴 B – R2DBC + TransactionalOperator

@Service
class PayService(
    private val operator: TransactionalOperator,
    private val repo: PayRepository
) {
    suspend fun pay(order: Order) = operator.executeAndAwait {
        repo.save(order)
        repo.logEvent(order.id)
    }
}
  • 완전 논블로킹; 트랜잭션 컨텍스트가 코루틴 Context로 전파돼 스레드 이동에도 안전.

5. 체크리스트

  • 트랜잭션이 필요한 suspend 함수 내부에서 withContext/async 남발 금지
  • 새 전파 옵션이 필요하면 별도 빈으로 분리해 프록시 타기
  • JDBC + Coroutine으로 병렬 처리 시 커넥션 수데드락을 반드시 부하 테스트
  • 고성능이 필수면 R2DBC Stack + TransactionalOperator로 전환 고려
  • 코루틴·가상 스레드·블로킹 JDBC 조합의 장단점을 프로젝트 규모에 맞춰 선택
반응형
Comments