[Spring Boot] SMTP 이론 — 서버에서 이메일은 어떻게 전송될까?

Hood·2026년 7월 28일

Spring Boot

목록 보기
9/19
post-thumbnail

들어가며

Spring Boot의 SMTP 내용을 바탕으로 작성하였습니다.

회원가입과 로그인까지 구현하고 나면 비밀번호 재설정, 이메일 인증, 알림 발송처럼
서버가 사용자에게 메일을 보내야 하는 상황을 만나게 됩니다.
이번 글에서는 코드를 작성하기 전에 SMTP가 어떤 역할을 하고,
Spring Boot 애플리케이션에서 메일 한 통이 어떤 경계를 지나 전달되는지 정리해보겠습니다.


SMTP란?

SMTP는 Simple Mail Transfer Protocol의 약자로, 네트워크를 통해 이메일을 전송하고 메일 서버 사이에서 중계하기 위한 표준 프로토콜입니다.

메일을 보낸다는 것은 애플리케이션이 곧바로 상대방의 받은편지함에 데이터를 넣는다는 뜻이 아닙니다.
일반적인 흐름은 다음과 같습니다.

사진 출처 : https://serversmtp.com/what-is-smtp/
Spring Boot 애플리케이션
        ↓ SMTP 연결·인증
발신 SMTP 서버
        ↓ 메일 중계
수신자 메일 서버
        ↓ 스팸·정책 검사 후 저장
수신자의 받은편지함

우리 서버는 먼저 Gmail 같은 발신 SMTP 서버에 메일 전송을 요청합니다. 그 뒤 발신 서버가 수신자의 메일 서버를 찾고 메시지를 전달합니다. 마지막으로 수신자 서버가 메시지를 받아 사서함에 저장해야 사용자가 받은편지함에서 확인할 수 있습니다.


✍ SMTP와 IMAP·POP3는 역할이 다르다

메일 관련 설정 화면을 보다 보면 SMTP, IMAP, POP3가 함께 등장하기 때문에 역할을 혼동하기 쉽습니다.

프로토콜주된 역할예시
SMTP메일 전송·중계Spring Boot 서버가 비밀번호 재설정 메일을 보냄
IMAP서버의 메일함을 여러 기기에서 동기화하며 조회휴대전화와 PC에서 같은 받은편지함 확인
POP3메일을 클라이언트로 내려받아 조회로컬 메일 프로그램에 메시지 다운로드

이번 실습의 목적은 서버에서 메일을 보내는 것이므로 SMTP가 핵심입니다.
Gmail의 전달 및 POP/IMAP 화면에서 IMAP을 활성화하는 과정은 Spring Boot의 SMTP 발송에 필요한 단계가 아닙니다.


✍ SMTP 전송 과정

비밀번호 재설정 메일을 예로 들어 조금 더 자세히 살펴보겠습니다.

1. 애플리케이션이 메일 메시지를 만든다

Spring Boot 애플리케이션은 다음과 같은 값을 조합합니다.

From: 발신자 주소
To: 수신자 주소
Subject: 비밀번호 재설정 안내
Body: 재설정 링크와 유효 시간

이때 메일 내용은 단순 문자열일 수도 있고, HTML 템플릿일 수도 있습니다.

2. SMTP 서버에 연결한다

애플리케이션은 smtp.gmail.com 같은 SMTP 서버의 주소와 포트를 사용해 연결합니다. Gmail을 STARTTLS 방식으로 연결할 때는 일반적으로 587 포트를 사용합니다.

Host: smtp.gmail.com
Port: 587
Authentication: true
STARTTLS: true

SMTP는 메일을 전송하는 프로토콜이고, 전송 구간의 암호화는 TLS 또는 STARTTLS 설정으로 보완합니다. 따라서 단순히 “SMTP를 사용하므로 안전하다”라고 보기보다 인증 정보와 TLS 설정을 함께 확인해야 합니다.

3. 발신 계정을 인증한다

SMTP 서버는 아무 서버나 특정 Gmail 주소를 발신자로 사용할 수 없도록 인증을 요구합니다. 실습에서는 일반 Google 계정 비밀번호를 소스에 넣는 대신 2단계 인증 후 발급한 앱 비밀번호를 사용합니다.

SPRING_MAIL_USERNAME=your-account@gmail.com
SPRING_MAIL_PASSWORD=your-app-password

이 값은 소스 코드가 아니라 .env 또는 배포 환경의 Secret에 저장해야 합니다.

4. SMTP 서버가 메시지를 수락한다

Spring의 JavaMailSender.send()가 오류 없이 반환되면, 애플리케이션은 적어도 연결한 SMTP 서버가 해당 전송 요청을 거절하지 않았다는 사실을 확인할 수 있습니다.

하지만 이것만으로 다음까지 보장되지는 않습니다.

받은편지함 도착
스팸함으로 분류되지 않음
수신자 서버가 이후 반송하지 않음
SPF·DKIM·DMARC 정책 통과

따라서 API의 200 OK는 “메일이 사용자의 받은편지함에 확실히 도착했다”가 아니라, 이 실습 기준으로는 SMTP 서버가 요청을 정상적으로 수락했다는 범위로 해석해야 합니다.


Spring Boot에서는 어떻게 SMTP를 사용할까?

Spring Boot는 spring-boot-starter-mail 의존성을 추가하고 spring.mail 설정을 제공하면 JavaMailSender를 자동 구성합니다.

// build.gradle.kts
implementation("org.springframework.boot:spring-boot-starter-mail")
spring:
  mail:
    host: ${SPRING_MAIL_HOST}
    port: ${SPRING_MAIL_PORT}
    username: ${SPRING_MAIL_USERNAME}
    password: ${SPRING_MAIL_PASSWORD}

