๐ ๊ฐ์
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);
}
โ๏ธ ์ถ๊ฐ ๋ฆฌํฉํ ๋ง
์์ ์ ์ํ ๋ฌธ์ ๋ค์ ํด๊ฒฐํ์ง๋ง ์ถ๊ฐ์ ์ผ๋ก ๋ฆฌํฉํ ๋ง ํ๊ณ ์ถ์ ๋ถ๋ถ๋ค์ด ์๋ค.
- ๋ฉ์ผ ์ ์ก ๊ตฌํ์ฒด(SpringMailService)๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ๋ณ๊ฒฝํด์ผ ํ๋ค.
- ์์ ์ถ์ฒ๊ณผ ํธ์ฌ ์๋ฆผ์ ๋ค๋ฅธ ๋๋ฉ์ธ์์๋ ๋ถ๊ตฌํ๊ณ ์์ ์ถ์ฒ ์๋น์ค์์ ๋ฉ์ผ ๋ด์ฉ์ ์์ฑํ๊ณ ์๋ค. ์๋ฆผ ํฌ๋งท์ด ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ ์๋ฆผ ์๋น์ค๊ฐ ์๋ ์์ ์ถ์ฒ ์๋น์ค์์ ๋ณ๊ฒฝํด์ผ ํ๋ค. ์ด๊ฑด ์์ ์ถ์ฒ ์๋น์ค์ ์ญํ ์ด ์๋ ๊ฒ ๊ฐ๋ค.
- ์์ ์ถ์ฒ ์๋น์ค์์ ์๋ฆผ๋ฉ์ผ์ ์ก์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ณ ์๋ค. ์๋ฆผ์ ๋ฉ์ผ๋ฟ๋ง ์๋๋ผ FCM, ์นด์นด์คํก, ๋ฌธ์ ๋ฉ์์ง ๋ฑ์ผ๋ก ์ ์กํ ์ ์๋ค. ๋ค๋ฅธ ์๋ฆผ ๋ฐฉ์์ด ์ถ๊ฐ๋ ๊ฒฝ์ฐ ์์ ์ถ์ฒ ์๋น์ค์ ์ถ๊ฐ์ ์ผ๋ก ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋๋ฐ ์ด ๋ถ๋ถ๋ ์์ ์ถ์ฒ ์๋น์ค์ ์ญํ ์ด ์๋ ๊ฒ ๊ฐ๋ค.
- ์ฌ์๋ ๋ฉ์นด๋์ฆ์ผ๋ก ๋ฉ์ผ ์ ์ก์ ์ ํฉ์ฑ์ ๋์์ง๋ง ๋ง์ฝ ์ง์ ํ ํ์๋งํผ ์ฌ์๋๋ฅผ ํ์์๋ ์ ์ก์ด ์คํจํ ๊ฒฝ์ฐ ํด๋น ์๋ฆผ ๋ฐ์ดํฐ๊ฐ ์์ค๋๋ ๋ฌธ์ ๊ฐ ์๋ค.
๐จ ์๋ฆผ ์๋น์ค ์ถ์ํ
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 ๋ฅผ ํตํด ์์ํ๋ ์๋ฆผ ๋ฐ์ดํฐ๋ฅผ ํตํด ์ฝ๊ฒ ๋์ฒํ ์ ์๊ฒ ๋์๋ค.
๐ ์ต์ข ์ค๊ณ
์์ ์ฌํญ์ ์ ๋ฆฌํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
- ์คํ๋ง ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํ์ฌ ์๋น์ค๊ฐ์ ๊ฐํ ๊ฒฐํฉ๋ ๊ฐ์
- ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ํตํ ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
- ์ถ์ํ์ ์ฑ ์ ๋ถ๋ฆฌ๋ฅผ ํตํ ํ์ฅ์ฑ๊ณผ ์ ์ง๋ณด์์ฑ ์ฆ๊ฐ
- ์๋ฆผ ๋ฐ์ดํฐ ์์ํ๋ฅผ ํตํ ์๋ฆผ ์๋น์ค ์์ ์ฑ ํ๋ณด