λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

ν”„λ‘œμ νŠΈ/Airbnb Clone

@Async 이메일 전솑 고도화: μž¬μ‹œλ„, μ˜ˆμ™Έ 처리, ν…ŒμŠ€νŠΈ

인증 이메일 전솑 μ‹œ, κ°„ν˜Ή λ„€νŠΈμ›Œν¬ 였λ₯˜λ‘œ 인해 메일이 μ •μƒμ μœΌλ‘œ μ „μ†‘λ˜μ§€ μ•ŠλŠ” κ²½μš°κ°€ λ°œμƒν•©λ‹ˆλ‹€.

μ΄λŸ¬ν•œ μƒν™©μ—μ„œ μ‚¬μš©μžμ—κ²Œ μ¦‰μ‹œ 였λ₯˜λ₯Ό μ•Œλ¦¬λŠ” λŒ€μ‹ , μž¬μ‹œλ„ λ‘œμ§μ„ 톡해 이메일 전솑 성곡 κ°€λŠ₯성을 λ†’μ΄λŠ” 것이 μ‚¬μš©μž κ²½ν—˜μ„ ν–₯μƒν•˜λŠ” μ€‘μš”ν•œ μš”μ†ŒλΌκ³  νŒλ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.

 

κ·Έλž˜μ„œ, μž¬μ‹œλ„ μΆ”κ°€ μ‹œ κ³ λ €ν•œ 것듀을 λ‹€μŒκ³Ό 같은 λͺ©μ°¨λ‘œ μ†Œκ°œν•©λ‹ˆλ‹€.

  • μž¬μ‹œλ„(@Retryable) 쑰건 μ„€μ •
  • μž¬μ‹œλ„ μ˜ˆμ™Έμ™€ μ“°λ ˆλ“œ ν’€ μ˜ˆμ™Έλ₯Ό κ΅¬λΆ„ν•΄μ„œ μ²˜λ¦¬ν•˜κΈ°
  • μž¬μ‹œλ„ 및 볡ꡬ가 잘 λ˜μ—ˆλŠ”μ§€ ν…ŒμŠ€νŠΈν•˜κΈ°

* λΉ„λ™κΈ°μ—μ„œ μž¬μ‹œλ„ 둜직 κ΅¬ν˜„μ„ μœ„ν•΄ Baeldung(async-retry)λ₯Ό μ°Έκ³ ν–ˆμŠ΅λ‹ˆλ‹€.

 

μž¬μ‹œλ„(@Retryable) 쑰건 μ„€μ •

λ¨Όμ €, 이메일 전솑에 μž¬μ‹œλ„ 쑰건을 μ„€μ •ν•˜κΈ° μœ„ν•΄ Spring Mail κ΄€λ ¨ μ˜ˆμ™Έλ₯Ό μ†Œκ°œν•©λ‹ˆλ‹€.

    • MailAuthenticationException : 인증 μ‹€νŒ¨ μ‹œ λ°œμƒν•˜λŠ” μ˜ˆμ™Έ
    • MailException : 메일 전솑과 κ΄€λ ¨λœ λͺ¨λ“  μ˜ˆμ™Έμ˜ κΈ°λ³Έ 클래슀둜 ꡬ체적인 였λ₯˜μ— 따라 μ„ΈλΆ€ μ˜ˆμ™Έλ“€μ΄ 이 클래슀λ₯Ό μƒμ†ν•©λ‹ˆλ‹€.
    • MailParseException : 잘λͺ»λœ λ©”μ‹œμ§€ 속성이 발견될 경우 λ°œμƒν•˜λŠ” μ˜ˆμ™Έ
    • MailPreparationException : μ‚¬μš©μž μ½”λ“œμ—μ„œ 메일 μ€€λΉ„ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμ„ λ•Œ λ˜μ Έμ§€λŠ” μ˜ˆμ™Έ
    • MailSendException : 메일 전솑 였λ₯˜ λ°œμƒ μ‹œ λ˜μ Έμ§€λŠ” μ˜ˆμ™Έ

