Skip to content

[COT-264] Feature: 메일 전송 API 구현 #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from

Conversation

gikhoon
Copy link
Member

@gikhoon gikhoon commented May 1, 2025

주의

아직 테스트 코드 작성 미완성
HTML 변경 가능

Motivation

메일 전송 시 Aws Ses로 메일 전송 + 로그 작성

Modification

메일 전송 시 NOT_SENT 또는 Failed 상태의 요청들에게 전송
전솓된 메일은 상태 저장
전송 로그 저장

Result

https://youthing.atlassian.net/browse/COT-264

Test (option)

SQL

@gikhoon gikhoon added the do not merge 의존성이나 여타 이유로 당장 머지하면 안되는 PR label May 1, 2025
@gikhoon gikhoon added this to the 모집 이메일 전송 milestone May 1, 2025
@gikhoon gikhoon self-assigned this May 1, 2025
@gikhoon gikhoon requested a review from Youthhing May 1, 2025 07:24
@gikhoon
Copy link
Member Author

gikhoon commented May 1, 2025

아직 테스트 코드 작성 미완성과 HTML 문구 미완성

refactor할 거 많을 것 같아서 미리 올리고 리뷰 받으면서 수정하겠습니다

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 왜 맨날 순서가 바뀌지 .. 둘이 쓰는 코드 포매터가 다른가

