posts

AWS SES 메일 발송과 이메일 인증 흐름

May 11, 2025 updated May 11, 2025 architectureauthawsemailspringboot

이 문서는 예전에 simple email service라고 대충 적어둔 메모를 다시 풀어쓴 버전입니다..
이름만 보면 너무 막연한데, 실제로는 AWS SES + 메일 어댑터 + 인증/재설정 플로우를 어떻게 붙일지 정리해둔 문서에 가깝습니다.

핵심은 이겁니다.

  • 인증번호 메일 보내기
  • 이메일 찾기 링크 보내기
  • 비밀번호 재설정 링크 보내기
  • 이 흐름을 controller -> usecase -> adapter 구조 안에 넣기

왜 이걸 정리했나

이메일 기능은 생각보다 금방 지저분해집니다.

  • 인증번호만 보낼지
  • 재설정 링크도 같이 보낼지
  • 링크 유효시간은 어떻게 둘지
  • 가입 여부를 숨겨야 하는 요청은 어떻게 응답할지
  • 메일 발송 실패를 어디서 잡을지

이런 게 한 번에 붙기 때문입니다. 그래서 SES 붙이는 법만 보는 것보다, 전체 플로우를 같이 보는 게 훨씬 덜 헷갈립니다.

AWS SES에서 먼저 필요한 것

우선 SES를 쓰려면 SMTP 자격 증명이나 SDK용 접근 키가 필요합니다.
이 부분은 원문에 실제 값이 들어 있었는데, 당연히 그건 전부 걷어냈습니다.

정리하면 준비물은 대략 이 정도입니다.

  • SES 콘솔에서 발신용 설정 확인
  • SMTP credentials 또는 SDK 자격 증명 생성
  • 애플리케이션 런타임에서 환경 변수로 주입

예를 들면 애플리케이션 설정은 이런 식으로 잡을 수 있습니다.

aws:
  accessKeyId: ${AWS_ACCESS_KEY_ID}
  secretKey: ${AWS_SECRET_ACCESS_KEY}

컨테이너에서는 보통 이렇게 넘기게 됩니다.

version: "3.1"
services:
  app:
    build: .
    environment:
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}

무적권 코드에 박아두면 안 되고, 환경 변수나 시크릿 스토어로 빼는 게 맞습니다.

메일 기능이 실제로 하는 일

이 메모에서 다루는 시나리오는 크게 3개입니다.

1. 회원가입 인증

  • 프론트에서 이메일 인증 요청
  • 백엔드에서 6자리 인증번호 생성
  • 메일 발송
  • 사용자가 인증번호 입력
  • 백엔드에서 검증 후 완료 처리

2. 이메일 찾기

  • 사용자가 정보를 넣고 이메일 찾기 요청
  • 가입된 정보가 맞으면 메일 발송
  • 메일 안에는 찾기 링크 또는 안내 정보 포함

3. 비밀번호 재설정

  • 사용자가 재설정 요청
  • 실제 가입 여부와 무관하게 응답 메시지는 최대한 비슷하게 유지
  • 백엔드에서 1회성 링크 생성
  • 메일에 링크 포함
  • 사용자가 링크 타고 들어와서 새 비밀번호 입력

여기서 중요한 건 이메일 찾기비밀번호 재설정은 링크를 타고 들어오는 시점의 검증이 꼭 필요하다는 점입니다.
그냥 링크만 만들고 끝내면 별로 안 안전합니다..

계층은 어떻게 나누는 게 편한가

원문도 결국 이 구조를 밀고 있었습니다.

graph TD
    A[프론트엔드] --> B[AuthController]
    B --> C[AuthUseCase]
    C --> D[MemberPersistenceAdapter]
    C --> E[MailAdapter]
    D --> F[데이터베이스]
    E --> G[Amazon SES]

