๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

design

[Indp] ์ด๋ฒคํŠธ์™€ ๋น„๋™๊ธฐ๋ฅผ ํ™œ์šฉํ•œ ํ‘ธ์‰ฌ ์•Œ๋ฆผ ๊ตฌํ˜„ํ•˜๊ธฐ

๐Ÿš€ ๊ฐœ์š”

Indp ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฉ”์ผ ํ‘ธ์‰ฌ ์•Œ๋ฆผ์ด ์กด์žฌํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์Œ์•…์„ ์ถ”์ฒœํ•˜๊ฑฐ๋‚˜ ๋ฌธ์˜ ์‚ฌํ•ญ์„ ๋“ฑ๋กํ•  ๊ฒฝ์šฐ ๊ด€๋ฆฌ์ž์˜ ์ด๋ฉ”์ผ๋กœ ์•Œ๋ฆผ์„ ์ „์†กํ•œ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ•œ ์•Œ๋ฆผ ๋กœ์ง์—๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๋ฌธ์ œ๋ฅผ ๋‚ด์žฌํ•˜๊ณ  ์žˆ๋‹ค. ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ์ ๋“ค์„ ์ •์˜ํ•˜๊ณ  ์ด๋ฅผ ์ ์ฐจ ํ•ด๊ฒฐํ•ด ๋ณด์ž.

๐Ÿšจ ๊ธฐ์กด ์ฝ”๋“œ

์Œ์•… ์ถ”์ฒœ ๊ธ€์ด ๋“ฑ๋ก๋˜์—ˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” ๋กœ์ง์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

@Service
public class RecommendationService {

    @Transactional
    public long registerRecommendation(RegisterRecommendationRequest request) {
        Store store = getStore(request);
        
        Recommendation recommendation = new Recommendation(store, request.information(), request.phoneNumber());
        Recommendation persistRecommendation = recommendationRepository.save(recommendation);

        Mail mail = new Mail(to, "[๋ฒ„๋น„] ์ธ๋””ํ”ผ ์„œ๋น„์Šค์— ์Œ์•…์ด ์ถ”์ฒœ๋˜์—ˆ์–ด์š”!",
            "์ถ”์ฒœ ์Œ์•… ์ •๋ณด: " + request.information() + "\n" +
            "์ถ”์ฒœ์ธ ์—ฐ๋ฝ์ฒ˜: " + request.phoneNumber() + "\n" +
            "๋งค์žฅ ์ด๋ฆ„: " + request.storeName() + "\n" +
            "๋งค์žฅ ์ฃผ์†Œ: " + request.storeAddress() + "\n");
        springMailService.sendMail(mail);

        return persistRecommendation.getRecommendationId();
    }
@Service
public class SpringMailService {

