프로젝트 상담

INSIGHTS · Technology

Spring 트랜잭션 전파 옵션 오남용으로 인한 데이터 불일치 실무 사례

BLICT · 2026. 9. 21. · 수정 2026. 9. 21.

트랜잭션 전파 옵션을 잘못 선택해 데이터 불일치가 발생한 실제 장애 사례를 통해, 서비스 계층 설계 시 옵션별 적용 기준을 명확히 판단할 수 있습니다.

트랜잭션 전파 옵션, 어디서부터 막히는가?

Spring Boot의 트랜잭션 전파(Propagation) 옵션은 Service 계층에서 복합업무를 구현할 때 자주 맞닥뜨리는 설계 포인트다. 하지만 실무에선 REQUIRED, REQUIRES_NEW, NESTED 등 옵션의 의미를 정확히 이해하지 못한 채, 단순히 “트랜잭션 분리” 혹은 “이중 커밋 방지”와 같은 키워드만으로 기능을 붙이는 경우가 많다. 이런 오남용은 곧바로 데이터 정합성 문제로 이어진다.

실제 사례로, 여러 서비스가 한 트랜잭션 내에서 실행될 때 하위 서비스 중 하나에서 예외가 발생해도 일부 데이터가 커밋되거나, 반대로 전부 롤백되는 것이 맞는 상황에서 일부만 롤백되어 데이터 불일치가 발생하는 장애가 있었다. 이때 막히는 지점은 “이 서비스가 트랜잭션의 경계를 직접 가져가야 하는가?” 또는 “기존 트랜잭션에 참여해야 하는가?”를 명확히 구분하지 못하는데 있다.

이런 혼란을 방지하려면 다음의 순서로 점검해야 한다.

  1. 해당 서비스 메서드가 단일 유스케이스 전체의 ACID를 보장해야 하는가?
  2. 하위 서비스 호출 중 일부 실패 시 어떤 데이터가 남아도 괜찮은가?
  3. 외부 시스템 연동, 비동기 메시지, 이벤트 발행 등 트랜잭션 경계 밖에서 일어나야 할 작업이 있는가?
  4. 서비스별로 전파 옵션을 명확하게 주석과 함께 남겼는가?

실무 장애 사례: 주문-포인트 동시 처리에서의 데이터 불일치

블릭트의 한 프로젝트에서는 주문(Order) 처리와 포인트 적립(Point Accrual)을 한 API 트랜잭션에서 처리하는 구조였다. Service 계층에서는 아래와 같은 호출 구조를 갖는다.

  • OrderService.createOrder()
    • 주문 데이터 저장 (DB)
    • PointService.addPoints() 호출

처음에는 두 서비스 모두 @Transactional(propagation = Propagation.REQUIRED) 옵션을 사용했다. 하지만 포인트 시스템 연동 중 외부 API 장애가 발생했을 때, 주문은 정상적으로 저장되고 포인트만 누락되는 문제가 발생했다. 반대로 포인트 적립 트랜잭션만 따로 REQUIRES_NEW로 바꿨더니, 주문이 롤백될 상황(예: 결제 실패)에서도 포인트는 적립되어버리는 데이터 불일치가 발생했다.

이 장애의 본질은 트랜잭션 전파 옵션을 서비스 간 독립성 관점에서만 판단하고, 유스케이스 전체의 정합성 보장이라는 관점에서 판단하지 않았던 데 있다. 주문과 포인트 적립이 논리적으로 하나의 작업 단위라면, 반드시 한 트랜잭션에 묶여야 하고, 둘 중 하나라도 실패하면 전체를 롤백해야 한다. 반대로, 포인트 적립은 외부 연동이나 별도의 보상 트랜잭션으로 처리할 수 있다면 분리해야 한다.

실제 장애를 복구할 때는 DB 백업 데이터를 바탕으로 주문과 포인트 테이블을 수동으로 맞췄고, 이후 서비스 설계시 전파 옵션을 명확히 명시하는 표준을 마련했다.

전파 옵션별 적용 기준과 실전 설계 포인트

