본문 바로가기
프로젝트 회고

메세징 큐 기반의 결제 영수증 메일 발송 데몬 구축하기(다국어 처리)

by woo00oo 2024. 5. 13.

이번 포스팅에서는 AWS SQS를 사용해서 결제 영수증 메일을 발송하는 데몬을 구축한 경험을 기록하고자 한다. 흔히 말하는 pub/sub 패턴이다.

 


문제 상황이 무엇이었나?

고객이 정상적으로 결제를 완료하면 고객 이메일로 결제 영수증 메일이 발송한다. 

기존 환경에서는 아래와 같은 프로세스로 처리되고 있었다.

  1. Map 객체를 활용하여 메일 전송 데이터를 초기화한다.
  2. 다국어 처리를 위해 language에 따라 메일 양식 html 파일을 생성한다.
  3. 사내 SMTP 서버와 통신을 위해 Thread 객체를 생성한다.
  4. 새로 생성된 Tread 객체에서 SMTP 서버로 메시지를 전송한다.

위 프로세스에서는 아래와 같은 문제점들을 야기했다.

  • Map 객체로 데이터를 전달하고 있어 코드 가독성이 매우 떨어졌다.
  • language에 따라 html 파일이 각각 존재하였다. EN, KR, CN, JP 등등.. 메일 양식 디자인이 변경되면 작업량이 * language배 였다. 아주 불필요한 리소스다.
  • 비동기 처리를 위해 새로운 Thread를 생성하였지만, 서버의 자원은 한정적이기에 매번 Thread를 생성하는 것은 가맹점 이벤트 시 한계에 도달할 것이라고 생각하였다. (ThreadPool로 관리 되는 것도 아니므로)
  • 순수 자바로 구현되어 있어 메일 전송하는 유틸 클래스의 코드 라인이 약 500라인이 넘는다.

 


어떻게 해결하였나?

위의 문제점들은 PUB/SUB 패턴으로 손쉽게 해결할 수 있을거라 생각하고 프로젝트를 시작하였다.

바로 아래와 같은 프로세스처럼 말이다.

실시간 결제 승인 서버 -> SQS -> 결제 영수증 메일 발송 데몬

실시간 결제 승인 서버 즉 Publisher가 되는 서버에서는 메일 전송에 필요한 데이터를 메시지로 발행하고 결제 영수증 메일 전송 데몬 즉 Subscriber가 되는 서버에서는 이를 구독하여 메일 폼을 만들어 STMP 서버와 통신하면 되는 것이다.

 

위에서 문제점들의 대해서 내가 기술적으로 해결한 키워드는 아래와 같다.

  • 스프링 메시지, 국제화 처리
  • AWS SQS 구축 
  • ThreadPoolTaskExecutor
  • Spring Boot Starter Mail 라이브러리

1. 스프링 메시지, 국제화 처리

서비스 특성상 해외 고객들이 주로 이용하며, 다국어 처리가 지원 되어야 한다.

기존에는 언어별로 html 파일이 각각 존재하여 기획 쪽에서 메일 양식 변경 요청이 오면 퍼블리셔단 부터 작업량이 배로 늘어나는 번거로움이 있었다. 스프링에서는 다국어 처리를 아주 편리한 기능으로 제공하고 있고 이를 도입하였다.

 

다국어 처리가 필요한 문구들을 모두 리스트업하였고 각 언어에 따른 프로퍼티 파일을 생성하였다.

  • messages.properties
  • messages_ja.properties
  • messages_ko.properties
  • ...

그리고 MessageSource 빈 설정 객체 세팅 후 메일 양식 html 에 타임리프 문법을 적용하였다.

어려운 단계는 아니였다. 문구가 많아 노가다를 많이 필요로한 것 빼고는..

이후 메시지로 전달 받은 Locale 객체를 통해 다국어 처리가 가능하도록 구현하였다.

결제 영수증 메일 양식

2. AWS SQS 구축

AWS SQS는 Simple Queue Service의 약자이다. 말 그대로 사용하기 아주 심플하다. 단순한 환경에서 큐를 사용해야 한다면 SQS가 최고인 거 같다. SQS 구축도 AWS 콘솔을 통해 클릭 몇 번이면 바로 구축이 된다. SQS에는 두가지 종류의 메시지 대기열을 제공하는데, 결제 영수증 메일 발송에 대해서는 정확한 순서가 필요하지 않기에 FIFO 대기열 보다는 최대 처리량을 제공하는 표준 대기열을 도입하였다.

 

코드 레벨에서도 작업해야하는 부분은 간단했다. AWS에서 라이브러리로 제공하기 때문이다.

프로퍼티 파일에 SQS의 엔드포인트 설정을 잡아주고, 라이브러리로 제공되는 클래스들을 통해 메시지를 발행하고, 구독하면 된다.

 

메시지를 발행하는 코드는 아래와 같다. 

QueueMessagingTemplate 객체를 통해 '무슨 큐'에, '어떤 메시지'를 발송할지 인자로 넘겨주면 된다.

