실무를 하다가 한가지 작업에 여러개의 트랜잭션이 필요한 상황이 있다.
예를 들어보겠다.
현재 배치 파일을 만드는 상황이다.
배치 파일을 만들기 위해서는 파일 내부도 만들어야하고, 파일 서버에 업로드도 해야하고, 배치 파일이 insert 되었다는 기록도 해야한다.
큰 틀에서 보면 3가지 작업이 있다.
한번의 작업을 하는데 적어도 3개의 메소드가 필요하다.
그렇다면 @Transactional 어노테이션은 어디에 걸어두는게 적절할까? 라는 고민을 하였다.
아래 코드는 최상위의 코드이다.
@Slf4j
@Service
public class DailyTransactionService {
private final TransactionBatchService transactionBatchService;
public DailyTransactionService(TransactionBatchService transactionBatchService) {
this.transactionBatchService = transactionBatchService;
}
public void save(LocalDate date) {
transactionBatchService.makeSettlementFile(date);
}
}
배치를 돌리는 스케쥴러는 위 save 메소드를 호출한다.
즉 위 메소드가 Task 가 시작하는 최상위 Service 이다.
만약 최상단에서 @Transactional 을 건다면?
[질문]
@Transactional을 어디에 적용해야 하는지에 대한 고민.
- 최상위 메서드(save)에 @Transactional을 걸어야 하는지?
- 실제로 트랜잭션이 발생하는 메서드(createBatchExecutionHistory)에만 걸어야 하는지?
- 전체적으로 연관된 서비스라면 모두 @Transactional을 적용하는 게 맞을지?
[질문의도]
- 트랜잭션의 적용 위치에 따라 예상되는 동작을 이해하고 싶은 것.
- 서비스가 여러 개로 분리된 상황에서, 어떤 기준으로 @Transactional을 적용하는 것이 좋은지 알고 싶은 것.
[응답]
결론부터 말하면, 트랜잭션이 필요한 위치에만 적용하는 것이 좋다.
즉, 트랜잭션을 반드시 보장해야 하는 DB 조작 메서드 (createBatchExecutionHistory)에만 @Transactional을 적용하는 것이 적절하다.
이유 1: 트랜잭션 전파 (Propagation) 원칙을 고려해야 함
Spring의 기본 트랜잭션 전파 속성은 REQUIRED이다.
즉, @Transactional이 적용된 메서드가 실행되면, 기존 트랜잭션이 있으면 그 안에서 실행되고, 없으면 새로운 트랜잭션이 생성된다.
- dailyTransactionService.save(date)에 @Transactional을 적용하면, 해당 메서드에서 호출하는 모든 하위 메서드(makeSettlementFile)가 하나의 트랜잭션으로 묶일 가능성이 있다.
- 하지만, makeSettlementFile이 여러 개의 파일을 생성한 후 마지막에 createBatchExecutionHistory를 호출하여 DB에 저장하는 구조라면, 파일 생성이 실패해도 트랜잭션이 롤백될 위험이 있다.
이유 2: 트랜잭션 경계를 최소화하여 성능 최적화
- 트랜잭션은 데이터베이스 커넥션을 점유하고 있기 때문에 트랜잭션 범위가 길어지면 성능에 영향을 줄 수 있다.
- 현재 구조상, save() → makeSettlementFile() → createBatchExecutionHistory() 순서로 진행되는데, 파일을 생성하는 과정에서 트랜잭션이 유지되면 불필요하게 DB 락이 길어질 위험이 있다.
- 따라서 트랜잭션은 DB 조작이 이루어지는 최소 단위(createBatchExecutionHistory)에서만 관리하는 것이 좋다.
이유 3: 롤백 범위 최소화
- save()에서 @Transactional을 걸면, 만약 createBatchExecutionHistory()에서 예외가 발생할 경우, makeSettlementFile()까지 포함된 모든 작업이 롤백될 수 있다.
- 하지만 파일 생성은 롤백할 수 없는 작업이므로, 이를 포함한 트랜잭션이 롤백되는 것은 부적절할 수 있다.
- DB 작업이 실패하면 해당 작업만 롤백되도록, 트랜잭션은 createBatchExecutionHistory()에서만 유지하는 것이 안전하다.
그러면, 다른 곳에 @Transactional을 걸면 안 되나?
❌ 최상위 서비스 (save())에 @Transactional을 걸면 안 되는 이유
- save()에 @Transactional을 적용하면 파일 생성과 DB 저장이 하나의 트랜잭션으로 묶일 가능성이 있음
- 트랜잭션이 길어지면 파일 생성 중에도 트랜잭션이 유지되며, 불필요한 리소스 사용 증가
- createBatchExecutionHistory()에서 예외 발생 시 파일 생성까지 롤백될 위험
⭕ 최하위 서비스 (createBatchExecutionHistory())에 @Transactional을 적용하는 것이 가장 적절함
- 트랜잭션을 최소한의 범위로 유지
- DB 작업이 실패하면 해당 트랜잭션만 롤백됨 (파일 작업에는 영향 없음)
- 성능 최적화 가능
추가적으로 고려할 점
- 비동기(@Async)를 사용할 경우
- @Transactional은 같은 클래스 내의 메서드 호출 시 적용되지 않는다.
- @Async와 @Transactional을 함께 사용할 경우, 별도의 트랜잭션을 관리해야 한다.
- 트랜잭션 전파 옵션 변경 가능성
- 만약 createBatchExecutionHistory()에서 기존 트랜잭션이 아닌 새로운 트랜잭션을 무조건 시작해야 한다면,
java복사편집@Transactional(propagation = Propagation.REQUIRES_NEW)
- REQUIRES_NEW는 기존 트랜잭션과 독립적인 트랜잭션을 시작하므로, 파일 생성과 무관하게 DB 작업을 개별적으로 처리 가능
- 만약 createBatchExecutionHistory()에서 기존 트랜잭션이 아닌 새로운 트랜잭션을 무조건 시작해야 한다면,
- 트랜잭션 read-only 설정
- 만약 makeSettlementFile()이 단순 조회만 수행한다면, @Transactional(readOnly = true)를 적용하면 성능 최적화 가능
결론 정리
✅ 최상위 메서드 (save())에는 @Transactional을 적용하지 않는다.
✅ 트랜잭션이 필요한 DB 조작 메서드 (createBatchExecutionHistory())에만 @Transactional을 적용한다.
✅ 파일 생성과 트랜잭션을 분리하여, 트랜잭션 롤백 범위를 최소화한다.
✅ 필요하면 Propagation.REQUIRES_NEW를 고려하여, 기존 트랜잭션과 분리할 수 있다.
이렇게 적용하면 트랜잭션 경계를 명확히 유지하면서도 성능을 최적화하고, 예상치 못한 롤백을 방지할 수 있다. 🚀