Spring에서 지원하는 전파 옵션은 총 7가지이나, 실무에서 가장 많이 쓰이는 것은 REQUIRED, REQUIRES_NEW, NESTED다. 각각의 적용 기준을 다음과 같이 정리할 수 있다.

  1. REQUIRED
    기본값으로, 이미 트랜잭션이 있으면 그 안에 참여하고 없으면 새 트랜잭션을 만든다. 유스케이스 내 모든 동작이 성공-실패를 같이 해야 할 때 적합하다.

  2. REQUIRES_NEW
    항상 새로운 트랜잭션을 만든다. 하위 작업이 상위 트랜잭션과 별개로 커밋/롤백되어야 하거나, 상위 트랜잭션의 롤백에도 하위 작업을 반드시 보존해야 할 때 사용한다.
    예) 감사 로그 기록, 외부 시스템에 알림 등.

  3. NESTED
    상위 트랜잭션이 있으면 savepoint를 만들어 부분 롤백이 가능하다. 단, 데이터베이스가 savepoint를 지원해야 하며, 실무에서는 잘못 사용하면 전체 트랜잭션 정합성에 혼란을 유발할 수 있다.

실제 설계시 가장 많이 막히는 지점은 “이 서비스가 상위 트랜잭션의 실패에도 불구하고 반드시 성공해야 하나?”이다. 예를 들어 포인트 적립이 주업무와 완전히 분리되어야 한다면 REQUIRES_NEW가 맞지만, 일반적인 주문-포인트 동시 처리라면 REQUIRED로 묶는 것이 맞다.

아래는 실무에서 자주 등장하는 패턴을 정리한 코드 예시다.

// 주문 생성과 포인트 적립이 반드시 같이 가야 할 때
@Service
public class OrderService {
    @Transactional
    public void createOrder(OrderDto orderDto) {
        saveOrder(orderDto);
        pointService.addPoints(orderDto.getUserId(), orderDto.getAmount());
    }
}

@Service
public class PointService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void addPoints(Long userId, int amount) {
        // 포인트 적립 로직
    }
}

만약 포인트 적립을 반드시 별도로 보존해야 한다면 REQUIRES_NEW를 검토할 수 있다. 하지만 이때도 주문 트랜잭션의 롤백과 포인트 적립의 분리 사이에 어떤 업무상 인과관계가 있는지 반드시 명확히 해야 한다.

전파 옵션 설계에서 반드시 거쳐야 하는 점검 순서

실제 설계 단계에서 트랜잭션 전파 옵션을 결정할 때는 다음과 같은 순서로 점검해야 예기치 않은 데이터 불일치나 장애를 막을 수 있다.

  1. 업무 단위(Usecase) 경계 확정
    먼저, 각 서비스 메서드가 담당하는 업무 단위가 논리적으로 어디까지인지 명확히 정의한다.
    예를 들어 “주문-결제-포인트”가 묶여야 하는지, “주문”만 단독으로 처리해도 되는지 결정한다.

  2. 실패 시 일관성 요구사항 확인
    각 하위 서비스가 실패할 때 전체 롤백이 필요한지, 일부만 롤백해도 업무상 무방한지 파악한다.
    예를 들어, 주문은 반드시 결제와 같이 롤백돼야 한다면 한 트랜잭션에 묶는다.

  3. 외부 연동/비동기 처리 여부 판단
    하위 서비스가 외부 시스템과 연동되거나 비동기 메시지를 발행해야 한다면, 트랜잭션 내외부 경계를 명확히 분리한다.

  4. 트랜잭션 전파 옵션 선택 및 명확한 주석
    각 서비스에 어떤 전파 옵션을 쓸지 결정하고, 왜 그렇게 했는지 이유를 코드 주석으로 남긴다.

  5. 통합 테스트 케이스 작성
    정상 처리와 예외 상황(하위 서비스 실패, 외부 연동 장애 등)에서 트랜잭션이 올바르게 동작하는지 실제 DB 상태를 검증하는 통합 테스트를 반드시 작성한다.

이 순서를 거치면, 서비스가 트랜잭션 경계 안에 있어야 할지, 분리해야 할지, 옵션을 무엇으로 설정해야 할지 명확히 판단할 수 있다. 또한 코드 리뷰와 장애 대응시 “왜 이렇게 설계했는가”를 명확히 소통할 수 있다.

트랜잭션 전파 옵션 적용 체크리스트

  • 전파 옵션 선택 시 “실패 시 전체 롤백이 필요한가?”를 가장 먼저 점검한다.
  • 외부 시스템 연동, 이벤트 발행 등은 트랜잭션 경계 밖에서 처리할 수 있는지 검토한다.
  • 서비스 계층마다 전파 옵션과 그 이유(업무상 근거)를 코드 주석으로 남긴다.
  • 통합 테스트를 통해 정상/예외 케이스별 데이터 일관성을 반드시 검증한다.
  • 장애 발생 시, 각 트랜잭션 경계별로 실제 커밋/롤백 내역을 추적해 원인 분석 체계를 마련한다.

관련 글

Spring 트랜잭션 전파 옵션 오남용으로 인한 데이터 불일치 실무 사례 | BLICT