๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ“• Spring Framework/Spring Project

ใ€ŒOutBox Patternใ€ ํ™œ์šฉ

by GroovyArea 2022. 6. 10.

https://github.com/GroovyArea/MyChickenBreastShop/wiki/Version-1

 

GitHub - GroovyArea/MyChickenBreastShop: ChikenBreastShop API with Spring boot

ChikenBreastShop API with Spring boot. Contribute to GroovyArea/MyChickenBreastShop development by creating an account on GitHub.

github.com

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ์ž‘์„ฑํ•œ Wiki ๋ฌธ์„œ์—์„œ ๊ณ„ํšํ•œ ๊ธฐ๋Šฅ์€ ๋‹ค ๊ตฌํ˜„์ด ๋˜์—ˆ๋‹ค. 
๊ฒŒ์‹œํŒ, ๋ฐฐ์†ก, ์ฑ„ํŒ… ๊ธฐ๋Šฅ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ๋ถ€์ˆ˜์ ์ด๋ฏ€๋กœ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์–•๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๊ธฐ๋Šฅ ํ•˜๋‚˜๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ๊ณ ๋ คํ•˜๋ฉฐ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ๋” ์˜๋ฏธ ์žˆ๊ฒ ๋‹ค๋Š” ํŒ๋‹จํ•˜์— ๊ธฐ๋Šฅ ๊ตฌํ˜„์€ ์—ฌ๊ธฐ์„œ ์ข…๋ฃŒํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

์ด๋ฒˆ 3์ผ ๊ฐ„ ๋ฆฌํŒฉํ„ฐ๋ง ํ•˜๋ฉฐ ๊ตฌํ˜„ํ•œ API๋Š” ์ด ๋‘ ๊ฐ€์ง€์ธ๋ฐ ์ด๋ฉ”์ผ ์ธ์ฆ ๋ฒˆํ˜ธ ์ „์†ก๊ณผ, ์ฃผ๋ฌธ API์ด๋‹ค. ๋‘ ๊ฐœ ๋ชจ๋‘ ๋™์‹œ์„ฑ๊ณผ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•˜์—ฌ ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ง„ํ–‰ํ–ˆ๋‹ค. 

๋‚ด๊ฐ€ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋Š๋‚€ ์ ์€ ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฑด ์ •๋ง ์‰ฝ๋‹ค.. ์•„๋ฌด๊ฒƒ๋„ ์•„๋‹Œ๋ฐ.
๊ธฐ๋Šฅ์ด ์ ๋”๋ผ๋„ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ณ€์ˆ˜๋ฅผ ๊ณ ๋ คํ•˜๋ฉฐ ํ™•์‹คํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒŒ ๋” ์˜๋ฏธ๊ฐ€ ์žˆ์ง€ ์•Š๋‚˜ ์‹ถ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€๋Š” ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ์„ ํ”„๋กœ์ ํŠธ๋กœ ํ•˜๋Š”๋ฐ ์—ฌ๋Ÿฌ ๋ฌธ์ œ๋“ค์„ ๊ณ ๋ คํ•˜๋ฉฐ ๋งŒ๋“ค์ž๋‹ˆ ๋˜๊ฒŒ ์–ด๋ ต๋‹ค๋Š” ์–˜๊ธฐ๋“ค์„ ํ•œ๋‹ค. ๊ทธ๋งŒํผ ๊ฐœ๋ฐœ ๊ณต๋ถ€์— ๋‚จ๋Š” ๊ฒŒ ์žˆ์„ ๊ฒƒ์ด๊ณ , ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ณ€์ˆ˜๋ฅผ ๊ณ ๋ คํ•˜๋ฉฐ ์‹œ์•ผ๊ฐ€ ๋„“์–ด์งˆ ๊ฒƒ์ด๋‹ค. 
์•ž์œผ๋กœ๋Š” ๋‹ค์–‘ํ•œ ์‹œ๊ฐ์œผ๋กœ ๋ฐ”๋ผ๋ณด๋Š” ์—ฐ์Šต์„ ํ•ด์•ผ๊ฒ ๋‹ค.

 

๋™์‹œ์„ฑ ๋ฌธ์ œ

1. ์ด๋ฉ”์ผ ์ธ์ฆ ๋ฒˆํ˜ธ ์ „์†ก 

=> ํšŒ์› ๊ฐ€์ž… ์‹œ ์ด๋ฉ”์ผ๋กœ ์ธ์ฆ ๋ฒˆํ˜ธ๋ฅผ ์ „์†กํ•ด์ค€๋‹ค. ํ•˜์ง€๋งŒ ๋ณธ ์„œ๋ฒ„์— ์žฅ์• ๊ฐ€ ์ƒ๊ธธ ์‹œ ์ด๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๋„ ๋งˆ๋น„๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ?

 

MSA ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•ด ๋ณด์•˜๋‹ค

https://sweeeetgoguma.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%82%B0-%EC%B2%98%EB%A6% AC-Micro-Service-Architecture

 

๋ฐ์ดํ„ฐ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ [Micro Service Architecture]

ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ ์ค‘์ด๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ ์ˆ˜๋ก ๊ณ„์ธต ๊ฐ„ DTO ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•˜๋Š” ์ผ์ด ๋งŽ์•„์กŒ๋‹ค. ๋ถˆ๋ณ€ ๊ฐ์ฒด๋ฅผ ์ ์ ˆํžˆ ์„ค๊ณ„ํ•ด์•ผ ํ•  ํ•„์š”๋ฅผ ๋Š๋ผ๋ฉฐ ์ตœ๋Œ€ํ•œ ํด๋ž˜์Šค ์„ค๊ณ„๋ฅผ ์ž˜ํ–ˆ๋‹ค. ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ›๋˜

sweeeetgoguma.tistory.com

 

๋‚ด๊ฐ€ ์ง„ํ–‰ํ•˜๋Š” ๋ฐฉ์‹์€ ๋ชจ๋†€๋ฆฌ์‹ ์•„ํ‚คํ…์ณ์ด๋‹ค. msa๋กœ์˜ ๋ณ€ํ™˜์€ ์–ด๋ ต์ง€๋งŒ ํ™˜๊ฒฝ ์ž์ฒด๋ฅผ ์ถฉ๋ถ„ํžˆ ๊ณ ๋ คํ•  ์ˆœ ์žˆ๋‹ค.

์ฐพ์•„๋ณด๋‹ˆ ๋ฉ€ํ‹ฐ ๋ชจ๋“ˆ ์ด๋ž€ ๊ฐœ๋…์ด ์žˆ์—ˆ๋‹ค. ๊ทธ๊ฒƒ์„ ์ ์šฉ์‹œ์ผœ ๋ณด๊ณ ์ž ํ–ˆ๋‹ค.

์ด๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๋ฅผ ๋‹ค๋ฅธ ๋ชจ๋“ˆ๋กœ ์ด์ „ํ•  ๊ฒฝ์šฐ ๋ณธ ์„œ๋ฒ„์™€ ๋ณ„๊ฐœ๋กœ ์„œ๋น„์Šค๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

=> ๋ณธ ์„œ๋ฒ„์™€ ๋ฉ”์ผ ์„œ๋ฒ„๋ฅผ ๊ฐ๊ฐ ๋ชจ๋“ˆ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค

 

๊ทธ๋Ÿผ ํšŒ์›๊ฐ€์ž… ์š”์ฒญ์„ ์–ด๋–ป๊ฒŒ ์•Œ๊ณ  ์ด๋ฉ”์ผ์„ ๋ณด๋‚ด๋Š” ๊ฑธ๊นŒ?

=> OutBox ํŒจํ„ด์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์•˜๋‹ค. 

=> ์ฐธ์กฐ : https://daddyprogrammer.org/post/14068/database-migration-by-transactional-outbox/

 

Database Migration by Transactional Outbox Pattern

์ด์ „ ์‹ค์Šต๊นŒ์ง€๋Š” ๋ฐ์ดํ„ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์œ„ํ•ด Database์—์„œ ์ œ๊ณตํ•˜๋Š” binlog๋‚˜ DynamoDB/MongoDB์—์„œ ์ œ๊ณตํ•˜๋Š” Change Stream์„ ํ†ตํ•ด ๋ณ€๊ฒฝ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ์‹œ์Šคํ…œ์ ์œผ๋กœ ์ง€์›

