이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • 읽기 전용 트랜잭션은 왜 붙이고, 무엇을 보장하지는 않는가?
  • 외부 API 호출과 메시지 발행은 트랜잭션 경계 안팎에서 어떻게 다뤄야 하는가?
  • 트랜잭션 범위를 줄이면서도 데이터 일관성을 지키는 실전 기준은 무엇인가?

개요

트랜잭션 설계에서 가장 어려운 부분은 “어디까지 감쌀 것인가”다. 너무 좁으면 유스케이스 일관성이 깨지고, 너무 넓으면 커넥션과 lock을 오래 붙잡아 성능과 장애 전파 문제가 생긴다.

읽기 전용 트랜잭션, 외부 API 호출 분리, commit 이후 이벤트 처리 같은 주제는 모두 이 경계 설계와 연결된다. 트랜잭션은 데이터베이스 작업의 기술 설정이 아니라 유스케이스의 생명선을 그리는 작업이다.

읽기 전용 트랜잭션

@Transactional(readOnly = true)는 읽기 유스케이스의 의도를 드러낸다. JPA 환경에서는 flush 동작 최적화에 영향을 줄 수 있고, 일부 데이터베이스나 드라이버에서는 읽기 전용 힌트로 전달될 수 있다.

하지만 read-only가 모든 환경에서 물리적 쓰기를 절대 차단한다는 뜻은 아니다. 읽기 메서드 안에서 엔티티를 변경하는 코드를 작성하지 않는 것이 먼저이고, 테스트와 설정으로 실제 동작을 확인해야 한다.

@Transactional(readOnly = true)
public OrderDetail getOrder(Long orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException(orderId));
    return OrderDetail.from(order);
}

이 코드는 읽기 유스케이스임을 명확히 표현한다. 팀원은 이 메서드에서 쓰기 부작용을 기대하지 않아야 한다.

외부 API와 트랜잭션

트랜잭션 안에서 외부 API를 호출하면 DB 커넥션과 lock을 잡은 채 네트워크 응답을 기다린다. 외부 시스템이 느리거나 장애가 나면 DB 리소스까지 같이 고갈될 수 있다.

가능하면 외부 호출을 트랜잭션 밖으로 빼거나, 먼저 외부 호출을 완료한 뒤 짧은 DB 트랜잭션으로 결과를 저장한다. 단, 결제처럼 외부 부작용과 DB 상태가 함께 맞아야 하는 문제는 outbox, saga, 보상 트랜잭션 같은 별도 설계가 필요하다.

Commit 이후 작업

메시지 발행이나 이메일 전송은 DB commit 이후에 실행해야 하는 경우가 많다. 트랜잭션 안에서 이메일을 보냈는데 DB가 rollback되면 사용자에게 존재하지 않는 주문 완료 메일이 갈 수 있다.

Spring은 트랜잭션 동기화와 @TransactionalEventListener를 통해 commit 이후 작업을 연결할 수 있다. 이벤트 listener phase를 AFTER_COMMIT으로 두면 DB commit이 끝난 뒤 부작용을 실행하는 의도를 표현할 수 있다. 다만 메시지 브로커까지 정확히 한 번 보장하려면 outbox 패턴 같은 더 강한 설계가 필요하다.

실전 팁

  • 읽기 메서드에는 readOnly = true로 의도를 표현하고, 쓰기 메서드는 별도로 드러낸다.
  • 외부 API 호출을 트랜잭션 안에 넣기 전에 lock과 커넥션 점유 시간을 계산한다.
  • commit 이후에 해야 하는 부작용은 트랜잭션 이벤트나 outbox 후보로 본다.
  • @TransactionalEventListener는 트랜잭션이 실제로 존재하는 흐름인지 확인한다. 트랜잭션 없이 발행된 이벤트를 어떻게 처리할지도 명확히 정한다.
  • 트랜잭션 안에서는 필요한 데이터 변경만 수행하고 응답 조립과 외부 변환은 밖으로 뺄 수 있는지 본다.
  • 긴 배치 작업은 전체를 하나의 트랜잭션으로 감싸지 말고 chunk 단위 경계를 검토한다.

위험 신호!

  • 조회 Service가 엔티티를 변경하고도 read-only로 표시되어 있다.
  • 트랜잭션 안에서 느린 외부 HTTP 호출을 기다린다.
  • 이메일이나 메시지를 보낸 뒤 DB commit이 실패할 가능성을 고려하지 않는다.
  • 트랜잭션 이벤트를 쓰면서 listener 실행 phase와 트랜잭션이 없는 경우의 동작을 확인하지 않는다.
  • 배치 작업 전체를 하나의 트랜잭션으로 묶어 lock과 undo 로그가 커진다.
  • Controller에서 View 렌더링까지 트랜잭션이 열려 있다.

확인 질문

확인 질문

  • readOnly = true의 핵심 의미는 무엇인가?
    • 읽기 유스케이스라는 의도와 최적화 힌트를 제공하는 것이다.
  • 외부 API 호출을 트랜잭션 안에 넣으면 어떤 문제가 생길 수 있는가?
    • 네트워크 지연 동안 DB 커넥션과 lock을 점유해 성능 저하와 장애 전파가 발생할 수 있다.
  • DB commit 이후에 메시지를 안정적으로 발행하려면 어떤 패턴을 검토할 수 있는가?
    • 트랜잭션 이벤트, transaction synchronization, outbox 패턴을 검토할 수 있다.

참고 문서