전체 목록
SpringMedium#71

@Transactional의 동작 원리와 주요 속성(propagation, isolation, rollbackFor)을 설명해주세요.

#Spring#트랜잭션#@Transactional#DB
힌트

프록시 기반 AOP로 동작하며, 전파 레벨과 격리 수준을 설정할 수 있습니다.

정답 및 해설

@Transactional의 동작 원리와 주요 속성(propagation, isolation, rollbackFor)을 설명해주세요.

@Transactional은 Spring에서 선언적으로 트랜잭션을 관리하는 어노테이션입니다. 개발자가 트랜잭션 시작/커밋/롤백 코드를 직접 작성하지 않아도, Spring AOP 프록시가 이를 자동으로 처리합니다.

동작 원리: AOP 프록시

@Transactional은 Spring AOP를 기반으로 동작합니다. 빈을 주입받으면 실제 객체가 아닌 프록시 객체를 받게 되며, 이 프록시가 메서드 호출을 가로채 트랜잭션을 제어합니다.

호출자
  │
  ▼
프록시 객체 (트랜잭션 시작)
  │
  ▼
실제 서비스 메서드 (비즈니스 로직)
  │
  ├─ 정상 종료 → 트랜잭션 커밋
  └─ 예외 발생 → 트랜잭션 롤백
// 내부적으로 동작하는 방식 (의사 코드)
public Object invoke(MethodInvocation invocation) throws Throwable {
    TransactionStatus status = transactionManager.getTransaction(transactionAttribute);
    try {
        Object result = invocation.proceed(); // 실제 메서드 실행
        transactionManager.commit(status);
        return result;
    } catch (RuntimeException | Error ex) {
        transactionManager.rollback(status);
        throw ex;
    }
}

propagation (전파 레벨)

트랜잭션이 이미 존재할 때 새 트랜잭션을 어떻게 처리할지 결정합니다.

주요 전파 레벨

옵션기존 트랜잭션 있을 때기존 트랜잭션 없을 때
REQUIRED (기본값)기존 트랜잭션에 참여새 트랜잭션 생성
REQUIRES_NEW기존 트랜잭션 일시 중단 후 새 트랜잭션 생성새 트랜잭션 생성
SUPPORTS기존 트랜잭션에 참여트랜잭션 없이 실행
NOT_SUPPORTED기존 트랜잭션 일시 중단 후 비트랜잭션 실행트랜잭션 없이 실행
MANDATORY기존 트랜잭션에 참여예외 발생
NEVER예외 발생트랜잭션 없이 실행
NESTED중첩 트랜잭션(Savepoint) 생성새 트랜잭션 생성
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final AuditLogService auditLogService;

    // REQUIRED: 기본값. 기존 트랜잭션이 있으면 참여
    @Transactional
    public void createOrder(OrderRequest request) {
        Order order = orderRepository.save(new Order(request));

        // REQUIRES_NEW: 항상 별도 트랜잭션으로 실행
        // createOrder가 롤백되어도 audit 로그는 남음
        auditLogService.log("ORDER_CREATED", order.getId());
    }
}

@Service
public class AuditLogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String action, Long targetId) {
        // 독립적인 트랜잭션으로 실행
        auditLogRepository.save(new AuditLog(action, targetId));
    }
}

NESTED vs REQUIRES_NEW

@Transactional
public void parentMethod() {
    // REQUIRES_NEW: 완전히 독립된 트랜잭션
    // 부모 롤백 → 자식은 영향 없음, 자식 롤백 → 부모도 영향 없음

    // NESTED: Savepoint 기반 중첩 트랜잭션
    // 부모 롤백 → 자식도 롤백, 자식 롤백 → 부모는 Savepoint로 복구 가능
    childMethod();
}

isolation (격리 수준)

여러 트랜잭션이 동시에 실행될 때 서로 어느 정도 격리할지 결정합니다. 격리 수준이 높을수록 동시성은 낮아지고 데이터 정합성은 높아집니다.

격리 수준과 발생 가능한 문제

격리 수준Dirty ReadNon-Repeatable ReadPhantom Read성능
READ_UNCOMMITTED발생발생발생가장 높음
READ_COMMITTED방지발생발생높음
REPEATABLE_READ방지방지발생보통
SERIALIZABLE방지방지방지가장 낮음

