회원 가입과 동시에 가입한 회원의 메일에 회원 가입 완료 메일을 보내는 것은 동시에 이루어져야 한다. 여기서 '동시에'란 메일 전송에 실패할 경우, 회원 가입도 실패로 돌린다는 것을 말한다.
회원 가입의 정보를 DB에 저장하는 로직과 메일을 전송하는 로직은 같은 트랜잭션에서 이루어져야 한다.
그럼 그냥 @Transactional
추가하면 쉽게 끝나는거 아냐? 뭐가 문제?🤔
Spring Boot 기반의 애플리케이션은 기본적으로 각 요청을 동기적으로 실행한다. 이 말은 하나의 로직의 결과값을 받기 전까지는 다른 로직을 실행할 수 없음을 말한다. 그런데 메일 전송과 같은 로직은 우리가 생성한 애플리케이션 안에서 이루어지는 것이 아니라 외부의 서버를 통해 실행된다.
외부에서 처리하는 작업이 엄청엄청엄청!! 느리다면? 애플리케이션은 그 상태 그대로 아무것도 하지 못한 채 멈춰있어야 한다.
그렇기 때문에 외부에서 실행하는 로직은 비동기적으로 실행하는 것이 바람직하다.
🧐 한가지 주의할 점
메일 전송이 비동기적으로 이루어지기 때문에 트랙잭션을 적용하는 것이 위험할 수 있다. 그렇기 때문에 자체적으로 메일 전송에 실패할 경우의 처리를 구현해야 한다.
로직을 비동기적으로 처리하려면 해당 메서드에 @Async
애너테이션을 추가해야 한다. @Async
를 추가하면 다른 쓰레드에서 실행되어 다른 로직들이 해당 메서드가 완료될 때까지 기다릴 필요가 없다.
@Async
애너테이션을 사용하기 위해서는 해당 애너테이션이 추가된 메서드가 @EnableAsync
가 추가된 Configuration 클래스에 위치해야 한다.
🧐 해당 애너테이션의 자세한 설명은 다음에 다른 글로 정리할 것.
참고블로그
메일 전송을 위한 외부 서버는 구글 계정만 있으면 무료로 사용할 수 있는 STMP 서버를 사용한다.
위 링크를 확인하면 SMTP 설정에 대한 정보를 확인할 수 있다.
이 정보를 바탕으로 애플리케이션의 application.yml
를 작성하면 된다.
☝️ 메일 발신자의 정보는 반드시!!! 환경변수로 지정하거나 Yml 파일을 공개적인 곳에 공개되지 않도록 주의해야 한다
🤔 이벤트?!
수도코드를 보면 해당 로직은 Service
계층에서 이루어지는 것을 알 수 있다.
꼭꼭 기억하기 노 컨트롤러 예스 서비스
메일 전송은 언제 이루어진다? 회원 등록 시점에서 이루어진다! 회원 등록은 뭐다? DB에 회원이 보내온 정보를 저장하는 것이다!
이 과정을 Service
클래스에서 진행한다. createMember()
메서드는 회원 정보에 대한 검증 후에 해당 객체를 memberRepository
에 저장한다.
회원의 정보를 Repository에 저장하는 것을 하나의 이벤트로 볼 수 있다.
🤔 그치만 혼자 이벤트하면 뭐해 아무도 모르는데??
이벤트가 발생했다는 것을 애플리케이션 전체에 알려야 할 필요가 있다🥳
이때 확성기 역할을 하는 것이 ApplicationEventPublisher 인터페이스 다.
ApplicationEventPublisher는 이벤트 게시 기능을 캡슐화하는 인터페이스로 publishEvent()
와 함께 사용된다.
💡 Docs
Such an event publication step is effectively a hand-off to the multicaster and does not imply synchronous/asynchronous execution or even immediate execution at all. Event listeners are encouraged to be as efficient as possible, individually using asynchronous execution for longer-running and potentially blocking operations.
공식문서에publishEvent()
에 관한 설명으로 간단하게 말하자면 해당 이벤트를 수신하는 로직은 장기 실행 및 잠재적인 차단 작업을 위해 비동기적 실행을 권장한다는 것이다.
해당 메서드는 발생한 이벤트를 EventListener에게 알리는 역할을 한다.Object 객체를 파라미터로 받기 때문에 어떠한 값도 들어갈 수 있다.
그런데 왜?WHY?🧐
Member객체를 MemberEvent로 감싸서 로직을 실행하도록 하는거지??
☝️🤓
로직 구현에 큰 의미가 있는 것이 아니라 개발자의 편의를 위해 사용된 클래스였다! Member 객체를 그대로 파라미터로 넘기는 것 보다는 Event에 사용하는 객체임을 알리기 위한 오직 이벤트를 위한! 객체임을 알리기 위해! 이런식으로 구현하는 것이었다!!!!!
실제로 그냥 Member 객체로 구현해도 문제없이 실행되었다.
개발자의 편의성이 제일 어려운듯
자! 이제 이벤트를 널리널리 알렸으니 이벤트를 수신해 처리할 메서드를 구현해야 한다!
한가지 의문🤔
이벤트만 알렸는데 어떻게 특정 메서드를 실행시킬 수 있지? 별도로 호출한 것도 아닌데..이 메서드, 저 메서드들이 나서서 자기가 실행하겠다고 하면 어떡해?ㅠㅠ
이때 사용하는 애너테이션이 @EventListener이다. 이름에서부터 알 수 있듯이 발생한 이벤트를 듣고 자신의 메서드를 실행한다!
publishEvent()
에 의해 실행되는 로직은 비동기적으로 진행하는 것을 권장한다고 했다!
그럼 이제 어떤 메서드가 비동기적으로 실행되어야 하지?
회원 등록이라는 이벤트를 수신한 handleEmailSending()
는 비동기적으로 실행되어야 하므로 @EnableAsync, @Async 를 추가했다.
본격적인 로직을 구현하기 전에 다시 기억해야 할 것이 있다. 메일 전송에 실패하면 트랙잭션처럼 모든 로직이 rollback 되어야 한다는 것이다.
여기서 rollback의 의미는 회원 등록이라는 이벤트가 일어나기 전으로 되돌려야 한다는 말이라는 것을 잊지말자!
너무 길어져서 2편으로 나눠서 올릴 것! 슝=3