개요
현재 결제 서버에서는 발생하는 모든 결제 승인 건에 대해 응답(결과) 코드를 반환한다.
특정 응답코드에 해당하는 승인건 발생 시 메일 알림 서비스 구축이라는 새로운 요구사항이 발생했다.
최대 TPS 약 2000 에서 승인 건에 latency, Exception 전파 없는 메일 전송 기능을 설계한다.
요구사항 분석
요구사항은 다음과 같다.
1. 특정 응답 코드에 해당하는 승인 건 발생 시, 메일을 ‘즉시’ 발송한다.
2. 메일에는 승인에 대한 데이터 (거래번호, 가맹점번호, 사유 등)을 포함한다.
3. 메일 수신자는 각 사내 담당자, 백오피스 페이지를 통해 메일 주소를 설정할 수 있어야 한다.
기능 구현 상황 분석
- 승인 로직의 최대 TPS는 약 2000
- 각 결제수단 별 승인 로직은 Interface 를 구현(doPay 메소드)하여 관리되고 있다.
- 현재 메일 발송은 Spring Batch 서버를 통해 각 테이블 마다 일, 월, 초 단위로 전송한다.
- 결과코드는 이미 백오피스에서 관리되고 있으며, 테이블 컬럼 추가를 통해 메일 주소 설정이 가능하다.
설계 고려사항 도출
- 승인로직을 수행하는 doPay() 메소드의 내부 코드 수정없이 동작해야 한다.
- 추가될 메일 전송 로직이 승인 로직에게 latency를 전파해선 안 된다.
- 메일 전송이 실패해도 승인 로직 Transaction 에 영향을 주어선 안 된다.
- 기능 확장 가능성을 고려한다.
기능 설계
메일 전송 기능은 “Best Effort” 구조로 설계한다 - 최선의 노력은 하되 확실한 보장은 하지 않는 구조
메일 전송 기능
✅ Spring AOP(AfterReturning) + @Async 사용
메일 전송 기능은 모든 승인 로직의 Cross-cutting concerns 이므로 Spring AOP를 통해 구현한다. 모든 승인 로직은 Payment 인터페이스의 doPay() 메소드를 구현하기에 해당 메소드에 JoinPoint 를 설정한다.
메일 전송 시 필요한 데이터의 Key 값은 doPay() 로직의 return 값에 포함되어 있다. AOP - AfterReturning 을 사용하여 JoinPoint 를 설정한 뒤, CommonMailService 의 메서드를 비동기로 호출하여 key 값을 통해 메일 내부 데이터를 조회한다.

- 현행 상 모든 doPay 로직 내부에서 수동으로 트랜잭션을 처리하고 있음 (@Transactional 사용 X)
- 또한, try ~ catch 로직 내부에서 commit/rollback 이 결정되며, 그 이후 return 처리함.
- 즉, retrun 시점에 이미 commit/rollback 이 적용되었으므로 하기 서술할 문제 발생 가능성 없음.
- 단, 추후 @Transactional 을 사용하는 경우 해당 문제를 다시 고려해야 함. (미미하지만 설계상의 의존성 발생)
AOP - AfterReturning 사용 시 유의사항
@Transactional + AfterReturning 사용 시 문제 발생 케이스
AfterReturning 은 동기처리된다. @Transactional 사용 시 AfterReturning 로직 또한 트랜잭션에 포함됨을 인지해야 한다.
Spring AOP의 AfterReturning은 트랜잭션이 종료된 후(메서드 호출 종료 후) 실행되는 것이 아니라, return할 객체가 생성되면 즉시 실행된다.
즉, AfterReturning에서 수행되는 로직은 동기적으로 처리됨을 유의해야 한다. AfterReturning 로직에서 Exception이 발생하는 경우 트랜잭션은 롤백되지만, 비동기로 처리된 메일 전송 기능은 이미 수행되어 잘못된 데이터 기반으로 메일이 발송될 가능성이 존재한다.
대체 설계 방안 - Spring Event
Spring AOP(AfterReturning) + Spring Event + Async Listener(AFTER_COMMIT)
- @Transcational 을 사용하는 경우에만 AFTER_COMMIT 이 동작함.
- 가장 이상적인 설계라고 생각하나 현재 수동 트랜잭션을 사용중인 doPay 구조에 적용 불가능
- 장점: doPay 메서드가 Commit 된 이후 비동기로 처리되기에 상기 서술한 문제 발생 가능성 없음
- 단점: Event 설계 및 구현 비용 발생
결론
최대 TPS 2,000 규모의 결제 승인 환경에서 기존 승인 로직의 성능, 안정성, 트랜잭션 정합성에 영향을 주지 않으면서 특정 응답 코드 발생 시 즉시 메일 알림을 제공하기 위한 구조를 설계했다.
승인 로직과 메일 전송 로직의 완전한 분리
승인 로직(doPay())의 내부 코드 수정 없이 동작해야 하므로
메일 전송은 Cross-cutting concern 으로 정의하고 Spring AOP를 통해 분리했다.
Latency 및 Exception 전파 차단
메일 전송은 승인 처리의 성공 여부와 무관해야 하며
승인 경로에 어떠한 지연이나 예외도 전파하지 않도록
@Async 기반 비동기 처리하였다.
Best-Effort 메일 전송 구조
메일 전송 실패 시 승인 트랜잭션에 영향을 주지 않도록
“최선의 노력은 하되, 성공을 보장하지 않는” 구조로 정의한다.
이는 승인 안정성을 최우선 가치로 두는 결제 시스템의 특성을 반영한 선택이다.
현행 트랜잭션 구조를 고려한 현실적인 선택
현재 승인 로직은 @Transactional을 사용하지 않고 수동 트랜잭션 처리 후 return 되는 구조이므로,
AfterReturning + @Async 조합은 트랜잭션 정합성 문제 없이 적용 가능하다.
Spring Event(AFTER_COMMIT) 기반 구조는 이상적이나,현 구조에서는 적용 불가능하여 차후 개선 과제로 남기려 한다.
성능 개선
매 승인 발생 시점마다 DB에서 메일 전송 타겟 응답코드를 조회하는 것은 트랜잭션의 증가를 야기하며, TPS 증가 시 DB 병목 문제로 이어질 수 있기에 성능을 개선했다. 해당 기능에 대한 정리는 다음 포스팅에서 정리하려 한다.
'트러블슈팅' 카테고리의 다른 글
| OOM 에러 해결 (GCLocker?) (2) | 2025.10.29 |
|---|---|
| 페이지네이션, 검색 조건 값 관리 및 검증 자동화 구현 (1) | 2025.06.12 |
| Transactional Outbox Pattern 적용기 (1) | 2025.06.11 |
| 20,000 TPS 처리 선착순 쿠폰 발급 서비스 설계 (0) | 2025.03.11 |
| 템플릿 메소드 패턴으로 관리 포인트 줄이기 (0) | 2025.02.17 |