일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 공변
- JDK14
- 재고 시스템
- 동시성
- method area
- 카카오 화재
- java
- CaffeineCache
- lazyloading
- JAVA8
- ci/cd
- 상태패턴
- 리팩터링
- springboot
- backend
- B+TREE
- Redis
- 웹캐시
- 지연로딩
- 제네릭
- Metaspace
- 주문
- 부하테스트
- GithubActions
- Spring Data Redis
- Jenkins
- Ehcache
- JPA
- 트랜잭션
- nonclustered index
- Today
- Total
NDM
5. 주문 트랜잭션 설계와 Transaction 내부에서의 호출 문제 본문
배달 서비스를 구현하는 slowDelivery 프로젝트 진행중입니다.
주문 로직에 대해 제가 고려한 것을 포스팅 하려고 합니다.
도메인은 장바구니 / 상품/ 재고 / 가게 / 주문이 있으며,
결제API는 클라이언트쪽에서 연동하기 때문에 결제데이터를 save하는 정도로만 구현했습니다.
장바구니와 재고는 Redis로 되어있으며, 나머지는 RDB를 사용했습니다.
제 프로젝트의 주문 제한사항은 다음과 같습니다.
- 장바구니를 거쳐서만 주문할 수 있으며,
- 장바구니에 다른 가게의 상품을 담을 수 없습니다.
주문 로직의 순서
주문은 다음과 같은 순서로 이루어집니다
- 장바구니와 가게 데이터를 조회해 주문 객체를 만듭니다.
- 가져온 데이터로 검증을 시작합니다. ( 가게가 오픈했는가 / 주문 금액이 가게의 최소주문금액을 넘어섰는가 )
- 재고를 먼저 감소시킵니다. ( 내부적으로 재고가 부족하지 않은지에 대한 검증이 먼저 이루어집니다. )
- 주문 데이터와 결제 데이터를 저장합니다.
무엇을 고민했는가?
가장 큰 고민이였던 것은 위에서 설명한 주문 로직을 한 트랜잭션 안에서 얼마나 효율적으로 구성할까? 였습니다.
주문 성공 -> 결제 성공으로 이어지는 로직을 작성하려고 할 때,
이렇게 되면 트랜잭션이 너무 오랜 시간을 기다리지 않을까? 하는 생각이 들었습니다.
결과적으로 주문 / 결제의 트랜잭션을 나누기로 했습니다. 이것은 결제대기 상태를 넣어두면 가능합니다.
- 트랜잭션이 서버의 api를 호출해 주문서를 발급하고, 결제상태가 "대기" 인 결제 데이터를 삽입하는 것까지 마무리합니다.
- 이후 클라이언트에서 성공/실패에 따라 상황에 맞는 서버쪽 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가 먼저 호출되기 떄문에 트랜잭션을 적용할 수 있게 됩니다.
'[프로젝트] Slow Delivery' 카테고리의 다른 글
7. GithubActions를 이용한 CI/CD 구축 일지 2편 (0) | 2022.10.24 |
---|---|
6. 재고 감소 -> 주문이 맞을까, 주문 -> 재고 감소가 맞을까? (0) | 2022.09.14 |
4. 재고 처리 로직 동시성 이슈 해결 일지 (0) | 2022.08.31 |
3. FetchType = Lazy를 명시해도 Lazy Loading이 적용되지 않는다?? (0) | 2022.08.18 |
2. GithubActions를 이용한 CI/CD 구축 일지 1편 (0) | 2022.08.04 |