트랜잭션의 범위 분석과 비즈니스 개선 방안 - 8주차
결제 유스케이스 트랜잭션 범위 분석
대표적으로 결제 유스케이스를 통해 트랜잭션의 범위를 분석해 보자.
먼저 결제 퍼사드의 비즈니스 로직은 아래와 같다고 가정하자.
위 비즈니스 로직은 크게 두 가지의 문제점이 있다.
- 긴 트랜잭션 범위
-> 외부 API가 현 트랜잭션에서 호출된다면, 이로 인한 지연이 있다. 비즈니스 로직에 영향을 주는 외부 API일 경우에는 지연시간이 불가피하지만, 타 플랫폼에 적재하는 사용이력이거나 알림전송 같은 외부API로 변경 또는 추가될 경우에는 지연 시간을 트랜잭션 바깥으로 빼주어야 한다.
또한, 비즈니스로직에 영향을 주지 않는 외부 API에서 실패가 발생했을 경우에는 모든 비즈니스로직이 롤백되지 않아야 한다. (알림전송 서비스가 추가될 경우) - 관심사의 분리 결여
-> 각자 다른 도메인의 많은 비즈니스 로직들이 하나의 메서드 안에서 사용되면 코드의 재사용성이 매우 낮아질 뿐더러 결합도가 매우 높아진다.
그렇다면 이 문제점은 어떻게 해결할지, 서비스가 확장됐을 때를 예시로 들어 해결방안을 생각해 보자.
서비스의 규모가 확장된다 가정하고 서비스 분리 방안
현재 결제서비스에서 유저 포인트 사용이력같은 기능들은 DBMS의 History 테이블에 저장 중이다.
이 서비스에서 유저 포인트 사용이력이 외부 플랫폼에 적재되고, 추가적으로 외부 API를 호출하는 (예를 들면 FCM, 카톡같은 알림전송) 기능들이 필요하다면 어떻게 구현을 해야 할까?
현재 모놀로식 아키텍처에서, 외부 서비스를 먼저 이벤트 기반으로 호출하도록 변경해 주었다.
위처럼 설계했을 때는 관심사의 분리가 완전히 지켜지지는 않았다. 개발 측면에서만 관심사의 분리가 지켜지고, 그것도 포인트 사용이력 저장, 알림전송만 관심사를 분리한 것이다.
이렇게 분리한 이유는 만일 알림 전송 로직이 트랜잭션 내부에 존재하게 된다면 불필요한 지연 시간이 생길 수 있기 때문이다. 기존의 로직에 영향을 주지 않기 때문에 이러한 지연시간은 쓸데없는 지연시간이다.
관심사의 분리를 제대로 지키고 싶다면 마이크로서비스 아키텍처 처럼 모든 도메인의 서비스들을 이벤트 기반으로 분리하고 분산 트랜잭션을 사용하는 것이 좋지만, 이번에는 유저 포인트 사용이력 저장 서비스와, 알림전송 두 가지만 이벤트로 변환해 보도록 하자.
데이터 정합성과 원자성을 위해, 유저 포인트 사용이력 저장 이벤트를 받는 리스너는 @TransactionalEventListener를 통해 BEFORE_COMMIT으로 설계하였다.
그리고 알림 전송은 트랜잭션이 커밋된 이후에 수행하여 알림 전송이 실패해도 전체 비즈니스 로직이 롤백되지 않도록 비동기와 @TransactionalEventListener, AFTER_COMMIT으로 설계하였다.
하지만 이러한 설계보다 분산 트랙잰션으로 구현하여 각각의 서비스가 독립적인 것이 더 효율적이다.
트랜잭션의 범위를 극한으로 축소시킬 수 있고, 각각 도메인에 대해 독립적이면서 결합도 또한 매우 낮기 때문이다. (관심사의 완벽 분리)
각각의 서비스들이 분산 트랜잭션에서 수행될 때를 고려해보고, 주의점을 확인
현재 결제 유스케이스는 분산 트랜잭션으로 구현(마이크로서비스로 분리)했을 경우에 일관성 유지에 문제가 발생할 수 있다. (발생할 것이다)
이러한 일관성 문제를 해결하기 위해 SAGA 패턴과 보상 트랜잭션을 고려해보아야 한다.
마이크로서비스 환경에서는 각 서비스가 독립적인 데이터베이스를 갖고 있어서 여러 서비스에 걸쳐 일관성 있는 트랜잭션 처리가 필요하다.
예를 들어 예약 서비스와 결제서비스가 각각 다른 데이터베이스를 사용할 때, 예약 상태를 변경하고 유저 포인트 차감 후 결제 성공까지 되어야만 예약 상태를 PAY_SUCCESS 로 변경할 수 있다.
분산 트랜잭션을 공부하기 위해서는 로컬 트랜잭션 개념과 글로벌 트랜잭션 개념을 알고 있어야 한다.
로컬 트랜잭션이란?
하나의 Transaction Context 내에서 하나의 데이터베이스만을 처리대상으로 하는 트랜잭션을 의미한다. 즉, 하나의 데이터베이스 커넥션 안에서 사용하는 서비스에서의 트랜잭션은 모두 로컬 트랜잭션이다.
MSA에서 각 서비스가 하나의 데이터베이스를 포함하고 트랜잭션 컨텍스트 내에서 해당 데이터베이스만을 대상으로 처리하는 경우에도 로컷 트랜잭션이다.
글로벌 트랜잭션이란?
글로벌 트랜잭션은 Transaction Context 안에서 여러 리소스를 처리하는 트랜잭션을 의미한다. 즉, 하나의 트랜잭션에서 여러 데이터베이스에 접근하여 작업하는 것을 의미한다.
대표적인 방법으로는 Two-Phase-Commit 방식이 존재하는데 이는 Coordinator를 통해 글로벌 트랜잭션을 관리한다.

