Spring Transaction Abstraction

3줄 요약

  • Spring 트랜잭션 추상화는 JDBC, JPA, JTA 같은 기술 차이를 PlatformTransactionManager 계층으로 감춘다.
  • 애플리케이션 코드는 같은 @Transactional 모델로 트랜잭션 경계, 전파, 격리, rollback 정책을 선언할 수 있다.
  • 트랜잭션은 데이터베이스 기능만이 아니라 서비스 유스케이스의 일관성 경계를 표현하는 설계 도구다.

핵심 정리

  • 트랜잭션은 여러 데이터 변경을 하나의 작업 단위로 묶어 모두 성공하거나 모두 실패하게 한다.
  • Spring은 트랜잭션 관리를 특정 데이터 접근 기술에 묶지 않고 추상화한다. JDBC는 DataSourceTransactionManager, JPA는 JpaTransactionManager 같은 구현을 사용한다.
  • 선언적 트랜잭션은 AOP 프록시를 통해 메서드 호출 앞뒤에 트랜잭션 시작, commit, rollback을 붙인다.
  • 트랜잭션 경계는 보통 Service 유스케이스 메서드에 둔다. Repository마다 트랜잭션을 잘게 나누면 하나의 유스케이스 일관성이 깨질 수 있다.
  • 트랜잭션은 짧고 명확해야 한다. 외부 API 호출, 긴 계산, 사용자 대기 시간을 트랜잭션 안에 넣으면 커넥션 점유와 lock 시간이 길어진다.
  • Spring의 기본 rollback 규칙은 unchecked exception과 Error 중심이다. checked exception을 실패 계약으로 쓰면 rollbackFor나 전역 rollback 정책을 명확히 해야 한다.

헷갈리는 지점

  • 트랜잭션을 Repository의 데이터 저장 기능으로만 보기 쉽다. SQL 실행과 가까운 곳에서 체감되기 때문이다.
    • 핵심은 트랜잭션 경계가 유스케이스 일관성 경계라는 점이다.
    • 주문 생성, 결제 기록, 재고 차감이 하나의 작업이라면 Service에서 하나로 묶어야 한다.
  • @Transactional을 붙이면 어떤 객체 호출에도 적용된다고 오해하기 쉽다. 애너테이션이 메서드에 직접 붙어 있기 때문이다.
    • 핵심은 Spring 프록시를 통과하는 Bean 외부 호출인지 확인하는 것이다.
    • 내부 호출이나 직접 생성 객체는 선언적 트랜잭션을 지나지 않을 수 있다.
  • 트랜잭션을 넓게 잡을수록 안전하다고 생각하기 쉽다. 많은 작업을 한 번에 rollback할 수 있기 때문이다.
    • 핵심은 넓은 트랜잭션이 lock과 커넥션 점유 시간을 늘린다는 점이다.
    • 데이터 일관성에 필요한 최소 범위로 경계를 잡아야 한다.
  • 예외를 잡아서 처리하면 트랜잭션도 자동 rollback될 것이라고 생각하기 쉽다. 메서드 안에서 실패를 처리했으니 Spring도 알 것처럼 느껴지기 때문이다.
    • 핵심은 선언적 트랜잭션은 주로 메서드 밖으로 던져진 예외와 rollback-only 표시를 보고 판단한다는 점이다.
    • 실패를 삼켜야 한다면 rollback 의도와 응답 의도를 별도로 표현해야 한다.

확인 질문

  • Spring 트랜잭션 추상화의 중심 인터페이스는 무엇인가?
    • PlatformTransactionManager이다.
  • 트랜잭션 경계는 보통 어느 계층에 두는가?
    • 하나의 유스케이스를 표현하는 Service 계층 메서드에 둔다.
  • 선언적 트랜잭션이 내부 호출에서 동작하지 않을 수 있는 이유는 무엇인가?
    • Spring AOP 프록시를 통과하지 않기 때문이다.

@Transactional 프록시와 적용 범위

3줄 요약

  • @Transactional은 메서드 실행 전후에 트랜잭션 처리를 붙이는 선언적 트랜잭션 모델이다.
  • Spring의 기본 선언적 트랜잭션은 프록시 기반이므로 외부 프록시 호출, Bean 등록, self-invocation 문제가 중요하다.
  • 트랜잭션 설정은 클래스와 메서드에 둘 수 있지만, 실제 경계가 어디서 시작되는지 호출 경로를 기준으로 확인해야 한다.

