-
Notifications
You must be signed in to change notification settings - Fork 1
[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
base: develop
Are you sure you want to change the base?
Conversation
아직 테스트 코드 작성 미완성과 HTML 문구 미완성 refactor할 거 많을 것 같아서 미리 올리고 리뷰 받으면서 수정하겠습니다 |
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.PutMapping; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요건 왜 맨날 순서가 바뀌지 .. 둘이 쓰는 코드 포매터가 다른가
@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; |
There was a problem hiding this comment.
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;
}
taskExecutor.setCorePoolSize(20); | ||
taskExecutor.setMaxPoolSize(100); | ||
taskExecutor.setQueueCapacity(10000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요거 각 필드 개수 선정한 기준이 있나여?
@Value("${cloud.aws.ses.emailAddress}") | ||
private String from; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기수를 위해서 만든다? 메서드명이 너무 모호한 것 같아요
모집 안내 전용인가..? 용도에 맞게 구체화되었으면 좋겠습니다 !
@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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EmailContent클래스를 정보 전달용으로 추가하신 것 같은데 파라미터는 또 변수를 하나씩 넘기는 이유가 있나요?
@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)); | ||
} |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
map은 valueByKey
와 같이 변수명 써주시면 좋을 것 같아요 !
Long타입으로 있는 키가 어떤 값을 의미하는지 알기 어려워서요
.map(r -> RecruitmentNotificationEmailLog.of( | ||
reqesterMap.get(r.requestId()), | ||
notification, | ||
r.success() | ||
)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요기도 내부 r이 어떤 의미인지 조금 더 구체적으로 들어가면 좋겠습니다 !
람다 내부라 축악형이라도 표현이 되면 좋을 것 같아요
@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)); | ||
} |
There was a problem hiding this comment.
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 문을 줄일 수 있을 것 같아요
@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 { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요기 recipient가 Email 형식인지 검증 안해도 괜찮을까여?
주의
아직 테스트 코드 작성 미완성
HTML 변경 가능
Motivation
메일 전송 시 Aws Ses로 메일 전송 + 로그 작성
Modification
메일 전송 시 NOT_SENT 또는 Failed 상태의 요청들에게 전송
전솓된 메일은 상태 저장
전송 로그 저장
Result
https://youthing.atlassian.net/browse/COT-264
Test (option)
SQL