STUDY NOTE · 개념 정리
트랜잭션 개념 정리: ACID, 격리 수준, 스프링 @Transactional 전파와 롤백 규칙
Backend
트랜잭션은 더 이상 쪼갤 수 없는 작업 단위다. 그 안의 작업은 전부 성공하거나(커밋) 전부 취소된다(롤백). 스프링의 @Transactional은 이 경계를 프록시로 자동 처리한다.1. 왜 필요한가
계좌 이체는 "A 계좌에서 출금"과 "B 계좌에 입금" 두 작업이다. 출금만 되고 입금 전에 서버가 죽으면 돈이 사라진다. 두 작업을 하나로 묶어 둘 다 되거나 둘 다 안 되게 하는 것이 트랜잭션이다.
2. ACID
| 성질 | 의미 | 보장 수단 (예) |
| Atomicity (원자성) | 전부 성공하거나 전부 실패 | Undo 로그로 롤백 |
| Consistency (일관성) | 트랜잭션 전후로 제약 조건이 지켜짐 | PK, FK, CHECK 제약, 애플리케이션 규칙 |
| Isolation (격리성) | 동시에 실행되는 트랜잭션이 서로 간섭하지 않음 | 락, MVCC, 격리 수준 |
| Durability (지속성) | 커밋된 결과는 장애가 나도 유지 | Redo 로그, 디스크 기록 |
3. 동시성 문제
| 현상 | 설명 |
| Dirty Read | 다른 트랜잭션이 아직 커밋하지 않은 값을 읽음 (나중에 롤백되면 없는 값을 읽은 셈) |
| Non-Repeatable Read | 같은 행을 두 번 읽었는데 그 사이 다른 트랜잭션의 수정·커밋으로 값이 달라짐 |
| Phantom Read | 같은 조건으로 두 번 조회했는데 그 사이 추가·삭제된 행 때문에 행 수가 달라짐 |
4. 격리 수준 (Isolation Level)
| 격리 수준 | Dirty Read | Non-Repeatable Read | Phantom Read |
| READ UNCOMMITTED | 발생 | 발생 | 발생 |
| READ COMMITTED | 방지 | 발생 | 발생 |
| REPEATABLE READ | 방지 | 방지 | 발생 가능 |
| SERIALIZABLE | 방지 | 방지 | 방지 |
- 아래로 갈수록 안전하지만 동시 처리 성능은 떨어진다.
- 기본값: MySQL/MariaDB(InnoDB)는 REPEATABLE READ, Oracle·PostgreSQL·SQL Server는 READ COMMITTED.
- InnoDB는 MVCC(스냅샷)로 일반 SELECT에서는 팬텀도 대부분 보이지 않게 하고, 잠금 읽기에서는 갭 락으로 막는다.
5. 스프링 @Transactional의 동작 원리: 프록시
스프링은 @Transactional이 붙은 빈 대신 프록시 객체를 주입한다. 프록시는 메서드 호출을 가로채서 다음 일을 한다.
- 트랜잭션 시작 (커넥션 획득, auto-commit 끔)
- 실제 객체의 메서드 호출
- 정상 종료면 커밋, 롤백 대상 예외면 롤백
- 커넥션 반환
이 구조에서 나오는 규칙이 있다. 트랜잭션은 프록시를 통해 호출될 때만 적용된다.
| 상황 | 적용 여부 | 이유 |
| 다른 빈에서 호출 | 적용 | 프록시를 거침 |
| 같은 클래스 내부에서 this.method() 호출 | 미적용 | 프록시를 거치지 않음 (self-invocation) |
| private 메서드 | 미적용 | 프록시가 가로챌 수 없음 |
6. 롤백 규칙
| 예외 종류 | 기본 동작 |
| RuntimeException (Unchecked) | 롤백 |
| Error | 롤백 |
| Exception (Checked, 예: IOException) | 커밋 |
체크 예외에도 롤백하려면 @Transactional(rollbackFor = Exception.class)를 쓴다. 그리고 메서드 안에서 예외를 catch하고 다시 던지지 않으면, 프록시는 정상 종료로 보고 커밋한다.
7. 전파 속성 (Propagation)
이미 트랜잭션이 진행 중일 때 새 @Transactional 메서드를 호출하면 어떻게 할지 정한다.
| 전파 속성 | 기존 트랜잭션이 있으면 | 없으면 |
| REQUIRED (기본값) | 참여 | 새로 시작 |
| REQUIRES_NEW | 기존을 보류하고 새로 시작 | 새로 시작 |
| SUPPORTS | 참여 | 트랜잭션 없이 실행 |
| MANDATORY | 참여 | 예외 발생 |
| NOT_SUPPORTED | 보류하고 트랜잭션 없이 실행 | 트랜잭션 없이 실행 |
| NEVER | 예외 발생 | 트랜잭션 없이 실행 |
| NESTED | 세이브포인트를 만들고 중첩 실행 | 새로 시작 |
REQUIRED로 참여한 안쪽 메서드에서 런타임 예외가 나면, 바깥에서 그 예외를 잡더라도 공유 중인 트랜잭션 전체가 rollback-only로 표시된다. 바깥이 커밋을 시도하면 UnexpectedRollbackException이 나며 전체가 롤백된다.
8. 그 밖의 속성
| 속성 | 의미 |
| readOnly = true | 읽기 전용 힌트. JPA는 변경 감지를 생략해 가벼워지고, 읽기 전용 DB로 라우팅하는 기준이 되기도 함 |
| isolation | 이 트랜잭션의 격리 수준 지정 (기본은 DB 설정) |
| timeout | 제한 시간 초과 시 롤백 |
9. 실제로 어떻게 적용되나
- 주문 생성: 주문 저장, 재고 차감, 포인트 사용을 한 서비스 메서드에
@Transactional로 묶는다. 재고 부족 예외(RuntimeException)가 나면 앞서 저장한 주문까지 롤백된다(원자성). - 댓글 저장과 포인트 지급 분리: 포인트 지급이 실패해도 댓글은 저장돼야 한다면, 포인트 지급을 REQUIRED로 두고 예외를 잡는 것만으로는 안 된다(rollback-only 표시 때문).
REQUIRES_NEW로 별도 트랜잭션을 열거나, 커밋 후 이벤트(@TransactionalEventListener(phase = AFTER_COMMIT))로 분리한다. - 같은 클래스 안에서 호출: 반복문 안에서 건별로 트랜잭션을 걸려고 같은 클래스의
@Transactional메서드를 호출하면 적용되지 않는다. 그 메서드를 다른 빈으로 분리해야 한다. - 외부 API 호출 위치: 트랜잭션 안에서 느린 외부 API를 호출하면 그동안 DB 커넥션을 붙잡아 커넥션 풀이 고갈될 수 있다. 외부 호출은 트랜잭션 밖으로 뺀다.
10. 면접 질문으로 정리
- Q. 트랜잭션과 ACID란? 트랜잭션은 전부 성공하거나 전부 실패해야 하는 작업 단위이며, 원자성·일관성·격리성·지속성을 보장해야 한다.
- Q. 격리 수준과 각 수준에서 발생하는 문제는? READ UNCOMMITTED는 Dirty Read, READ COMMITTED는 Non-Repeatable Read, REPEATABLE READ는 Phantom Read가 발생할 수 있고, SERIALIZABLE은 모두 방지하지만 동시성이 가장 낮다. MySQL InnoDB의 기본은 REPEATABLE READ다.
- Q. @Transactional은 어떻게 동작하나? 스프링이 빈을 프록시로 감싸 메서드 호출 전후에 트랜잭션 시작·커밋·롤백을 처리한다. 그래서 같은 클래스 내부 호출이나 private 메서드에는 적용되지 않는다.
- Q. 어떤 예외에서 롤백되나? 기본적으로 RuntimeException과 Error에서 롤백되고 체크 예외는 커밋된다. rollbackFor로 변경할 수 있다.
- Q. REQUIRED와 REQUIRES_NEW의 차이는? REQUIRED는 기존 트랜잭션에 참여해 하나로 커밋·롤백되고, REQUIRES_NEW는 기존 트랜잭션을 보류하고 독립된 새 트랜잭션으로 커밋·롤백된다.