Spring Transaction
Spring의 선언적 트랜잭션(@Transactional)은 AOP 프록시 패턴을 기반으로 동작해, 핵심 비즈니스 로직과 데이터베이스 트랜잭션 관리 로직을 분리한다.[1] 이 페이지는 그 프록시 동작 원리로 인해 발생하는 내부 호출(Self-Invocation) 한계, 예외 타입에 따른 롤백 규칙(Rollback Rules), 다중 트랜잭션 환경에서의 전파 속성 및 격리 수준의 표준 패턴을 다룬다.
프록시 기반 트랜잭션 아키텍처
- Spring은
@Transactional어노테이션이 선언된 빈을 컨테이너에 등록할 때, 실제 객체를 감싸는 Proxy 객체를 동적으로 생성한다.[1:1] - 클라이언트(Controller 등)의 요청은 반드시 이 Proxy를 거치며, 여기서
PlatformTransactionManager를 통해 DB 연결과 트랜잭션 경계가 설정된다.[1:2]
sequenceDiagram
autonumber
actor Client as Client (e.g., Controller)
participant Proxy as AOP Proxy
(Transaction Barrier)
participant TxManager as PlatformTransactionManager
participant Target as Target Service
(Business Logic)
participant DB as Connection/DB
Note over Client, Proxy: 외부 호출 시에만 Proxy 작동
Client->>Proxy: 비즈니스 메서드 호출
activate Proxy
Proxy->>TxManager: 트랜잭션 시작 (begin)
activate TxManager
TxManager->>DB: SET autocommit = false
TxManager-->>Proxy: 경계 설정 완료
deactivate TxManager
Proxy->>Target: 실제 타겟 메서드 위임 호출
activate Target
alt 정상 종료 (No Exception)
Target-->>Proxy: 결과 리턴
deactivate Target
Proxy->>TxManager: Commit 요청
activate TxManager
TxManager->>DB: COMMIT
TxManager-->>Proxy: 완료
deactivate TxManager
else 런타임 예외 발생 (Unchecked)
Target-->>Proxy: Throw RuntimeException
Proxy->>TxManager: Rollback 요청
activate TxManager
TxManager->>DB: ROLLBACK
TxManager-->>Proxy: 완료
deactivate TxManager
end
Proxy-->>Client: 최종 응답 또는 예외 전파
deactivate Proxy아키텍처적 제약: 내부 호출(Self-Invocation)
- 트랜잭션은 Client가 Proxy를 호출할 때만 시작된다.[1:3]
- Service 내부에서
@Transactional이 없는 메서드가 같은 클래스 내의@Transactional메서드를 직접 호출(this.method())하면, Proxy를 우회하게 되어 트랜잭션이 전혀 적용되지 않는다.[2] - 바깥 메서드에도
@Transactional을 붙이면 롤백은 일어나지만, 롤백을 일으키는 주체는 바깥 메서드의 Proxy다. 이는 위 두 원리를 적용해 이 Wiki가 도출한 해석이며 원문이 직접 다룬 사례는 아니다.this로 호출된 안쪽 메서드의@Transactional은 여전히 적용되지 않고, 안쪽 코드는 바깥 Proxy가 이미 연 트랜잭션 안에서 실행될 뿐이다. 따라서 안쪽 메서드에 따로 지정한 설정은 반영되지 않는다.[1:4][2:1] - 해결책: 트랜잭션 경계가 필요한 로직을 완전히 별도의 Service Bean으로 분리해 항상 외부 호출(Proxy 경유)이 일어나도록 설계하는 것이 가장 권장되는 방법이다.[2:2]
예외 롤백 규칙(Rollback Rules)
Spring은 자바의 예외 계층에 따라 롤백 여부를 판단하며, 개발자가 의도치 않은 데이터 손실이나 잔류를 겪지 않도록 한다.
- Unchecked Exception(
RuntimeException,Error) → 자동 롤백[3] - Checked Exception(
Exception하위 등) → 자동 커밋(비즈니스적 대안 응답으로 간주)[3:1]
실무에서는 IOException·SQLException 같은 Checked Exception 상황에서도 데이터 무결성을 위해 롤백해야 하는 경우가 대부분이다. 따라서 @Transactional(rollbackFor = Exception.class) 옵션을 명시하는 것이 표준이다.[4]
- 롤백 판단은 메서드 밖으로 던져져 Proxy에 도달한 예외에만 적용된다. 메서드 안에서 예외를
try-catch로 잡고 정상 반환하면 Proxy는 예외를 보지 못하므로,rollbackFor설정과 관계없이 커밋된다. 이는 원문의 Proxy 흐름(Target이 Exception을 던지면 Proxy가 롤백을 요청)에서 이 Wiki가 도출한 해석이며, 원문이 직접 다룬 사례는 아니다.[5] - 단, 잡은 예외가 리포지토리처럼 안쪽의
@Transactional메서드를 거쳐 나온 경우는 다르다. Spring은 리포지토리에서 예외가 나는 순간 바깥 트랜잭션을 "롤백 전용"으로 표시하므로, 바깥 메서드가 예외를 catch하고 정상 반환해도 커밋할 때UnexpectedRollbackException이 난다.[6] 리포지토리 메서드가 바깥 트랜잭션에REQUIRED로 참여해 같은 트랜잭션을 공유하기 때문으로 볼 수 있다. - Postgres는 여기에 더해 트랜잭션 안에서 에러가 한 번 나면 그 트랜잭션 전체를 버리므로, catch 뒤에 실행하는 쿼리도
current transaction is aborted로 실패한다.[6:1] MySQL은 에러가 난 문장만 롤백하지만, Spring의 롤백 전용 표시 때문에@Transactional+ catch 조합은 여전히 500이 된다.[6:2] - 이런 메서드는 바깥
@Transactional을 빼서 리포지토리 호출마다 따로 트랜잭션이 돌게 할 수 있다. Spring Data가save()와delete()에 기본으로@Transactional을 붙여 두므로 각 쓰기는 여전히 보호된다.[6:3] 적용 사례는 Unique Constraint Duplicate Prevention에 있다. - 반대로 서비스 메서드 단위의 큰 트랜잭션은 쓰기가 2개 이상이고 같이 성공하거나 같이 실패해야 할 때(예: 송금의 출금과 입금) 필요하다. 쓰기가 하나뿐이면 묶을 짝이 없다.[7]
트랜잭션 전파 속성(Propagation)
기존 트랜잭션이 진행 중일 때 새로운 메서드가 트랜잭션을 요구할 경우의 합류/분리 전략이다.[8]
| 전파 옵션 | 동작 메커니즘 | 실무 유스케이스 |
|---|---|---|
REQUIRED(Default) |
기존 트랜잭션에 참여(없으면 신규 생성) | 대부분의 단일 비즈니스 플로우 묶음 |
REQUIRES_NEW |
항상 독립적인 신규 트랜잭션 생성(기존 보류) | 실패해도 무조건 기록해야 하는 **[[Wiki/backend/patterns/audit-log.md |
MANDATORY |
반드시 기존 트랜잭션 내에서만 실행 | 부모 트랜잭션의 존재를 강제할 때 |
NESTED |
기존 트랜잭션 내 중첩(Savepoint) 생성 | 자식 실패가 부모에게 영향 주지 않아야 할 때(단, JPA 미지원) |
격리 수준(Isolation Level) 및 이상 현상
동시성 환경에서 다른 트랜잭션이 수정 중인 데이터를 어떻게 읽게 허용할 것인지에 대한 RDBMS 격리 정책이다.
| 수준(Level) | Dirty Read | Non-Repeatable Read | Phantom Read | RDBMS 채택 |
|---|---|---|---|---|
READ_UNCOMMITTED |
발생 | 발생 | 발생 | 거의 사용 안 함[9] |
READ_COMMITTED |
방지 | 발생 | 발생 | Oracle, PostgreSQL 기본값[9:1] |
REPEATABLE_READ |
방지 | 방지 | 발생 | MySQL(InnoDB) 기본값[9:2] |
SERIALIZABLE |
방지 | 방지 | 방지 | 완벽 격리(성능 이슈로 금융 등 극히 제한적 사용)[9:3] |
관련
- spring-aop — 프록시 패턴의 핵심 동작 원리.
- database-isolation — RDBMS 관점에서의 격리 수준 상세.
- transaction-propagation — 분산/다중 트랜잭션 제어 기법.
- Unique Constraint Duplicate Prevention — 예외를 catch해야 하는 메서드에서 바깥 트랜잭션을 빼는 실제 사례.
출처
Transaction.md — "@Transactional이 선언된 빈(Bean)이 컨테이너에 등록될 때, 스프링은 실제 객체를 감싸는 프록시 객체(Proxy)를 자동으로 만들어 등록합니다.", "클라이언트가 메서드를 호출하면 프록시 객체의 메서드가 대신 먼저 호출되며, 내부의 PlatformTransactionManager를 통해 DB 트랜잭션을 시작(begin)합니다." ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Transaction.md — "클래스 내부에서 @Transactional이 없는 메서드가 동일 클래스 내의 다른 @Transactional 메서드를 직접 호출(this 호출)하는 경우, 프록시를 거치지 않고 실제 객체의 레퍼런스를 직접 참조하므로 트랜잭션이 적용되지 않습니다.", "가장 권장되는 방법은 트랜잭션이 필요한 비즈니스 단위를 별도의 클래스(서비스 빈)로 분리하여 외부 호출 구조로 변경하는 것입니다." ↩︎ ↩︎ ↩︎
Transaction.md — "RuntimeException과 그 하위 클래스들, 그리고 시스템 레벨 오류인 Error가 발생한 경우 스프링은 비즈니스 실패로 판단하여 자동으로 트랜잭션을 롤백합니다.", "RuntimeException을 상속받지 않은 일반 Exception 계열(Checked Exception)이 발생하면... 트랜잭션을 커밋해 버립니다." ↩︎ ↩︎
Transaction.md — "실무에서는 비즈니스 로직 중 발생하는 어떠한 예외에 대해서도 데이터 무결성을 위해 롤백을 수행해야 하는 경우가 대부분입니다. 따라서 @Transactional 선언 시 Checked Exception에 대해서도 롤백이 수행되도록 명시적 설정을 해 주는 것이 표준입니다.", 예외 계층도 "Checked Exception (그 외)", "- IOException, SQLException 등" (L129-130) ↩︎
Transaction.md — "메서드가 정상 종료되면 트랜잭션을 커밋(Commit)합니다.", "메서드 실행 중 예외(Exception)가 발생하면 예외 종류에 따라 롤백(Rollback) 또는 커밋을 수행합니다.", sequence diagram "Target-->>Proxy: Exception 던짐", "Proxy->>TxManager: 롤백 요청" ↩︎
인증 실패 응답·좋아요 중복 방지 노트 — "Spring: 리포지토리에서 예외가 나는 순간 바깥 트랜잭션을 "롤백 전용"으로 표시한다. 조회를 안 해도 커밋할 때
UnexpectedRollbackException이 난다.", "Postgres: 트랜잭션 안에서 에러가 한 번 나면 그 트랜잭션 전체를 버린다. 이후 쿼리는current transaction is aborted로 실패한다.", Postgres vs MySQL 표("그 문장만 롤백", "그래도 500 (Spring 롤백 전용 표시 때문)"), "save()와delete()에는 Spring Data가 기본으로@Transactional을 붙여 둔다." ↩︎ ↩︎ ↩︎ ↩︎인증 실패 응답·좋아요 중복 방지 노트 — "큰 트랜잭션이 꼭 필요한 경우: 쓰기가 2개 이상이고 같이 성공하거나 같이 실패해야 할 때(예: 송금의 출금과 입금). 좋아요 토글은 쓰기가 하나(추가 또는 삭제)라 묶을 짝이 없다." ↩︎
Transaction.md — 전파 속성 표: REQUIRED("기본값. 진행 중인 트랜잭션이 있으면 참여하고, 없으면 새로 생성합니다."), REQUIRES_NEW("항상 새로운 트랜잭션을 시작합니다. 기존 트랜잭션은 보류(Suspend)됩니다."), MANDATORY, NESTED("DB Savepoint 기술 사용... JPA에서는 미지원") 등. ↩︎
Transaction.md — 격리 수준 표: "READ_UNCOMMITTED... 데이터 부정확성이 매우 높아 실무 미사용.", "READ_COMMITTED... Oracle, PostgreSQL 등 다수 RDBMS의 기본값.", "REPEATABLE_READ... MySQL(InnoDB)의 기본값.", "SERIALIZABLE... 성능 저하가 심해 특수한 금융 거래 등 외에는 거의 미사용." ↩︎ ↩︎ ↩︎ ↩︎