|

트랜잭션 경계란 무엇일까: 서비스 메서드가 길어지는 진짜 이유

트랜잭션 경계는 왜 설계를 바꿀까 대표 이미지
트랜잭션은 DB 옵션이 아니라 서비스 책임 분해 방식과 강하게 연결된다

트랜잭션 경계는 왜 설계를 바꿀까라는 질문은 보통 서비스 메서드가 점점 길어질 때 현실적으로 다가옵니다. 조회, 검증, 상태 변경, 저장, 외부 호출, 이벤트 발행이 한 메서드 안에 몰리기 시작하면 단순히 코드가 긴 문제가 아니라 책임이 어디까지 한 덩어리인지가 흔들리기 때문입니다.

즉 트랜잭션은 단순한 DB 기술 옵션이 아니라, 어디까지를 한 번에 성공하거나 실패하게 묶을 것인가라는 설계 결정에 가깝습니다.


왜 서비스 메서드가 길어질까

트랜잭션 안에서 같이 처리해야 할 일이 많아질수록 서비스 메서드는 자연스럽게 비대해집니다. 이때 개발자는 종종 “코드를 잘게 나누면 되지 않나?”라고 생각하지만, 실제 문제는 메서드 길이보다 경계 안에서 보장해야 하는 일이 너무 많다는 점입니다.

  • 검증과 계산이 함께 들어온다
  • 여러 엔티티 변경이 한 번에 묶인다
  • 저장 순서와 예외 처리까지 같이 고려해야 한다
  • 외부 시스템 호출을 어디에 둘지도 고민해야 한다

Martin Fowler의 Transaction Script 설명을 같이 보면, 요청 하나를 하나의 절차로 다루는 관점이 왜 서비스 메서드를 비대하게 만들 수 있는지 더 선명하게 보입니다.


트랜잭션 스크립트 감각으로 보면 왜 이해가 쉬울까

Martin Fowler가 설명한 Transaction Script는 각 요청을 하나의 절차로 다루는 구조입니다. 이 관점으로 보면 서비스 메서드가 길어지는 이유가 더 명확해집니다. 요청 하나가 곧 하나의 처리 시나리오이기 때문에, 관련 검증과 계산이 한곳에 모이기 쉽기 때문입니다.

문제는 이것이 항상 나쁘다는 뜻이 아니라, 경계 안에 무엇을 넣을지 분명하지 않으면 절차가 끝없이 비대해지기 쉽다는 데 있습니다.


설계가 흔들리는 순간은 언제일까

@Transactional
public void placeOrder(OrderRequest request) {
    validateCustomer(request.customerId());
    Product product = productRepository.findById(request.productId());
    product.decreaseStock(request.quantity());
    Order order = Order.create(request, product.price());
    orderRepository.save(order);
    paymentGateway.request(order);
    eventPublisher.publish(order);
}

이 코드는 얼핏 자연스러워 보이지만, paymentGateway 호출이 실패하면 어디까지 롤백할지, event 발행은 트랜잭션 안인지 밖인지, 재시도는 어디서 할지 같은 질문이 바로 따라옵니다. 즉 경계는 코드 구조를 끌어당깁니다.


그래서 무엇을 먼저 생각해야 할까

  1. 정말 함께 원자적으로 묶여야 하는 상태 변경은 무엇인가
  2. 외부 시스템 호출은 같은 경계 안에 둘 수 있는가
  3. 재시도와 보상 처리는 어디서 맡을 것인가
  4. 도메인 객체 책임과 서비스 절차 책임을 어디서 나눌 것인가

이 질문을 먼저 하지 않으면 서비스 메서드는 계속 자라고, 도메인 모델은 점점 빈혈 상태가 되기 쉽습니다.

이 흐름은 애그리거트 글, 빈혈 도메인 모델 글과 아주 강하게 연결됩니다.


마무리

트랜잭션 경계가 설계를 바꾸는 이유는, 무엇을 같이 성공시키고 무엇을 분리할지에 따라 서비스 절차, 객체 책임, 외부 호출 위치가 함께 달라지기 때문입니다.

즉 좋은 설계는 @Transactional을 어디에 붙일지보다, 어디까지를 하나의 일관성 단위로 볼 것인가를 먼저 분명히 하는 데서 시작합니다.

함께보면 좋은 글