https://drive.google.com/file/d/16UXXGbPy61zsl5XK9IR90zKzXtvwyt6f/view?usp=sharing

📌 이메일 발송

✅ EmailService 비즈니스 로직 V1

/**
 * 이메일 발송 기능을 담당하는 서비스 클래스입니다.
 * Spring Boot의 JavaMailSender를 이용해 실제 이메일을 발송하며,
 * 실패 시 커스텀 예외를 발생시킵니다.
 *
 * 메일 발송 성공/실패 여부는 로그로 남기고, 국제화 메시지를 통해 에러 메시지를 구성합니다.
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class EmailService {

    // Spring Boot가 자동 설정한 JavaMailSender 빈을 주입받습니다.
    private final JavaMailSender mailSender;

    // 국제화 메시지 처리용 MessageSource (messages.properties 등에서 메시지 읽기)
    private final MessageSource messageSource;

    // 발신자 이메일 주소 (application.yml의 spring.mail.username 값으로 설정됨)
    @Value("${spring.mail.username}")
    private String fromEmail;

    /**
     * 이메일을 발송하는 메서드입니다.
     * 내부적으로 MimeMessage를 생성 후 JavaMailSender로 전송합니다.
     *
     * @param toEmail 수신자 이메일 주소
     * @param subject 이메일 제목
     * @param body    이메일 본문 (HTML 허용)
     * @throws CustomException 이메일 발송 실패 시 사용자 정의 예외 발생
     */
    @Transactional
    public void sendEmail(String toEmail, String subject, String body) {
        MimeMessage message = createMimeMessage(toEmail, subject, body);
        try {
            mailSender.send(message);
            log.info("Email successfully sent to {}", toEmail);
        } catch (MailException e) {
            log.error("Failed to send email to {}", toEmail, e);
            // 국제화 메시지 사용 + 커스텀 예외로 변환하여 핸들러로 전달
            throw new CustomException(
                    FAIL_500.code(),
                    messageSource.getMessage("mail.send.fail", null, Locale.getDefault()),
                    HttpStatus.INTERNAL_SERVER_ERROR
            );
        }
    }

    /**
     * MimeMessage 객체를 생성하고 필요한 설정(From, To, Subject, Body)을 구성합니다.
     *
     * @param to      수신자 이메일 주소
     * @param subject 이메일 제목
     * @param body    이메일 본문 (HTML 포함 가능)
     * @return 구성된 MimeMessage 객체
     * @throws CustomException 메시지 구성 중 에러가 발생할 경우
     */
    private MimeMessage createMimeMessage(String to, String subject, String body) {
        try {
            MimeMessage message = mailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);

            helper.setFrom(fromEmail);  // 설정된 발신자 이메일
            helper.setTo(to);           // 수신자 이메일
            helper.setSubject(subject); // 메일 제목
            helper.setText(body, true); // HTML 지원 본문

            return message;
        } catch (MessagingException e) {
            log.error("Failed to construct email message", e);
            // 국제화 메시지 사용 + 커스텀 예외로 변환
            throw new CustomException(
                    FAIL_500.code(),
                    messageSource.getMessage("mail.message.create.failed", null, Locale.getDefault()),
                    HttpStatus.INTERNAL_SERVER_ERROR
            );
        }
    }
}

✅ EmailService Junit

    @Test
    void passwordResetEmailSend() {
        //given
        String toEmail = "[email protected]";
        String subject = "5월 부터 지원하자!";
        String body = "가자!!!";

        // when & then
        try {
            emailService.sendEmail(toEmail, subject, body);
            log.info("발송 성공!!");
        } catch (Exception e) {
            Assertions.fail("발송 실패");
        }
    }

🚀 결과

스크린샷 2025-03-22 오후 11.48.43.png

하지만 이 junit은 통합 테스트지 내가 추구하는 TDD가 아니다. 🙅

TDD 스타일로 다시 한번 코딩 🧑‍💻


package com.parker.service.api.v1.user.service;

import com.parker.common.service.EmailService;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.MessageSource;
import org.springframework.mail.javamail.JavaMailSender;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;

/**
 * EmailService의 단위 테스트 클래스입니다.
 * 외부 의존성(JavaMailSender, MessageSource)을 Mock으로 대체하고,
 * 메일 발송이 실제로 호출되는지를 검증합니다.
 */
@ExtendWith(MockitoExtension.class) // Mockito 기반 테스트 확장 활성화 (JUnit5용)
class EmailServiceTest {

    @Mock
    JavaMailSender javaMailSender;  // 이메일 발송 컴포넌트를 Mock 객체로 대체

    @Mock
    MessageSource messageSource;   // 다국어 메시지 처리를 위한 MessageSource도 Mock 처리

    @InjectMocks
    EmailService emailService;    // 위의 Mock들을 주입받은 실제 테스트 대상 EmailService

    /**
     * 이메일을 발송했을 때, JavaMailSender.send() 메서드가 정확히 1번 호출되었는지를 검증합니다.
     */
    @Test
    void sendEmail_ShouldInvokeMailSender() {
        // given - 사전 조건 설정
        // JavaMailSender가 반환할 MimeMessage 객체를 직접 모킹
        MimeMessage mockMessage = mock(MimeMessage.class);
        when(javaMailSender.createMimeMessage()).thenReturn(mockMessage);

        // when - 실제 서비스 메서드 호출
        emailService.sendEmail("[email protected]", "제목", "내용");

        // then - 결과 검증
        // JavaMailSender의 send() 메서드가 정확히 1번 호출되었는지 검증
        verify(javaMailSender, times(1)).send(mockMessage);
    }
}


✅ 동작 설명 요약