daddyprogrammer.org

 

์‰ฝ๊ฒŒ ์š”์•ฝํ•˜์ž๋ฉด ๋ฉ”์‹œ์ง€ ํ์™€ ๋น„์Šทํ•œ ๊ฐœ๋…์ด๋‹ค.

์–ด๋–ค ์š”์ฒญ -> API ์‘๋‹ต -> ์š”์ฒญ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  OutBox ํ…Œ์ด๋ธ”์— ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ €์žฅ -> ํ…Œ์ด๋ธ”์„ ๋ฐ”๋ผ๋ณด๋Š” ๋ชจ๋“ˆ์—์„œ ์š”์ฒญ ๊ฑด ์ˆœ์ฐจ์ฒ˜๋ฆฌ

 

=> ์ด๋ ‡๊ฒŒ ํ•ด์„œ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜ ๋‹จ์œ„๋กœ ๋ฌถ์œผ๋ฉด ๋ถ„์‚ฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ํ™˜๊ฒฝ์—์„œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์ด ํ•ด์†Œ๊ฐ€ ๋œ๋‹ค.

=> ์Šค์ผ€์ฅด๋Ÿฌ๋ฅผ ์ด์šฉ ์‹œ ALO (At Least Once) ๋„ ์ ์šฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

 

์ฝ”๋“œ 

/**
 * ์ด๋ฉ”์ผ์„ ๋ฐ›๊ณ  ๊ด€๋ จ ์ •๋ณด๋ฅผ DB์— ์ €์žฅ ํ›„ ์•„์›ƒ๋ฐ•์Šค ํŒจํ„ด์„ ์œ„ํ•ด ์ด๋ฒคํŠธ ๋ฐœ์ƒ
 * @param emailRequestDTO ์ด๋ฉ”์ผ DTO
 */
@Transactional
public void saveEmailKey(UserEmailRequestDTO emailRequestDTO) {
    LocalDateTime now = LocalDateTime.now();
    LocalDateTime expiredTime = now.plusMinutes(5);
    EmailKey emailKey = toEmailKeyVO(expiredTime, emailRequestDTO.getUserEmail());

    emailKeyMapper.insertEmailKey(emailKey);
    EmailKey saved = emailKeyMapper.selectEmailKey(emailKey.getId());

    applicationEventPublisher.publishEvent(
            outBoxEventBuilder.createOutBoxEvent(EmailKeyCreated.builder()
                    .emailKeyId(saved.getId())
                    .emailKey(saved.getEmailKey())
                    .email(saved.getEmail())
                    .build())
    );
}

์ด๋ฉ”์ผ ์ธ์ฆ ์š”์ฒญ์ด ์™”๋‹ค. ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ DTO๋ฅผ ๋ฐ›๋Š”๋‹ค.

์ด๋ฉ”์ผ์„ ์ „์†กํ•ด์•ผ ํ•˜๋‹ˆ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. (ApplicationEventPublisher ์ด์šฉ)

 

/**
 * ์ด๋ฒคํŠธ ๋นŒ๋” <br>
 * ๋ฐœ์ƒ๋œ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ€๊ณต, payload๋ฅผ json ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ ํ›„ ๊ฐ์ฒด๋กœ ๋ฐ˜ํ™˜
 */
@Component
@Slf4j
public class EmailEventBuilder implements OutBoxEventBuilder<EmailKeyCreated> {

    private static final String EVENT_ACTION = "์ด๋ฉ”์ผ";

    @Override
    public OutBoxEvent createOutBoxEvent(EmailKeyCreated domainEvent) {

        JsonNode jsonNode = ObjectMapperUtil.getMapper().convertValue(domainEvent, JsonNode.class);

        return new OutBoxEvent.OutBoxEventBuilder()
                .aggregateId(domainEvent.getEmailKeyId())
                .aggregateType(EmailKey.class.getSimpleName())
                .eventType(domainEvent.getClass().getSimpleName())
                .eventAction(EVENT_ACTION)
                .payload(jsonNode.toString())
                .build();

    }
}

