[23.12.26] Outbox Transaction Pattern
이번 주제는 트랜잭션 패턴 중 하나인 Outbox Pattern에 관한 주제입니다.
소개
기존 모놀리틱 서버에서의 Transaction은 간단하게 처리가 가능했다.
그러나 서버가 나뉘어지며 이벤트 중심의 로직 처리가 중요해졌다.
결과적으로 Transaction을 처리하기 위해 새로운 패턴이 필요하게 되었다.
패턴들이 필요한 이유에 대해서 상세히 알아보자.
실패를 성공으로, 성공을 실패로 만들 수 있는 상황
사례 1: 성공을 실패로
<ProductService.class>
@Transactional
public Product registerProduct(RegisterProduct product) {
Product product = Product.toEntity(product);
productorRepository.save(product);
return product;
}
....
<ProductController.class>
@PostMapping(Api.PRODUCT)
public void registerProduct(@RequestBody RegisterProduct product) {
Product product = productService.registerProduct(product);
applicationEventPublisher.publish(product.getMessage());
}
위 케이스에서 트랜잭션은 성공돼서 DB에 커밋이 되었고 pulbish를 하는 순간 에러가 발생했다고 가정하자.
그러면 실제 트랜잭션이 성공했는데 이벤트가 발행되지 않는 현상이 발생한다.
그럼 주문재고가 감소되지 않는다던지 배송 등록이 안된다던지 문제가 발생할 것이다.
사례 2: 실패를 성공으로
<ProductService.class>
@Transactional
public Product registerProduct(RegisterProduct product) {
Product product = Product.toEntity(product);
productorRepository.save(product);
applicationEventPublisher.publish(product.getMessage());
return product;
}
....
<ProductController.class>
@PostMapping(Api.PRODUCT)
public void registerProduct(@RequestBody RegisterProduct product) {
Product product = productService.registerProduct(product);
}
위 케이스는 @Transactional에 의해 begin() -> commit() -> rollback() 순서로 실행이 된다.
publish가 끝나고 commit시 에러가 발생했다고 가정하자. 이건 실패하여 롤백이 되어야한다.
그러나 이미 발송되어진 이벤트에 대해선 처리가 불가능하다.
결과 주문은 취소되었지만 배송이 잡혀있는 경우, 재고가 줄어드는 경우가 발생할 수 있다.
위 2가지 상황을 막기 위해서 Outbox Pattern을 적용해보자.
Outbox Pattern
Step1: Outbox Table 생성
이벤트 형식에 맞게 Table을 생성.
create table outbox_product(
id bigint,
message longtext
)
Step2: Message Publish Server 생성
@Scheduled(fixedRate = 1000)
public void publishMessages() {
List<OutBox> outBoxs = outBoxRepository.findAll();
if (!outBoxList.isEmpty()) {
List<Long> outBoxCompletedList = new LinkedList<>();
outBoxList.forEach(outBox -> {
String payload = outBox.getMessage();
MailDto mailDto = MailDto.createContentForMail(payload);
try {
sendMailService.sendMail(mailContent);
outBoxCompletedList.add(outBox.getId());
} catch (MailException e) {
log.error("메일을 보내는 과정에 문제가 발생하였습니다.");
}
});
if(!outBoxCompletedList.isEmpty())
outBoxRepository.deleteAllByOutBoxId(outBoxCompletedList);
}
}
Step3: 기존 Server에 Outbox Table 이벤트 저장
<ProductService.class>
@Transactional
public Product registerProduct(RegisterProduct product) {
Product product = Product.toEntity(product);
productorRepository.save(product);
outboxProductoRepository.save(new OutboxProduct(product.getMessage()));
return product;
}
....
<ProductController.class>
@PostMapping(Api.PRODUCT)
public void registerProduct(@RequestBody RegisterProduct product) {
Product product = productService.registerProduct(product);
}
Outbox Pattern 문제점
문제1: 중복 메세지 발행
만약 일시적으로 네트워크 지연이 생겨 하나의 인스턴스(Message Publish Server)가 메일을 보내는 중 다른 인스턴스 서버도 같이 스케줄링이 걸리는 경우 중복으로 메세지가 발생된다.
이런 경우는 이벤트를 받는 서버에서 멱등성 검증을 하던지 중복 확인을 위한 ID 체크를 하는 방법이 존재한다.
문제2: 메세지 순서
주문 후 즉시 취소를 누를 경우 메세지 순서가 변경될 수 있다. 그럼 취소 메세지가 먼저 도착하고 취소할 물건이 없다는 것을 확인하고 돌아간 후 주문 메세지가 도착하여 주문이 생성 될 것이다. 이것은 큰 문제이다.
이것을 해결하기 위해선 메세지에 순서 ID를 부여해 주문 및 취소 이벤트를 받는 서버에서 확인하여 처리하는 방법이 있다.
참고: 블로그 링크
https://devocean.sk.com/blog/techBoardDetail.do?ID=165445&boardType=techBlog
[MSA 패턴] SAGA, Transactional Outbox 패턴 활용하기
devocean.sk.com