    public void sendMail(Mail mail) {
        SimpleMailMessage message = new SimpleMailMessage();

        message.setTo(mail.to());
        message.setSubject(mail.subject());
        message.setText(mail.text());

        mailSender.send(message);
    }
}

์ด ๋กœ์ง์€ ๋ฌธ์ œ๊ฐ€ ์—†์–ด ๋ณด์ด์ง€๋งŒ ๋งŽ์€ ๋ฌธ์ œ๋ฅผ ๋‚ด์žฌํ•˜๊ณ  ์žˆ๋‹ค. 

โ˜ ๏ธ ๋ฌธ์ œ์  ์ •์˜

๋ฌธ์ œ 1: ์•Œ๋ฆผ๊ณผ ์Œ์•… ์ถ”์ฒœ์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์— ์กด์žฌ

ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์Œ์•… ์ถ”์ฒœ ์ •๋ณด๊ฐ€ ์ €์žฅ๋˜๊ณ  ๋ฉ”์ผ ์•Œ๋ฆผ์ด ์ „์†ก๋œ๋‹ค. ์ด ๊ฒฝ์šฐ ์Œ์•… ์ถ”์ฒœ ์ •๋ณด๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ปค๋ฐ‹๋˜์ง€ ์•Š์•˜๋Š”๋ฐ ๋ฉ”์ผ ์•Œ๋ฆผ์ด ์ „์†ก๋  ์ˆ˜ ์žˆ๊ณ  ๋ฉ”์ผ ์•Œ๋ฆผ ์‹คํŒจ๋กœ ์ธํ•ด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒ๋  ๊ฒฝ์šฐ ์ถ”์ฒœ ์ •๋ณด๊ฐ€ ์ปค๋ฐ‹๋˜์ง€ ์•Š๊ณ  ๋กค๋ฐฑ์ด ๋  ์ˆ˜ ์žˆ๋‹ค. ๋ถ„๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

๋ฌธ์ œ 2: ๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์— ์กด์žฌ

์ œ 3์ž ์„œ๋น„์Šค์ธ ๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์— ์กด์žฌํ•˜์—ฌ ๋ฉ”์ผ ์„œ๋น„์Šค์— ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ํ•œ์ •์ ์ธ ๋ฆฌ์†Œ์Šค์ธ ์ปค๋„ฅ์…˜์ด ๊ณ„์† ๋ฌผ๋ ค ์žˆ์„ ์ˆ˜ ์žˆ๊ณ  ์‹œ์Šคํ…œ ์ „์ฒด์˜ ์žฅ์• ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์ปค๋„ฅ์…˜์ด 20๊ฐœ์ธ ๊ฒฝ์šฐ ๋ฉ”์ผ ์„œ๋น„์Šค์— ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ 20๊ฐœ์˜ ์ปค๋„ฅ์…˜์ด ๋ชจ๋‘ ๋ฐ˜ํ™˜๋˜์ง€ ์•Š๊ณ  ์‚ฌ์šฉ ์ค‘์ธ ์ƒํƒœ๋กœ ๋จธ๋ฌผ๋Ÿฌ ์žˆ๋‹ค๋ฉด ๋‹ค๋ฅธ ์š”์ฒญ๋“ค์€ ์ปค๋„ฅ์…˜์„ ํš๋“ํ•˜์ง€ ๋ชปํ•ด ์•„๋ฌด๋Ÿฐ ๋™์ž‘์„ ํ•  ์ˆ˜ ์—†๊ฒŒ ๋œ๋‹ค.

๋ฌธ์ œ 3: ๋™๊ธฐ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ๋ฌธ์ œ

์Œ์•… ์ถ”์ฒœ๊ณผ ํ‘ธ์‰ฌ ์•Œ๋ฆผ์€ ๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†๋Š” ๋กœ์ง์ด๋‹ค. ๋˜ํ•œ ๋ฉ”์ผ ์ „์†ก์€ ์„ฑ๋Šฅ์ด ๋ณด์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์Œ์•…์„ ์ถ”์ฒœํ•˜๋Š”๋ฐ 100ms, ๋ฉ”์ผ ์ „์†ก์ด 5000ms ๊ฐ€ ์†Œ์š”๋œ๋‹ค๋ฉด ์‚ฌ์šฉ์ž๋Š” ์Œ์•… ์ถ”์ฒœ ์š”์ฒญ์„ ๋ณด๋‚ธ ํ›„ 5100ms ํ›„์— ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž๋Š” ์Œ์•…์„ ์ถ”์ฒœํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ด์ง€ ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฉ”์ผ ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ด ์•„๋‹ˆ๋‹ค. ์ด ๋˜ํ•œ ๋ถ„๋ฆฌํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค.

๋ฌธ์ œ 4: ๋ฉ”์ผ ์ „์†ก์ด ์‹คํŒจํ•  ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ

๋ฉ”์ผ ์ „์†ก์€ ์ œ 3์ž ์„œ๋น„์Šค์ด๊ณ  ์–ธ์ œ๋“ ์ง€ ์‹คํŒจํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌํ•œ๋‹ค. ํ•˜์ง€๋งŒ ๋ฉ”์ผ ์ „์†ก์ด ์‹คํŒจ๋  ๊ฒฝ์šฐ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ๋งŒ ํ•  ๋ฟ ์•„๋ฌด๋Ÿฐ ๋™์ž‘์ด ์ˆ˜ํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค. ๋ฉ”์ผ ์ „์†ก ์‹คํŒจ์˜ ๊ฒฝ์šฐ๋ฅผ ๋Œ€๋น„ํ•œ ๋ฉ”์นด๋‹ˆ์ฆ˜์ด ํ•„์š”ํ•˜๋‹ค.

โœ… ์Šคํ”„๋ง ์ด๋ฒคํŠธ ์ ์šฉ

์Šคํ”„๋ง ์ด๋ฒคํŠธ๋Š” ์Šคํ”„๋ง ํ”„๋ ˆ์ž„ ์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์Šคํ”„๋ง ๋นˆ ๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐฉ์‹ ์ค‘ ํ•˜๋‚˜๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜์—ฌ ์†Œ๋น„ํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฆฌ์Šค๋„ˆ์—๋Š” EventListener ์™€ TransactionalEventListener ๊ฐ€ ์žˆ์œผ๋ฉฐ ์šฐ๋ฆฌ๋Š” ์ด ์ค‘ ํŠธ๋žœ์žญ์…˜์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ด ์ฃผ๋Š” TransactionalEventListener ๋ฅผ ์ ์šฉํ•ด ๋ณผ ๊ฒƒ์ด๋‹ค. (๋ฌผ๋ก  ํ•ด๋‹น ๋กœ์ง์— ์Œ์•… ์ถ”์ฒœ์„ ์ €์žฅํ•˜๋Š” save ๋งŒ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— @Transactional ์• ๋…ธํ…Œ์ด์…˜์„ ๋–ผ๋ฒ„๋ฆฌ๋ฉด ์ปค๋ฐ‹ ํ›„์— ์‹คํ–‰๋˜๊ธด ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์˜์†์„ฑ ๋กœ์ง์ด ์ถ”๊ฐ€๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฒคํŠธ ๋ฐฉ์‹์„ ์„ ํƒํ•˜์˜€๋‹ค.)

1. ๋ฐœํ–‰ํ•  ์ด๋ฒคํŠธ์ธ MailSendEvent ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

public record SendMailEvent(
    Mail mail
) {
}

2. MailSendEvent ๋ฅผ ์ˆ˜์‹ ํ•  TransactionalEventListener ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ํŠธ๋žœ์žญ์…˜์ด ์ •์ƒ์ ์œผ๋กœ ์ปค๋ฐ‹์ด ๋œ ํ›„ ์‹คํ–‰๋˜๋„๋ก phase ์˜ต์…˜์„ AFTER_COMMIT ์œผ๋กœ ์„ค์ •ํ•œ๋‹ค.

@Service
public class SendMailListener {