=> ์ด๋ฉ”์ผ ๊ด€๋ จ๋œ OutBoxEmail ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 

/**
 * ์•„์›ƒ๋ฐ•์Šค ๊ฐ์ฒด DB์— ์ €์žฅ
 */
@Component
@RequiredArgsConstructor
public class OutBoxEventHandler {

    private static final String ORDER = "์ฃผ๋ฌธ";
    private static final String EMAIL = "์ด๋ฉ”์ผ";
    private static final String CART = "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ฃผ๋ฌธ";

    private final OutBoxMapper outBoxMapper;

    @EventListener
    public void doOutBoxEvent(OutBoxEvent outBoxEvent) {
        switch (outBoxEvent.getEventAction()) {
            case EMAIL:
                outBoxMapper.insertEmailOutBox(OutBox.builder()
                        .aggregateId(outBoxEvent.getAggregateId())
                        .aggregateType(outBoxEvent.getAggregateType())
                        .eventType(outBoxEvent.getEventType())
                        .payload(outBoxEvent.getPayload())
                        .build());
                break;

=> ๋งŒ๋“ค์–ด์ง„ ๊ฐ์ฒด๋ฅผ ํ† ๋Œ€๋กœ OutBox ํ…Œ์ด๋ธ”์— ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.

 

/**
 * ์ด๋ฉ”์ผ ์Šค์ผ€์ฅด๋Ÿฌ <br>
 * 10์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ outbox ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์กฐํšŒ ํ›„ ๋ฉ”์ผ ๋ฐœ์†ก
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class EmailSendScheduler {

    private static final long EXPIRE_DURATION = 60 * 5L;

    private final RedisService redisService;
    private final OutBoxEmailMapper outBoxMapper;
    private final SendMailService sendMailService;
    private final MailContentService mailContentService;

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

        log.info("์ด๋ฉ”์ผ ์ „์†ก ์ค‘...");

        List<OutBox> outBoxList = outBoxMapper.selectAllOutBox();
        if (!outBoxList.isEmpty()) {
            List<Long> completedList = new LinkedList<>();
            outBoxList.forEach(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("๋ฉ”์ผ ๋ฐœ์†ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ . . .");
                } catch (FailedPayloadConvertException | JsonProcessingException e) {
                    log.error(e.getMessage());
                }
            });
            if (!completedList.isEmpty()) {
                outBoxMapper.deleteAllById(completedList);
            }
        }
    }

=> ์Šค์ผ€์ฅด๋Ÿฌ๊ฐ€ OutBox ํ…Œ์ด๋ธ”์„ ํ•ญ์‹œ ๋ฐ”๋ผ๋ณด๊ณ  ์žˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ ์ €์žฅ๋œ ์ •๋ณด๋ฅผ ํ†ตํ•ด ์ด๋ฉ”์ผ์„ ์ „์†ก์‹œํ‚จ๋‹ค. ๊ทธ ํ›„ Redis์— ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•œ๋‹ค. -> ์ธ์ฆ์„ ์œ„ํ•จ

 

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ ํ™˜๊ฒฝ์—์„œ๋„ ๋ณด์žฅ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค. ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์—ฌ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

2.  ์ฃผ๋ฌธ ๊ฑด์— ๋Œ€ํ•œ ์ƒํ’ˆ ์žฌ๊ณ  ์กฐํšŒ

=> ์ƒํ’ˆ ์ฃผ๋ฌธ ์‹œ ์žฌ๊ณ  ํŒŒ์•…์„ ๋จผ์ € ํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ฒฐ์ œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์•ผ ํ•œ๋‹ค. ๋ฉ€ํ‹ฐ ์Šค๋ ˆ๋“œ ํ™˜๊ฒฝ์„ ์ƒ๊ฐํ•ด๋ณด์ž. ๋™์‹œ์— ์ ‘์†ํ•œ ์œ ์ €๋“ค์ด ๋„ˆ๋„ ๋‚˜๋„ ์ธ๊ธฐ๊ฐ€ ๋งŽ์€ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด ์ƒํ’ˆ์€ ์žฌ๊ณ ๊ฐ€ ํ•œ์ •์ ์ด๋‹ค. ์‹ฌ์ง€์–ด 1๊ฐœ๊ฐ€ ๋‚จ์•„์žˆ์„ ๋•Œ 2๋ช…์ด ๋™์‹œ์— ์ฃผ๋ฌธ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‘ ๊ฐœ์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ์กฐํšŒ๋ฅผ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. 

 

์ฐธ์กฐ : https://sweeeetgoguma.tistory.com/entry/20220602-%E3%80%8CDB-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%E3%80%8D

 

2022.06.02 ใ€ŒDB ๋™์‹œ์„ฑ ๋ฌธ์ œใ€

์ž๋ฐ” ๊ธฐ๋ฐ˜ ์›น ํ”„๋กœ๊ทธ๋žจ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋”ฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋™์‹œ์„ฑ ๊ด€๋ จ ๋ฌธ์ œ๋ฅผ ์ž˜ ํ•ด๊ฒฐํ•ด์•ผ ํ•œ๋‹ค๊ณ  ๋“ค์—ˆ๋‹ค. ์ด๋ฒˆ์— ๋‚ด๊ฐ€ ํ•˜๋Š” ์‡ผํ•‘๋ชฐ ํ”„๋กœ์ ํŠธ์—์„œ๋„ ๊ทธ ์ด์Šˆ๊ฐ€ ๋”ฑ ํ„ฐ์กŒ๋‹ค. ์˜ˆ๋ฅผ

sweeeetgoguma.tistory.com

 

๋‚ด DBMS : Mysql 

=> ๊ธฐ๋ณธ์ ์œผ๋กœ ๋„ฅ์ŠคํŠธ ํ‚ค๋ฝ์„ ์ง€์›ํ•˜๊ณ  Repeatable Read ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์ด๋ฏ€๋กœ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ค์ง„ ์•Š์ง€๋งŒ, 

๊ธฐ์กด์— ๋ ˆ์ฝ”๋“œ ๋ฝ์„ ์ถ”๊ฐ€๋กœ ๊ฑธ์–ด์„œ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ, ํŠธ๋ž˜ํ”ฝ์ด ๋งŽ์ด ๋ชฐ๋ฆด ๊ฒฝ์šฐ ์„ฑ๋Šฅ ์ƒ ์†๋„๊ฐ€ ๋„ˆ๋ฌด ๋Š๋ ค์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

๊ทธ๋ž˜์„œ ์—ญ์‹œ OubBox ํŒจํ„ด์„ ์ ์šฉ์‹œ์ผฐ๋‹ค.

 

์ฝ”๋“œ 

List<OrderCreated> orderCartList =
        getOrderCreatedList(productNoArr, productNameArr, productStockArr, totalAmount);

/* ์žฌ๊ณ  ํ™•์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ */
applicationEventPublisher.publishEvent(
        outBoxEventCartBuilder.createOutBoxEvent(orderCartList)
);

์žฅ๋ฐ”๊ตฌ๋‹ˆ๋กœ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค. ์œ„์™€ ๋™์ผ

์ด๋ฒคํŠธ ๋ฐœ์ƒ

 

/**
 * ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ฃผ๋ฌธ ์•„์›ƒ๋ฐ•์Šค ์ด๋ฒคํŠธ ๊ฐ์ฒด ๋นŒ๋”
 */
@Component
@Slf4j
public class CartOrderEventBuilder implements OutBoxEventBuilder<List<OrderCreated>> {

    private final static String EVENT_ACTION = "์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ฃผ๋ฌธ";

    @Override
    public OutBoxEvent createOutBoxEvent(List<OrderCreated> domainEvent) {
        long firstId = domainEvent.get(0).getItemNumber();

        return new OutBoxEvent.OutBoxEventBuilder()
                .aggregateId(firstId)
                .aggregateType(List.class.getSimpleName())
                .eventType(domainEvent.getClass().getSimpleName())
                .eventAction(EVENT_ACTION)
                .cartList(domainEvent)
                .build();
    }
}

=> OutBox ์ด๋ฒคํŠธ ๊ฐ์ฒด ์ƒ์„ฑ (List ์ •๋ณด ๋‹ด์•„์„œ) ๋ฐ ๋ฐ˜ํ™˜

 

/**
 * ์ฃผ๋ฌธ ์•„์›ƒ ๋ฐ•์Šค ์กฐํšŒ ์Šค์ผ€์ค„๋Ÿฌ <br>
 * 10์ดˆ๋งˆ๋‹ค ์ฃผ๋ฌธ ๊ฑด์— ๋Œ€ํ•œ ์žฌ๊ณ  ํŒŒ์•…์„ ํ•œ๋‹ค.
 */
@Component
@RequiredArgsConstructor
@Slf4j
public class CheckStockScheduler {

    private final OutBoxMapper outBoxMapper;
    private final ProductMapper productMapper;
    private final KakaoPayService kakaoPayService;

    @Transactional
    @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 -> {
                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());
                }
            });
            if (!completedList.isEmpty()) {
                outBoxMapper.deleteAllById(completedList);
            }
        }
    }
}

