이메일 인증기능을 구현하기 위해 알아봤던 내용에 대해 공유합니다.
이메일 프로토콜
이메일을 송수신하기 위한 프로토콜은 따로 존재합니다. SMTP
, IMAP
, POP3
프로토콜들입니다.
java에서 메일을 다루기 전에 각 프로토콜이 무엇이고 어떻게 구성되어 있는지 한번 알아보겠습니다
메일 netwrok 구성
각 프로토콜에 대해 설명드리기 전에 눈으로 파악하기 쉽게 먼저 어떻게 구성되어 있는지에 대해 보여드리겠습니다.
- MUA ( Mail User Agent ) : 사용자의 이메일 클라이언트, 메일을 보내거나 받을 수 있는 역할을 가지고 있습니다.
- Ex) Server, Email Client
- MTA ( Mail Transfer Agent ) : 메시지를 수신하고 전달하는 서버, SMTP 프로토콜을 활용하여 메시지를 수신 및 송신합니다.
- Ex) Mail Server
- MDA ( Mail Delivery Agent ) : 메세지를 최종 수신자
- Ex) Email Client

SMTP 프로토콜
Simple Mail Transfer Protocol의 약자. 메시지를 보내고 받는 데 사용하는 통신 프로토콜
smtp는 Email Client와 메일 서버간의 데이터 교환에 관한 프로토콜입니다. 즉 데이털 송수신 하는 역할 자체를 가지고 있다고 봐도 됩니다.
SMTP 동작방식
SMTP 연결
- smtp또한 tcp 프로토콜을 사용하기 때문에, Mail Client와 Mail Server간 TCP연결을 진행합니다.
- Mail Client가
HELO
나EHLO
와 같은 명령으로 서버에 보내게 되면, 연결이 가능하다는220
을 반환하게 됩니다. - 이후, Mail Client는 서버에 SMTP-AUTH 와 같은 인증 절차를 거치게 되며 연결을 위한 신원 인증 및 메일 전송에 필요한 파라미터를 설정하게 됩니다.
데이터 전송
- SMTP로 통신하기 위한 연결이 성공한 후, 수신자 세부 정보, 제목 및 본문이 포함된 이메일을 전송한다.
데이터 처리 및 응답
- 서버는 Mail Client로부터 받은 데이터가 유효한지 등 검사 후 응답을 보내게 됩니다
- 250: OK
- 421: Service Not Available
- 550: Requested action not token
연결 종료
- 데이터 처리 및 응답이 오면 SMTP 연결은 종료 됩니다.
위 과정에서도 볼 수 있듯이 SMTP에서 데이터 송신 과정은 메일이 수신자에게 수신이 제대로 갔는지에 대해 관심이 없고, 오직 '내가 보낸 메일이 메일 서버에 제대로 전달되었는가?`에 대해서만 관심을 가지는 것을 볼 수 있습니다.
SMTP의 PORT
Http가 80포트를 사용하는 것처럼 데이터를 송수신하기 위한 통로가 존재하는데 SMTP의 경우 이러한 포트가 몇 개 존재합니다.
Port 25
: 초기 포트, 가장 처음 SMTP를 사용하기 위한 포트였습니다. 하지만 아무런 보안이 없기 때문에 최근에는 접근을 막는 경우도 존재합니다.Port 465
: 보안 소켓 계층 (SSL) 암호화를 적용한 포트입니다. 다만, SSL은 TLS로 대체되었고, (465포트 자체가 25번 포트를 사용하면서 보안에 이슈가 있다 보니 SSL을 적용하여 465포트에서 사용하기 시작했는데, 해당 포트의 사용 허가를 받지 못한 상태에서 사용하다 TLS가 나오면서 새로 배정받는 게 587이라는 얘기도..) 해당 포트는 레거시 시스템에서만 사용됩니다.Port 587
: 이메일 전송을 위한 기본 포트, TLS 암호화를 적용시켜야 합니다.
POP3
Post Office Protocol의 약자, 메시지를 받을 때 사용하는 프로토콜
메일을 받아 놓은 서버, 해당 메일 서버에서 메일들을 가져오기 위해서 각 메일들은 전부 다운로드해야 하며, 다운로드된 메일은 메일서버에서 삭제가 된다.
Port 143
: 일반 포트Port 993
: TLS암호화를 사용한 포트
IMAP
Internet Message Access Protocol의 약자, 메시지를 받을 때 사용하는 프로토콜
IMAP또한 POP3와 같이 메시지를 받을 때 사용하는 프로토콜인데 약간의 차이가 있습니다. 먼저 IMAP이 뭔지부터 알아보겠습니다.
IMAP은 쉽게 생각해서 사용자는 Mail Client에 접속을 하여 메일들을 이용할 수 있는 Naver Mail 서비스라 생각하면 될 거 같습니다. 따라서 서버에서 모든 메일들을 관리하다 보니, 다운로드가 필요 없어지게 되지만, 트래픽이 높습니다.
여기서 이제 POP3와 IMAP의 차이를 알 수 있습니다.
POP3는 메일 서버의 메일을 다운로드하는 데에 비해, IMAP은 메일들을 가지고 있고, 이 메일들을 바로 볼 수 있게 해 주지만 트래픽이 몰리게 되면 속도가 좀 느리다는 점이 다른 점으로 볼 수 있습니다.
Java Mail
JavaMailSender로 springboot에서 제공해 주는 mail관련 라이브러리가 존재하지만 먼저 Java에서 제공해주는 Mail 라이브러리로 구현하는 부분부터 보겠습니다.
의존성
implementation 'com.sun.mail:jakarta.mail:2.0.1'
먼저 Mail Sever에 connection을 하기 위해 관련 설정 값들을 지정해 줘야 합니다.
Properties props = System.getProperties();
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.port", "587");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.ssl.trust", SMTP_HOST);
props.setProperty("mail.debug", "true");
따라서 연결하고자 하는 설정 값들을 넣어주면 됩니다.
처음부터 보면, 기존에 설정을 해놓았던 시스템 정보들을 기반으로 property들을 가져왔습니다 물론 Java Mail에 관한 정보들만 있어도 되기 때문에 new Properties()
로 시작하여도 됩니다.
이후 메일서버와 연결하기 위해 값들을 넣어줘야 하는데
com.sun.mail.smtp (JavaMail API documentation) (javaee.github.io)
com.sun.mail.smtp (JavaMail API documentation)
Class Summary Class Description SMTPMessage This class is a specialization of the MimeMessage class that allows you to specify various SMTP options and parameters that will be used when this message is sent over SMTP. SMTPSSLTransport This class implemen
javaee.github.io
넣어줄 수 있는 파라미터들이 위 페이지에 나와있으니 추가적인 정보가 필요하다면 넣으시면 될 거 같습니다.
Session session = Session.getInstance(props,
new jakarta.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(USER_EMAIL, USER_PW);
}
});
이후 메일 서버에 연결할 때 인증(smtp 연결 중 3번)이 필요하기 때문에 인증 ID
와 앱 비밀번호
가 필요하게 됩니다.
사용하려는 메일서버에서 제공해 주는 비밀번호를 통해서 값을 가져오면 됩니다.
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(USER_EMAIL));
// 받는 이메일
message.setRecipients(
Message.RecipientType.TO,
InternetAddress.parse(TO_EMAIL)
);
// 제목
message.setSubject("테스트 메일입니다.");
// 내용
message.setText("화이팅!");
// 발송
Transport.send(message);
} catch (MessagingException e) {
e.printStackTrace();
}
메일을 보낼 때는 Java Mail에서 제공해 주는 MimeMessage
를 사용하면 됩니다. MimeMessage는 일반 text뿐 아니라 multipart형식으로도 보낼 수 있기 때문에 다양한 방식으로 보낼 수 있습니다.
Message의 받는 사람 중에는 TO, CC, BCC가 존재하는데 이건 코드와 관련이 있는 건 아니지만 조금만 부연설명을 넣자면
- TO : 받을 사람
- CC : 받는 사람 이외의 내용을 참고하는 사람
- BCC : 이메일을 전송시 누구에게 보냈는지 숨길때 많이 사용합니다.
또한 addRecipients 메서드가 있으므로 해당 메서드를 통해 보내는 사람의 유형을 변경하면 됩니다.
마지막으로 메일을 connect하고 전송역할을 해주는 Transport.send(message)
입니다.
JavaMailSender
JavaMailSender의 경우 spring boot에서 제공하는 mail 인터페이스입니다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
따라서 위와 같이 spring-boot에서 제공하는 라이브러리를 가져와야 합니다.
이 역시 property들을 동일하게 설정해 줘야 하는데 yml에 넣어주면 자동으로 빈을 등록할 때 같이 넣어주게 됩니다.
다만 spring.mail.*
로 위치를 잡기 때문에 Java Mail과 다른 property를 넣는다는 점만 주의하면 될 거 같습니다.
JavaMailSender의 경우 추상화가 잘 되어있기 때문에 넣어줄 메시지만 생성한다면 따로 작업할 필요가 없습니다.
MimeMessagePreparator mimeMessagePreparator = new MimeMessagePreparator() {
@Override
public void prepare(MimeMessage mimeMessage) throws Exception {
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true, "UTF-8");
message.setFrom(USER_EMAIL);
message.setTo(TO_EMAIL);
message.setSubject("테스트 메일입니다.");
message.setText("화이팅!", true);
}
};
javaMailSender.send(mimeMessagePreparator);
와 같이 MimeMessage만 전달해 주면 됩니다.
여기서 좀 다른 부분이 있는데
JavaMailSender에서 받는 메시지 종류로 MimeMessagePreparator
와 MimeMessage
가 존재합니다.
MimeMessage
MimeMessage는 기존에 작성했던 방식과 동일합니다 MimeMessage자체를 생성해 주어 전달하게 되면 동작을 바로 수행할 수 있도록 되어있습니다.
@Override
public void send(MimeMessage... mimeMessages) throws MailException {
doSend(mimeMessages, null);
}
MimeMessagePreparator
MimeMessagePreparator
는 함수형 인터페이스로 내부적으로 실행을 할 수 있도록 도와줍니다.

