오늘은 회원가입시 이메일 전송 로직을 확인중
org.springframework.mail.MailAuthenticationException와 같은
예외를 만났고 해당관련 해결방법을 적어가보려한다.


✅ 작업환경

Spring Boot 2.7.7  
Java 11 
macOs Monterey 12.6 버전 
로컬환경에서 테스트 진행


📌 에러 확인 및 해결

첫번째로 Spring 에서 지원하는 mail 서비스를 이용해보려한다.
메일을 보내는 주체는 Google의 gmail을 이용하려한다.

implementation 'org.springframework.boot:spring-boot-starter-mail'

org.springframewokr.mail, org.springframewokr.javamail 패키지를 사용한다.

JavaMailSender 인터페이스의 send();메서드를
Override 하여 구현하였고 테스트 진행도중 아래와 같은 예외를 만나게 되었다.

2023-01-21 15:40:59.934 ERROR 281 --- [         task-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void codestates.frogroup.indiego.global.email.event.MemberRegistrationEventListener.listen(codestates.frogroup.indiego.global.email.event.MemberRegistrationApplicationEvent) throws java.lang.Exception

org.springframework.mail.MailAuthenticationException: Authentication failed; nested exception is javax.mail.AuthenticationFailedException: 534-5.7.9 Application-specific password required. Learn more at
534 5.7.9  https://support.google.com/mail/?p=InvalidSecondFactor t1-20020a63d241000000b004c974bb9a4esm11189895pgi.83 - gsmtp

내용을 잘 읽어보니, 인증에 실패하였고
Application-specific password required 애플리케이션의 스펙에 맞는
비밀번호를 요구한다고한다. 그리고 참고용 주소를 하나가 콘솔에 찍혀있다.
앱 비밀번호로 로그인관련 주소이며, 정말 친절한 Spring… 감동..

예외가 발생한 코드를 쭉 따라가다 보면
JavaMailSender인터페이스의

	protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
		Map<Object, Exception> failedMessages = new LinkedHashMap<>();
		Transport transport = null;

		try {
			for (int i = 0; i < mimeMessages.length; i++) {

				// Check transport connection first...
				if (transport == null || !transport.isConnected()) {
					if (transport != null) {
						try {
							transport.close();
						}
						catch (Exception ex) {
							// Ignore - we're reconnecting anyway
						}
						transport = null;
					}
					try {
						transport = connectTransport();
					}
					catch (AuthenticationFailedException ex) {
						throw new MailAuthenticationException(ex);
					}
					catch (Exception ex) {
						// Effectively, all remaining messages failed...
						for (int j = i; j < mimeMessages.length; j++) {
							Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]);
							failedMessages.put(original, ex);
						}
						throw new MailSendException("Mail server connection failed", ex, failedMessages);
					}
				}
        }        
                
                ... 생략
	}

구현체인 JavaMailSenderImpl의 아래의 메서드에서
connectTransport(); 메서드를 호출할 때 예외가 발생한 것을 볼 수 있다.

	protected Transport connectTransport() throws MessagingException {
		String username = getUsername();
		String password = getPassword();
		if ("".equals(username)) {  // probably from a placeholder
			username = null;
			if ("".equals(password)) {  // in conjunction with "" username, this means no password to use
				password = null;
			}
		}

		Transport transport = getTransport(getSession());
		transport.connect(getHost(), getPort(), username, password);
		return transport;
	}

해당 코드에서도 transport.connect(); 메서드 안에서
host, port, user, password의 정보로 연결 시도를 하는데 정보가 맞지않아
연결이 정상적으로 실행되지 않을 경우에 AuthenticationFailedException 예외가
발생하는 거이라고 추측이된다… 좀더 어드벤스한 공부는 추후해 해봐야할 것 같다.


그래서 스프링에서 친절하게 알려준 콘솔내용대로 해결을 해보자.

현재 yml 설정 파일이다.

mail:
  smtp:
    host: smtp.gmail.com
    port: 587
    username: ${GOOGLE_ID}
    password: ${GOOGLE_PASSWORD}
    auth: true
    starttls:
      enable: true
  subject:
    member:
      registration: 개굴단의 인디고 서비스에 성공적으로 가입하셨습니다.
  template:
    name:
      member:
        join: email-registration-member

환경변수를 이용해 설정해두었고
username에는 구글 이메일 주소를 넣었고
password에는 구글 비밀번호를 넣었었다.

여기서 비밀번호 스펙이 맞지않다고 예외가 발생한 것이였고
앱 비밀번호로 로그인으로 접속해 앱 비밀번호를 설정해주었다.

image

최종적으로 비밀번호를 받게된다면
16자의 비밀번호를 발급받게 될 것이고

password 환경변수에 공백없이 16자를 입력해주면 된다.
ex) abcdefghijklmnop


수정을 하고 다시 회원가입 서비스를 진행해보았다.

image

간단한 템플릿으로 우선 테스트를 진행했고
회원가입을한 유저의 메일로 정상적으로 회원가입 안내 메일이 보내진 것을 확인해볼 수 있다!!



✨ 참고 블로그
문제 해결 참고 블로그