NDM

5. 주문 트랜잭션 설계와 Transaction 내부에서의 호출 문제 본문

[프로젝트] Slow Delivery

5. 주문 트랜잭션 설계와 Transaction 내부에서의 호출 문제

ndm.jr 2022. 9. 14. 00:42

배달 서비스를 구현하는 slowDelivery 프로젝트 진행중입니다.

주문 로직에 대해 제가 고려한 것을 포스팅 하려고 합니다.

 

도메인은 장바구니 / 상품/ 재고 / 가게 / 주문이 있으며,

결제API는 클라이언트쪽에서 연동하기 때문에 결제데이터를 save하는 정도로만 구현했습니다.

 

장바구니와 재고는 Redis로 되어있으며, 나머지는 RDB를 사용했습니다.

 

제 프로젝트의 주문 제한사항은 다음과 같습니다.

  • 장바구니를 거쳐서만 주문할 수 있으며,
  • 장바구니에 다른 가게의 상품을 담을 수 없습니다.

주문 로직의 순서

주문은 다음과 같은 순서로 이루어집니다

  • 장바구니와 가게 데이터를 조회해 주문 객체를 만듭니다.
  • 가져온 데이터로 검증을 시작합니다. ( 가게가 오픈했는가 / 주문 금액이 가게의 최소주문금액을 넘어섰는가 )
  • 재고를 먼저 감소시킵니다. ( 내부적으로 재고가 부족하지 않은지에 대한 검증이 먼저 이루어집니다. )
  • 주문 데이터와 결제 데이터를 저장합니다.

 


무엇을 고민했는가?

가장 큰 고민이였던 것은 위에서 설명한 주문 로직을 한 트랜잭션 안에서 얼마나 효율적으로 구성할까? 였습니다.

 

주문 성공  -> 결제 성공으로 이어지는 로직을 작성하려고 할 때,

이렇게 되면 트랜잭션이 너무 오랜 시간을 기다리지 않을까? 하는 생각이 들었습니다.

 

결과적으로 주문 / 결제의 트랜잭션을 나누기로 했습니다. 이것은 결제대기 상태를 넣어두면 가능합니다.

  1. 트랜잭션이 서버의 api를 호출해 주문서를 발급하고, 결제상태가 "대기" 인 결제 데이터를 삽입하는 것까지 마무리합니다.
  2. 이후 클라이언트에서 성공/실패에 따라 상황에 맞는 서버쪽 api를 호출하게 하고, 이 때 "대기"상태인 결제데이터를 성공/실패 처리하고, 주문의 상태도 같이 업데이트 해줍니다. 

서버의 api가 무조건적으로 2번 호출된다는 점이 걸릴수도 있지만,

주문-결제(대기)까지의 로직을 한 트랜잭션 안에서 관리할 수 있고, 이후에는 상태값만 변경하는 api를 통해 효율적으로 데이터를 관리할 수 있습니다. 

 

아임포트라는 결제API를 제공하는 서비스의 공식 문서를 참조하여

결제 대기상태를 추가해 주도록 하고, <주문>로직에서는 성공여부와 관계없이 결제 대기까지의 로직만 작성했습니다.

  <script>
    function requestPay() {
      IMP.request_pay({ // param
          // 결제데이터
            // 내가 만든 서버쪽 결제 api 호출
      }, function (rsp) { // callback
          if (rsp.success) {
              ...,
              // 결제 성공 시 로직,
              ...
          } else {
              ...,
              // 결제 실패 시 로직,
              ...
          }
      });
    }
  </script>
  
  출처 : 아임포트 docs

 

이렇게 하면 결과적으로

  • 사용자가 결제를 하기위해 절차를 밟는동안 트랜잭션을 유지할 필요가 없으며,
  • 대기 상태 이후의 성공과 실패에 대한 메소드를 각각 만듦으로써 SRP를 지키고 한 메소드 안에서의 if문을 통한 분기처리를 피할 수 있습니다.

또한 대기상태 없이 <주문-성공> 으로 처리해두고 실패 시 <주문-실패>로 변경할 경우

결제상태가 "성공"인 데이터를 가져와서 실패 상태로 수정해야하는 이상한 상황이 발생하는 경우도 방지할 수 있습니다.

 


또 다른 문제

하지만 결과적으로 또 하나의 문제가 발생했습니다.

같은 객체의 @Transactional이 붙은 다른 메소드끼리의 호출 시, 호출되는 쪽은 @Transactional이 적용되지 않습니다. 

 

 

waitingAfterOrder() 메소드를 통해 주문서를 발급한 뒤

결제 대기 상태를 넣어주려고 했으나 작동하지 않는 문제가 있었습니다.

waitingAfterOrder() 메소드는 같은 서비스 객체 안의 메소드이기 떄문입니다.

 

Spring의 @Transactional은 런타임 위빙으로 동작하기 떄문에 프록시를 통해 런타임 시점에 트랜잭션을 적용할지 말지를 결정합니다.

 

출처 : 김영한 - 핵심원리 고급편

 

프록시 -> aop적용 여부 확인 -> 실제 객체 메소드 호출 의 순서로 이루어지는 방식입니다.

하지만 같은 객체의 다른 두 개의 메소드끼리 호출을 한다면

프록시 -> aop적용 여부 확인 -> 실제 객체 메소드 호출 -> 실제 객체 메소드 호출 의 순서로 동작하게 됩니다.

 

컨트롤러나 외부로부터 서비스 객체의 메소드를 호출하는 것은 aop의 영향으로 프록시가 먼저 호출되지만,

이미 실제 객체 메소드를 호출하고 있는 상황에서 또 프록시를 갔다가 같은 실제 객체로 돌아올 필요는 없기 때문이죠

 

 

결과적으로는 결제 대기 상태의 결제정보를 넣어주는 로직을 PayService라는 외부의 객체를 생성함으로써 해결했습니다.

이렇게 하면 PayService가 호출 될 때 Proxy가 먼저 호출되기 떄문에 트랜잭션을 적용할 수 있게 됩니다.