MimeMessage vs MimeMessagePreparator
MimeMessage와 MimeMessagePreparaotr의 차이를 알아보면, 먼저 spring에서 추천하는 방식으로는 MimeMessagePreparator입니다.

그 이유는 내부적인 동작을 보면 알 수 있습니다.
@Override
public void send(MimeMessagePreparator mimeMessagePreparator) throws MailException {
send(new MimeMessagePreparator[] {mimeMessagePreparator});
}
@Override
public void send(MimeMessagePreparator... mimeMessagePreparators) throws MailException {
try {
List<MimeMessage> mimeMessages = new ArrayList<>(mimeMessagePreparators.length);
for (MimeMessagePreparator preparator : mimeMessagePreparators) {
MimeMessage mimeMessage = createMimeMessage();
preparator.prepare(mimeMessage);
mimeMessages.add(mimeMessage);
}
send(mimeMessages.toArray(new MimeMessage[0]));
}
catch (MailException ex) {
throw ex;
}
catch (MessagingException ex) {
throw new MailParseException(ex);
}
catch (Exception ex) {
throw new MailPreparationException(ex);
}
}
MimeMessagePreparator
또한 내부적으로는 MimeMessage를 만들어서 실행을 도와주지만, MimeMessage자체를 생성하고 관리해 주는 역할이 JavaMailSender에 있는 것이고, 반대로 MimeMesssage를 그대로 준다면 생성에 대한 역할을 우리 코드가 가져가게 된다는 차이가 있습니다.
이러한 차이를 제외하곤 doSend
메서드를 통해 내부적으로 메일서버와의 연결을 하고, 메시지를 보내며 연결을 끊는 작업을 하게 됩니다.
결과적으로

와 같이 메일이 온 것을 볼 수 있습니다