핵심 정리

  • @Transactional이 붙은 Bean은 트랜잭션 Advisor 대상이 되며, 외부 호출자는 원본 객체가 아니라 프록시를 호출할 수 있다.
  • 프록시는 메서드 호출 전에 트랜잭션을 시작하고, 정상 종료 시 commit하며, rollback 대상 예외가 발생하면 rollback한다.
  • 같은 클래스 내부에서 this.otherTransactionalMethod()처럼 호출하면 프록시를 다시 거치지 않아 새 트랜잭션 설정이 적용되지 않을 수 있다.
  • 클래스 레벨 @Transactional은 기본 정책을 주고, 메서드 레벨은 더 구체적인 정책을 덮어쓸 때 사용한다.
  • Spring 6의 class-based proxy는 protected/package-visible 메서드도 트랜잭션 후보가 될 수 있지만, interface proxy와 일관된 설계를 원한다면 트랜잭션 경계는 외부에서 호출되는 공개 유스케이스 메서드에 둔다.
  • private 메서드에 붙인 @Transactional은 의도대로 동작하지 않는다. private 메서드는 트랜잭션 경계가 아니라 이미 열린 트랜잭션 안의 세부 구현으로 본다.

헷갈리는 지점

  • 애너테이션이 붙은 메서드라면 항상 트랜잭션이 시작된다고 생각하기 쉽다. 코드 위치만 보면 그렇게 보이기 때문이다.
    • 핵심은 애너테이션 위치가 아니라 호출이 프록시를 통과했는지다.
    • 테스트에서도 실제 Spring Bean을 주입받아 호출해야 프록시 효과를 확인할 수 있다.
  • private 메서드나 non-public 메서드를 트랜잭션 경계로 삼으려 하기 쉽다. 내부 구현을 작게 나누다 보면 그 메서드가 저장 로직처럼 보이기 때문이다.
    • 핵심은 선언적 트랜잭션은 외부 호출 경계를 감싸는 모델이라는 점이다.
    • non-public 메서드 적용 가능성은 proxy 종류와 Spring 버전에 따라 다를 수 있으므로 팀 기준은 공개 Service 유스케이스 중심으로 맞춘다.
  • 클래스 레벨에 @Transactional을 붙이면 세부 메서드 정책을 신경 쓰지 않아도 된다고 생각하기 쉽다. 전체가 한 번에 보호되는 것처럼 보이기 때문이다.
    • 핵심은 읽기 전용, 전파, timeout, rollback 정책이 메서드마다 다를 수 있다는 점이다.
    • 기본값은 클래스에 두더라도 중요한 쓰기 유스케이스는 메서드에서 명확히 표현한다.

확인 질문

  • @Transactional은 Spring에서 주로 어떤 방식으로 적용되는가?
    • AOP 프록시가 메서드 호출 전후에 트랜잭션 처리를 붙이는 방식으로 적용된다.
  • self-invocation 문제가 무엇인가?
    • 같은 객체 내부에서 자기 메서드를 직접 호출해 프록시를 거치지 않아 트랜잭션 설정이 적용되지 않는 문제다.
  • 트랜잭션 경계로 적합한 메서드는 어떤 메서드인가?
    • 외부에서 호출되는 Service 유스케이스 메서드처럼 일관성 경계를 표현하는 공개 메서드다.

Propagation Isolation Rollback

3줄 요약

  • Propagation은 이미 트랜잭션이 있을 때 새 메서드가 그 트랜잭션에 참여할지, 새로 만들지, 거부할지를 정한다.
  • Isolation은 동시에 실행되는 트랜잭션 사이에서 어떤 읽기 이상 현상을 허용할지 정한다.
  • Rollback 정책은 어떤 예외가 발생했을 때 트랜잭션을 되돌릴지 정하며, 기본적으로 런타임 예외 중심이다.

핵심 정리

  • REQUIRED는 기존 트랜잭션이 있으면 참여하고 없으면 새로 만든다. 대부분의 서비스 유스케이스에서 기본으로 적합하다.
  • REQUIRES_NEW는 기존 트랜잭션을 잠시 중단하고 새 트랜잭션을 시작한다. 감사 로그처럼 본 작업 rollback과 독립적으로 남길 작업에 쓰지만 커넥션을 추가로 쓴다.
  • 내부 트랜잭션 범위가 rollback-only로 표시되면 바깥 호출자는 commit을 기대하다가 UnexpectedRollbackException을 받을 수 있다.
  • NESTED는 저장점 기반으로 부분 rollback을 지원할 수 있지만 데이터베이스와 트랜잭션 매니저 지원 여부를 확인해야 한다.
  • Isolation은 데이터베이스 동시성의 비용과 정확성 균형이다. 높일수록 안전해 보이지만 lock 비용과 처리량 문제가 생길 수 있다.
  • Spring의 기본 rollback은 unchecked exception과 Error 중심이다. checked exception rollback이 필요하면 rollbackFor를 명시해야 한다.

