website: 매직링크, 이메일 발송 E2E 테스트로 커버리지 갭 메우기
멋사 경희대 'website' 프로젝트에서, 피드 기능과 관련된 매직링크 발급 및 이메일 발송 로직의 E2E(End-to-End) 테스트 커버리지를 확장한 Pull Request입니다.
요약
이번 PR은 #113 이슈에서 제기된, 매직링크 관련 기능의 HTTP 엔드포인트 레벨에서의 E2E 테스트 커버리지 부족 문제를 해결하는 데 집중했습니다. 주로 테스트 코드만으로는 검증되지 않았던 매직링크 만료 시 HTTP 레벨에서의 거부 처리와, 실제 관리자 초대 및 비밀번호 재설정 이메일 발송이 HTTP 호출부터 email_log 기록 및 메일 수신까지 정상적으로 이루어지는지, 그리고 링크가 실제 작동하는지를 검증하는 테스트 코드를 추가했습니다. 또한, SMTP 서버 장애 시 발생하는 500 에러를 502 에러로 수정하고 email_log에 실패 기록이 남도록 하는 프로덕션 코드 수정도 포함되었습니다. 이 PR은 2026년 7월 17일에 생성되어 2026년 7월 21일에 dev 브랜치로 병합되었습니다.
배경 및 목적
이 PR의 주요 목적은 #113 이슈에서 정의된 완료 기준 중 하나인 "기존 매직링크(feed) 흐름 기준 자동화 E2E 테스트"를 달성하는 것이었습니다. 기존에는 API 호출에 대한 단위 테스트나 서비스 레이어 테스트는 존재했지만, 실제 HTTP 요청이 발생했을 때 이메일 발송 로직이 email_log에 기록되고, 발송된 이메일에 포함된 링크가 정상적으로 작동하는지까지 검증하는 E2E 테스트가 부족했습니다.
구체적으로 해결하고자 했던 문제는 다음과 같습니다.
- 매직링크 만료 시 HTTP 레벨에서의 검증 누락: 서비스 레이어에서는 만료를 검증했지만, 실제 API 엔드포인트에서 만료된 토큰으로 요청 시 적절한 에러 응답(410)을 반환하는지 확인하는 테스트가 없었습니다.
- 초대/비밀번호 재설정 이메일 발송 흐름의 불완전한 검증: 기존 테스트들은
EmailService를 목(mock) 객체로 대체하거나 서비스 레이어만 검증하여, 실제 HTTP 요청이email_log에 기록되고 메일이 실제로 발송되는 전체 경로를 검증하지 못했습니다. - SMTP 장애 시 에러 처리: 이메일 발송 중 SMTP 서버에 문제가 발생했을 때, 500 에러가 반환되는 문제가 있었습니다. 이는 다른 API들의 에러 응답 형식과 일관성이 없었으며, 실패 시
email_log에 기록이 남더라도 사용자에게는 명확한 에러 코드를 전달하지 못했습니다. - 메일에 포함된 링크의 실제 동작 검증: 발송된 이메일에 포함된 링크가 실제로 유효한 토큰을 포함하고 있으며, 해당 링크를 통해 인증/재설정 과정이 성공적으로 완료되는지까지 검증하는 테스트가 부족했습니다.
구현 내용
이번 PR에서는 위에서 언급된 문제들을 해결하기 위해 다양한 테스트 코드를 신설하고 기존 코드를 수정했습니다.
주요 변경사항
PostControllerTest에 매직링크 만료 검증 테스트 추가:createPost_ExpiredToken_Returns410테스트를 추가하여, 만료된 매직링크 토큰으로 게시글 생성 API를 호출했을 때 410 에러가 반환되는 것을 검증했습니다.
AuthEmailHttpEndToEndIntegrationTest신설:- Testcontainers를 사용하여 Mailpit 컨테이너를 띄우고, 실제 HTTP 요청을 통해 관리자 초대 및 비밀번호 재설정 이메일 발송 과정을 End-to-End로 검증했습니다.
- 이메일 발송 후 Mailpit에서 해당 메일을 가져와 HTML 내용에서 토큰을 추출하고, 추출된 토큰을 이용해 초대 수락 또는 비밀번호 재설정 과정을 실제 HTTP 요청으로 완주시키는 테스트를 포함했습니다.
- 초대 및 비밀번호 재설정 후
email_log에SUCCESS상태로 기록되는 것을 확인했습니다.
AuthEmailHttpFailureIntegrationTest신설:- Mailpit 컨테이너를 의도적으로 중지시킨 상태에서 이메일 발송 API를 호출하여 SMTP 서버 연결 실패 시나리오를 테스트했습니다.
- 관리자 초대 시에는 502 Bad Gateway 에러와 함께
EMAIL_SEND_FAILED코드를 반환하고,email_log에FAILURE상태로 기록되는 것을 확인했습니다. 또한, 이메일 발송 실패 시 트랜잭션이 롤백되어 초대 레코드가 생성되지 않는 것을 검증했습니다. - 비밀번호 재설정 시에는 #90 스펙에 따라 계정 열거 공격 방지를 위해 200 OK를 반환하지만,
email_log에는FAILURE기록이 남고 비밀번호 재설정 토큰은 생성되지 않는 것을 확인했습니다.
MailpitContainerSupport클래스 분리:- 이메일 관련 통합 테스트 클래스들에서 반복적으로 사용되던 Mailpit 컨테이너 설정 및
@DynamicPropertySource코드를 공통 베이스 클래스로 추출하여 코드 중복을 제거했습니다.
- 이메일 관련 통합 테스트 클래스들에서 반복적으로 사용되던 Mailpit 컨테이너 설정 및
GlobalExceptionHandler수정:EmailSendException에 대한 핸들러를 추가하여, SMTP 장애 발생 시 500 에러 대신 502 Bad Gateway 에러를 반환하도록 수정했습니다.
변경된 파일 목록
backend/docs/email-module.mdbackend/src/main/java/likelion/khu/website/admin/password/AdminPasswordResetService.javabackend/src/main/java/likelion/khu/website/common/GlobalExceptionHandler.javabackend/src/test/java/likelion/khu/website/email/AuthEmailHttpEndToEndIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/AuthEmailHttpFailureIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/EmailServiceFailureIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/EmailServiceFailureTransactionBoundaryIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/EmailServiceIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/EmailServiceStageProfileIntegrationTest.javabackend/src/test/java/likelion/khu/website/email/MailpitContainerSupport.javabackend/src/test/java/likelion/khu/website/feed/post/PostControllerTest.java
핵심 코드 설명
// AuthEmailHttpEndToEndIntegrationTest.java 에서 발췌
// 메일 HTML에서 실제 링크의 토큰 부분을 추출하는 로직
private String extractTokenFromMessage(String messageId, String linkPathPrefix) throws Exception {
JsonNode detail = fetchMessageDetail(messageId);
String html = detail.get("HTML").asText();
Matcher matcher = Pattern.compile(Pattern.quote(linkPathPrefix) + "([^\"\\s<]+)").matcher(html);
if (!matcher.find()) {
throw new AssertionError("메일 HTML에서 " + linkPathPrefix + " 링크를 찾지 못했어요");
}
return matcher.group(1);
}
// AuthEmailHttpFailureIntegrationTest.java 에서 발췌
// SMTP 서버 중단 시, 초대 API 호출 후 502 에러 반환 및 email_log FAILURE 확인
mockMvc.perform(post("/api/admin/invitations")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"" + to + "\"}"))
.andExpect(status().isBadGateway())
.andExpect(jsonPath("$.code").value("EMAIL_SEND_FAILED"));
List<EmailLog> logs = awaitEmailLogFor(to);
assertThat(logs).hasSize(1);
assertThat(logs.get(0).getStatus()).isEqualTo(EmailStatus.FAILURE);
배운 점 및 개선점
이번 PR을 진행하며 다음과 같은 점들을 배우고 개선할 수 있었습니다.
배운 점
- E2E 테스트의 중요성: 단위 테스트와 통합 테스트만으로는 놓칠 수 있는 실제 사용자의 흐름을 E2E 테스트를 통해 검증하는 것이 얼마나 중요한지 다시 한번 체감했습니다. 특히 이메일 발송과 같이 외부 시스템과의 연동이 필요한 경우, 실제 HTTP 호출과 메일 수신까지 검증하는 것이 신뢰성을 높이는 데 필수적임을 알게 되었습니다.
- Testcontainers 활용: Mailpit과 같은 외부 의존성을 로컬 개발 환경이나 CI/CD 파이프라인에서 일관되게 관리하고 테스트에 활용하는 방법을 익혔습니다.
- 에러 처리의 일관성: 프로덕션 코드에서 예외 처리 시, 다른 API들과의 에러 응답 계약을 일관되게 유지하는 것이 사용자 경험과 API 클라이언트 개발의 편의성을 위해 중요하다는 것을 배웠습니다. 500 에러 대신 502 에러를 반환하도록 수정한 것이 좋은 예입니다.
- 코드 중복 제거 및 리팩토링: 반복되는 테스트 설정 코드를 공통 클래스로 분리하며 코드 가독성과 유지보수성을 높이는 경험을 했습니다.
개선점
- 이메일 발송 실패 시 재시도 정책: 현재는 SMTP 장애 시 502 에러를 반환하지만, 클라이언트 측에서 이를 어떻게 처리하고 재시도할지에 대한 구체적인 정책이나 가이드라인이 추가적으로 논의될 필요가 있습니다.
- 테스트 환경의 복잡성: Mailpit 컨테이너를 사용하고 TLS 설정을 포함하는 등 테스트 환경 설정이 다소 복잡해졌습니다. CI/CD 파이프라인에서 안정적으로 동작하도록 설정에 대한 세심한 관리가 필요합니다.
- 발송 실패 시 이메일 내용 검증: 현재는 실패 시
email_log기록을 확인하는 것에 집중했지만, 실패가 발생하더라도 어떤 종류의 이메일(초대, 비밀번호 재설정 등)을 보내려고 했는지에 대한 정보가email_log에 더 명확하게 기록되면 좋을 것 같습니다.