본문 바로가기
개발 이야기

도르마무! (대충 트랜젝션 롤백 한다는 의미)

by 황인태(intaehwang) 2025. 3. 23.
반응형

 

트랜잭션은 DB만의 것일까?

많은 개발자들이 트랜잭션이라는 단어를 들으면 가장 먼저 떠올리는 게 있습니다.
바로 데이터베이스의 커밋(commit)과 롤백(rollback) 입니다.

그런데 정말 트랜잭션은 DB에서만 필요한 걸까요?

실제 서비스를 운영하다 보면 단순히 DB 쿼리 하나만 처리하는 경우는 드뭅니다.
보통은 여러 개의 작업이 순차적으로 이어지거나 서로 의존 관계를 갖고 실행됩니다.

그렇기 때문에 오늘은 service layer의 트랜젝션(a.k.a. @Transactional)에 대해 알아보겠습니다.

처음 백엔드 개발을 하는 신입 개발자들은 다음같이 코드를 작성할 수 있습니다.습니다.

예시 1)

상품 주문을 하려면

  1. 상품 재고가 줄고
  2. 내 계좌에서 돈이 빠지고
  3. 주문 정보가 저장되고
  4. 배송 준비가 시작됨

어? 그러다가 중간에 계좌에 돈이 없으면 어떡하지?
아! 트랜젝션 롤백하면 되겠구나!
그럼 파사드를 만들어서 여러 서비스들을 호출해야겠다.

그런데 정말 이렇게 하는게 최선일까요?

시니어 피드백
  • OrdersFacade 코드가 너무 길어서 이해하기가 어려워요..ㅠㅋㅋ
  • 트랜잭션의 범위를 조금 크게 설정하면 코드가 좀 더 나아질까요? ㅎㅎ


최악의 상황

service의 @Transactional은 Exception이 발생했을 경우 롤백을 하기 때문에
4가지 작업이 전부 성공해야 진짜 주문/결제 성공이라고 할 수 있습니다.

그렇기 때문에 예상되는 문제가 발생할 수 있는데요. 여러개의 service가 한 트랜젝션에서 호출되었을 때 가장 큰 문제점은 다음과 같습니다.

핵심 서비스 성능 저하

모든 서비스를 묶어서 호출하면 특정 service에서

병목(Service3) 이 생겨 전체 service가 느려지거나
실패(Service5) 할 수 있습니다.

이것은 결론적으로 서비스가 작동하지 않아 사용자 경험에 안좋은 영향을 미칩니다.

안되잖아? 에이 안써

길어지는 코드 베이스

주문/결제에 기능 추가를 해야하는데 오우... OrdersFacade 코드 너무 긴데? 어떻게 수정해야하지..?

하기 싫다.

어떻게 해결할 수 있을까?

EVENT를 발행하자

JAVA에서는 ApplicationEventPublisher를 사용하여 이벤트를 발행하고
@TransactionalEventListener를 사용하여 트랜젝션을 분리할 수 있습니다.

이렇게 했을 경우 관심사를 확실히 분리할 수 있다는 이점이 생깁니다.

Transaction commit을 기점으로 commit 이전에는 무엇을 추가할지,
commit 이후의 작업으로는 무엇을 할 수 있을지 고민할 수 있게 되었습니다.

예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Getter
public class OrderSuccessEvent {
    private final String traceId;
    private final Long userId;
    private final Long orderId;
    private final List<OrderItemInfo> orderItemInfos;
 
    public OrderSuccessEvent(String traceId, Long userId, Long orderId, List<OrderItemInfo> orderItemInfos) {
        this.traceId = traceId;
        this.userId = userId;
        this.orderId = orderId;
        this.orderItemInfos = orderItemInfos;
    }
}
 
@Component
@RequiredArgsConstructor
public class OrdersEventPublisher {
private final ApplicationEventPublisher eventPublisher;
 
    public void success(OrderingSuccessEvent event) {
        eventPublisher.publishEvent(event);
    }
    
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void orderInitEventSuccessHandler(OrderPaySuccessEvent event) {
        // ...
    }
 
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void orderPayEventKafkaPublish(OrderPaySuccessEvent event) {
        // ...
    }
}
cs

보상 트랜젝션

트랜젝션이 분리가 되었을 때 문제가 되는 부분이

어떻게 보상을 할지에 대한 고민이다.

이때 가장 중요한 부분은 '이벤트를 어떻게 추적할것인가?' 이다.

동일한 이벤트임을 확인할 수 있는것은 UUID를 통해 추적 가능하다.

예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
public class OrderFailEvent {
    private final String traceId;
    // ..
}
@Component
@RequiredArgsConstructor
public class OrdersEventPublisher {
private final ApplicationEventPublisher eventPublisher;
    // ...
    
    public void fail(OrderFailEvent event) {
        eventPublisher.publishEvent(event);
    }
 
    // ...
 
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void orderInitEventSuccessHandler(OrderFailEvent event) {
        // 보상 트랜젝션을 위한 코드
    }
}
cs

마치며

시니어 피드백
  • 저도 이벤트를 통해 서비스간 책임 분리를 잘 격리하는게 중요한 포인트라고 생각하고, 트랜잭션을 관리하는 장점도 크다고 생각해요.
반응형
Buy me a coffeeBuy me a coffee

댓글