헷갈리는 지점

  • REQUIRES_NEW를 쓰면 항상 더 안전하다고 생각하기 쉽다. 독립 트랜잭션이라는 이름이 강해 보이기 때문이다.
    • 핵심은 독립성이 커넥션 추가 점유와 commit 타이밍 분리를 만든다는 점이다.
    • 본 작업이 실패해도 새 트랜잭션의 변경은 남을 수 있다는 사실을 의도해야 한다.
  • Isolation을 높이면 모든 동시성 문제가 해결된다고 보기 쉽다. 격리라는 이름이 완전한 보호처럼 들리기 때문이다.
    • 핵심은 isolation은 DB 읽기 현상 제어이고, 비즈니스 경쟁 조건은 별도 설계가 필요할 수 있다는 점이다.
    • 재고 차감 같은 문제는 낙관적 lock, 비관적 lock, unique 제약, 재시도 정책을 함께 검토한다.
  • checked exception이 발생하면 당연히 rollback된다고 생각하기 쉽다. Java 예외라면 실패로 느껴지기 때문이다.
    • 핵심은 Spring 기본 rollback 규칙이 런타임 예외 중심이라는 점이다.
    • checked exception을 실패 계약으로 쓴다면 rollbackFor를 명시하거나 예외 모델을 재검토한다.
  • 예외를 잡고 boolean만 반환하면 rollback도 함께 일어날 것이라고 보기 쉽다. 실패 결과를 반환했기 때문이다.
    • 핵심은 rollback은 반환값이 아니라 트랜잭션 규칙과 rollback-only 상태로 결정된다는 점이다.
    • 실패를 응답으로 바꾸더라도 데이터 변경을 되돌려야 하면 예외 전파나 명시적 rollback 처리를 설계해야 한다.

확인 질문

  • REQUIRED 전파는 어떤 동작을 하는가?
    • 기존 트랜잭션이 있으면 참여하고 없으면 새 트랜잭션을 시작한다.
  • REQUIRES_NEW를 사용할 때 주의할 점은 무엇인가?
    • 기존 트랜잭션과 독립적으로 commit될 수 있고 추가 커넥션을 점유할 수 있다는 점이다.
  • Spring 선언적 트랜잭션의 기본 rollback 대상은 무엇인가?
    • unchecked exception과 Error가 기본 rollback 대상이다.

트랜잭션 경계 설계

3줄 요약

  • 트랜잭션 경계는 데이터 일관성이 필요한 최소 유스케이스 단위로 잡는다.
  • 읽기 전용 트랜잭션은 의도와 최적화 힌트를 제공하지만 모든 성능 문제를 자동 해결하지 않는다.
  • 외부 API 호출, 메시지 발행, 파일 처리처럼 DB 트랜잭션 밖의 작업은 commit 이후 처리 전략을 따로 설계해야 한다.

핵심 정리

  • Service 메서드 하나가 하나의 유스케이스라면 그 메서드가 트랜잭션 경계가 되는 경우가 많다.
  • Controller에 트랜잭션을 두면 HTTP 요청 처리 전체가 DB 커넥션과 묶일 수 있어 경계가 흐려진다.
  • Repository마다 트랜잭션을 두면 여러 저장 작업을 하나의 원자적 유스케이스로 묶기 어렵다.
  • readOnly = true는 읽기 의도를 드러내고 ORM flush 동작이나 데이터베이스 힌트에 영향을 줄 수 있지만 쓰기를 물리적으로 항상 막는 장치는 아니다.
  • 트랜잭션 안에서 외부 API를 호출하면 DB lock과 커넥션을 잡은 채 네트워크 지연을 기다리게 된다. 가능한 한 DB 작업과 외부 통신의 순서를 분리한다.

헷갈리는 지점

  • Controller에 @Transactional을 붙이면 모든 요청이 안전해진다고 생각하기 쉽다. 요청 전체를 하나로 감싸서 단순해 보이기 때문이다.
    • 핵심은 HTTP 요청 전체와 데이터 일관성 경계가 같지 않다는 점이다.
    • 트랜잭션은 Service 유스케이스 단위로 좁히는 편이 추적과 성능에 유리하다.
  • readOnly = true가 쓰기를 절대 막는다고 오해하기 쉽다. 이름이 강한 제약처럼 보이기 때문이다.
    • 핵심은 read-only가 주로 의도와 최적화 힌트라는 점이다.
    • 실제 쓰기 차단 여부는 DB, 드라이버, ORM, 설정에 따라 달라질 수 있다.
  • 트랜잭션 안에서 메시지를 발행하면 데이터와 메시지가 함께 보장된다고 생각하기 쉽다. 코드 순서상 하나의 메서드 안에 있기 때문이다.
    • 핵심은 DB 트랜잭션과 외부 메시지 시스템 트랜잭션은 별개일 수 있다는 점이다.
    • outbox, transaction synchronization, after-commit event 같은 전략을 검토해야 한다.

확인 질문

  • 트랜잭션 경계는 어떤 기준으로 잡는가?
    • 데이터 일관성이 필요한 최소 유스케이스 단위로 잡는다.
  • readOnly = true는 무엇을 의미하는가?
    • 읽기 전용 의도와 최적화 힌트를 제공하지만 모든 환경에서 물리적 쓰기를 완전히 막는다는 뜻은 아니다.
  • 트랜잭션 안에서 외부 API를 호출할 때 위험은 무엇인가?
    • DB 커넥션과 lock을 잡은 채 네트워크 지연을 기다려 성능과 장애 전파 문제가 생길 수 있다.