백엔드 면접 용어 사전
Spring·JPApropagation

트랜잭션 전파

이미 트랜잭션이 있을 때 어떻게 처리할지 정하는 속성. REQUIRED가 기본이고 REQUIRES_NEW는 별도 트랜잭션을 만든다.

이미 트랜잭션이 진행 중일 때 새 트랜잭션 메서드를 만나면 어떻게 할지 정하는 규칙 (propagation).

왜 필요한가

@Transactional
public void order() {          // 트랜잭션 A 시작
    saveOrder();
    sendNotification();        // ← 이 안에도 @Transactional 이 있다면?
}                              //   A에 참여? 따로 시작? 무시?

이 질문에 답하는 것이 전파 속성이다.

주요 옵션

옵션진행 중인 트랜잭션이 있으면없으면
REQUIRED (기본)참여새로 시작
REQUIRES_NEW일시 중단하고 새로 시작새로 시작
SUPPORTS참여트랜잭션 없이 실행
NOT_SUPPORTED일시 중단하고 트랜잭션 없이트랜잭션 없이
MANDATORY참여예외
NEVER예외트랜잭션 없이
NESTED세이브포인트 생성새로 시작

실무에서 쓰는 것은 사실상 REQUIRED와 REQUIRES_NEW 둘이다.

REQUIRED — 하나로 묶인다

order()  ─── 트랜잭션 A 시작
  └─ saveOrder()      A에 참여
  └─ sendNotification() A에 참여
                       모두 A. 하나라도 실패하면 전부 롤백

물리 트랜잭션은 하나, 논리 트랜잭션은 여럿이라는 구조다. 가장 바깥이 커밋을 결정한다.

가장 헷갈리는 지점 — 내부에서 예외를 잡아도 롤백된다

@Transactional
public void outer() {
    try {
        inner();                    // 여기서 예외 → 롤백 표시(rollback-only)
    } catch (Exception e) {
        log.warn("무시하고 계속");     // 잡았으니 괜찮다고 생각하지만
    }
    save();                          // 저장한 것 같지만
}   // 커밋 시점: UnexpectedRollbackException — 전부 롤백된다  ✗

왜 그런가 — 같은 물리 트랜잭션이므로, 안쪽에서 실패하면 트랜잭션 전체에 rollback-only 플래그가 찍힌다. 바깥에서 예외를 삼켜도 이 플래그는 지워지지 않는다.

"일부만 실패해도 계속 진행"이 필요하면 REQUIRES_NEW를 써야 한다.

REQUIRES_NEW — 완전히 분리된다

order()  ─── 트랜잭션 A
  └─ writeAuditLog()  ─── A를 잠시 멈추고 트랜잭션 B 시작
                          B 커밋 (독립적)
  └─ A 계속 → A가 롤백돼도 B는 남는다

용도 — 감사 로그, 실패 이력처럼 본 작업이 롤백돼도 남아야 하는 기록.

대가

  • 커넥션을 2개 동시에 점유한다 (A는 멈춘 채로 유지)
  • 커넥션 풀 크기보다 중첩이 깊어지면 데드락
  • 풀 크기 10인데 10개 요청이 각각 REQUIRES_NEW 를 부르면
    • 전부 두 번째 커넥션을 기다리며 멈춘다 ★

★ 이것이 실무에서 실제로 발생하는 사고다. REQUIRES_NEW를 남발하면 안 된다.

NESTED — 세이브포인트

  • A 트랜잭션 안에서 세이브포인트를 만든다
  • 안쪽이 실패하면 세이브포인트까지만 롤백, 바깥은 계속
  • 바깥이 롤백되면 안쪽도 함께 롤백된다 (REQUIRES_NEW와 다른 점) JDBC 세이브포인트를 지원해야 하며, JPA에서는 잘 쓰이지 않는다.

롤백 규칙 — 또 하나의 함정

@Transactional
public void save() throws IOException {
    repo.save(entity);
    throw new IOException();      // checked 예외 → 롤백되지 않는다!
}

기본값은 unchecked(RuntimeException, Error)에만 롤백한다.

@Transactional(rollbackFor = Exception.class)   // 명시해야 한다

"예외가 났는데 데이터가 저장돼 있다"의 원인 대부분이 이것이다.

readOnly

@Transactional(readOnly = true)
  • 하이버네이트가 스냅샷을 만들지 않아 더티 체킹을 건너뛴다 → 메모리·성능 이득
  • JDBC 드라이버·DB에 읽기 전용 힌트 전달
  • 읽기 전용 복제본으로 라우팅하는 근거로 쓸 수 있다

조회 전용 서비스에는 붙이는 것이 좋다.

프록시 기반이라는 전제

AOP 프록시로 동작하므로 자기 호출에는 전파가 적용되지 않는다.

@Transactional
public void outer() { this.inner(); }       // inner의 @Transactional 무시

@Transactional(propagation = REQUIRES_NEW)
public void inner() { }                     // 그냥 outer의 트랜잭션에서 실행

클래스를 분리해야 한다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Spring·JPA — IoC/DI·AOP·영속성 컨텍스트·N+1