본문 바로가기
Java & Spring

@Transactional 전파/고립수준 이해

by yamoojin83 2025. 10. 3.

@Transactional 전파/고립수준 이해

스프링 트랜잭션은 기본만 알아도 충분할 것 같지만, 실제로는 전파(Propagation)고립수준(Isolation)을 모르면 장애가 납니다. 본문은 자주 틀리는 경우를 중심으로 “왜” 그런지와 “어떻게” 해결하는지 코드를 통해 설명합니다.

전파(Propagation) 핵심만 정리

  • REQUIRED(기본): 현재 트랜잭션이 있으면 합류, 없으면 새로 시작
  • REQUIRES_NEW: 무조건 새 트랜잭션 시작(기존은 일시 정지)
  • SUPPORTS/NOT_SUPPORTED: 있어도/없어도 수행, 또는 트랜잭션 없이 수행
  • MANDATORY/NEVER: 반드시 필요/있으면 안 됨
  • NESTED: 동일 트랜잭션 내 세이브포인트(JDBC/드라이버 지원 필요)

예시: 알림 실패는 롤백시키지 않기


@Service
@RequiredArgsConstructor
class OrderService {
  private final PaymentService payment;
  private final NotificationService notify;

  @Transactional
  public void place(OrderReq req) {
    payment.pay(req); // REQUIRED
    notify.asyncNotify(req); // 실패해도 주문은 커밋
  }
}

@Service
class NotificationService {
  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void asyncNotify(OrderReq req) {
    // 외부 API 호출
  }
}

REQUIRES_NEW로 별도 트랜잭션을 만들면, 알림 실패가 주문 트랜잭션을 더럽히지 않습니다.

고립수준(Isolation)

  • DEFAULT: DB 기본(대부분 READ_COMMITTED)
  • READ_COMMITTED: 커밋 데이터만 읽음(더티리드 방지)
  • REPEATABLE_READ: 같은 트랜잭션에서 같은 행은 같은 값(팬텀리드는 DB마다 다름)
  • SERIALIZABLE: 완전 직렬화(성능 비용 큼)

@Transactional(isolation = Isolation.REPEATABLE_READ)
public Order load(Long id) { ... }

롤백 규칙과 흔한 오해

  • 런타임 예외에서만 자동 롤백. 체크 예외는 rollbackFor로 지정.
  • 예외를 잡고 넘어가면 롤백되지 않습니다. 로깅만 하고 던질지 결정하세요.
  • 자기 호출(self-invocation)은 프록시를 우회 → @Transactional 미적용. 구조를 바꾸거나, 내부 호출도 인터페이스/별도 빈으로 분리.

NESTED를 사용할 때


@Transactional
public void importAll() {
  for (Row row : rows) {
    try {
      importOne(row); // 실패해도 다음 row 계속
    } catch (Exception e) { log.warn("row fail", e); }
  }
}

@Transactional(propagation = Propagation.NESTED)
public void importOne(Row row) { ... }

NESTED는 세이브포인트가 필요한 배치 시 유용합니다(드라이버/TxManager 지원 필요).

읽기 전용과 flush


@Transactional(readOnly = true)
public List<Order> search(...) { ... }

readOnly는 힌트를 통해 쓰기 지연/더티 체킹을 최적화합니다. 검색 전용 서비스에 기본으로 설정하세요.

테스트 팁

  • 전파/고립 조합은 통합 테스트로 확인(두 서비스 간 호출, 예외 전파 등).
  • Tx 로그(org.springframework.transaction)를 DEBUG로 켜고 경계 확인.

FAQ

Q. REQUIRES_NEW 남용하면?
A. 커넥션을 하나 더 점유하고, 전체 일관성이 깨질 수 있습니다. 코어 로직에는 사용을 자제.

Q. 글로벌 readOnly?
A. 조회 전용 모듈이면 AOP/프록시로 기본 readOnly 적용을 고려하세요.

 

 

👉 1편: 연관관계 주인/지연로딩 N+1 체크리스트

👉 2편: 메서드명 쿼리 vs @Query vs QueryDSL 비교

👉 3편: @Transactional 전파/고립수준 이해

👉 4편: 페이징 성능: 카운트 최적화·키셋 페이징