    private final SpringMailService springMailService;

    @TransactionalEventListener(phase = AFTER_COMMIT)
    public void handleSendMailEvent(SendMailEvent event) {
        springMailService.sendMail(event.mail());
    }

}

3. ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์—์„œ SendMailEvent ๋ฅผ ๋ฐœํ–‰ํ•˜๋„๋ก ์ˆ˜์ •ํ•œ๋‹ค.

@Service
public class RecommendationService {
    ...
    private final ApplicationEventPublisher applicationEventPublisher;

    @Transactional
    public long registerRecommendation(RegisterRecommendationRequest request) {
        Store store = getStore(request);
        
        Recommendation recommendation = new Recommendation(store, request.information(), request.phoneNumber());
        Recommendation persistRecommendation = recommendationRepository.save(recommendation);

        Mail mail = new Mail(to, "[๋ฒ„๋น„] ์ธ๋””ํ”ผ ์„œ๋น„์Šค์— ์Œ์•…์ด ์ถ”์ฒœ๋˜์—ˆ์–ด์š”!",
            "์ถ”์ฒœ ์Œ์•… ์ •๋ณด: " + request.information() + "\n" +
            "์ถ”์ฒœ์ธ ์—ฐ๋ฝ์ฒ˜: " + request.phoneNumber() + "\n" +
            "๋งค์žฅ ์ด๋ฆ„: " + request.storeName() + "\n" +
            "๋งค์žฅ ์ฃผ์†Œ: " + request.storeAddress() + "\n");
        applicationEventPublisher.publishEvent(new SendMailEvent(mail));

        return persistRecommendation.getRecommendationId();
    }

์ด์ œ ApplicationEventPublisher ๋ฅผ ๋ชจํ‚นํ•˜์—ฌ SendMailEvent ๋ฐœํ–‰ ์—ฌ๋ถ€๋ฅผ ํ…Œ์ŠคํŠธํ•ด ๋ณด์ž.

@Test
@DisplayName("์„ฑ๊ณต: ์ถ”์ฒœ ์Œ์•… ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.")
void registerRecommendation() {
    ...

    // then
    verify(applicationEventPublisher, times(1)).publishEvent(any(SendMailEvent.class));
}

ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜์˜€์ง€๋งŒ ํ™•์‹คํ•˜๊ฒŒ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด ์Œ์•… ์ถ”์ฒœ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋œ ํ›„์— ์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•œ ๊ฑด์ง€ ๋กœ๊น…์„ ํ†ตํ•ด ํ™•์ธํ•ด ๋ณด์ž.

logging:
  level:
    org:
      springframework:
        orm: DEBUG

๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด ๋ณด๋ฉด ์Œ์•… ์ถ”์ฒœ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋œ ํ›„ ๋ฉ”์ผ์ด ์ „์†ก ๋กœ์ง์ด ์ƒ ํ–‰ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด๋กœ์จ ๋ฌธ์ œ 1: ์•Œ๋ฆผ๊ณผ ์Œ์•… ์ถ”์ฒœ์ด ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์— ์กด์žฌ๋Š” ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค. ํ•˜์ง€๋งŒ ํ•œ ๊ฐ€์ง€ ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค. 

@TransactionalEventListener(phase = AFTER_COMMIT)

์ปค๋ฐ‹๋œ ํ›„์— ์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•˜๋Š” ๊ฑด ์•Œ๊ฒ ๋Š”๋ฐ, ์ปค๋„ฅ์…˜์„ ๋ฐ˜ํ™˜ํ•˜๊ณ  ์†Œ๋น„ํ•˜๋Š” ๊ฑธ๊นŒ?

ProxyConnection ์˜ close() ๋ฉ”์„œ๋“œ์— ๋””๋ฒ„๊ทธ ํฌ์ธํŠธ๋ฅผ ์ฐ์–ด ์ปค๋„ฅ์…˜์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์‹œ์ ์„ ํ™•์ธํ•ด ๋ณด์•˜๋”๋‹ˆ ํŠธ๋žœ์žญ์…˜์ด ์ปค๋ฐ‹๋˜๊ณ  ๋‚˜์„œ ๋ฉ”์ผ์ด ์ „์†ก๋œ ์งํ›„๊นŒ์ง€ connection ์ด ๋ฌผ๋ ค์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

Spring ๊ณต์‹๋ฌธ์„œ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

WARNING: if the TransactionPhase is set to AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION, the transaction will have been committed or rolled back already, but the transactional resources might still be active and accessible.

๊ฒฝ๊ณ : ํŠธ๋žœ์žญ์…˜ ๋‹จ๊ณ„๊ฐ€ AFTER_COMMIT(๊ธฐ๋ณธ๊ฐ’), AFTER_ROLLBACK ๋˜๋Š” AFTER_COMPLETION์œผ๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ ํŠธ๋žœ์žญ์…˜์ด ์ด๋ฏธ ์ปค๋ฐ‹๋˜๊ฑฐ๋‚˜ ๋กค๋ฐฑ๋˜์—ˆ์ง€๋งŒ ํŠธ๋žœ์žญ์…˜ ๋ฆฌ์†Œ์Šค๋Š” ์—ฌ์ „ํžˆ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๊ณ  ์•ก์„ธ์Šค ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฆ‰, AFTER_COMMIT ์ด๋”๋ผ๋„ ์ปค๋„ฅ์…˜์ด ๋ฌผ๋ ค์žˆ๋Š” ์ƒํƒœ๋กœ ๋ฉ”์ผ ์ „์†ก API ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ๋ฌธ์ œ 2: ๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์— ์กด์žฌ ๊ฐ€ ์•„์ง ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜๋‹ค.

โœ… ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•˜๋Š” ์Šค๋ ˆ๋“œ๋ฅผ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒํ•˜๋Š” ์Šค๋ ˆ๋“œ์™€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„  ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” @Async ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

1. AsyncConfig ๋ฅผ ์„ค์ •ํ•œ๋‹ค.

@EnableAsync
@Configuration
public class AsyncConfig {

}

2. ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•ด์•ผ ํ•˜๋Š” EventListener ์— @Async ๋ฅผ ์ ์šฉํ•œ๋‹ค.

@Async
@TransactionalEventListener(phase = AFTER_COMMIT)
public void handleSendMailEvent(SendMailEvent event) {
    springMailService.sendMail(event.mail());
}

ํ˜„์žฌ ์Šค๋ ˆ๋“œ์˜ id ๋ฅผ ๋กœ๊ทธ๋ฅผ ์ฐ์–ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„œ๋กœ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด์ œ ๋ฌธ์ œ 2: ๋ฉ”์ผ ์ „์†ก ์„œ๋น„์Šค๊ฐ€ ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์— ์กด์žฌ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ฌธ์ œ 3: ๋™๊ธฐ์ ์ธ ๋ฐฉ์‹์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ๋‹ค.

๋‚จ์€ ๋ฌธ์ œ๋Š” ๋ฌธ์ œ 4: ๋ฉ”์ผ ์ „์†ก์ด ์‹คํŒจํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์กด์žฌ๋‹ค. ๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ•  ๊ฒฝ์šฐ ์ง€์ •๋œ ํšŸ์ˆ˜๋งŒํผ ์žฌ์‹œ๋„๋ฅผ ํ•˜๊ณ  ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๊ณ„์† ๋ฐœ์ƒํ•˜๋ฉด ERROR ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋„๋ก ์ˆ˜์ •ํ–ˆ๋‹ค.(ERROR ๋กœ๊ทธ ๋ฐœ์ƒ ์‹œ slack ์œผ๋กœ ์•Œ๋ฆผ์ด ๊ฐ€๋„๋ก logback ์„ ์„ค์ •ํ•œ ์ƒํƒœ)

private void send(SimpleMailMessage message) {
    for (int count = 0; count < MAX_RETRY_COUNT; count++) {
        try {
            mailSender.send(message);
            return;
        } catch (MailParseException | MailAuthenticationException exception) {
            log.error("๋ฉ”์ผ ์ „์†ก์„ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.", exception);
            return;
        } catch (MailSendException exception) {
            log.warn("๋ฉ”์ผ ์ „์†ก์„ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ํšŸ์ˆ˜: {}", count);
        }
    }
    log.error("๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ํšŸ์ˆ˜: {}", MAX_RETRY_COUNT);
}

โš’๏ธ ์ถ”๊ฐ€ ๋ฆฌํŒฉํ† ๋ง

์•ž์„œ ์ •์˜ํ•œ ๋ฌธ์ œ๋“ค์€ ํ•ด๊ฒฐํ–ˆ์ง€๋งŒ ์ถ”๊ฐ€์ ์œผ๋กœ ๋ฆฌํŒฉํ† ๋ง ํ•˜๊ณ  ์‹ถ์€ ๋ถ€๋ถ„๋“ค์ด ์žˆ๋‹ค.