λ§Œμ•½ μ΅œμƒμœ„ μ˜ˆμ™ΈμΈ MailException을 μž¬μ‹œλ„ 쑰건으둜 μ„€μ •ν•œλ‹€λ©΄, 인증 μ‹€νŒ¨λ‚˜ λ©”μ‹œμ§€ ꡬ문 였λ₯˜κ°€ λ°œμƒν•΄λ„ 이λ₯Ό μΈμ‹ν•˜μ§€ λͺ»ν•˜κ³  κ³„μ†ν•΄μ„œ μž¬μ‹œλ„λ₯Ό λ°˜λ³΅ν•˜κ²Œ λ©λ‹ˆλ‹€. 그리고, MailSendException을 μ œμ™Έν•œ λ‹€λ₯Έ μ˜ˆμ™Έλ“€μ€ μ„€μ • 였λ₯˜λ‚˜ 메일 λ‚΄μš©κ³Ό κ΄€λ ¨λœ 문제둜, μž¬μ‹œλ„κ°€ μ•„λ‹Œ μ„€μ • μˆ˜μ •μ΄λ‚˜ 메일 λ‚΄μš©μ„ κ³ μΉ˜λŠ” 것이 ν•„μš”ν•©λ‹ˆλ‹€.

 

λ”°λΌμ„œ, λ„€νŠΈμ›Œν¬ 였λ₯˜λ‘œ μΈν•œ MailSendException λ°œμƒ μ‹œμ—λ§Œ μž¬μ‹œλ„κ°€ 이루어지도둝 μ œν•œμ„ λ‘μ—ˆκ³ , 이 μ˜ˆμ™Έκ°€ λ°œμƒν•  경우 1초의 λ”œλ ˆμ΄ ν›„ μ΅œλŒ€ 3회 μž¬μ‹œλ„ν•˜λ„λ‘ μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€. (κΈ°λ³Έ μž¬μ‹œλ„ νšŸμˆ˜λŠ” 3νšŒμž…λ‹ˆλ‹€)

 

μž¬μ‹œλ„ μ˜ˆμ™Έμ™€ μ“°λ ˆλ“œ ν’€ μ˜ˆμ™Έλ₯Ό κ΅¬λΆ„ν•΄μ„œ μ²˜λ¦¬ν•˜κΈ°

운영 ν™˜κ²½μ—μ„œλŠ” 둜그λ₯Ό 톡해 문제λ₯Ό λΉ λ₯΄κ²Œ νŒŒμ•…ν•  수 μžˆμ–΄μ•Ό ν•œλ‹€λŠ” 것이 μ€‘μš”ν•˜λ‹€κ³  μƒκ°ν•©λ‹ˆλ‹€.

κ·Έλž˜μ„œ, @Async와 @Retryable이 적용된 sendAsyncAuthenticationMail λ©”μ„œλ“œμ—μ„œ 두 가지 μœ ν˜•μ˜ μ˜ˆμ™Έλ₯Ό κ΅¬λΆ„ν•˜μ—¬ μ²˜λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€. 

  • μž¬μ‹œλ„κ°€ λͺ¨λ‘ μ‹€νŒ¨ν•΄μ„œ λ°œμƒν•œ μ˜ˆμ™Έ
  • 비동기 μ“°λ ˆλ“œμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έ

1. μž¬μ‹œλ„κ°€ λͺ¨λ‘ μ‹€νŒ¨ν•΄μ„œ λ°œμƒν•œ μ˜ˆμ™Έ μ²˜λ¦¬ν•˜κΈ°

javaMailSender.send() λ©”μ„œλ“œ 호좜 μ‹œ λ°œμƒν•  수 μžˆλŠ” MailSendException 외에도, 신원 인증 μ‹€νŒ¨ μ‹œ MailAuthenticationException이 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

μ΄λ•Œ, μž¬μ‹œλ„ μ‹€νŒ¨ μ‹œ λ³΅κ΅¬ν•˜λŠ” @Recover λ©”μ„œλ“œκ°€ MailSendException만 μ²˜λ¦¬ν•˜λ„λ‘ μ„€μ •λ˜μ–΄ μžˆλ‹€λ©΄, MailAuthenticationException이 λ°œμƒν•  경우, org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method μ—λŸ¬κ°€ λ°œμƒν•˜λ©° μ‹€μ œ 원인인 MailAuthenticationException이 λ‘œκ·Έμ— λ“œλŸ¬λ‚˜μ§€ μ•Šμ•„ κ΄€λ ¨ μ—λŸ¬λ₯Ό ν•΄κ²°ν•˜λŠ”λ° μ‹œκ°„μ΄ 더 였래 κ±Έλ¦¬λŠ” κ²½ν—˜μ„ ν–ˆμŠ΅λ‹ˆλ‹€.

 

λ”°λΌμ„œ μ΅œμƒμœ„ μ˜ˆμ™ΈμΈ MailException을 @Recover에 νŒŒλΌλ―Έν„°λ‘œ μ£Όμž…ν•˜μ—¬, λ‹€ν˜•μ„±μ„ μ΄μš©ν•΄ λͺ¨λ“  메일 κ΄€λ ¨ μ˜ˆμ™Έλ“€μ„ μ²˜λ¦¬ν•˜μ—¬ μž¬μ‹œλ„κ°€ μ‹€νŒ¨ν•œ 원인을 둜그 λΉ λ₯΄κ²Œ λ‚¨κΈ°μ–΄μ„œ λΉ λ₯΄κ²Œ λŒ€μ‘ν•  수 있게 ν–ˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ, μž¬μ‹œλ„(@Retryable)λŠ” MailSendException인 λ„€νŠΈμ›Œν¬ 였λ₯˜κ°€ λ°œμƒν–ˆμ„ λ•Œλ§Œ, μž¬μ‹œλ„λ₯Ό ν•˜λ„λ‘ ν–ˆμŠ΅λ‹ˆλ‹€.

 

2. 비동기 μ“°λ ˆλ“œμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έ

제 @Asyncλ©”μ„œλ“œ(sendAsyncAuthenticationMail)λŠ” λ°˜ν™˜ νƒ€μž…μ΄ voidμ΄λ―€λ‘œ, 비동기 μŠ€λ ˆλ“œμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έκ°€ μƒμœ„ μŠ€λ ˆλ“œλ‘œ μ „νŒŒλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

비동기 λ©”μ„œλ“œμ˜ λ°˜ν™˜ νƒ€μž…μ„ CompletableFuture둜 μ„€μ •ν•˜λ©΄ μƒμœ„ μŠ€λ ˆλ“œλ‘œ μ˜ˆμ™Έλ₯Ό 전달할 수 μžˆμ§€λ§Œ, 메일 전솑 μ‹€νŒ¨ μ˜ˆμ™Έκ°€ μ€‘μš”ν•˜μ§€ μ•Šμ•„ void둜 μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€.

 

κ·Έλ ‡μ§€λ§Œ, void λ°˜ν™˜νƒ€μž…μΈ @Asyncμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜μ§€ μ•ŠμœΌλ©΄ μ˜ˆμ™Έκ°€ μ „νŒŒλ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, μ˜ˆμ™Έ λ°œμƒ μ‹œ 치λͺ…μ μž…λ‹ˆλ‹€.

이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄, 비동기 처리λ₯Ό μœ„ν•œ μŠ€λ ˆλ“œ ν’€ μ„€μ •μ—μ„œ AsyncUncaughtExceptionHandlerλ₯Ό μ˜€λ²„λΌμ΄λ“œν•˜μ—¬ 비동기 μŠ€λ ˆλ“œμ—μ„œ λ°œμƒν•œ μ˜ˆμ™Έλ₯Ό 둜그둜 좜λ ₯ν•˜λ„λ‘ μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€.(ν•΄λ‹Ή Config μ½”λ“œ)

 

μž¬μ‹œλ„ 및 볡ꡬ가 잘 적용 λ¬λŠ”μ§€ ν…ŒμŠ€νŠΈν•˜κΈ°

μž¬μ‹œλ„ 및 볡ꡬ가 μ˜λ„λŒ€λ‘œ μž‘λ™ν•˜λŠ”μ§€ ν…ŒμŠ€νŠΈν•˜μ—¬ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. μ£Όμš” ν…ŒμŠ€νŠΈ ν¬μΈνŠΈλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • μž¬μ‹œλ„κ°€ 총 3회 μ‹œλ„λ˜λŠ”μ§€: μ½”λ“œμ˜ verify()와 times()둜 검증
  • μž¬μ‹œλ„ μ‹€νŒ¨ ν›„ @Recover둜 정상 νλ¦„μœΌλ‘œ μ „ν™˜λ˜λŠ”μ§€: μ½”λ“œμ˜ assertThatCode()둜 확인