이렇게 두면 역할이 좀 분명해집니다.

  • Controller 요청/응답 계약
  • UseCase 인증번호 생성, 링크 생성, 만료시간 정책, 예외 흐름
  • Persistence Adapter 인증 정보 저장/조회
  • Mail Adapter 실제 메일 발송

즉 SES를 직접 컨트롤러에서 부르면 안 되고, 메일 발송은 아웃바운드 어댑터로 내리는 게 제일 덜 꼬입니다.

인터페이스는 이렇게 시작하면 편하다

애플리케이션 레이어에서는 일단 메일 포트를 잡아두고,

public interface MailAdapter {
    void sendVerificationEmail(String to, String verificationLink);
    void sendPasswordResetLink(String to, String resetLink);
}

인프라에서는 SES 구현체를 붙입니다.

@Component
public class MailAdapterImpl implements MailAdapter {
    private final AmazonSimpleEmailService amazonSES;

    public MailAdapterImpl(AmazonSimpleEmailService amazonSES) {
        this.amazonSES = amazonSES;
    }

    @Override
    public void sendVerificationEmail(String to, String verificationLink) {
        SendEmailRequest request = new SendEmailRequest()
            .withDestination(new Destination().withToAddresses(to))
            .withMessage(new Message()
                .withBody(new Body().withHtml(new Content().withCharset("UTF-8").withData(
                    "회원가입을 완료하려면 다음 링크를 클릭하세요: <a href='" + verificationLink + "'>인증하기</a>"
                )))
                .withSubject(new Content().withCharset("UTF-8").withData("회원가입 인증 메일")));

        amazonSES.sendEmail(request);
    }
}

진짜 핵심은 메일을 보냈다가 아니라, 유즈케이스가 어떤 메일을 어떤 정책으로 보내기로 했는가입니다.
어댑터는 그걸 실행만 해주면 됩니다.

링크 메일은 어떻게 다루는 게 낫나

원문에는 이메일 찾기 링크랑 비밀번호 재설정 링크를 같이 고민한 흔적이 있었는데, 방향은 나쁘지 않았습니다.

  • 링크는 1회성으로 본다
  • 만료시간을 둔다
  • DB에 토큰 또는 검증 상태를 저장한다
  • 요청이 끝나면 인증 정보를 정리한다
  • 재요청이 오면 이전 정보는 무효화한다

이 기준만 있어도 나중에 사고가 많이 줄어듭니다.

예를 들면 이런 흐름입니다.

@Override
public void sendPasswordResetLink(String email) {
    if (memberPersistenceAdapter.existsByEmail(email)) {
        String token = tokenGenerator.generate();
        memberPersistenceAdapter.saveResetToken(email, token);
        mailAdapter.sendPasswordResetLink(email, token);
    }
}

여기서 실제 서비스에서는 더 봐야 할 게 있습니다.

  • 너무 자주 요청하면 rate limit 걸기
  • 이미 사용한 토큰 재사용 막기
  • 만료시간 체크
  • 성공/실패 응답 메시지에서 정보 노출 줄이기

구현할 때 체크해둘 것

원문 메모의 TODO를 정리하면 결국 이 정도입니다.

  • 인증 요청 시 이전 인증 정보 삭제
  • 인증 완료 시 인증 정보 정리
  • 재인증 요청 시 메일 재발송 + 기존 코드 무효화
  • 이메일 찾기 / 재설정 링크 진입 시 유효성 검증
  • 메일 형식과 링크 동작 확인
  • SES 예외를 애플리케이션 예외로 감싸기

이건 나중에 보면 별거 아닌 것 같아도, 실제로는 여기서 많이 새더라고요.

한 줄로 정리하면

AWS SES 붙였다에서 끝나는 문제가 아니고,
실제로는 인증 정책 + 링크 만료 + usecase/adapter 경계 + 예외 처리를 같이 봐야 메일 기능이 덜 불안정해집니다.

SES는 그냥 발송 수단이고, 진짜 중요한 건 그 위에 얹는 인증 흐름입니다.