  1. ๋ฉ”์ผ ์ „์†ก ๊ตฌํ˜„์ฒด(SpringMailService)๊ฐ€ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค.
  2. ์Œ์•… ์ถ”์ฒœ๊ณผ ํ‘ธ์‰ฌ ์•Œ๋ฆผ์€ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์—์„œ ๋ฉ”์ผ ๋‚ด์šฉ์„ ์ƒ์„ฑํ•˜๊ณ  ์žˆ๋‹ค. ์•Œ๋ฆผ ํฌ๋งท์ด ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ ์•Œ๋ฆผ ์„œ๋น„์Šค๊ฐ€ ์•„๋‹Œ ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์—์„œ ๋ณ€๊ฒฝํ•ด์•ผ ํ•œ๋‹ค. ์ด๊ฑด ์Œ์•… ์ถ”์ฒœ ์‹œ๋น„์Šค์˜ ์—ญํ• ์ด ์•„๋‹Œ ๊ฒƒ ๊ฐ™๋‹ค.
  3. ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์—์„œ ์•Œ๋ฆผ๋ฉ”์ผ์ „์†ก์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ  ์žˆ๋‹ค. ์•Œ๋ฆผ์€ ๋ฉ”์ผ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ FCM, ์นด์นด์˜คํ†ก, ๋ฌธ์ž ๋ฉ”์‹œ์ง€ ๋“ฑ์œผ๋กœ ์ „์†กํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋ฅธ ์•Œ๋ฆผ ๋ฐฉ์‹์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์— ์ถ”๊ฐ€์ ์œผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๋Š”๋ฐ ์ด ๋ถ€๋ถ„๋„ ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค์˜ ์—ญํ• ์ด ์•„๋‹Œ ๊ฒƒ ๊ฐ™๋‹ค.
  4. ์žฌ์‹œ๋„ ๋ฉ”์นด๋‹ˆ์ฆ˜์œผ๋กœ ๋ฉ”์ผ ์ „์†ก์˜ ์ •ํ•ฉ์„ฑ์„ ๋†’์˜€์ง€๋งŒ ๋งŒ์•ฝ ์ง€์ •ํ•œ ํšŸ์ˆ˜๋งŒํผ ์žฌ์‹œ๋„๋ฅผ ํ–ˆ์Œ์—๋„ ์ „์†ก์ด ์‹คํŒจํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ๊ฐ€ ์†Œ์‹ค๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

๐Ÿ”จ ์•Œ๋ฆผ ์„œ๋น„์Šค ์ถ”์ƒํ™”

MailService ๋ฅผ ์ธํ„ฐํŽ˜์ด์Šคํ™” ํ•ด์„œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌํ˜„์ฒด๊ฐ€ ์•„๋‹Œ ์ถ”์ƒ์ฒด๋ฅผ ์˜์กดํ•˜๋„๋ก ์ˆ˜์ •ํ•œ๋‹ค.

public interface MailService {
    void sendMail(Mail mail);
}