동작 과정은 위 그림처럼 phase1과 phase2(각 커밋) 단계를 통해 트랜잭션을 원자적으로 처리한다.
- Database 1,2가 각각 쓰기 수행을 한다.
- Coordinator가 Database에 커밋을 할 준비가 되었는지 Prepare 요청을 보낸다.
- 모든 Database로부터 Prepare 응답을 받으면 커밋을 처리한다.
마이크로서비스는 지속적인 사용이 가능한 단순함을 추구하여 운영의 어려움이 존재하는 글로벌 트랜잭션(대표적으로 Two-Phase-Commit)은 추천하지 않으며, 로컬 트랜잭션을 적극 권장하고 있다.
또한 글로벌 트랜잭션은 컴포넌트의 느슨한 결합을 방해할 수 있다는 부정적인 인상도 존재한다고 한다.
그리하여 MSA환경에서 데이터베이스 간 동기화를 위한 솔루션으로 SAGA패턴을 권장하고 있다.
SAGA란 로컬 트랜잭션, 이벤트, 보상 트랜잭션 등의 기술 및 기법을 사용하여 여러 리소스간 동기화를 취하는 디자인 패턴을 뜻하며, 크게 두 가지 전략이 있다.
그것은 바로 Orchestration 전략과 Choreography 전략이다. (발음이 좀 어렵다)
SAGA패턴 - Orchestration 전략
Orchestration전략은 중앙 오케스트레이터가 각 서비스의 트랜잭션 수행 순서를 관리하고, 성공 및 실패 여부에 따라 다음 트랜잭션을 진행하거나 보상 트랜잭션(롤백)을 수행한다.
Orchestration 전략 장점
- SAGA Orchestrator가 SAGA 제어 로직을 담당하고 도메인 계층 서비스는 비즈니스 로직과 데이터 처리에 위힘하여 역할을 명확하게 분리할 수 있어, 개발팀의 효율성이 매우 향상된다!
- 서비스 간 흐름 제어가 SAGA Orchestrator 내에 구현되어 있어 흐름을 파악하기 쉽다
Orchestration 전략 단점
- 트랜잭션을 관리하는 오케스트레이터를 추가해야 한다.
- 고려할 사항으로는 SAGA Orchestrator가 무거워지지 않도록 역할 분담에 유의하여야 하는 점이 있다.
SAGA 패턴 - Choreography 전략
Choreography 전략은 중앙 오케스트레이터 없이 각 서비스가 자신이 수행할 작업을 수행하고, 다음 단계의 서비스를 호출하여 트랜잭션을 이어가는 방식이다. 이 또한 실패한다면 보상 트랜잭션을 수행한다.
Choreography 전략 장점
- 메시징 플랫폼(Kafka, RabbitMQ)를 사용하여 서비스를 개발하면 되어서 구조가 간단하다.
Choreography 전략 단점
- 각 서비스 내부에 서비스간 흐름 제어 로직이 있어 SAGA 전체를 파악하기 쉽지 않다.
- 트랜잭션 실행 시 프로세스의 진행 상태 확인이나 추적이 어렵다. 같은 이유로 인해 통합테스트와 디버깅이 어렵다.
- 각 서비스 내부에는 비즈니스 로직이나 보상 트랜잭션 로직에 추가로 사가를 성립시키기 위한 제어로직도 구현해야 한다. 서비스 안에 사가 제어 로직이 함께 동작하여 관심사 분리가 명확하지 않다.
보상 트랜잭션이란?
이미 커밋된 트랜잭션의 작업을 취소하기 위해 수행되는 트랜잭션이다.
SAGA패턴의 일환으로 트랜잭션 실패 시 이전에 완료된 트랜잭션을 취소하는 반대 작업을 수행하여 시스템을 원래대로 되돌리는 작업이다.
SAGA 패턴의 각 전략에서의 보상 트랜잭션 동작 방식은 아래와 같다.
Orchestration 패턴에서의 보상 트랜잭션 동작
오케스트레이션 방식에서는 중앙 오케스트레이터가 트랜잭션을 관리하며, 보상 트랜잭션 역시 오케스트레이터가 제어한다.
중앙 오케스트레이터가 트랜잭션 단계들을 순차적으로 수행하다가, 특정 단계에서 오류가 발생하거나 실패가 발생하면 이전에 성공한 모든 단계에 대해 보상 트랜잭션을 역순으로 호출하여 각 단계를 취소하거나 롤백한다.
Choreography 패턴에서의 보상 트랜잭션 동작
코레오그래피 방식에서는 중앙관리자가 없고, 각 서비스가 발생한 이벤트를 보고 필요한 작업을 수행한다. 보상 트랜잭션도 각 서비스가 스스로 처리한다.
각 서비스가 독립적으로 이벤트를 발행하고 다른 서비스가 이를 수신하여 필요한 트랜잭션을 수행하다가 오류가 발생하면, 실패한 서비스가 보상 트랜잭션이 필요하다는 이벤트를 역으로 발행하고, 이전에 성공한 서비스들이 이를 수신하여 보상 트랜잭션을 수행한다.
결론
이렇게 서비스가 확장되어 불가피하게 MSA로 전환하게 된다면 서비스 경계와 도메인의 분리를 명확하게 하고, 데이터 관리 또는 일관성 문제에 주의해야 한다.
특히 서비스 간에 결합도를 최소화하기 위해 이벤트 기반의 통신으로 서비스 간에 직접적인 호출을 지양해야 한다.
단일 데이터베이스 트랜잭션으로는 이를 구현할 수 없으므로, 보상 트랜잭션의 사용에 주의를 요해야 하고, 이에 대한 전략을 제대로 설계해야 할 것이다.
추가적으로 서킷브레이커, 리트라이 전략 등 장애 격리 및 복구 전략도 고려해보아야 한다.
11월 11일, 네이버 DAN24 컨퍼런스에 다녀왔는데 놀랍게도 세션중에 하나가 네이버 결제 시스템 개선이었다. (심지어 이전 세션은 캐싱으로 비상대응 하는 세션이었다.)
해당 세션에는, 저번 주차 발제 내용과 이번주차 발제 내용이 너무 연관이 많이 되어있었다.
첫 번째로, 분산 DB 전환에 대한 내용이었는데, 샤딩과 같은 개념이 아직은 어렵게 느껴지지만 데이터베이스 시야에 대한 큰 그림을 그리는데 많은 도움이 되었다.
평소 단일 DB만으로 개발해 온 나한테는 분산 환경에서 데이터의 일관성과 성능을 유지하는 방식은 정말 새로운 경험이었다.
두번째로는 결제 시스템에 EDA를 적용했다는 것이었다. 이 세션에서는 이벤트 기반으로 네이버의 결제시스템이 어떤 흐름으로 동작하는지에 대해 자세히 설명해주었는데, 발제를 듣고 들으니까 좀 더 이해하기 수월하다는 생각이 들었다. (그래도 완전히 이해하기는 힘들었다..)
마지막으로 결제 시스템에서 무중단 결제를 위해 레디스로 주문서 대기열을 만들어 트래픽을 제어하는 흐름을 설명해주었는데, 정말 감회가 새로웠다. 레디스를 실제 대규모 결제 시스템에서 효율적으로 활용하는걸 보고 레디스의 실무적 강점을 체감할 수 있었던 것 같다.
이번 컨퍼런스는 항플에서 배우고 있는 것들이 실무에서 얼마나 활용도가 높고 중요한 기술들인지 새삼 깨닫는 시간이엇던 것 같다.
