Transactional
스프링은 PlatformTransactionManager 인터페이스를 통해 트랜잭션을 추상화하고 선언적 트랜잭션을 지원하여 트랜잭션을 편리하게 사용할 수 있도록 지원한다.
- 스프링 컨테이너는
@Transactional애노테이션이 적용된 빈을 찾으면, 해당 빈의 실제 객체 대신 트랜잭션 로직을 담은 프록시 객체를 생성하여 컨테이너에 등록 - 다른 빈에서 의존성을 주입받을 때, 스프링 컨테이너는 실제 객체 대신 프록시 객체를 주입
- 클라이언트의 요청은 주입받은 프록시 객체를 통해 전달되며, 프록시는 트랜잭션 처리 후 실제 객체의 메서드를 호출
트랜잭션 동작 방식과 동기화
Section titled “트랜잭션 동작 방식과 동기화”스프링 트랜잭션은 AOP, 영속성 컨텍스트(JPA), 실제 DB 커넥션이 결합되어 동작한다.
sequenceDiagram participant C as 클라이언트 participant P as 트랜잭션 프록시 participant TM as PlatformTransactionManager participant TSM as TransactionSynchronizationManager participant PC as 영속성 컨텍스트 (JPA) participant DB as 데이터베이스 (커넥션) C ->> P: 메서드 호출 P ->> TM: getTransaction() TM ->> DB: getConnection() & setAutoCommit(false) TM ->> TSM: 커넥션 및 리소스 바인딩 P ->> PC: 영속성 컨텍스트 생성/참여 P ->> P: 비즈니스 로직 실행 P ->> TM: commit() TM ->> PC: flush() (DB 상태 동기화) TM ->> DB: commit() (실제 DB 커밋) TM ->> TSM: 바인딩 해제 및 리소스 정리 TM ->> DB: setAutoCommit(true) & releaseConnection() P ->> C: 결과 반환트랜잭션 커밋 과정 및 코드 분석
Section titled “트랜잭션 커밋 과정 및 코드 분석”트랜잭션 커밋 과정은 다음과 같은 단계로 이루어진다.
@Transactional메서드가 호출되면, 스프링은 AOP를 통해 트랜잭션 처리 시작- 해당 프록시는 실제 비즈니스 로직 실행 전후로 트랜잭션 관련 부가 기능을 수행하는
TransactionInterceptor에 제어를 위임 TransactionInterceptor는PlatformTransactionManager의 구현체를 사용하여 트랜잭션을 시작하고, 비즈니스 로직이 정상적으로 완료되면 커밋 요청- 실제 커밋 과정은
PlatformTransactionManager의 추상 클래스인AbstractPlatformTransactionManager의processCommit메서드에서 단계적으로 처리
processCommit 메서드에서 커밋의 핵심 로직이 수행되며, 주요 단계는 다음과 같다.
private void processCommit(DefaultTransactionStatus status) { try { boolean beforeCompletionInvoked = false;
try { prepareForCommit(status); // 1. 커밋 준비 triggerBeforeCommit(status); // 2. 커밋 전 콜백 triggerBeforeCompletion(status); // 3. 완료 직전 콜백 beforeCompletionInvoked = true;
// ... 세이브포인트/트랜잭션 분기 처리 등 else if (status.isNewTransaction()) { doCommit(status); // 4. DB 트랜잭션 커밋 (이 시점에 DB 트랜잭션 종료) }
} catch (Exception ex) { // 예외 발생 시 롤백 및 예외 전파 throw ex; }
try { triggerAfterCommit(status); // 5. 커밋 이후 콜백 } finally { triggerAfterCompletion( // 6. 완료 후 콜백 status, TransactionSynchronization.STATUS_COMMITTED ); }
} finally { cleanupAfterCompletion(status); // 7. 트랜잭션 컨텍스트 정리 (리소스 언바인딩) }}prepareForCommit: 플러시 등 커밋 직전 준비 수행triggerBeforeCommit: 커밋 전 콜백 실행- 아직 DB 트랜잭션이 살아있는 상태
triggerBeforeCompletion: 완료 직전 콜백 실행doCommit: 실제 DB 커밋 수행- 이 시점에 DB 트랜잭션이 종료
triggerAfterCommit: 커밋 이후 콜백 실행- DB 트랜잭션은 이미 종료되었으나, 스프링 트랜잭션 컨텍스트는 아직 살아있는 상태
triggerAfterCompletion: 완료 후 콜백 실행cleanupAfterCompletion: 스레드 컨텍스트 정리
트랜잭션 우선 순위
Section titled “트랜잭션 우선 순위”@Transactional 애노테이션은 클래스, 인터페이스, 메서드에 적용할 수 있으며, 우선순위는 더 구체적이고 자세한 것이 높은 우선순위를 가지는 것을 원칙으로 한다.
- 클래스의 메서드
- 클래스
- 인터페이스의 메서드
- 인터페이스
자기 호출(Self Invocation)
Section titled “자기 호출(Self Invocation)”@Transactional이 적용 됐을 때 트랜잭션이 적용은 프록시 객체를 통해 수행되는데, 만약 프록시 객체를 거치지 않고 대상 객체를 직접 호출하게 되면 트랜잭션이 적용되지 않는다.
class Example {
public void external() { // do something internal(); // 프록시를 거치지 않고 대상 객체 내부에서 메서드 호출하기 때문에 트랜잭션이 적용되지 않는다. }
@Transactional public void internal() { // do something }}별도 클래스로 분리하여 호출하여 방지하는 것이 가장 좋다.
초기화 시점
Section titled “초기화 시점”스프링 초기화 시점에는 트랜잭션 AOP가 적용되지 않을 수 있기 때문에 트랜잭션이 필요한 로직이 필요한 메서드 실행 시점을 스프링 컨테이너가 완전히 생성되고 난 뒤에 호출할 수 있도록 설정하는 것이 좋다.
class Hello {
@PostConstruct @Transactional public void init1() { boolean isActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("tx active={} ", isActive); // false }
@EventListener(ApplicationReadyEvent.class) @Transactional public void init2() { boolean isActive = TransactionSynchronizationManager.isActualTransactionActive(); log.info("tx active={} ", isActive); // true }}트랜잭션 옵션
Section titled “트랜잭션 옵션”@Transactional 애노테이션을 통해 트랜잭션을 적용할 때 아래와 같이 옵션을 설정할 수 있으며, 지정하지 않은 경우엔 기본값이 적용된다.
@Transactional(isolation = Isolation.DEFAULT, readOnly = false)class Example {
}애노테이션에 적용할 수 있는 옵션들은 아래와 같다.
1. rollbackFor / noRollbackFor
Section titled “1. rollbackFor / noRollbackFor”예외 발생시 스프랑 트랜잭션의 롤백 정책으로 기본 정책은 아래와 같다.
- 언체크 예외: 롤백
- 체크 예외: 롤백하지 않고 커밋
이 옵션에 추가로 롤백할 예외를 지정하게 되면, 해당 예외가 발생했을 때 롤백하게 된다.
@Transactional(rollbackFor = Exception.class)class Example {
}반대로 noRollbackFor 옵션은 롤백하지 않을 예외를 지정할 수 있다.
2. isolation
Section titled “2. isolation”트랜잭션 격리 수준 지정으로 보통 데이터베이스에서 설정한 트랜잭션 수준을 사용하는 DEFAULT를 사용한다.
3. timeout
Section titled “3. timeout”트랜잭션 타임아웃을 지정하는 옵션으로 기본값은 -1로 무제한이다.
4. readOnly
Section titled “4. readOnly”false: 읽기 쓰기가 모두 가능한 트랜잭션true: 읽기 전용 트랜잭션(드라이버나 DB에 따라 읽기 전용 트랜잭션을 지원하지 않을 수 있음)
5. propagation
Section titled “5. propagation”트랜잭션 전파 옵션으로 기본값은 REQUIRED로, 대부분 이 옵션을 사용한다.
| 옵션 | 설명 | 기존 트랜잭션 X | 기존 트랜잭션 O |
|---|---|---|---|
| REQUIRED | 하나의 트랜잭션 사용 | 새로운 트랜잭션 생성 | 기존 트랜잭션 사용 |
| REQUIRES_NEW | 항상 새로운 트랜잭션 사용(커넥션 추가 점유) | 새로운 트랜잭션 생성 | 새로운 트랜잭션 생성 |
| SUPPORT | 트랜잭션 지원 | 트랜잭션 없이 진행 | 기존 트랜잭션 사용 |
| NOT_SUPPORTED | 트랜잭션 미지원 | 트랜잭션 없이 진행 | 트랜잭션 없이 진행(기존 트랜잭션 보류) |
| MANDATORY | 트랜잭션이 반드시 존재해야 함 | 예외 발생 | 기존 트랜잭션 사용 |
| NEVER | 트랜잭션을 사용하지 않음 | 트랜잭션 없이 진행 | 예외 발생 |
isolation , timeout , readOnly 는 트랜잭션이 처음 시작될 때만 적용되며, 트랜잭션에 참여하는 경우에는 적용되지 않는다.
트랜잭션 전파 흐름 - REQUIRED 옵션
Section titled “트랜잭션 전파 흐름 - REQUIRED 옵션”트랜잭션 전파 옵션이 REQUIRED인 경우 이미 트랜잭션이 존재하면 해당 트랜잭션을 사용하고 없으면 새로운 트랜잭션을 생성하게 된다.
하나의 커밋이라도 발생하면 전체 트랜잭션이 커밋되고, 하나의 롤백이라도 발생하면 전체 트랜잭션이 롤백되며, 그 원리와 순서는 아래와 같다.
flowchart TD C([클라이언트]) --> OTS
subgraph OT["외부 트랜잭션"] OTS[외부 트랜잭션 시작 선언] -->|6. 트랜잭션 커넥션 사용| OL[로직 수행] end
OL --> ITS
subgraph IT["내부 트랜잭션"] ITS[내부 트랜잭션 시작 선언] -->|11. 트랜잭션 커넥션 사용| IL[로직 수행] end
OTS -->|"1. 외부 트랜잭션 시작"| OTM[트랜잭션 매니저] OTM -->|"2. 커넥션 생성 → 3. 물리 트랜잭션 시작 → 4. 커넥션 보관"| TSM[트랜잭션 동기화 매니저] OTM -->|"5. 결과 반환(신규=true)"| OTS ITS -->|"7. 내부 트랜잭션 시작"| ITM[트랜잭션 매니저] ITM -->|"8. 기존 트랜잭션 존재 확인 → 9. 기존 트랜잭션에 참여"| TSM ITM -->|"10. 결과 반환(신규=false)"| ITS응답 흐름 - 성공
Section titled “응답 흐름 - 성공”flowchart TD subgraph IT["내부 트랜잭션"] IL[로직 수행] --> ITS[내부 트랜잭션 시작 선언] end
ITS -->|"1. 내부 트랜잭션 커밋"| ITM[트랜잭션 매니저] ITM -->|"2. 신규 트랜잭션이 아니므로 실제 커밋 X"| ITM
ITS --> OL
subgraph OT["외부 트랜잭션"] OL[로직 수행] --> OTS[외부 트랜잭션 시작 선언] end
OTS -->|"3. 외부 트랜잭션 커밋"| OTM[트랜잭션 매니저] OTM -->|"4. 신규 트랜잭션이므로 실제 커밋 호출 → 5. 물리적 커밋"| TSM[트랜잭션 동기화 매니저]
OTS --> C([클라이언트])응답 흐름 - 외부 트랜잭션 실패
Section titled “응답 흐름 - 외부 트랜잭션 실패”flowchart TD subgraph IT["내부 트랜잭션"] IL[로직 수행] --> ITS[내부 트랜잭션 시작 선언] end
ITS -->|"1. 내부 트랜잭션 커밋"| ITM[트랜잭션 매니저] ITM -->|"2. 신규 트랜잭션이 아니므로 실제 커밋 X"| ITM
ITS --> OL
subgraph OT["외부 트랜잭션"] OL[로직 수행] --> OTS[외부 트랜잭션 시작 선언] end
OTS -->|"3. 외부 트랜잭션 롤백 요청"| OTM[트랜잭션 매니저] OTM -->|"4. 신규 트랜잭션이므로 실제 롤백 호출 → 5. 물리적 롤백"| TSM[트랜잭션 동기화 매니저]
OTS --> C([클라이언트])응답 흐름 - 내부 트랜잭션 실패
Section titled “응답 흐름 - 내부 트랜잭션 실패”flowchart TD subgraph IT["내부 트랜잭션"] IL[로직 수행] --> ITS[내부 트랜잭션 시작 선언] end
ITS -->|"1. 내부 트랜잭션 롤백 요청"| ITM[트랜잭션 매니저] ITM -->|"2. 신규 트랜잭션이 아니므로 실제 롤백 X"| ITM ITM -->|"3. rollbackOnly=true 설정"| TSM[트랜잭션 동기화 매니저]
ITS --> OL
subgraph OT["외부 트랜잭션"] OL[로직 수행] --> OTS[외부 트랜잭션 시작 선언] end
OTS -->|"4. 커밋 요청"| OTM[트랜잭션 매니저] OTM -->|"5. rollbackOnly=true 확인 → 6. 커밋 대신 롤백"| TSM
OTS -->|"7. UnexpectedRollbackException"| C([클라이언트])내부 트랜잭션 실패로 외부 트랜잭션이 롤백되는 예시
Section titled “내부 트랜잭션 실패로 외부 트랜잭션이 롤백되는 예시”// 외부 트랜잭션@Servicepublic class BatchRegistrationService {
private final MemberService memberService;
public BatchRegistrationService(MemberService memberService) { this.memberService = memberService; }
@Transactional public void registerMultipleMembers() { for (long point = 0; point < 5; point++) { try { memberService.registerMember(point); } catch (Exception e) { System.out.println("예외 발생: " + e.getMessage()); } } }}
// 내부 트랜잭션@Servicepublic class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
@Transactional public void registerMember(long point) { if (point == 2L) { throw new RuntimeException("포인트가 2인 회원은 등록할 수 없습니다."); } Member member = new Member(point); memberRepository.save(member); }}외부 트랜잭션에서 예외를 감싸서 외부 트랜잭션에서의 예외 발생을 방지했지만, 내부 트랜잭션에서 예외가 발생하여 롤백 마킹됐기 때문에 전체 트랜잭션이 롤백된다.