이후 애플리케이션 코드에서는 SMTP의 저수준 명령을 직접 작성하지 않고 JavaMailSender 추상화를 사용할 수 있습니다.

@Component
class SmtpMailSender(
    private val javaMailSender: JavaMailSender
) {
    fun send(message: SimpleMailMessage) {
        javaMailSender.send(message)
    }
}

JavaMailSender는 “메일을 보낼 수 있는 인터페이스”를 제공하고,
실제 SMTP 호스트·포트·인증·TLS 설정은 외부 설정으로 분리합니다.
덕분에 코드가 Gmail이라는 구체적인 메일 서비스에 과도하게 결합되는 것을 줄일 수 있습니다.


✍ 메일 본문은 문자열일까, HTML일까?

가장 간단한 방법은 SimpleMailMessage로 텍스트 메일을 보내는 것입니다.

val message = SimpleMailMessage().apply {
    from = "no-reply@example.com"
    setTo("user@example.com")
    subject = "비밀번호 재설정 안내"
    text = "아래 링크에서 비밀번호를 변경해주세요."
}

조금 더 보기 좋은 HTML 메일이 필요하다면 MimeMessage와 템플릿 엔진을 사용할 수 있습니다.
Thymeleaf를 이용하면 HTML 파일의 변수에 값을 넣을 수 있도록 도와줍니다.

<a th:href="${resetLink}">비밀번호 재설정하기</a>

다만 이번 05-answer 브랜치의 실제 구현은 Thymeleaf HTML 메일이 아니라 SimpleMailMessage 기반의 텍스트 메일입니다.
따라서 실습 글에서는 저장소의 현재 코드에 맞춰 텍스트 메일을 먼저 구현하고,
HTML 템플릿은 확장 과제로 분리하는 편이 자연스럽습니다.


✍ 비밀번호 재설정 메일에서 SMTP보다 먼저 생각할 것

메일을 보내는 코드만 동작한다고 계정 복구 기능이 안전해지는 것은 아닙니다.
비밀번호 재설정 기능에서는 다음 정책이 함께 필요합니다.

원본 토큰을 데이터베이스에 그대로 저장하지 않는다

사용자에게 보내는 원본 토큰은 메일 링크에만 넣고, 데이터베이스에는 SHA-256 해시를 저장합니다.

메일 링크: raw token
데이터베이스: SHA-256(raw token)

데이터베이스가 노출되더라도 저장된 값만으로 즉시 재설정 링크를 사용할 수 없게 하기 위한 경계입니다.

토큰은 짧게 만료되고 한 번만 사용할 수 있어야 한다

이번 실습에서는 토큰을 15분 후 만료시키며, 정상적으로 비밀번호가 변경되면 사용 시각을 기록합니다.
재발급하면 기존 토큰도 무효화합니다.

너무 자주 메일을 요청할 수 없게 한다

메일 전송은 외부 시스템 비용과 스팸 위험을 동반합니다.
사용자별 재요청 제한, IP·장치 단위 제한, CAPTCHA 같은 추가 정책을 상황에 맞게 고려해야 합니다.

메일 전송 실패와 토큰 저장을 따로 생각한다

토큰을 저장하기 전에 메일부터 보내면, 메일에는 링크가 전달됐지만 데
이터베이스 트랜잭션이 실패해 링크가 무효가 되는 문제가 생길 수 있습니다.

이번 실습에서는 다음 순서를 사용합니다.

1. 재설정 토큰 생성
2. 토큰 해시 저장 및 트랜잭션 commit
3. SMTP 메일 전송
4. 성공이면 200
5. SMTP 실패이면 방금 만든 미사용 토큰 정리 후 오류 반환

SMTP 설정에서 자주 발생하는 문제

인증 실패

원인 예시
- 일반 계정 비밀번호를 사용함
- 앱 비밀번호가 잘못됨
- 발신 주소와 SMTP 인증 계정이 다름
- 조직 계정 정책이 앱 비밀번호 사용을 차단함

연결 시간 초과

SMTP 서버가 응답하지 않을 때 기본 타임아웃을 무한대로 두면 요청 스레드가 장시간 막힐 수 있습니다. 연결·읽기·쓰기 타임아웃을 유한한 값으로 설정하는 것이 좋습니다.

SPRING_MAIL_PROPERTIES_MAIL_SMTP_CONNECTIONTIMEOUT=5000
SPRING_MAIL_PROPERTIES_MAIL_SMTP_TIMEOUT=5000
SPRING_MAIL_PROPERTIES_MAIL_SMTP_WRITETIMEOUT=5000

API는 성공했지만 메일이 보이지 않음

- 스팸함·프로모션함 확인
- 수신 주소 오타 확인
- 발신 도메인의 SPF·DKIM·DMARC 확인
- 메일 원본에서 From, Return-Path, mailed-by, signed-by 확인

SMTP 서버의 수락과 최종 전달은 서로 다른 증거라는 점을 기억해야 합니다.


결론

SMTP는 애플리케이션이 메일을 작성해 발신 서버로 전달하고, 메일 서버끼리 메시지를 중계하기 위한 프로토콜입니다.
Spring Boot에서는 spring-boot-starter-mailJavaMailSender를 통해 이를 비교적 간단하게 사용할 수 있습니다.

그러나 실제 계정 복구 기능에서는 SMTP 연결만큼이나 Secret 관리, TLS, 타임아웃, 토큰 해시 저장, 만료·단일 사용, 재요청 제한, 전송 실패 보상 처리가 중요합니다. 다음 글에서는 이 원리를 바탕으로 Gmail SMTP와 Spring Boot를 연결해 비밀번호 재설정 메일을 직접 보내보겠습니다.


참고 자료

profile
달을 향해 쏴라, 빗나가도 별이 될 테니 👊

0개의 댓글