
안녕하세요. 오늘은 회원가입 시 이메일 인증 기능에 대해 알아볼게요.

SMTP
SMTP는 Simple Mail Transfer Protocol의 약자입니다. 단어 그대로 해석하면 간이 우편 전송 규약 이에요..
인터넷 세상에서 "이메일을 보낼 때 지켜야 하는 약속(규칙)"이라고 정의할 수 있어요.
SMTP 핵심 특징
SMTP는 보내는 전용이에요.
즉 SMTP는 오로지 메일을 발송(PUSH)할 때만 사용하고,
반대로 메일을 확인(수신)할 때는 POP3나 IMAP이라는 다른 프로토콜을 사용해요.
현 포스팅 내용은 회원가입 인증번호를 보내기만 하기 때문에 SMTP만 있으면 돼요.
사전 준비 (Google 앱 비밀번호 발급)
❌ 일반 구글 비밀번호로는 접근이 불가능해요!
1. Google 계정 관리 접속 후 보안 탭으로 들어가요.

2. 보안 탭 목록 중 2단계 인증이 되었는지 확인 후 안되었다면 인증을 진행해 주세요.

3. 검색창에 "앱 비밀번호"검색 후 결괏값 클릭 해주세요.

4. 본인이 원하는 앱 이름을 작성 후 만들기.

5. 생성된 앱 비밀번호 16자리를 확인 후 메모장에 저장해 둡니다.
(단, 띄어쓰기 돼 있으면 안 되니 꼭 띄어쓰기는 붙여주세요!)

