Backend/Java

회원가입 이메일 인증 기능 (Google smtp 활용)

개발자의 첫 걸음 2025. 12. 29. 18:00

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

 

 

 

 

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