μΆ”κ°€λ‘œ, μ²˜μŒμ—λŠ” μž¬μ‹œλ„ 횟수(3번) 확인 ν…ŒμŠ€νŠΈμ™€ @Recover둜 μ˜ˆμ™Έκ°€ λ³΅κ΅¬λ˜λŠ”μ§€ ν™•μΈν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό 각각 μž‘μ„±ν–ˆλŠ”λ°, μ€‘λ³΅λœ 둜직이 λ§Žμ•„ μ½”λ“œ 가독성이 λ–¨μ–΄μ‘Œκ³ , 두 ν…ŒμŠ€νŠΈκ°€ ν•˜λ‚˜μ˜ λ©”μ„œλ“œλ₯Ό κ²€μ¦ν•˜λŠ”μ§€ λ‹€λ₯Έ μ‚¬λžŒμ—κ²ŒλŠ” λͺ…ν™•ν•˜μ§€ μ•Šμ„ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이에 따라 DynamicTestλ₯Ό ν™œμš©ν•˜μ—¬ μ‹œλ‚˜λ¦¬μ˜€ ν…ŒμŠ€νŠΈλ‘œ μž‘μ„±ν•˜κ³  쀑볡 λ‘œμ§μ„ κ°œμ„ ν•˜μ—¬ 가독성을 λ†’μ˜€μŠ΅λ‹ˆλ‹€.

 

전체 ν…ŒμŠ€νŠΈ μ½”λ“œμž…λ‹ˆλ‹€.

@DisplayName("이메일 전솑 μ‹€νŒ¨ μ‹œ μž¬μ‹œλ„ 및 볡ꡬ μ‹œλ‚˜λ¦¬μ˜€")
@TestFactory
Collection<DynamicTest> sendAsyncAuthenticationMail() {
    // given
    String email = "test@naver.com";
    String authenticationCode = "authCode";

    # MockBean을 μ‚¬μš©ν•΄ 메일 μš”μ²­μ„ λ³΄λƒˆλ‹€κ³  κ°€μ •
    willThrow(MailSendException.class)   
            .given(javaMailSender)
            .send(any(SimpleMailMessage.class));

    // when then
    return List.of(
           dynamicTest(
                "μž¬μ‹œλ„κ°€ λͺ¨λ‘ μ‹€νŒ¨ν•΄λ„ @Recover에 μ˜ν•΄ μ˜ˆμ™ΈλŠ” λ°œμƒν•˜μ§€ μ•ŠλŠ”λ‹€", () ->
                    assertThatCode(() -> 
                        mailClient.sendAsyncAuthenticationMail(email, authenticationCode))
                            .doesNotThrowAnyException())
            ),
            dynamicTest("인증 이메일 보내기에 μ‹€νŒ¨ν•˜λ©΄ 총 3번의 μ‹œλ„λ₯Ό ν•œλ‹€", () -> {
                Thread.sleep(5000); # 메인 μŠ€λ ˆλ“œκ°€ μž¬μ‹œλ„ ν•˜λŠ” 비동기 μ“°λ ˆλ“œλ³΄λ‹€ λ¨Όμ € μ’…λ£Œλ˜μ§€ μ•ŠκΈ° μœ„ν•¨
                verify(javaMailSender, times(3)).send(any(SimpleMailMessage.class));
            })
    );
}

 

 

λ§ˆλ¬΄λ¦¬ν•˜λ©°

μ΅œκ·Όμ— μš΄μ˜μ—μ„œλŠ” 무엇이 μ€‘μš”ν• κΉŒ? 에 λŒ€ν•œ 고민이 λ§Žμ•„μ§€λ©΄μ„œ μ˜ˆμ™Έ μ²˜λ¦¬μ™€ 둜그 κ΄€λ¦¬μ˜ μ€‘μš”μ„±μ„ μ‹€κ°ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. 이와 κ΄€λ ¨λœ μš”μ†ŒκΉŒμ§€ κ³ λ €ν•˜λ©° κ°œλ°œν•˜λ‹€ λ³΄λ‹ˆ, 비동기 μ²˜λ¦¬λ‚˜ 메일 κ΄€λ ¨ λ¬Έμ„œλ₯Ό 더 깊이 읽고 이해할 수 있게 λ˜μ–΄ 개발 이해도도 λ†’μ•„μ§€λŠ” μž₯점을 느끼고 μžˆμŠ΅λ‹ˆλ‹€. μ•žμœΌλ‘œ μž₯μ•  λ°œμƒ μ‹œ μ‹ μ†ν•˜κ²Œ λŒ€μ‘ν•  수 μžˆλŠ” 운영 λ‹¨κ³„μ˜ 방법듀에 λŒ€ν•΄ 더 κ³΅λΆ€ν•˜λ € ν•©λ‹ˆλ‹€. 이와 κ΄€λ ¨ν•΄ μΆ”μ²œν•  λ§Œν•œ λ ˆνΌλŸ°μŠ€κ°€ μžˆλ‹€λ©΄ λŒ“κΈ€λ‘œ μ•Œλ €μ£Όμ‹œλ©΄ κ°μ‚¬ν•˜κ² μŠ΅λ‹ˆλ‹€. 읽어 μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€!