기존에는 Map 객체를 활용하여 코드 가독성이 떨어졌지만, EmailMessage 라는 DTO 타입을 활용하였다.

/**
 * SQS 메시지 발행
 */
public <T> void sendMessage(T message) {
	queueMessagingTemplate.convertAndSend(queueName, message);
}

 

메시지를 구독하는 코드는 아래와 같다.

@SqsListener 어노테이션으로 손쉽게 구현할 수 있는데 폴링 방식으로 메시지를 구독해온다.

실질적으로 메일을 발송하는 메소드는 CompletableFuture로 비동기 처리하고 있는데 그 사유는 바로 다음 챕터에서 설명하겠다.

/**
 * 폴링 방식으로 메시지를 Subscribe 하여 결제 영수증 메일 발송
 */
 @SqsListener
 public void receiveMessage(@Payload EmailMessage message) {
    log.info("SQS 메시지를 수신하였습니다. " + message.toString());
    CompletableFuture.runAsync(() -> mailSender.processReceiptEmail(message), executor);
 }

 

참고자료 : https://lannstark.tistory.com/88

3. ThreadPoolTaskExecutor

ThreadPoolTaskExecutor는 Spring에서 제공해주는 클래스로 자바에서 제공하는 ThreadPoolExecutor를 사용하기 쉽게 만들어 사용하도록 구현 되어 있어 쓰레드 풀을 쉽고 간단하게 설정하고 관리할 수 있다. 말그대로 Spring에서 제공해주는 쓰레드 풀이다.

비동기 처리에 있어 아주 유용하게 사용할 수 있다.

 

메일 발송 처리를 비동기로 처리한 사유는 아래와 같다.

 

  • @SqsListener 어노테이션은 내부적으로는 폴링을 하고 있는데 관련 코드는 AbstractMessageListenerContainer와 그 구현체인 SimpleMessageListenerContainer에서 확인할 수 있다. 이때 메시지를 처리하는 기본 tread 개수는 2개이다.
  • Thread 역할의 분리
    • SQS에 지속적으로 폴링 처리를 진행하는 쓰레드
    • SMTP와 통신하여 메일을 발송하는 쓰레드

따라서, ThreadPoolTaskExecutor Bean 설정 클래스에서 '기본 쓰레드 수 세팅', '최대 쓰레드 수 세팅', '큐 사이즈', '큐가 꽉 찼을 경우 예외 핸들러 처리 방식' 등을 설정할 수 있다. 위 설정들은 서버 구성 환경에 따라 성능을 좌지우지하므로 최적의 설정 값을 찾는 것이 중요하다. 

 

요즘 비동기 처리에 관심이 많아 정수원님의 자바 동시성 프로그래밍 수강 중이다. 강의를 다 듣고 나서 ThreadPoolTaskExecutor에 대해서 심도있게 정리할 계획이다.

 

 

4. springframework.mail

해당 패키지 하위의 JavaMailSender와 MimeMessageHelper를 통해서 SMTP 서버로 메일 메시지를 편리하게 전송할 수 있다.

기존 코드에서는 순수 자바로 구현 되어 있어 MimeMessage를 만들기 위해 한땀한땀 노력한 모습이 보였다. 코드라인이 약 500라인이 넘어간다. 비즈니스 로직을 개발하는 개발자는 부가 기능 처리에 있어서 힘을 뺄 이유가 전혀 없다. 라이브러리를 찾아 적극 활용하면 된다.

 

public void sendEmail(ReceiptEmail receiptEmail) {
    // 메일 수신자 설정
    mimeMessageHelper.setTo(..);
    
    // 메일 발신자 설정
    mimeMessageHelper.setFrom(..);
    
    // 메일 제목 설정
    mimeMessageHelper.setSubject(..);
    
    // 메일 내용 설정
    mimeMessageHelper.setText(..);
    
    // 메일 전송
    javaMailSender.send(mimeMessage);
    
}

무엇이 개선 되었는가?

  • 소스 코드 품질 향상
    • Layer간 데이터 전달 시 사용하던 Map 객체를 DTO 로 변경하면서 '코드 가독성'과 '데이터 유효성'을 용이하게 가져 갈 수 있었다.
    • 스프링 프레임워크를 활용하여 메일 전송하는 코드 라인 수를 약 500 라인 -> 8라인으로 줄일 수 있었다.
  • 불필요한 리소스 제거
    • 스프링 메시지 국제화를 적용하기 전에는 기획부터 퍼블리싱, 소스 레벨까지 각 국가 언어별로 관리해야하는 메일 양식 폼이 여러 파일로 존재 하였고, 수정 작업 시 각 파일별로 수정 작업이 필요로 하였다.
    • 적용 후 한가지의 메일 양식 폼에 관리하면 되기에 6/1로 작업량이 감소되었다.
  • 실시간 결제 승인 서버 부하 분산
    • 한 서버에서 처리 되던 비즈니스 로직을 비동기 메시지 큐인 SQS를 도입하면서 가맹점 이벤트 발생 시 부하 분산 효과를 가져올 수 있었다.