=> ์Šค์ผ€์ฅด๋Ÿฌ๋ฅผ ํ†ตํ•ด ์ฃผ๊ธฐ์ ์œผ๋กœ ์กฐํšŒ ๋ฐ ์žฌ๊ณ  ํŒŒ์•… 

์žฌ๊ณ  ํŒŒ์•…์— ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์‹œ flag ๋…ผ๋ฆฌ ๋ณ€์ˆ˜๋ฅผ false๋กœ ๋ฐ”๊พผ๋‹ค.

 

public void changeStockFlag(boolean flag) {
    this.exceptionFlag = flag;
}

=> ํ”Œ๋ž˜๊ทธ ๋ณ€๊ฒฝ ๋ฉ”์„œ๋“œ

 

/* ์žฌ๊ณ  ํ’ˆ์ ˆ ์˜ˆ์™ธ ๋ฐœ์ƒ */
if (!this.exceptionFlag) {
    throw new RunOutOfStockException();
}

ํŠธ๋žœ์žญ์…˜ ์„œ๋น„์Šค ๋‚ด์˜ ์กฐ๊ฑด๋ฌธ (์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ๋ชจ๋‘ ๋กค๋ฐฑ๋œ๋‹ค)

 

@ExceptionHandler(RunOutOfStockException.class)
public ResponseEntity<String> runOutOfException(RunOutOfStockException e) {
    return ResponseEntity.ok().body(e.getMessage());
}

=> ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ ExceptionHandler๋กœ ์ฒ˜๋ฆฌ

 

๊ฒฐ๋ก 

์•„์›ƒ ๋ฐ•์Šค ํŒจํ„ด์„ ํ™œ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ ํ™˜๊ฒฝ์—์„œ ์ •ํ•ฉ์„ฑ ๋ฌธ์ œ์™€ ALO๋ฅผ ์ ์šฉ์‹œ์ผœ ๋ฆฌํŒฉํ„ฐ๋ง์„ ์ง„ํ–‰ํ–ˆ๋‹ค. ์ฒ˜์Œ์—์•ผ ์–ด๋ ต์ง€ ํ•˜๋‹ค ๋ณด๋‹ˆ ๋‚˜๋ฆ„ ๊ดœ์ฐฎ๊ฒŒ ์ดํ•ดํ•œ ๊ฒƒ ๊ฐ™๋‹ค. 

ํ™•์‹คํžˆ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์„ ๊ณ ๋ คํ•˜๋ฉฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ๋  ๊ฒƒ ๊ฐ™๋‹ค. ๋” ํ˜„์‹ค์— ๊ฐ€๊นŒ์šด ์ฝ”๋“œ๋ฅผ ์งœ๋ ค๊ณ  ๋…ธ๋ ฅํ•ด๋ณด์ž!

์•ž์œผ๋กœ์˜ ๋ฆฌํŒฉํ„ฐ๋ง๋„ ์ˆ˜์ค€ ๋†’๊ฒŒ ํ•ด๋ณด์ž์•„!!!!

 

๊นƒํ—™ : https://github.com/GroovyArea/MyChickenBreastShop

 

๋ฐ˜์‘ํ˜•