지난번 포스팅을 이후로 3일간 테스트 코드에 관한 공부를 하며 리팩터링을 진행했다.
데이터 분산 환경에서의 트랜잭션의 고려도 충분히 중요한 설계 같다. 그 리팩터링 과정을 정리해보겠다.
https://sweeeetgoguma.tistory.com/entry/%E3%80%8COutBox-Pattern%E3%80%8D-%ED%99%9C%EC%9A%A9
> 지난 포스팅에서 정리했다시피 기능은 완료가 되었다
문제점을 정리해보겠다.
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의 추가적인 공부가 필요할 것 같다.
반응형
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
「컨트롤러 단위 테스트」 (0) | 2022.06.23 |
---|---|
「테스트 코드 & Spring REST Docs」 (0) | 2022.06.20 |
「OutBox Pattern」 활용 (0) | 2022.06.10 |
2022.06.07 「프로젝트 중간 점검」 (0) | 2022.06.07 |
2022.06.02 「DB 동시성 문제」 (0) | 2022.06.02 |