Skip to content

Commit 7665c17

Browse files
committed
feat: 이메일/카카오 알림 발송 재시도 로직 추가
1 parent e8623e8 commit 7665c17

5 files changed

Lines changed: 41 additions & 34 deletions

File tree

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ dependencies {
4343
implementation 'software.amazon.awssdk:s3:2.26.12'
4444
runtimeOnly 'org.postgresql:postgresql'
4545

46+
// 헬스 체크
4647
implementation 'org.springframework.boot:spring-boot-starter-actuator'
4748

49+
// 재시도
50+
implementation 'org.springframework.retry:spring-retry:2.0.10'
51+
52+
// 롬복
4853
compileOnly 'org.projectlombok:lombok'
4954
annotationProcessor 'org.projectlombok:lombok'
5055

src/main/java/io/wisoft/prepair/prepair_api/PrepairApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.retry.annotation.EnableRetry;
67
import org.springframework.scheduling.annotation.EnableScheduling;
78

9+
@EnableRetry
810
@EnableJpaAuditing
911
@EnableScheduling
1012
@SpringBootApplication
Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,40 @@
11
package io.wisoft.prepair.prepair_api.external.notification.email;
22

3-
import io.wisoft.prepair.prepair_api.common.exception.BusinessException;
4-
import io.wisoft.prepair.prepair_api.common.exception.ErrorCode;
53
import jakarta.mail.MessagingException;
64
import jakarta.mail.internet.MimeMessage;
75
import lombok.RequiredArgsConstructor;
8-
import lombok.extern.slf4j.Slf4j;
96
import org.springframework.mail.MailException;
107
import org.springframework.mail.javamail.JavaMailSender;
118
import org.springframework.mail.javamail.MimeMessageHelper;
9+
import org.springframework.retry.annotation.Backoff;
10+
import org.springframework.retry.annotation.Retryable;
1211
import org.springframework.stereotype.Service;
1312

14-
@Slf4j
1513
@Service
1614
@RequiredArgsConstructor
1715
public class EmailService {
1816

1917
private final EmailTemplateBuilder emailTemplateBuilder;
2018
private final JavaMailSender mailSender;
2119

22-
public void sendInterviewQuestion(String email, String nickname, String questionTag, String question) {
20+
@Retryable(
21+
value = {MessagingException.class, MailException.class},
22+
maxAttempts = 3,
23+
backoff = @Backoff(delay = 1000, multiplier = 2)
24+
)
25+
public void send(String email, String nickname, String questionTag, String question) throws MessagingException {
2326
String html = emailTemplateBuilder.buildInterviewQuestionHtml(nickname, questionTag, question);
24-
send(email, "[PrePair] 오늘의 면접 질문이 도착했어요!", html);
27+
sendMail(email, "[PrePair] 오늘의 면접 질문이 도착했어요!", html);
2528
}
2629

27-
private void send(String to, String subject, String html) {
28-
try {
29-
MimeMessage message = mailSender.createMimeMessage();
30-
MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8");
30+
private void sendMail(String to, String subject, String html) throws MessagingException {
31+
MimeMessage message = mailSender.createMimeMessage();
32+
MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8");
3133

32-
helper.setTo(to);
33-
helper.setSubject(subject);
34-
helper.setText(html, true);
34+
helper.setTo(to);
35+
helper.setSubject(subject);
36+
helper.setText(html, true);
3537

36-
mailSender.send(message);
37-
} catch (MessagingException | MailException e) {
38-
throw new BusinessException(ErrorCode.EMAIL_SEND_FAILED);
39-
}
38+
mailSender.send(message);
4039
}
41-
}
40+
}

src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
package io.wisoft.prepair.prepair_api.external.notification.kakao;
22

33
import lombok.RequiredArgsConstructor;
4-
import lombok.extern.slf4j.Slf4j;
54
import org.springframework.http.MediaType;
5+
import org.springframework.retry.annotation.Backoff;
6+
import org.springframework.retry.annotation.Retryable;
67
import org.springframework.stereotype.Service;
78
import org.springframework.util.LinkedMultiValueMap;
89
import org.springframework.util.MultiValueMap;
9-
import org.springframework.web.client.HttpClientErrorException;
10+
import org.springframework.web.client.HttpServerErrorException;
11+
import org.springframework.web.client.ResourceAccessException;
1012
import org.springframework.web.client.RestClient;
1113

12-
@Slf4j
1314
@Service
1415
@RequiredArgsConstructor
1516
public class KakaoService {
1617

1718
private final RestClient restClient;
1819
private static final String URL = "https://kapi.kakao.com/v2/api/talk/memo/default/send";
1920

20-
public void sendInterviewQuestion(String kakaoAccessToken, String question, String questionTag) {
21+
@Retryable(
22+
value = {HttpServerErrorException.class, ResourceAccessException.class},
23+
maxAttempts = 3,
24+
backoff = @Backoff(delay = 1000, multiplier = 2)
25+
)
26+
public void send(String kakaoAccessToken, String question, String questionTag) {
2127
String templateJson = """
2228
{
2329
"object_type": "text",

src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void notifyMember(MemberSchedulerInfo member, InterviewQuestion question)
3232

3333
private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) {
3434
try {
35-
emailService.sendInterviewQuestion(
35+
emailService.send(
3636
member.email(),
3737
member.nickname(),
3838
question.getQuestionTag(),
@@ -45,9 +45,13 @@ private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) {
4545
}
4646

4747
private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) {
48-
if (!isValidKakaoToken(member)) return;
48+
if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) {
49+
log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id());
50+
return;
51+
}
52+
4953
try {
50-
kakaoService.sendInterviewQuestion(
54+
kakaoService.send(
5155
member.kakaoAccessToken(),
5256
question.getQuestion(),
5357
question.getQuestionTag()
@@ -59,13 +63,4 @@ private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) {
5963
log.error("카카오 발송 실패 | memberId={}", member.id(), e);
6064
}
6165
}
62-
63-
private boolean isValidKakaoToken(MemberSchedulerInfo member) {
64-
if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) {
65-
log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id());
66-
return false;
67-
}
68-
return true;
69-
}
70-
7166
}

0 commit comments

Comments
 (0)