5. 프로그램 내 스타터 추가를 진행해 주세요.
// 이메일 발송 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-mail'
6. 스프링 부트 yml 기준 파일에 구글 SMTP 설정 코드를 작성해 주세요.
spring:
mail:
smtp:
# SMTP 서버 인증 활성화 (ID/PW 검사 여부)
auth: true
# START TLS (보안 연결)
starttls:
# 비 암호화 연결을 암호과 연결(TLS)로 승격 활성화
enable: true
required: true
# 타임 아웃 설정 - 서버 간 통신 기준
# 소켓 연결 제한 시간 (단위 밀리초)
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
7. 이메일 인증번호 생성하는 헬프 메서드 생성해 주세요.
package org.example.demo_ssr_v1_1._core.utils;
import java.util.Random;
public class MailUtils {
// 정적 메서드로 랜덤 번호 6자리 생성하는 헬프 메서드
public static String generateRandomCode() {
Random random = new Random();
// 0 ~ 899999 (하나의 랜덤 숫자 생성)
// 1. 0
// 2. 1 + 100_000 = 100_001
// 3. 반드시 여섯자리 번호를 생성 시켜야 함
int code = 100_000 + random.nextInt(900_000);
return String.valueOf(code);
}
}
8. 이메일인증으로 DTO 수정해주세요.
@Data
public static class EmailCheckDTO {
private String email;
private String code;
// 추후 이메일 인증번호도 추가할 예정
public void validate() {
if(email == null || email.trim().isEmpty()) {
// Exception400 추후 수정
throw new Exception400("이메일을 입력해주세요");
}
if(!email.contains("@")) {
throw new Exception400("올바른 이멩리 형식이 아닙니다");
}
}
}
9. 추가된 내용들을 컨트롤러, 서비스 수정 해주세요.
(서비스에 추가할 경우 너무 길어지기 때문에 MailService(예)를 새로 생성 후 작성하는 것을 권장드려요.)
// Controller
package org.example.demo_ssr_v1_1.user;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RequiredArgsConstructor
@RestController // @Controller + @ResponseBody
public class UserApiController {
private final UserService userService;
private final MailService mailService;
@PostMapping("/api/email/send")
public ResponseEntity<?> 인증번호발송(@RequestBody UserRequest.EmailCheckDTO reqDTO) {
// 1. 유효성 검사
reqDTO.validate();
// 2. 서비스 단에서 구글 메일 서버로 이메일 전송 처리
mailService.인증번호발송(reqDTO.getEmail());
return ResponseEntity.ok().body(Map.of("message", "인증번호가 발송되었습니다"));
}
@PostMapping("/api/email/verify")
public ResponseEntity<?> 인증번호확인(@RequestBody UserRequest.EmailCheckDTO reqDto) {
reqDto.validate();
// 인증번호 입력 확인
if(reqDto.getCode() == null || reqDto.getCode().trim().isEmpty()) {
return ResponseEntity
.badRequest()
.body(Map.of("message", "인증번호를 입력해주세요"));
}
// 메일 서비스단에서 인증번호 확인
boolean isVerified = mailService.인증번호확인(reqDto.getEmail(), reqDto.getCode());
// 결과값에 따라 분기 처리
if (isVerified) {
// 인증 성공
return ResponseEntity.
ok()
.body(Map.of("message", "인증되었습니다"));
} else {
return ResponseEntity.badRequest()
.body(Map.of("message", "인증번호가 일치하지 않습니다"));
}
}
}
// Service
package org.example.demo_ssr_v1_1.user;
import jakarta.mail.internet.MimeMessage;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.example.demo_ssr_v1_1._core.utils.MailUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class MailService {
// 의존성 주입 받았던 클래스 (JavaMailSender - 편지를 쓰게하는 클래스)
private final JavaMailSender javaMailSender;
private final HttpSession session;
public void 인증번호발송(String email) {
// 1. 인증번호 생성
// email -> 인증번호(123456) -> 임시로 세션메모리 저장 --> 메일 발송 요청
String code = MailUtils.generateRandomCode();
// 2. 이메일 전송 내용 설정
// MimeMessage / SimpleMailMessage(순수하게 텍스트만 보낼 때 사용)
// MimeMessage - 텍스트 뿐만 아니라 HTML, 첨부파일, 포함할 수 있는 표준 포맷
MimeMessage message = javaMailSender.createMimeMessage();
// 3. 구글 메일 서버로 전송 - 우리 서버가 아니고 외부 서버로 통신 요청 (...)
// 외부로 통신하는 코들 일 때 도 기본적으로 try catch를 사용해줘야 한다.
try {
// 3.1 도우미 객체를 사용
// 인자값 2번: 멀티파트 허용
// 인자값 3번: 인코딩 설정
MimeMessageHelper helper =
new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(email); // 받는 사람 이메일 주소
helper.setSubject("[MyBlog] 회원가입 이메일 전송");
helper.setText("<h3>인증번호는 [" + code + "] 입니다</h3>", true);
javaMailSender.send(message);
// 5. 세션에 임시 코드 저장
// sessionUser : User(....)
// code_a@naver.com: 123456
// 동시에 접속자가 많아도 이메일 주소로 누구의 인증번호인지 구별할 수 있음
session.setAttribute("code_" + email, code);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public boolean 인증번호확인(String email, String code) {
// 1. 세션에서 저장된 코드 가져오기
// key: code_ + "a@nvaer.com"
String savedCode = (String) session.getAttribute("code_" + email);
// 2. 세션에 가지고 온 code 값과 사용자가 입력한 인증번호 일치 여부 확인
if(savedCode != null && savedCode.equals(code)) {
// 세션 메모리에서 제거 해주어야 함
session.removeAttribute("code_" + email);
return true;
}
return false;
}
}
10. 회원가입 인증을 추가한 만큼 프론트엔드(머스테치) 수정 및 로직 추가해 주세요.
{{> layout/header}}
<div class="container p-5">
<!--
파일 업로드를 위한 form 태그 설정
enctype="multipart/form-data : 파일 형식을 포함한 MIME 타입을 의미한다
즉, 이 속성 없이는 파일이 서버로 전송되지 않는다 !!!
일반 텍스트 데이터는 - application/x-www-form-urlencoded
파일 데이터는 - multipart/form-data 필요 하다.
-->
<div class="card">
<div class="card-header"><b>회원가입을 해주세요</b></div>
<div class="card-body">
<form action="/join" method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="text" class="form-control" placeholder="Enter username" name="username">
</div>
<div class="mb-3">
<input type="password" class="form-control" placeholder="Enter password" name="password">
</div>
<!-- 이메일 입력 및 인증번호 발송 -->
<div class="mb-3">
<div class="input-group" >
<input type="email" class="form-control" placeholder="Enter email" id="email" name="email" required>
<button type="button" class="btn btn-outline-secondary" onclick="callSendApi()" >인증번호전송</button>
</div>
</div>
<!-- 인증번호 입력 (처음에는 숨김) -->
<div class="mb-3" id="code-box" style="display: none">
<label for="">인증번호</label>
<div class="input-group">
<input type="text" id="code" class="form-control" placeholder="인증번호 6자리" maxlength="6">
<button type="button" class="btn btn-outline-secondary" onclick="callVerifyApi()" >확인</button>
</div>
<!-- 인증 결과 메세지 표시 영역 -->
<div id="msg" class="form-text"></div>
</div>
<!-- 인증 완료 여부 저장 (hidden 필드) -->
<input type="hidden" id="isEmailVerified" value="false">
<div class="mb-3">
<label for="profileImage" class="form-label">프로필 사진(선택사항)</label>
<input type="file" class="form-control" id="profileImage" name="profileImage" accept="image/*">
<small class="form-text text-muted mt-1">이미지 파일만 업로드 가능합니다(JPG, PNG, GIF)</small>
</div>
<button type="submit" class="btn btn-primary form-control">회원가입</button>
</form>
</div>
</div>
</div>
<script>
// 이메일 전송 API 호출 (AJAX)
async function callSendApi() {
// DOM 엘리먼트에 있는 value 값을 가져와 한다(사용자가 입력한 이메일 주소를)
// 값이 담긴 상태
let email = document.querySelector("#email").value;
if(!email) {
alert("이메일을 입력해주세요");
return;
}
// js 에서 통신하는 코드는 기본적으로 try catch 무조건 걸어 주세요
try {
// fetch API 로 POST 요청 해보기
let response = await fetch("/api/email/send", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({email: email}) // js 객체를 JSON 문자열로 변환 처리
});
// 응답 처리
if(response.ok) {
// response.ok HTTP 상태 코드 200 ~ 299 범위를 의미 함
alert("인증번호가 메일로 전송 되었습니다. 메일을 확인해주세요");
document.querySelector("#code-box").style.display = "block";
} else {
// 서버에서 에러 응답 (400, 500)
let error = response.json(); // "문자열" --> js object 로 변환 됨.
alert("이메일 전송 실패");
}
} catch (e) {
// 네트워크 오류 처리
alert("서버 통신 오류 " + e.message);
}
}
// 인증 번호 확인 API 호출 (AJAX)
async function callVerifyApi() {
// 이메일 인증번호 확인시에는 반드시 이메일과,인증번호 동시 비교
let email = document.querySelector("#email").value;
let code = document.querySelector("#code").value;
if(!code) {
alert("인증번호를 입력해주세요");
return;
}
try {
// fetch API POST 요청
let response = await fetch("/api/email/verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({email:email, code: code})
});
let result = await response.json();
// <div id="msg" class="form-text"></div>
let msgBox = document.querySelector("#msg");
if(response.ok) {
// 인증 성공
msgBox.innerHTML = "<span style='color: green' >인증되었습니다</span>"
// dom 접근해서 false --> true 변경 처리
document.querySelector("#isEmailVerified").value = true;
// 이메일 수정 불가 처리
document.querySelector("#email").readOnly = true;
} else {
msgBox.innerHTML = "<span style='color: red' >인증번호가 틀렸습니다</span>"
document.querySelector("#isEmailVerified").value = false;
}
} catch (e) {
console.log(e);
alert("네트워크 오류 발생");
}
}
</script>
{{> layout/footer}}

여기까지 회원가입 이메일인증에 대해 알아봤는데요.
처음 이메일인증에 대해 어렵게 생각했지만 배워보니 조금 알기가 쉬웠던 것 같아요.
여러분들은 어떠셨나요?
'Backend > Java' 카테고리의 다른 글
| 결제 시스템 구축 - 핵심 개념 정리 (0) | 2026.01.02 |
|---|---|
| 이메일 중복방지 (0) | 2025.12.31 |
| Spring - security crypto (0) | 2025.12.25 |
| Spring - RestTemplate (0) | 2025.12.23 |
| Spring - OSIV (0) | 2025.12.18 |