Dirty Read: 커밋되지 않은 데이터를 읽는 문제 Non-Repeatable Read: 같은 쿼리를 두 번 실행했을 때 결과가 다른 문제 (다른 트랜잭션의 UPDATE/DELETE) Phantom Read: 범위 쿼리를 두 번 실행했을 때 행 수가 달라지는 문제 (다른 트랜잭션의 INSERT)

@Transactional(isolation = Isolation.READ_COMMITTED)
public UserBalance getBalance(Long userId) {
    // 커밋된 데이터만 읽음 (대부분의 경우 적합)
    return balanceRepository.findById(userId).orElseThrow();
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
    // 같은 트랜잭션 내에서 동일 행을 읽으면 항상 같은 값 보장
    UserBalance from = balanceRepository.findById(fromId).orElseThrow();
    UserBalance to = balanceRepository.findById(toId).orElseThrow();
    from.withdraw(amount);
    to.deposit(amount);
}

MySQL InnoDB의 기본 격리 수준은 REPEATABLE_READ이며 Phantom Read를 MVCC로 대부분 방지합니다. PostgreSQL, Oracle의 기본 격리 수준은 READ_COMMITTED입니다.

rollbackFor

기본적으로 Spring은 **unchecked 예외(RuntimeException, Error)**만 롤백합니다. checked 예외는 롤백하지 않습니다.

// 기본 동작
@Transactional
public void defaultBehavior() throws Exception {
    // RuntimeException → 롤백 O
    // Exception(checked) → 롤백 X (커밋됨!)
}

// 모든 예외에서 롤백
@Transactional(rollbackFor = Exception.class)
public void rollbackOnAllExceptions() throws Exception {
    // 모든 예외에서 롤백
}

// 특정 예외에서만 롤백
@Transactional(rollbackFor = {DataIntegrityException.class, IOException.class})
public void rollbackOnSpecificExceptions() throws IOException {
    orderRepository.save(order);
    fileService.writeReceipt(order); // IOException 발생 시 롤백
}

// 특정 예외는 롤백하지 않음
@Transactional(noRollbackFor = OptimisticLockException.class)
public void noRollbackForOptimisticLock() {
    // OptimisticLockException 발생해도 롤백하지 않음
}

기타 주요 속성

@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    rollbackFor = Exception.class,
    timeout = 30,           // 30초 초과 시 롤백
    readOnly = true         // 읽기 전용 최적화 (쓰기 불가, 성능 향상)
)
public List<Order> getOrders() {
    return orderRepository.findAll();
}

readOnly = true를 사용하면:

  • Dirty Checking(변경 감지) 비활성화로 성능 향상
  • 영속성 컨텍스트의 스냅샷 저장 생략
  • 일부 데이터베이스 드라이버에서 읽기 전용 최적화 적용

Self-Invocation 주의사항

같은 클래스 내에서 메서드를 호출하면 프록시를 거치지 않아 @Transactional이 적용되지 않습니다.

@Service
public class UserService {

    @Transactional
    public void createUser(UserRequest request) {
        userRepository.save(new User(request));
        // 내부 호출 — @Transactional 미적용!
        this.sendWelcomeEmail(request.getEmail());
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendWelcomeEmail(String email) {
        // 별도 트랜잭션으로 실행되지 않음
        emailRepository.save(new EmailLog(email));
    }
}

해결 방법:

@Service
@RequiredArgsConstructor
public class UserService {

    private final EmailService emailService; // 별도 클래스로 분리

    @Transactional
    public void createUser(UserRequest request) {
        userRepository.save(new User(request));
        emailService.sendWelcomeEmail(request.getEmail()); // 외부 빈 호출
    }
}

@Service
public class EmailService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendWelcomeEmail(String email) {
        emailRepository.save(new EmailLog(email)); // 올바르게 별도 트랜잭션 실행
    }
}

정리

속성주요 옵션기본값
propagationREQUIRED, REQUIRES_NEW, NESTEDREQUIRED
isolationREAD_COMMITTED, REPEATABLE_READ, SERIALIZABLEDEFAULT(DB 설정 따름)
rollbackForException.class 등RuntimeException, Error
readOnlytrue / falsefalse
timeout초 단위 정수-1(무제한)