 SpringMailService ์ด MailService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋„๋ก ํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
public class SpringMailService implements MailService {

    @Override
    public void sendMail(Mail mail) {
        ...
    }
}

์ด์ œ ํด๋ผ์ด์–ธํŠธ์ธ SendMailListender ๊ฐ€ ์ถ”์ƒ์ฒด์ธ MailService ์— ์˜์กดํ•˜๋„๋ก ์ˆ˜์ •ํ•ด ๋ณด์ž.

@Service
@RequiredArgsConstructor
public class SendMailListener {

    private final MailService mailService;

    public void handleSendMailEvent(SendMailEvent event) {
        mailService.sendMail(event.mail());
    }
}

 

์ด์ œ ๋ฉ”์ผ ์ „์†ก ๊ตฌํ˜„์ฒด๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด๋„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ถ”์ƒํ™”์— ์˜์กดํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ์˜ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

๐Ÿ”จ  ์ฑ…์ž„ ๋ถ„๋ฆฌ

NotificationService ๋ฅผ ์ƒ์„ฑํ•˜์—ฌ RecommendationService ์—์„œ ๊ฐ–๊ณ  ์žˆ๋˜ ๋ฉ”์ผ ์ƒ์„ฑ๊ณผ ์ „์†ก์˜ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜์ž.

์šฐ์„  ์•Œ๋ฆผ์˜ ์ข…๋ฅ˜์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๊ฐ€ ์ „์†กํ•ด์•ผ ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์Œ์•… ์ถ”์ฒœ ์šฉ dto ์™€ ์ด๋ฒคํŠธ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

public record RecommendationMail(
    String to,
    String information,
    String phoneNumber,
    String storeName,
    String storeAddress
) {
}
public record RecommendationMailEvent(
   RecommendationMail request
) {
}

NotificationService ์—์„œ Mail ์„ ์ƒ์„ฑํ•˜๊ณ  ์ „์†กํ•˜๋„๋ก ์ˆ˜์ •ํ•œ๋‹ค.

@Service
public class NotificationService {
    ...
    public void sendRecommendationMail(RecommendationMail mail) {
        Mail mail = new Mail("[๋ฒ„๋น„] ๋ฌธ์˜๊ฐ€ ๋“ค์–ด์™”์–ด์š”!",
            "๋ฌธ์˜ ๋‚ด์šฉ: " + mail.content() + "\n" +
                "๋ฌธ์˜์ž ์„ฑํ•จ: " + mail.userName() + "\n" +
                "๋ฌธ์˜์ž ์—ฐ๋ฝ์ฒ˜: " + mail.phoneNumber() + "\n", request.to());
        mailService.sendMail(mail)
    }
}

RecommendationMailListener ์—์„œ NotificationService ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ฉ”์ผ์€ ์ „์†กํ•˜๋„๋ก ํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
public class RecommendationMailListener {

