본문 바로가기
📕 Spring Framework/Spring Project

OutBox Pattern & Saga Pattern & Transaction

by GroovyArea 2022. 6. 13.
지난번 포스팅을 이후로 3일간 테스트 코드에 관한 공부를 하며 리팩터링을 진행했다. 
데이터 분산 환경에서의 트랜잭션의 고려도 충분히 중요한 설계 같다. 그 리팩터링 과정을 정리해보겠다.

 

https://sweeeetgoguma.tistory.com/entry/%E3%80%8COutBox-Pattern%E3%80%8D-%ED%99%9C%EC%9A%A9

 

「OutBox Pattern」 활용

https://github.com/GroovyArea/MyChickenBreastShop/wiki/Version-1 GitHub - GroovyArea/MyChickenBreastShop: ChikenBreastShop API with Spring boot ChikenBreastShop API with Spring boot. Contribute to G..

sweeeetgoguma.tistory.com

> 지난 포스팅에서 정리했다시피 기능은 완료가 되었다

 

문제점을 정리해보겠다. 

 

1. 아웃 박스 패턴을 이용 후 트랜잭션 처리의 문제

 

얼핏 보면 문제가 없지만 문제가 있다.

바로 아웃박스 데이터 리스트의 처리가 하나의 트랜잭션 단위로 묶여 있기 때문이다. 

리스트 중 하나라도 잘못된 데이터가 들어있어 예외가 발생할 경우 모두 롤백 후 다시 무한 반복을 하게 될 것이다. 

 

어떻게 해야 할까?

 

@Scheduled(cron = "0/10 * * * * ?")
public void schedulingCheckStock() {
    ObjectMapper objectMapper = new ObjectMapper();

    log.info("재고 확인 중 . . .");

    List<OutBox> outBoxList = outBoxMapper.selectAllOrderOutBox();
    if (!outBoxList.isEmpty()) {
        List<Long> completedList = new LinkedList<>();
        outBoxList.forEach(outBox -> {
            outBoxStockCheck(objectMapper, completedList, outBox);
        });
        if (!completedList.isEmpty()) {
            outBoxMapper.deleteAllById(completedList);
        }
    }
}

@Transactional
void outBoxStockCheck(ObjectMapper objectMapper, List<Long> completedList, OutBox outBox) {
    String payload = outBox.getPayload();
    try {
        JsonNode jsonNode = objectMapper.readTree(payload);
        String itemName = jsonNode.get("item_name").asText();

        if (productMapper.selectStockOfProduct(itemName) < Integer.parseInt(jsonNode.get("quantity").asText())) {
            kakaoPayService.changeStockFlag(false);
        }

        completedList.add(outBox.getId());

    } catch (JsonProcessingException e) {
        log.error(e.getMessage());
        outBoxMapper.insertOrderOutBox(outBox);
    }
}
@Scheduled(cron = "0/10 * * * * ?")
public void schedulingValidNumberEmail() {
    ObjectMapper objectMapper = new ObjectMapper();

    log.info("이메일 전송 중...");

    List<OutBox> outBoxList = outBoxMapper.selectAllEmailOutBox();
    if (!outBoxList.isEmpty()) {
        List<Long> completedList = new LinkedList<>();
        outBoxList.forEach(outBox -> {
            validateEmailNumber(objectMapper, completedList, outBox);
        });
        if (!completedList.isEmpty()) {
            outBoxMapper.deleteAllById(completedList);
        }
    }
}

@Transactional
void validateEmailNumber(ObjectMapper objectMapper, List<Long> completedList, OutBox outBox) {
    String payload = outBox.getPayload();
    try {
        JsonNode jsonNode = objectMapper.readTree(payload);
        String userEmail = jsonNode.get("email").asText();
        String authKey = jsonNode.get("email_key").asText();

        MailDTO content = mailContentService.createMailContent(payload);

        sendMailService.sendEmail(content);

        completedList.add(outBox.getId());

        redisService.setDataExpire(userEmail, authKey, EXPIRE_DURATION);
    } catch (MailException e) {
        log.error("메일 발송 중 오류 발생 . . .");
        outBoxMapper.insertOutBox(outBox);
    } catch (FailedPayloadConvertException | JsonProcessingException e) {
        log.error(e.getMessage());
        outBoxMapper.insertOutBox(outBox);
    }
}

=> 트랜잭션 단위를 세분화시켰다. 에러 발생 시 다시 아웃박스 큐의 마지막으로 문제가 생긴 데이터를 추가한다. 그렇게 되면 문제 있는 레코드 이후의 데이터가 정상적으로 큐에서 꺼내져 처리가 가능해진다. 

 

2. SAGA  패턴

SAGA 패턴 구현 시 고려해야 할 사항?

- 잠재하는 일시적인 오류들을 처리할 수 있어야 하며, 데이터 일관성을 보장하기 위해 멱등성 제공이 필요

- Work Flow를 항상 모니터링하고 추적하는 관찰 가능성을 구현하는 것이 좋다. 

 

 

3. OutBox 패턴

발생 가능한 문제?

- 데이터의 일관성 보장 문제

- 메세지의 전달 보증 수준을 잘 따져야 한다.

 

 

4. 스프링에서 대표적으로 사용되는 AOP

- @Transactional이 대표적

- aop의 추가적인 공부가 필요할 것 같다.

 

 

반응형