이번 글에서는 스프링 애플리케이션에서 자주 사용되는 어노테이션인
@Transactional의 동작 원리, 실무 이슈인 저장 직후 조회 실패 문제,
그리고 트랜잭션 격리 수준(Isolation Level)을 통한 정합성 제어 전략까지 정리하였다.
@Transactional의 기본 개념
- 트랜잭션(Transaction): 데이터베이스 작업의 논리적인 단위. 전부 성공하거나, 전부 실패해야 함
@Transactional: 해당 메서드를 트랜잭션 범위 내에서 실행되도록 지정
- 스프링에서는 AOP(프록시) 기반으로 트랜잭션을 적용
클래스 vs 메서드 단위 적용
| 위치 |
적용 대상 |
특징 |
| 클래스 |
모든 public 메서드 |
전역적인 트랜잭션 관리 가능 |
| 메서드 |
개별 메서드 |
세밀한 제어 가능, 우선순위 높음 |
실무 이슈: 저장 직후 조회가 안 되는 이유
상황 예시
@PostMapping("/create-and-get")
public ResponseEntity<?> createAndGet() {
service.saveSomething(); // 저장
Entity result = service.findSomething(); // 조회 → 조회 실패
return ResponseEntity.ok(result);
}
@Transactional이 붙은 두 메서드를 같은 컨트롤러 내에서 호출
- 같은 트랜잭션 안에서 실행되므로 저장이 아직 DB에 flush되지 않음
- 따라서 조회는 이전 DB 상태를 기준으로 진행됨
해결 방법 ①: 명시적 flush() 호출
@Transactional
public void saveSomething() {
repository.save(...);
entityManager.flush(); // DB 반영 강제
}
- 즉시 DB에 반영되므로 이후 find() 호출 시 정상 조회 가능
- 단, 여전히 같은 트랜잭션 범위 내임에 유의
해결 방법 ②: 트랜잭션 분리
방법 1: 다른 서비스 빈으로 분리
@Service
public class SaveService {
@Transactional
public void saveSomething() { ... }
}
@Service
public class ReadService {
@Transactional(readOnly = true)
public Entity findSomething() { ... }
}
- 컨트롤러에서 두 서비스를 분리 호출 → 다른 트랜잭션에서 실행됨
방법 2: 이벤트 발행 방식 활용
applicationEventPublisher.publishEvent(new SomethingCreatedEvent(...));
해결 방법 ③: @Modifying + flushAutomatically
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("update User u set u.status = :status where u.id = :id")
void updateStatus(@Param("id") Long id, @Param("status") String status);
트랜잭션 격리 수준 (Isolation Level)
정의:
트랜잭션이 다른 트랜잭션과 데이터를 얼마나 공유할 수 있는지 결정하는 설정
설정 방법:
@Transactional(isolation = Isolation.SERIALIZABLE)
| 수준 |
설명 |
허용 현상 |
| DEFAULT |
DB 기본 설정 사용 (보통 READ_COMMITTED) |
DB에 따라 다름 |
| READ_UNCOMMITTED |
커밋되지 않은 데이터도 읽음 |
Dirty Read 허용 |
| READ_COMMITTED |
커밋된 데이터만 읽음 |
Non-repeatable Read 가능 |
| REPEATABLE_READ |
트랜잭션 동안 동일 데이터 보장 |
Phantom Read 가능 |
| SERIALIZABLE |
가장 강한 격리 → 완전한 정합성 |
없음 (성능 저하) |
저장 직후 조회 이슈 vs 격리 수준
| 문제 상황 |
격리 수준 효과 |
실제 해결책 |
| 저장 후 즉시 조회 안 됨 |
영향 없음 (flush 문제) |
flush() or 트랜잭션 분리 |
| 동시에 같은 데이터 수정 |
효과 있음 (정합성 제어) |
SERIALIZABLE |
| Phantom Read 방지 |
효과 있음 |
REPEATABLE_READ |
Spring Boot 환경 DB별 트랜잭션 격리 수준
| DB |
기본 트랜잭션 레벨 |
지원 트랜잭션 레벨 |
트랜잭션 지원 여부 |
| MySQL |
REPEATABLE READ |
READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE |
✅ (InnoDB 기준) |
| PostgreSQL |
READ COMMITTED |
READ COMMITTED, REPEATABLE READ, SERIALIZABLE |
|
학습 정리
@Transactional은 매우 강력한 도구이지만, 동작 방식(AOP 프록시, flush 타이밍 등)을 명확히 이해해야 한다.
- 같은 트랜잭션 내에서는 save 직후의 find가 예상대로 동작하지 않을 수 있다.
- 트랜잭션 격리 수준은 동시성 문제나 정합성 제어에서 매우 유용하지만, flush 타이밍 이슈와는 별개이다.
- 트랜잭션 설계는 데이터 흐름, 호출 구조, 예외 상황을 모두 고려한 전략적 접근이 필요하다.