    private final NotificationService notificationService;

    @Async
    @TransactionalEventListener(phase = AFTER_COMMIT)
    public void handleRecommendationMailEvent(RecommendationMailEvent event) {
        notificationService.sendRecommendationMail(event.request());
    }

}

RecommendationService ์—์„œ๋Š” ์Œ์•… ์ถ”์ฒœ ์•Œ๋ฆผ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ด์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค.

@Service
@Transactional(readOnly = true)
public class RecommendationService {
    ...
    @Transactional
    public long registerRecommendation(RegisterRecommendationRequest request) {
        Store store = getStore(request);

        Recommendation recommendation = new Recommendation(store, request.information(), request.phoneNumber());
        recommendationRepository.save(recommendation);

        RecommendationMail recommendationMail = RecommendationMail.of(to, request.information(), request.phoneNumber(), store.getName(), store.getAddress());
        applicationEventPublisher.publishEvent(new RecommendationMailEvent(recommendationMail));

        return persistRecommendation.getRecommendationId();
    }

์ด์ œ ์•Œ๋ฆผ ์ƒ์„ฑ ๋ฐ ์ „์†ก์˜ ์ฑ…์ž„์ด NotificationService ๋กœ ๋ถ„๋ฆฌ๋˜์—ˆ๋‹ค. ์•Œ๋ฆผ ํฌ๋งท์ด ๋ณ€๊ฒฝ๋˜๊ฑฐ๋‚˜ ์•Œ๋ฆผ ๋งค์ฒด๊ฐ€ ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ ์Œ์•… ์ถ”์ฒœ ์„œ๋น„์Šค๊ฐ€ ์•„๋‹Œ ์•Œ๋ฆผ ์„œ๋น„์Šค๊ฐ€ ๋ณ€๊ฒฝ์˜ ์ฑ…์ž„์„ ๊ฐ–๊ฒŒ ๋œ๋‹ค.

๐Ÿ”จ  ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์ €์žฅ

์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์†Œ์‹ค์„ ๋Œ€๋น„ํ•˜์—ฌ ๋ฉ”์ผ ์•Œ๋ฆผ ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜์ž. ๋ฉ”์ผ ์•Œ๋ฆผ์ผ ๊ฒฝ์šฐ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ˆ˜์‹ ์ž ์ด๋ฉ”์ผ ์ปฌ๋Ÿผ์ด ํ•„์š”ํ•˜๋‹ค.

์ด์ œ ์•Œ๋ฆผ ์ „์†ก ๋ฉ”์„œ๋“œ์— ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์ด๋ฒคํŠธ ๋น„๋™๊ธฐ๋ฅผ ํ†ตํ•ด ๋ฉ”์ผ์„ ์ „์†กํ•˜๋„๋ก ์ˆ˜์ •ํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class NotificationService {

    private final NotificationRepository notificationRepository;
    private final ApplicationEventPublisher applicationEventPublisher;

    @Transactional
    public void sendRecommendationMail(RecommendationMail request) {
        MailNotification mailNotification = new MailNotification("[๋ฒ„๋น„] ์ธ๋””ํ”ผ ์„œ๋น„์Šค์— ์Œ์•…์ด ์ถ”์ฒœ๋˜์—ˆ์–ด์š”!",
            "์ถ”์ฒœ ์Œ์•… ์ •๋ณด: " + request.information() + "\n" +
                "์ถ”์ฒœ์ธ ์—ฐ๋ฝ์ฒ˜: " + request.phoneNumber() + "\n" +
                "๋งค์žฅ ์ด๋ฆ„: " + request.storeName() + "\n" +
                "๋งค์žฅ ์ฃผ์†Œ: " + request.storeAddress() + "\n", request.to());
        MailNotification persistMailNotification = notificationRepository.save(mailNotification);

        Mail mail = Mail.from(persistMailNotification);
        applicationEventPublisher.publishEvent(new SendMailEvent(mail));
    }
}

๋ฉ”์ผ ์ „์†ก ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•  ๋•Œ mail_notification_id ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋กœ๊น…ํ•  ๋•Œ ์ถ”๊ฐ€ํ•ด ์ฃผ์ž.

public record Mail(
    long id,
    String to,
    String subject,
    String text
) {
}
private void send(long id, SimpleMailMessage message) {
    for (int count = 0; count < MAX_RETRY_COUNT; count++) {
        try {
            mailSender.send(message);
            return;
        } catch (MailParseException | MailAuthenticationException exception) {
            log.error("๋ฉ”์ผ ์ „์†ก์„ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. mailNotificationId: {}", id, exception);
            return;
        } catch (MailSendException exception) {
            log.warn("๋ฉ”์ผ ์ „์†ก์„ ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. mailNotificationId: {} ์žฌ์‹œ๋„ ํšŸ์ˆ˜: {}", id, count, exception);
        }
    }
    log.error("๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค. mailNotificationId: {} ์žฌ์‹œ๋„ ํšŸ์ˆ˜: {}", id, MAX_RETRY_COUNT);
}

์ด์ œ ๋ฉ”์ผ ์ „์†ก API ์— ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ ERROR log ๋ฅผ ํ†ตํ•ด slack ์œผ๋กœ ์•Œ๋ฆผ์ด ์ „์†ก๋˜๊ณ  ๊ฐœ๋ฐœ์ž๋Š” ์•Œ๋ฆผ id ๋ฅผ ํ†ตํ•ด ์˜์†ํ™”๋œ ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด ์‰ฝ๊ฒŒ ๋Œ€์ฒ˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

๐Ÿ‘ ์ตœ์ข… ์„ค๊ณ„

์ˆ˜์ • ์‚ฌํ•ญ์„ ์ •๋ฆฌํ•ด๋ณด๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  1. ์Šคํ”„๋ง ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋น„์Šค๊ฐ„์˜ ๊ฐ•ํ•œ ๊ฒฐํ•ฉ๋„ ๊ฐ์†Œ
  2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 
  3. ์ถ”์ƒํ™”์™€ ์ฑ…์ž„ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•œ ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ ์ฆ๊ฐ€
  4. ์•Œ๋ฆผ ๋ฐ์ดํ„ฐ ์˜์†ํ™”๋ฅผ ํ†ตํ•œ ์•Œ๋ฆผ ์„œ๋น„์Šค ์•ˆ์ •์„ฑ ํ™•๋ณด