From fe8d36b63bc0bb220cf789ee57fd1194c359adf1 Mon Sep 17 00:00:00 2001 From: Woomin-Wang Date: Fri, 8 May 2026 00:40:50 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=B0=9C=EC=86=A1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/QuestionNotificationService.java | 71 +++++++++++++++++++ ...Service.java => TodayQuestionService.java} | 63 ++-------------- .../scheduler/TodayQuestionScheduler.java | 6 +- 3 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java rename src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/{DailyQuestionGenerationService.java => TodayQuestionService.java} (59%) diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java new file mode 100644 index 0000000..3b1402b --- /dev/null +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java @@ -0,0 +1,71 @@ +package io.wisoft.prepair.prepair_api.interview.question.service; + +import io.wisoft.prepair.prepair_api.external.member.dto.MemberSchedulerInfo; +import io.wisoft.prepair.prepair_api.external.member.enums.Notification; +import io.wisoft.prepair.prepair_api.external.notification.email.EmailService; +import io.wisoft.prepair.prepair_api.external.notification.kakao.KakaoService; +import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class QuestionNotificationService { + + private final EmailService emailService; + private final KakaoService kakaoService; + + public void notifyMember(MemberSchedulerInfo member, InterviewQuestion question) { + Notification notification = member.notification(); + + if (notification == Notification.EMAIL || notification == Notification.BOTH) { + sendEmail(member, question); + } + + if (notification == Notification.KAKAO || notification == Notification.BOTH) { + sendKakao(member, question); + } + } + + private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) { + try { + emailService.sendInterviewQuestion( + member.email(), + member.nickname(), + question.getQuestionTag(), + question.getQuestion() + ); + log.info("이메일 발송 완료 | memberId={}", member.id()); + } catch (Exception e) { + log.error("이메일 발송 실패 | memberId={}", member.id(), e); + } + } + + private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) { + if (!isValidKakaoToken(member)) return; + try { + kakaoService.sendInterviewQuestion( + member.kakaoAccessToken(), + question.getQuestion(), + question.getQuestionTag() + ); + log.info("카카오 발송 완료 | memberId={}", member.id()); + } catch (HttpClientErrorException.Unauthorized e) { + log.warn("카카오 발송 실패 | memberId={} | reason=token_expired", member.id()); + } catch (Exception e) { + log.error("카카오 발송 실패 | memberId={}", member.id(), e); + } + } + + private boolean isValidKakaoToken(MemberSchedulerInfo member) { + if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) { + log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id()); + return false; + } + return true; + } + +} diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java similarity index 59% rename from src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java rename to src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java index 1c918ac..9ca5169 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/DailyQuestionGenerationService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java @@ -1,19 +1,15 @@ package io.wisoft.prepair.prepair_api.interview.question.service; -import io.wisoft.prepair.prepair_api.external.member.enums.Notification; import io.wisoft.prepair.prepair_api.external.member.MemberServiceClient; import io.wisoft.prepair.prepair_api.external.member.dto.MemberSchedulerInfo; import io.wisoft.prepair.prepair_api.external.openai.OpenAiClient; import io.wisoft.prepair.prepair_api.external.openai.dto.QuestionWithTags; -import io.wisoft.prepair.prepair_api.external.notification.email.EmailService; -import io.wisoft.prepair.prepair_api.external.notification.kakao.KakaoService; import io.wisoft.prepair.prepair_api.interview.prompt.PromptBuilder; import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; import java.time.DayOfWeek; import java.time.LocalDate; @@ -23,15 +19,14 @@ @Slf4j @Service @RequiredArgsConstructor -public class DailyQuestionGenerationService { +public class TodayQuestionService { - private final QuestionRepository questionRepository; private final QuestionPersistenceService interviewQuestionService; + private final QuestionNotificationService questionNotificationService; + private final QuestionRepository questionRepository; private final MemberServiceClient memberServiceClient; private final OpenAiClient openAiClient; private final PromptBuilder promptBuilder; - private final EmailService emailService; - private final KakaoService kakaoService; public void generateTodayQuestions() { List members = memberServiceClient.getMembers(); @@ -58,54 +53,12 @@ private void processTodayQuestion(MemberSchedulerInfo member) { InterviewQuestion question = interviewQuestionService.saveTodayQuestion(member.id(), result); log.info("질문 저장 완료 | memberId={}", member.id()); - notifyMember(member, question); + questionNotificationService.notifyMember(member, question); } catch (Exception e) { log.error("질문 처리 실패 | memberId={}", member.id(), e); } } - private void notifyMember(MemberSchedulerInfo member, InterviewQuestion question) { - Notification notification = member.notification(); - - if (notification == Notification.EMAIL || notification == Notification.BOTH) { - sendEmail(member, question); - } - - if (notification == Notification.KAKAO || notification == Notification.BOTH) { - sendKakao(member, question); - } - } - - private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) { - try { - emailService.sendInterviewQuestion( - member.email(), - member.nickname(), - question.getQuestionTag(), - question.getQuestion() - ); - log.info("이메일 발송 완료 | memberId={}", member.id()); - } catch (Exception e) { - log.error("이메일 발송 실패 | memberId={}", member.id(), e); - } - } - - private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) { - if (!isValidKakaoToken(member)) return; - try { - kakaoService.sendInterviewQuestion( - member.kakaoAccessToken(), - question.getQuestion(), - question.getQuestionTag() - ); - log.info("카카오 발송 완료 | memberId={}", member.id()); - } catch (HttpClientErrorException.Unauthorized e) { - log.warn("카카오 발송 실패 | memberId={} | reason=token_expired", member.id()); - } catch (Exception e) { - log.error("카카오 발송 실패 | memberId={}", member.id(), e); - } - } - private boolean shouldSendToday(MemberSchedulerInfo member, DayOfWeek today) { return switch (member.frequency()) { case EVERY -> true; @@ -113,13 +66,6 @@ private boolean shouldSendToday(MemberSchedulerInfo member, DayOfWeek today) { }; } - private boolean isValidKakaoToken(MemberSchedulerInfo member) { - if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) { - log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id()); - return false; - } - return true; - } private boolean isValidNotification(MemberSchedulerInfo member) { if (member.notification() == null) { @@ -144,5 +90,4 @@ private boolean isValidJob(MemberSchedulerInfo member) { } return true; } - } diff --git a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java index c28bc45..27b238b 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java @@ -1,6 +1,6 @@ package io.wisoft.prepair.prepair_api.scheduler; -import io.wisoft.prepair.prepair_api.interview.question.service.DailyQuestionGenerationService; +import io.wisoft.prepair.prepair_api.interview.question.service.TodayQuestionService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; @@ -15,7 +15,7 @@ @RequiredArgsConstructor public class TodayQuestionScheduler { - private final DailyQuestionGenerationService dailyQuestionGenerationService; + private final TodayQuestionService todayQuestionService; @Scheduled(cron = "0 0 9 * * *") public void generateTodayQuestions() { @@ -23,7 +23,7 @@ public void generateTodayQuestions() { MDC.put("correlationId", correlationId); try { log.info("오늘의 질문 생성 스케줄러 시작 - {}", LocalDateTime.now()); - dailyQuestionGenerationService.generateTodayQuestions(); + todayQuestionService.generateTodayQuestions(); log.info("오늘의 질문 생성 스케줄러 종료"); } finally { MDC.remove("correlationId"); From a8c3718aa44dd32d43967107a5c2e380deb05e47 Mon Sep 17 00:00:00 2001 From: Woomin-Wang Date: Fri, 8 May 2026 00:57:24 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=20TimeZone=20=EB=AA=85=EC=8B=9C=20=EB=B0=8F=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=C2=B7=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=C2=B7=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/TodayQuestionService.java | 41 +++++++++---------- .../scheduler/TodayQuestionScheduler.java | 4 +- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java index 9ca5169..5a15e91 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/TodayQuestionService.java @@ -21,14 +21,14 @@ @RequiredArgsConstructor public class TodayQuestionService { - private final QuestionPersistenceService interviewQuestionService; + private final QuestionPersistenceService questionPersistenceService; private final QuestionNotificationService questionNotificationService; private final QuestionRepository questionRepository; private final MemberServiceClient memberServiceClient; private final OpenAiClient openAiClient; private final PromptBuilder promptBuilder; - public void generateTodayQuestions() { + public void sendTodayQuestions() { List members = memberServiceClient.getMembers(); DayOfWeek today = LocalDate.now(ZoneId.of("Asia/Seoul")).getDayOfWeek(); @@ -40,17 +40,17 @@ public void generateTodayQuestions() { .toList(); log.info("질문 생성 대상: {} / {} 명", targetMembers.size(), members.size()); - targetMembers.forEach(this::processTodayQuestion); + targetMembers.forEach(this::generateAndSend); } - private void processTodayQuestion(MemberSchedulerInfo member) { + private void generateAndSend(MemberSchedulerInfo member) { try { List previousQuestions = questionRepository.findByMemberId(member.id()); String prompt = promptBuilder.buildDailyQuestionPrompt(member.job(), previousQuestions); QuestionWithTags result = openAiClient.generateQuestion(prompt); log.info("질문 생성 완료 | memberId={}", member.id()); - InterviewQuestion question = interviewQuestionService.saveTodayQuestion(member.id(), result); + InterviewQuestion question = questionPersistenceService.saveTodayQuestion(member.id(), result); log.info("질문 저장 완료 | memberId={}", member.id()); questionNotificationService.notifyMember(member, question); @@ -59,22 +59,6 @@ private void processTodayQuestion(MemberSchedulerInfo member) { } } - private boolean shouldSendToday(MemberSchedulerInfo member, DayOfWeek today) { - return switch (member.frequency()) { - case EVERY -> true; - case WEEKLY -> today == DayOfWeek.MONDAY; - }; - } - - - private boolean isValidNotification(MemberSchedulerInfo member) { - if (member.notification() == null) { - log.warn("멤버 스킵 | memberId={} | reason=no_notification", member.id()); - return false; - } - return true; - } - private boolean isValidFrequency(MemberSchedulerInfo member) { if (member.frequency() == null) { log.warn("멤버 스킵 | memberId={} | reason=no_frequency", member.id()); @@ -90,4 +74,19 @@ private boolean isValidJob(MemberSchedulerInfo member) { } return true; } + + private boolean isValidNotification(MemberSchedulerInfo member) { + if (member.notification() == null) { + log.warn("멤버 스킵 | memberId={} | reason=no_notification", member.id()); + return false; + } + return true; + } + + private boolean shouldSendToday(MemberSchedulerInfo member, DayOfWeek today) { + return switch (member.frequency()) { + case EVERY -> true; + case WEEKLY -> today == DayOfWeek.MONDAY; + }; + } } diff --git a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java index 27b238b..f41839f 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java @@ -17,13 +17,13 @@ public class TodayQuestionScheduler { private final TodayQuestionService todayQuestionService; - @Scheduled(cron = "0 0 9 * * *") + @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") public void generateTodayQuestions() { String correlationId = "SCHEDULER-" + UUID.randomUUID().toString(); MDC.put("correlationId", correlationId); try { log.info("오늘의 질문 생성 스케줄러 시작 - {}", LocalDateTime.now()); - todayQuestionService.generateTodayQuestions(); + todayQuestionService.sendTodayQuestions(); log.info("오늘의 질문 생성 스케줄러 종료"); } finally { MDC.remove("correlationId"); From e8623e87e789be3bd82fb22e4c95aa1abe35f8e7 Mon Sep 17 00:00:00 2001 From: Woomin-Wang Date: Fri, 8 May 2026 13:47:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/TodayQuestionScheduler.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java index f41839f..f38c6fc 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/scheduler/TodayQuestionScheduler.java @@ -7,7 +7,6 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; import java.util.UUID; @Slf4j @@ -19,13 +18,18 @@ public class TodayQuestionScheduler { @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") public void generateTodayQuestions() { - String correlationId = "SCHEDULER-" + UUID.randomUUID().toString(); + String correlationId = "SCHEDULER-" + UUID.randomUUID(); MDC.put("correlationId", correlationId); + long startTime = System.currentTimeMillis(); + try { - log.info("오늘의 질문 생성 스케줄러 시작 - {}", LocalDateTime.now()); + log.info("오늘의 질문 생성 스케줄러 시작"); todayQuestionService.sendTodayQuestions(); - log.info("오늘의 질문 생성 스케줄러 종료"); - } finally { + log.info("오늘의 질문 생성 스케줄러 종료 | elapsed={}ms", System.currentTimeMillis() - startTime); + } catch(Exception e) { + log.error("오늘의 질문 생성 스케줄러 실패 | elapsed={}ms", System.currentTimeMillis() - startTime, e); + } + finally { MDC.remove("correlationId"); } } From 7665c17b40712e39e9a559b4ab029be43b7fe958 Mon Sep 17 00:00:00 2001 From: Woomin-Wang Date: Sat, 9 May 2026 00:55:03 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC/?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EC=95=8C=EB=A6=BC=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++ .../prepair_api/PrepairApiApplication.java | 2 ++ .../notification/email/EmailService.java | 35 +++++++++---------- .../notification/kakao/KakaoService.java | 14 +++++--- .../service/QuestionNotificationService.java | 19 ++++------ 5 files changed, 41 insertions(+), 34 deletions(-) diff --git a/build.gradle b/build.gradle index 0e0afc5..e383495 100644 --- a/build.gradle +++ b/build.gradle @@ -43,8 +43,13 @@ dependencies { implementation 'software.amazon.awssdk:s3:2.26.12' runtimeOnly 'org.postgresql:postgresql' + // 헬스 체크 implementation 'org.springframework.boot:spring-boot-starter-actuator' + // 재시도 + implementation 'org.springframework.retry:spring-retry:2.0.10' + + // 롬복 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/io/wisoft/prepair/prepair_api/PrepairApiApplication.java b/src/main/java/io/wisoft/prepair/prepair_api/PrepairApiApplication.java index e131e0d..9502b3d 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/PrepairApiApplication.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/PrepairApiApplication.java @@ -3,8 +3,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableRetry @EnableJpaAuditing @EnableScheduling @SpringBootApplication diff --git a/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java index 1c078ff..ce43567 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/email/EmailService.java @@ -1,17 +1,15 @@ package io.wisoft.prepair.prepair_api.external.notification.email; -import io.wisoft.prepair.prepair_api.common.exception.BusinessException; -import io.wisoft.prepair.prepair_api.common.exception.ErrorCode; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; -@Slf4j @Service @RequiredArgsConstructor public class EmailService { @@ -19,23 +17,24 @@ public class EmailService { private final EmailTemplateBuilder emailTemplateBuilder; private final JavaMailSender mailSender; - public void sendInterviewQuestion(String email, String nickname, String questionTag, String question) { + @Retryable( + value = {MessagingException.class, MailException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 1000, multiplier = 2) + ) + public void send(String email, String nickname, String questionTag, String question) throws MessagingException { String html = emailTemplateBuilder.buildInterviewQuestionHtml(nickname, questionTag, question); - send(email, "[PrePair] 오늘의 면접 질문이 도착했어요!", html); + sendMail(email, "[PrePair] 오늘의 면접 질문이 도착했어요!", html); } - private void send(String to, String subject, String html) { - try { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); + private void sendMail(String to, String subject, String html) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); - helper.setTo(to); - helper.setSubject(subject); - helper.setText(html, true); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(html, true); - mailSender.send(message); - } catch (MessagingException | MailException e) { - throw new BusinessException(ErrorCode.EMAIL_SEND_FAILED); - } + mailSender.send(message); } -} +} \ No newline at end of file diff --git a/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java index f882536..8ff740b 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/external/notification/kakao/KakaoService.java @@ -1,15 +1,16 @@ package io.wisoft.prepair.prepair_api.external.notification.kakao; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; -@Slf4j @Service @RequiredArgsConstructor public class KakaoService { @@ -17,7 +18,12 @@ public class KakaoService { private final RestClient restClient; private static final String URL = "https://kapi.kakao.com/v2/api/talk/memo/default/send"; - public void sendInterviewQuestion(String kakaoAccessToken, String question, String questionTag) { + @Retryable( + value = {HttpServerErrorException.class, ResourceAccessException.class}, + maxAttempts = 3, + backoff = @Backoff(delay = 1000, multiplier = 2) + ) + public void send(String kakaoAccessToken, String question, String questionTag) { String templateJson = """ { "object_type": "text", diff --git a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java index 3b1402b..d7e852e 100644 --- a/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java +++ b/src/main/java/io/wisoft/prepair/prepair_api/interview/question/service/QuestionNotificationService.java @@ -32,7 +32,7 @@ public void notifyMember(MemberSchedulerInfo member, InterviewQuestion question) private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) { try { - emailService.sendInterviewQuestion( + emailService.send( member.email(), member.nickname(), question.getQuestionTag(), @@ -45,9 +45,13 @@ private void sendEmail(MemberSchedulerInfo member, InterviewQuestion question) { } private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) { - if (!isValidKakaoToken(member)) return; + if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) { + log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id()); + return; + } + try { - kakaoService.sendInterviewQuestion( + kakaoService.send( member.kakaoAccessToken(), question.getQuestion(), question.getQuestionTag() @@ -59,13 +63,4 @@ private void sendKakao(MemberSchedulerInfo member, InterviewQuestion question) { log.error("카카오 발송 실패 | memberId={}", member.id(), e); } } - - private boolean isValidKakaoToken(MemberSchedulerInfo member) { - if (member.kakaoAccessToken() == null || member.kakaoAccessToken().isBlank()) { - log.warn("멤버 스킵 | memberId={} | reason=no_kakao_token", member.id()); - return false; - } - return true; - } - }