결제 로직에서의 트랜잭션 분리
트랜잭션 범위에 대한 고민
트랜잭션의 범위가 넓으면 편리할 수 있으나 읽기 성능이 저하되고, 데드락 발생 가능성이 높아진다.
모든 프로젝트들이 마찬가지이겠으나 특히 티켓팅처럼 한순간에 많은 요청들이 몰리는 경우, 트랜잭션의 범위가 너무 넓다면 곤란하다.
그렇다면 트랜잭션의 범위가 지나치게 짧다면 어떨까? DB 를 사용하는 모든 상황에서 트랜잭션을 시작하고 끝맺는다면 역시 성능이 좋지 않을 것이다. 트랜잭션의 비용은 생각보다 크다.
그렇다면 적정수준이 어느정도일까? 소거법으로 트랜잭션에서 제거하는게 확실히 좋은 상황먼저 지워보자.
(트랜잭션이 꼭 필요한 상황과 꼭 제거해야 하는 상황을 고려해보자)
토스 API 는 트랜잭션에서 제외하자
우리 프로젝트에서 결제를 담당하고있는 토스 API..
트랜잭션에서 제외하였을 때 만약 결제에 실패한다면 문제가 발생하지 않을까?
발생하지 않는다. 외부 API 와 통신하는 것은 롤백을 신경쓰지 않아도 된다. 그리고 외부 API 를 트랜잭션 범위 안에 넣는 경우 외부 API 에서 문제가 발생하였을 때 트랜잭션이 무자비하게 길어져버리는 문제가 발생한다.
외부 API 는 1순위 제외 대상이다
트랜잭션의 특성을 생각하자
트랜잭션을 나누기 위해 한 클래스 내에서 메서드를 통해 분리하는 경우, 트랜잭션이 적용되지 않을 것이다. 프록시의 특성을 생각해보자..!
해결 방법으로는 Service 의 계층을 분리하는 방법도 있을수 있겠다.
PaymentFacade / PaymentCommandService, PaymentQueryService로 구분하였다.
PaymentFacade 가 PaymentCommandService, PaymentQueryService 를 가지고있는 형태이다.
이런 방식은 쓰기 전용 Service 와 읽기 전용 Service 를 나누었기 때문에 트랜잭션 이외의 장점또한 크다
(의존성이 분리되었다는 전제하에 쓰기 전용, 조회 전용 DB로 다른 DB 를 사용하기 편하다)
하지만 우리가 이렇게 분리를 한 이유는 CQRS 를 위한 것이 아닌, 트랜잭션의 범위를 잘게 쪼개기 위해서였다.
예를 들어, 검증하며 조회 / update 이렇게 트랜잭션을 세세하게 분리하고자하는 목적이었다.
그런데 이렇게 CommandService 와 QueryService 단위로 트랜잭션을 걸었을 때, 트랜잭션의 범위가 너무나도 잘게 쪼개진다는 문제점이 있다 → 데이터 정합성의 문제도 발생할 수 있다
결국은 Controller..!
결국 우리가 선택한 방법은 Facade 단위로 트랜잭션을 걸고, 트랜잭션에서 제외해주어야 하는 부분(ex. 외부 API 사용)은 따로 클래스를 만들어서 Controller 에서 호출해주었다.
여기서 tossApiService 가 외부 API 를 사용하는 부분이다. (Redis 역시 같은 논리로 분리해주어야한다)
사실, 이벤트 기반 아키텍처라면 이렇게 분리하는것 보다는 이벤트를 사용해서 비동기처리 하는 것이 더 좋을 것 같다