Comment on lines +11 to +19
@Configuration
public class AwsSesConfig {

@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

환경변수에 있는 값들 이렇게 Bean으로 등록해서 관리할 수 있는데 외부에서 주입 필요할땐 다음에 이렇게 쓰면 좋을 것 같아요

@Getter
@Setter
@ConfigurationProperties(prefix = "cloud.aws.s3")
public class S3Properties {
	private String accessKey;
	private String secretKey;
	private String region;
	private String bucketName;
}

Comment on lines +27 to +29
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(100);
taskExecutor.setQueueCapacity(10000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 각 필드 개수 선정한 기준이 있나여?

Comment on lines +22 to +23
@Value("${cloud.aws.ses.emailAddress}")
private String from;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 딱 이런 부분에서 위에 언급한 것처럼 yaml 파일 상수로 가져와서 쓰면 좋을 것 같아요

package org.cotato.csquiz.domain.recruitment.email;

public class RecruitmentEmailFactory {
private static final String LINK_URL = "https://www.cotato.kr";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요것도 환경변수로 넣어서 QA랑 main에서 활용을 구분하면 좋을 것 같습니다 !

public class RecruitmentEmailFactory {
private static final String LINK_URL = "https://www.cotato.kr";

public static EmailContent createForGeneration(int generation) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기수를 위해서 만든다? 메서드명이 너무 모호한 것 같아요

모집 안내 전용인가..? 용도에 맞게 구체화되었으면 좋겠습니다 !

Comment on lines +17 to +19
@Modifying
@Query("update RecruitmentNotificationRequester r set r.sendStatus = :status where r.id in :ids")
void updateSendStatusByIds(@Param("status") SendStatus sendStatus, @Param("ids") List<Long> ids);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update 쿼리인데 트랜잭션없어도 괜찮을까여?


private final JdbcTemplate jdbcTemplate;

public void saveAllWithBatch(List<RecruitmentNotificationEmailLog> logs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update 를 하는 부분인데 transaction 안걸어도 될까여?

//메일 전송 + 로그 작성 비동기 처리
List<CompletableFuture<NotificationResult>> notificationTasks = allNotSentOrFailRequester.stream()
.map(requester -> recruitmentNotificationSender.sendNotificationAsync(requester,
emailContent.htmlBody(), emailContent.subject()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EmailContent클래스를 정보 전달용으로 추가하신 것 같은데 파라미터는 또 변수를 하나씩 넘기는 이유가 있나요?

Comment on lines +20 to +38
@Async("emailSendThreadPoolExecutor")
public CompletableFuture<NotificationResult> sendNotificationAsync(final RecruitmentNotificationRequester requester,
final String htmlBody, final String subject
) {
boolean success = true;
try {
awsMailSender.sendRawMessageBody(
requester.getEmail(),
htmlBody,
subject
);
} catch (Exception e) {
success = false;
requester.updateSendStatus(SendStatus.FAIL);
log.info("메일 전송 실패 email: {} exception: {}", requester.getEmail(), e.getMessage());
}

return CompletableFuture.completedFuture(NotificationResult.of(requester.getId(), success));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우선은 실패시 requester 객체를 update하는 부분이 어색합니다.

메일 전송이라는 로직에서 결과를 업데이트하는 로직까지 들어가는 부분이 어색합니다
추가로, 성공을 한 경우엔 updateSendStatus(SUCCESS)로 수정할 필요가 없는걸까요?

어색함을 떠나서 해당 로직을 고려해보면 아래와 같은 문제가 있을 것 같아요.

비동기로 처리하면 기존 스레드가 아닌 스레드 풀에 있는 새로운 스레드를 활용하는데 새로운 스레드는 기존 트랜잭션의 영향을 받지 않고 결국 기존 영속성 컨텍스트에서 관리하지 않고 결국 변경 사항은 flush 되지 않아서 .. 해당 부분에서 update를 해도 반영이 안되는 것 아닌가 싶습니다.

private void saveEmailLogs(List<RecruitmentNotificationRequester> requesters,
List<NotificationResult> results,
RecruitmentNotification notification) {
Map<Long, RecruitmentNotificationRequester> reqesterMap = requesters.stream()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map은 valueByKey와 같이 변수명 써주시면 좋을 것 같아요 !
Long타입으로 있는 키가 어떤 값을 의미하는지 알기 어려워서요

Comment on lines +102 to +106
.map(r -> RecruitmentNotificationEmailLog.of(
reqesterMap.get(r.requestId()),
notification,
r.success()
))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기도 내부 r이 어떤 의미인지 조금 더 구체적으로 들어가면 좋겠습니다 !

람다 내부라 축악형이라도 표현이 되면 좋을 것 같아요

Comment on lines +20 to +38
@Async("emailSendThreadPoolExecutor")
public CompletableFuture<NotificationResult> sendNotificationAsync(final RecruitmentNotificationRequester requester,
final String htmlBody, final String subject
) {
boolean success = true;
try {
awsMailSender.sendRawMessageBody(
requester.getEmail(),
htmlBody,
subject
);
} catch (Exception e) {
success = false;
requester.updateSendStatus(SendStatus.FAIL);
log.info("메일 전송 실패 email: {} exception: {}", requester.getEmail(), e.getMessage());
}

return CompletableFuture.completedFuture(NotificationResult.of(requester.getId(), success));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요건 살짝 별개인데 CompletableFutrue의 run-async와 exceptionally를 아래와 같이 활용하면 Try-catch 문을 줄일 수 있을 것 같아요

Suggested change
@Async("emailSendThreadPoolExecutor")
public CompletableFuture<NotificationResult> sendNotificationAsync(final RecruitmentNotificationRequester requester,
final String htmlBody, final String subject
) {
boolean success = true;
try {
awsMailSender.sendRawMessageBody(
requester.getEmail(),
htmlBody,
subject
);
} catch (Exception e) {
success = false;
requester.updateSendStatus(SendStatus.FAIL);
log.info("메일 전송 실패 email: {} exception: {}", requester.getEmail(), e.getMessage());
}
return CompletableFuture.completedFuture(NotificationResult.of(requester.getId(), success));
}
@Async("emailSendThreadPoolExecutor")
public CompletableFuture<NotificationResult> sendNotificationAsync(
final RecruitmentNotificationRequester requester,
final String htmlBody, final String subject
) {
return sendEmailAsync(requester, htmlBody, subject)
.thenApply(v -> NotificationResult.of(requester.getId(), true))
.exceptionally(e -> {
log.error("메일 전송 중 예외 발생 email: {}, exception: {}", requester.getEmail(), e.getMessage());
requester.updateSendStatus(SendStatus.FAIL);
return NotificationResult.of(requester.getId(), false);
});
}
private CompletableFuture<Void> sendEmailAsync(RecruitmentNotificationRequester requester, String htmlBody, String subject) {
return CompletableFuture.runAsync(() -> {
awsMailSender.sendRawMessageBody(
requester.getEmail(),
htmlBody,
subject
);
});
}

@Slf4j
@Component
@RequiredArgsConstructor
public class AwsMailSender {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구체적으로 어느 서비스를 활용한 구현체인지 알아야하니까 Ses Email Sender로 가시죠 !!

@Value("${cloud.aws.ses.emailAddress}")
private String from;

public void sendRawMessageBody(String recipient, String htmlBody, String subject) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기 recipient가 Email 형식인지 검증 안해도 괜찮을까여?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do not merge 의존성이나 여타 이유로 당장 머지하면 안되는 PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants