Transaction outbox 패턴

김성지·2023년 7월 22일
0

그냥공부

목록 보기
10/10

배경

분산환경에서
Command성 서비스는(create/update/delete) aggregates 들로 묶이는 경우가 많다.
이때 saga에 참여하는 것들은 하나의 트랜잭션 단위로 묶여서 데이터의 정합성을 보장해야 하는 경우가 많다.

전통적으로 two phase commit을 사용했는데..
문제점이 많다(문제점)

그래서 요즘은 domain event가 발생할 때마다
message를 발행하는데 이는 하나의 aggregate에서 여러번 발행된다.
즉 한 aggregate에 포함되는 특정 서비스마다 제어할 수 있는 transaction의 scope를 벗어나게 된다.

각 service마다 기존 트랜잭션(db트랜잭션)의 scope를 벗어나 발행한 event(메시지)로 서비스간 통신하게 되면 여러 문제가 발생한다.

문제 상황 1

seller가 상품을 등록 후
상품 등록 이벤트를 발생시켜
Search DB에 저장하는 가장 기본적인 상황이다.

public class MarketService {

    private final IMarketRepository repository;

    ...

    @Transactional
    public Product registerProduct(RegisterProduct command) {
        validate(command);
        var product = Product.create(command);
        repository.save(product);
        return product;
    }
}
@RestController
public class MarketController {

    private final MarketService service;
    private final IMessagePublisher publisher;

    ...

    @PostMapping(...)
    public void registerProduct(
        @RequestBody RegisterProduct command
    ) {
        Product product = service.registerProduct(command);
        var message = ProductRegistered.from(product);
        publisher.publish(message);
    }
}

위 방식의 문제점은 발행되어야 하는 메시지가 발행되지 않을 수도 있다.
registerProduct 함수가 정상적으로 종료된 후
publisher.publish(message) 가 실패하면
Market DB에는 정상적으로 영속화 되었지만
Search DB에는 해당 정보가 들어있지 않게된다.

이벤트를 발행하는 곳에서 retry Handler를 도입하는 방법이 떠오르지만
외부 메시지 브로커 시스템의 장애가 원인이면 문제를 완벽히 방지할 수 없게된다.
이에 따라 외부 시스템(브로커) 장애가 내부시스템(Market Api Server)에 까지 크게 영향을 미칠 수 있다.

문제 상황 2

1번 상황을 개선하고자 다음과 같은 설계를 적용해보는 상황이다.
의도는 메시지 발행이 실패하면 상품 등록을 실패시키려고 하는 것이다.

@RestController
public class MarketController {

    private final MarketService service;

    ...

    @PostMapping(...)
    public void registerProduct(
        @RequestBody RegisterProduct command
    ) {
        service.registerProduct(command);
    }
}
public class MarketService {

    private final IMarketRepository repository;
    private final IMessagePublisher publisher;

    ...

    @Transactional
    public void registerProduct(RegisterProduct command) {
        validate(command);
        var product = Product.create(command);
        repository.save(product);
        var message = ProductRegistered.from(product);
        publisher.publish(message);
    }
}

publisher.publish(message) 가 실패하면
registerProduct 도 실패하게끔 의도한 것 같다.

하지만 registerProduct 는 해당 코드블럭을 완전히 수행한 후에야
commit을 하게되어.. 발행되지 않아야 하는 메시지가 발행될 수도 있다.

그 외 문제들

많다.,..
전부 @Transactional 에 message 발행 로직이 껴있어서 발생하는 문제이다.

해결할 수 있는 다양한 방법들이 있는데
이 중 outbox를 활용한 방법을 알아보려고 한다.

Transactional outbox 패턴

해당패턴을 나의 생각으로 한마디로 정리하자면
발행해야 하는 메시지를 outbox에 저장한다
즉 레이어(여기서는 db)를 하나 더 둔 것이다.


(Outbox가 추가되었다.)

Market API Server 에서 생성되는 transaction은
두 가지의 데이터를 하나의 트랜잭션에서 관리하게된다.
1. Market DB에 대한 정보
2. Outbox DB(발행해야 하는 메시지)에 대한 정보

즉 해당 도메인에 관한 데이터와
해당 도메인이 발행해야 하는 이벤트를
하나의 트랜잭션으로 묶어버린거다.

이를 적용하게되면
Market DB에 관한 데이터의 영속화와 그에 따른 이벤트가 하나로 묶여서
메시지발행에 있어서도 전부 성공 or 전부 실패 를 보장할 수 있게 된거다.

이제 Outbox DB를 관찰하고 있고
Outbox DB에 변경사항이 감지되면 message broker를 통해 메세지를 발행해주는
역할을 가진 놈을 하나 추가해주기만 하면 된다..

두가지 관찰자

폴링 발행기

스케줄러 같은거로 db에 계속 access하는 거 같다..

로그 테일링

로그테일링

outbox 데이터베이스가 log가 추가되는 상황이 오면
log miner에게 알려주어 얘가 pub하는 상황이다.

구현 난이도가 높아서 관련 툴을 사용하면 편하다고 한다.

메시지 전달 개념

  1. At-most-once(최대 한번): 최대 한 번만 전송한다 (메시지를 한번만 전송하고 상대가 받았는지 받지 못했는지는 확인하지 않음)
  2. At-least-once(최소 한번): 메시지를 전송하고 최소한 상대방이 하나의 메시지는 받았는지 확인한다.
  3. Exatly-once(정확히 한번): 메시지를 정확히 한번만 전송한다.

정리

두가지 관찰자 + 메시지 전달 개념
를 적당히 섞어서 필요한 상황에 맞게끔 구현하면 될 것 같다.

중요한 것은 outbox라는 레이어를 추가해서

도메인 이벤트를 실제 도메인에 관한 영속화 관련 트랜잭션과 묶어서
이벤트 발행에 대한 정합성을 챙겼다라고 보면 될 것 같다..

참고자료

https://microservices.io/patterns/data/transactional-outbox.html
https://blog.gangnamunni.com/post/transactional-outbox

0개의 댓글

관련 채용 정보