1. 트랜잭션(Transaction)
- DB 상태를 변화시키는 작업의 논리적 단위
- 반드시 모두 성공하거나 모두 실패해야 함
- 데이터 무결성(일관성) 보장과 예외 상황 대비
트랜잭션 속성: ACID
- 트랜잭션이 신뢰성과 무결성을 갖추기 위한 핵심 원칙
| 속성 |
의미 |
목적 |
실무 포인트 |
| A |
Atomicity (원자성) |
모두 성공 또는 모두 실패(All or Nothing) |
작업 단위 보호 |
| C |
Consistency (일관성) |
트랜잭션 전후 데이터 일관성(무결성) 유지 |
데이터 오염 방지 |
| I |
Isolation (격리성) |
동시에 실행되는 트랜잭션 독립성 유지 |
동시성 이슈 방지 |
| D |
Durability (지속성) |
성공된 작업은 영구 보존 |
장애 발생 시 복구 보장 |
- Isolation (격리성)
- 각 트랜잭션이 서로 영향을 받지 않도록 분리
- 격리 수준(Isolation Level)
| 수준 |
특징 |
| READ UNCOMMITTED |
다른 트랜잭션의 변경 내용을 읽음 (Dirty Read 허용) |
| READ COMMITTED |
커밋된 데이터만 읽음 |
| REPEATABLE READ |
같은 데이터를 항상 동일하게 읽음 |
| SERIALIZABLE |
가장 엄격, 트랜잭션 순차 실행 |
- 적절한 격리 수준 선택 필요
- 높은 격리성일수록 성능 저하 주의(SERIALIZABLE은 성능이 가장 낮음
- Dirty Read
- 다른 트랜잭션이 아직 commit 하지 않은 데이터를 읽는 것
- 읽지 말아야 할 데이터를 읽음
실무 TIP
- 모든 트랜잭션 작업을 ACID 준수를 전제로 설계
- 일관성 유지는 DB 설계부터 엄격하게!
- 참조할 수 있는 값만 FK로 받기
트랜잭션 상태 변화
[BEGIN]
│
└─> [ACTIVE] ──> [PARTIALLY COMMITTED]
│
┌────────────┴─────────────┐
(예외 없음) (예외 발생)
COMMIT ROLLBACK
│ │
[COMMITTED] [FAILED / ABORTED]
│ │
[END / 영구 반영] [END / 원상복구]
2. Spring과 트랜잭션
Spring의 트랜잭션 처리 방식
1) 선언적 트랜잭션
- 스프링에서는 AOP(Aspect-Oriented Programming) 기반으로 트랜잭션 관리
- @Transactional 어노테이션 선언
- 적용 위치
| 적용 위치 |
의미 |
| 클래스 레벨 |
모든 public 메서드에 적용 |
| 메서드 레벨 |
해당 메서드에만 적용 (가장 세밀한 제어 가능) |
- 주요 속성
| 속성 |
설명 |
자주 사용되는 값 |
| propagation |
전파 방식 |
REQUIRED (기본값) |
| isolation |
격리 수준 |
DEFAULT |
| timeout |
제한 시간 (초) |
-1 (무제한) |
| readOnly |
읽기 전용 여부 |
true / false |
| rollbackFor |
롤백 발생 예외 |
Exception.class |
| noRollbackFor |
롤백 제외 예외 |
DataIntegrityViolationException.class |
- 예시
@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Transactional
public void registerUser(User user) {
userRepository.save(user);
emailService.sendWelcomeEmail(user.getEmail());
}
@Transactional(readOnly = true)
public User findUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
}
- 예외 발생 시 자동으로 Rollback
- 성공 시 자동으로 commit
- @Transactional(readOnly = true)
주의
- 내부 호출, private 메서드는 AOP 적용 X
2) 프로그래밍 방식 트랜잭션
Spring AOP와 트랜잭션
1) AOP란?
- Aspect-Oriented Programming
- 관심사를 분리하여 관리하는 프로그래밍 방식
- Spring의 트랜잭션은 AOP 기반 프록시 객체를 통해 관리됨
- 핵심 용어 정리
| 용어 |
설명 |
예시 |
| Aspect |
공통 관심사 |
트랜잭션 관리 |
| Join Point |
특정 시점 |
메서드 실행 |
| Advice |
특정 Join Point에서 실행되는 코드 |
트랜잭션 시작/커밋/롤백 |
| Pointcut |
Join Point의 집합 지정 |
@Transactional 달린 메서드 |
| Proxy |
대상 객체를 감싸는 객체, Advice 적용 |
|
2) Proxy 기반 트랜잭션 처리
- Spring에서 @Transactional을 통한 선언적 트랜잭션 처리는 프록시 객체를 통해 동작
- 실제 비즈니스 로직 호출 X, 프록시가 가로채 트랜잭션을 시작하고 종료
Client
↓
Service(Proxy)
↓
Transaction 시작 / 종료
↓
Real Service(Bean)
3. 트랜잭션 전파 (Propagation)
- 현재 트랜잭션이 진행 중일 때, 하위 메서드가 새로운 트랜잭션을 사용할지 기존 트랜잭션에 참여할지를 결정하는 속성
- @Transactional의 propagation 속성으로 설정
- 트랜잭션마다 각각의 영속성 컨텍스트 존재
- 병합하지 않으면 각자 commit
- 병합하면 영속성 컨텍스트를 공유하여 한 번만 commit
- 주요 전파 옵션
| 옵션 |
설명 |
사용 예시 |
| REQUIRED |
현재 트랜잭션 있으면 참여, 없으면 생성 |
대부분의 비즈니스 로직 |
| REQUIRES_NEW |
항상 새로운 트랜잭션 시작 |
감사 로그, 이력 저장 |
| SUPPORTS |
트랜잭션 있으면 참여, 없으면 비트랜잭션으로 수행 |
조회, 로깅 |
| MANDATORY |
반드시 기존 트랜잭션 필요, 없으면 예외 발생 |
상위 트랜잭션 필수 로직 |
| NEVER |
트랜잭션이 있으면 예외 발생, 없으면 비트랜잭션 |
외부 API 호출 |
| NOT_SUPPORTED |
트랜잭션을 중단하고 비트랜잭션으로 수행 |
로그 저장 등 |
| NESTED |
중첩 트랜잭션, SavePoint 사용 |
대용량 처리 중 일부 롤백 |
4. 트랜잭션 격리 수준
- 격리 수준에 따라 한 트랜잭션이 다른 트랜잭션에서 변경한 데이터를 어느 정도까지 볼 수 있는지 결정됨
- 여러 트랜잭션이 동시에 실행될 때 DB의 일관성과 동시성 제어
- @Transactional의 isolation 속성으로 설정
- 주요 문제점
- 1) Dirty Read : 커밋되지 않은 데이터를 다른 트랜잭션이 읽는 현상
- 2) Non-repeatable Read
- 부정합 현상
- 같은 쿼리를 실행했을 때 이전과 다른 결과가 나오는 현상
- 3) Phantom Read
- 일정 범위의 레코드를 두 번 이상 읽을 때 이전에 없던 레코드가 나타나는 현상
- 스냅샷을 못 읽는 경우 발생
- 격리 수준
- READ UNCOMMITTED
- @Transactional(isolation = Isolation.READ_UNCOMMITTED)
- 커밋되지 않은 데이터도 모두 조회 가능
- 속도가 중요한 경우 사용하나, 정확성을 보장하기 어려워 거의 사용 X
- READ COMMITTED
- @Transactional(isolation = Isolation.READ_COMMITTED)
- PostgreSQL 기본값
- 커밋된 데이터만 읽을 수 있음
- 트랜잭션 도중 동일 테이터를 다시 조회하면 변경된 값을 볼 수 있음
- 일반적인 서비스에서 많이 사용
- Dirty Read 방지
- Non-repeatable Read, Phantom Read 발생 가능
- REPEATABLE READ
- @Transactional(isolation = Isolation.REPEATABLE_READ)
- 트랜잭션이 시작될 때 스냅샷을 생성하고, 끝날 때까지 동일한 데이터만 조회
- 트랜잭션 동안 동일 데이터를 반복 조회 시 값이 유지됨
- 읽기/쓰기 동시 작업 가능
- Dirty Read, Non-repeatable Read 방지
- Phantom Read 발생 가능
- SERIALIZABLE
- @Transactional(isolation = Isolation.SERIALIZABLE)
- 누군가가 사용 중이면 모두 기다려야 함
- 가장 높은 격리 수준, 모든 트랜잭션을 순차 실행처럼 보이게 함
- Dirty Read, Non-repeatable Read, Phantom Read 모두 방지
- 교착 상태(데드락) 발생 가능
5. 트랜잭션 예외 처리
예외 유형
- 체크 예외(Exception)
- 컴파일 시 강제 처리 → throws 또는 try-catch 필수
- 스프링 트랜잭션: 기본 커밋, 필요 시 rollbackFor
- 외부 API 연동 실패 → Checked Exception, rollbackFor 필요
- throw new IOException("파일 입출력 실패");
- 언체크 예외(RunTimeException)
- JVM이 강제 처리 X → throws / try-catch 불필요
- 스프링 트랜잭션: 자동 롤백, 필요 시 noRollbackFor
- 도메인 오류 → RunTimeException으로 통일, 일관된 롤백 처리 가능
- throw new IllegalArgumentException("잘못된 입력");
트랜잭션 롤백 규칙
| 예외 유형 |
기본 롤백 여부 |
설명 |
| 언체크 예외 (RuntimeException 계열) |
롤백 |
스프링 기본 정책, 런타임 에러는 대부분 롤백 필요 |
| 체크 예외 (Exception 계열) |
커밋 (롤백 X) |
명시적 지정 없으면 커밋, 외부 의존 로직 등 예상된 상황 |
| 에러 (Error) |
롤백 |
JVM 치명적 에러, 롤백 수행 |
- RunTimeException 또는 Error가 발생하면 자동 롤백
- 체크 예외는 예외처리 대상 X
활용 코드
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
/**
* 언체크 예외(RuntimeException) - 스프링 기본 동작: 자동 롤백
*/
@Transactional
public void uncheckedRollback(Order order) {
System.out.println("[언체크 예외 발생 전] 저장 시도");
orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
// RuntimeException은 기본적으로 롤백 대상
throw new IllegalArgumentException("RuntimeException - 자동 롤백");
}
/**
* 체크 예외(Exception) - rollbackFor 지정으로 롤백 강제
* 기본적으로 체크 예외는 커밋되지만, rollbackFor 사용 시 롤백 가능
*/
@Transactional(rollbackFor = Exception.class)
public void checkedRollback(Order order) throws Exception {
System.out.println("[체크 예외 발생 전] 저장 시도");
orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
// 명시적으로 rollbackFor 설정했으므로 롤백됨
throw new Exception("Checked Exception - rollbackFor로 롤백");
}
@Transactional
public void checkedRollback(Order order) {
try {
System.out.println("[체크 예외 발생 전] 저장 시도");
orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
// 명시적으로 rollbackFor 설정했으므로 롤백됨
throw new Exception("Checked Exception - rollbackFor로 롤백");
} catch(Exception.class) {
throw new RuntimeException();
}
}
/**
* 언체크 예외(RuntimeException) - noRollbackFor 지정 시 롤백 방지
* 기본 롤백 대상이지만, noRollbackFor로 커밋 유도
*/
@Transactional(noRollbackFor = IllegalArgumentException.class)
public void uncheckedNoRollback(Order order) {
System.out.println("[언체크 예외 + noRollbackFor] 저장 시도");
orderRepository.save(order); // DB 저장 (커밋됨)
// RuntimeException인데도 롤백되지 않고 커밋됨
throw new IllegalArgumentException("RuntimeException - noRollbackFor로 커밋");
}
/**
* 체크 예외(Exception) - rollbackFor 미지정 시 커밋 (스프링 기본)
*/
@Transactional
public void checkedNoRollback(Order order) throws Exception {
System.out.println("[체크 예외 발생 전] 저장 시도");
orderRepository.save(order); // DB 저장 (커밋됨)
// rollbackFor 없으므로 커밋됨
throw new Exception("Checked Exception - rollbackFor 없으면 커밋");
}
/**
* 에러(Error) - 스프링 기본 동작: 자동 롤백
*/
@Transactional
public void errorRollback(Order order) {
System.out.println("[에러 발생 전] 저장 시도");
orderRepository.save(order); // DB 저장 (하지만 롤백 예정)
// Error는 스프링에서 롤백 대상
throw new StackOverflowError("Error 발생 - 자동 롤백");
}
}
정리
| 예외 유형 |
기본 동작 |
rollbackFor |
noRollbackFor |
| RuntimeException |
롤백 |
유지 |
롤백 방지 가능 |
| Checked Exception |
커밋 |
롤백 가능 |
의미 없음 |
6. 실무 적용 가이드
트랜잭션 경계 설정
- Service 계층에만 @Transactional 선언
- 조회 전용 메서드는 readOnly = true 적극 사용
성능 최적화를 위한 트랜잭션 관리
1) Read Only 속성 활용
- 트랜잭션 내에서 조회만 수행하는 경우 readOnly = true 사용 → 변경 감지를 막고, Flush / Dirty Checking 생략 → 성능 최적화
- Dirty Checking (변경 감지)
- JPA는 트랜잭션 내에서 조회한 엔티티의 상태를 스냅샷으로 기억
- 트랜잭션 종료 시점에 원본 스냅샷과 변경된 상태의 스냅샷을 비교해, 달라진 값이 있으면 UPDATE 쿼리를 자동으로 날림
- 이것을 Dirty Cheking이라 함
2) 트랜잭션 시간 최소화
- DB Connection 오래 점유 X
- 외부 API 호출은 트랜잭션 안에 금지
3) 불필요한 트랜잭션 제거
| 상황 |
트랜잭션 필요 여부 |
| 단순 조회 |
X (readOnly 사용) |
| 로그 기록 |
X (DB 필요 없음) |
| 외부 시스템 연동 (Kafka 등) |
X (DB 필요 없음) |
| 데이터 변경 (Insert/Update) |
O |
- 습관적으로 모든 Service에 @Transactional을 붙이지 말자!
실무 TIP
| 상황 |
권장 처리 방식 |
이유 |
| 조회 전용 |
@Transactional(readOnly = true) 사용 |
변경감지 / flush 비활성화, |
| 성능 최적화 |
|
|
| DB 저장 / 수정 / 삭제 |
일반 @Transactional 사용 |
변경 사항 일관성 보장 필요 |
| 외부 시스템 호출 (Kafka, Email 등) |
트랜잭션 사용 금지 |
DB와 무관, |
| 롤백 의미 없음 |
|
|
| 로그 출력, 파일 기록 |
트랜잭션 사용 금지 |
DB와 무관, |
| 트랜잭션 자원 낭비 |
|
|
| 트랜잭션 범위가 긴 경우 |
짧게 분리 |
|
| (저장 따로, 외부 호출 따로) |
DB Connection 점유 최소화, |
|
| 성능 개선 |
|
|
| </aside> |
|
|
흔한 실수와 해결방안
1) 내부 메서드 호출 문제(Proxy 우회)
2) 트랜잭션 전파 실수(Propagation)
| 옵션 |
설명 |
| REQUIRED |
부모 있으면 합류, 없으면 새로 시작 |
| REQUIRES_NEW |
무조건 새 트랜잭션 시작 |
3) 긴 트랜잭션 범위 문제
- 외부 API 호출할 때 발생
- 불필요하게 Connection을 오래 점유해 성능저하나 교착상태 위험
4) 예외 처리 누락으로 인한 롤백 실패
5) 외부 시스템 트랜잭션 오용
요약
| 문제 |
원인 |
해결 방안 |
| 내부 호출 |
프록시 우회 |
자기주입 / 분리 |
| 전파 실수 |
REQUIRED 혼용 |
REQUIRES_NEW 구분 사용 |
| 긴 트랜잭션 |
범위 과다 지정 |
최소화 분리 |
| 예외 누락 |
체크예외 rollbackX |
rollbackFor 설정 |
- 내부 호출 → AOP 우회 방지 필요 (self-injection or 분리)
- 전파 옵션 → rollback 범위 명확히 구분
- 긴 트랜잭션 → 최소한, 외부 호출 제외
- Exception → rollbackFor로 명확히
- 구조상 Controller → Service → Repository 유지