기존 로직의 문제점 및 관심사 분리
현재 콘서트 예약 서비스는 모놀로식 아키텍처 형태로 개발되어있고, 모든 도메인의 기능이 하나의 서버에 존재한다.
만약 특정 서비스 메소드에서 다수의 도메인 트랜잭션 처리가 필요한 상황이라고 생각해보자. 일단 코드를 읽기 힘들뿐만 아니라
메소드 Latency도 느려질 것이다. 그리고 예를 들어 아래와 같은 로직이 있을경우
@Transactional
public void 상품주문() {
상품재고차감(); // 1
주문정보생성(); // 2
사용자포인트차감(); // 3
네이버페이결제();
}
만약 네이버페이결제 실행시 예외가 발생한다면 어떻게 될까? 네이버페이 같은 Third Party API 에서 알 수 없는 에러로 인해 예외를 발생시킨다면 1, 2, 3번 로직도 Rollback이 되어버리는 문제가 발생한다. 이게 가장 큰 문제이다. 네이버페이측 서버에러가 내 서비스의 결과로까지 이어지는게 과연 맞는지 고민이 필요하다. 그리고 결제가 실패한다고 다른 도메인들의 결과에도 영향이 가는것도 문제가 있다.
이처럼 강결합 되어있는 도메인간의 책임을 분리하기 위해서는 이벤트 기반 로직 처리가 필요하다.
그렇다면 콘서트 예약 서비스에서 강결합되어있는 로직은 어디에 있는지 확인해보자.
현재 콘서트 예약 서비스에서 CUD(CREATE, UPDATE, DELETE) 트랜잭션을 처리하는 기능은 아래와 같다.
1. 대기열 토큰 발급
2. 포인트 충전
3. 좌석 예약
4. 결제
1, 2, 3번 같은 경우는 하나의 도메인에 대한 영속처리를 하기 때문에 크게 문제는 없을거라고 생각된다. 하지만 결제 로직을 살펴보자.
@Transactional
public void 예약결제() {
포인트차감(); // 1
포인트사용내역추가(); // 2
결제내역추가(); // 3
예약상태변경(); // 4
콘서트좌석정보변경(); // 5
대기열토큰삭제(); // 6
}
결제 도메인외에 부가적인 도메인들의 트랜잭션처리가 포함되어있기때문에 앞서 말했던 문제가 발생할 확률이 높다. 그렇다면 이벤트 처리가 필요한 도메인은 어디일까? 6번 대기열 토큰 삭제가 실패한다고 결제 실패가 되는건 로직상 문제라고 생각된다. 1, 2, 3, 4, 5번 로직을 하나의 트랜잭션으로 묶고 대기열 토큰 삭제 로직을 이벤트 처리를 하면 될것같다.
그렇다면 이벤트 처리는 어떻게 진행할 수 있을까? 먼저 단일 서버내에서 처리를 하려면 스프링의 ApplicationEventListener/@EventListener를 활용하면 관심사 분리가 가능하다. 간단히 수도 코드로 살펴본다면,
public class 결제클래스 {
private final ApplicationEventPublisher applicationEventPublisher;
@Transactional
public void 예약결제() {
포인트차감();
포인트사용내역추가();
결제내역추가();
예약상태변경();
콘서트좌석정보변경();
applicationEventPublisher.publishEvent(대기열토큰이벤트 생성);
}
}
@Component
public class 대기열토큰이벤트리스너 {
private final ReservationTokenRepository reservationTokenRepository;
@EventListener
@Async
public void listen(대기열토큰이벤트) {
reservationTokenRepository.토큰삭제(대기열토큰이벤트.토큰정보);
}
}
대략 위와 같은 코드가 될것같다. 관심사가 분리되었기 때문에 @Async 처리를 통해 트랜잭션 분리를 할 수 있고 성능향상도 가능하다.
푸시 발송과 같은 외부 API 요청이 추가된다면?
푸시 발송 역시 결제와는 관심사가 분리되어야 한다. 그렇기 때문에 대기열토큰 이벤트처리를 한것처럼 진행하면 되지않을까 생각된다.
public class 결제클래스 {
private final ApplicationEventPublisher applicationEventPublisher;
@Transactional
public void 예약결제() {
...
applicationEventPublisher.publishEvent(푸시이벤트 생성);
}
}
@Component
public class 푸시발송리스너 {
private final PushClient pushClient;
@EventListener
@Async
public void listen(푸시이벤트) {
pushClient.create(푸시이벤트.푸시정보);
....
}
}
MSA 환경으로 전환된다면?
만약 모놀로식 환경에서 MSA 환경으로 전환된다면 어떻게 로직이 수정되어야 할까?
@Transactional
public void 예약결제() {
포인트차감(); // 포인트 서버 요청
포인트사용내역추가(); // 포인트 서버 요청
결제내역추가(); // 결제 서버 요청
예약상태변경();
콘서트좌석정보변경();
대기열토큰삭제(); // 대기열 서버 요청
}
MSA 환경으로 이루어져 있는 개발조직의 경우는 도메인별로 팀이 꾸려져있는걸로 알고있다. 아마도 포인트, 결제, 대기열 팀들이 존재할 것이고 각각의 서버가 존재할것이다.
그렇기때문에 예약상태, 콘서트좌석정보는 내부 서버(내가 소속한 팀)에서 관리되고 다른 도메인 로직은 메시지 생성만을 통해서 처리가 가능할 것이다. 하지만 스프링의 ApplicationEventListener/@EventListener 는 Bean 형태의 의존성 주입을 통해 관리되기 때문에 다른 서버의 EventListener와는 연동이 불가능하다. 다른 서버와 이벤트 기반 통신을 하기 위해서는 Kafka나 RabbitMQ 같은 메시지큐를 활용해서 해결할 수 있다. 예를 들어 Kafka를 사용한다면 Kafka Producer로 메시지를 발행하고, 해당 도메인 서버에서 Kafka Listener를 구현해서 처리할 수 있다. 만약 Listener쪽에서 에러가 발생해서 Producer쪽 로직 역시 Rollback이 되어야한다면? 보상 트랜잭션과 SAGA 패턴을 적용해서 해결할 수 있다고 하는데 아직은 어려워서 좀 더 공부가 필요하다..
백엔드 개발의 꽃이라 할 수 있는 트랜잭션 관리에 대해 알아보았는데, 해당 기술들의 특징들을 잘 파악해서 본인이 처해있는 개발환경에 맞는 기술 선택이 필요할 것 같다.