진행중인 프로젝트를 진행하면서 결제 후 포인트 적립 로직에서 아래와 같은 고민이 생겨 정리합니다.
결제가 완료되면 Front 서버에서 API 서버로 결제 정보를 넘겨주고,
넘겨받은 데이터를 DB 결제(Payment) 테이블에 저장하게됩니다.
그런데, 결제가 완료된 시점에 주문한 금액 * 회원 등급 포인트 정책 만큼 포인트 적립 로직도 실행합니다.
또한, 포인트가 적립되었으므로 포인트 내역에 주문 적립 이라는 사유로 내역 등록 진행해야합니다.
이러한 경우에 결제완료와 포인트 적립( + 내역 등록)을 한 개의 트랜잭션으로 묶어버리게 된다면,
결제가 완료되었는데 포인트 적립에서 문제가 발생해도 결제 완료 처리가 되지 않습니다.
따라서 컨트롤러에서
@PostMapping
public ResponseEntity<PaymentSuccessResponseDto> createPayment(@RequestBody PaymentRequestDto requestDto) {
// 결제 SAVE
PaymentSuccessResponseDto payment = paymentFacade.createPayment(requestDto);
// 포인트 적립 + 포인트 기록
paymentFacade.createRewardPoint(payment.getSaleNumber());
return ResponseEntity.status(HttpStatus.CREATED).body(payment);
}
위와 같이 호출하고, Service(Facade)는 다음과 같이 진행하였습니다.
@Transactional
public PaymentSuccessResponseDto createPayment(PaymentRequestDto requestDto) {
...
paymentService.createPayment(sale.getSaleId(), requestDto);
saleService.updateSalePaymentPaidStatus(sale.getSaleId());
return PaymentSuccessResponseDto.builder()
.saleNumber(sale.getSaleNumber())
...
.build();
}
@Transactional
public void createRewardPoint(String saleNumber) {
SaleResponseDto sale = saleService.getSaleBySaleNumber(saleNumber);
if (Objects.nonNull(sale.getMemberEmail())) {
memberService.updateRewardPoint(sale.getMemberEmail(), sale.getSaleTotalPrice());
}
}
위와 같은 내용으로 진행하였지만, @Transactional 옵션에서 propagation (전파) 속성에 대해 공부하면 좋다고 해서 다음과 같이 수정하였습니다.

JPA에서 제공하는 @Transactioanl 어노테이션의 default propagation 옵션은 REQUIRED 입니다.
이 옵션의 경우 기존 트랜잭션에 포함된다는 특징을 가지고 있습니다.
이러한 경우에 기대했던 결과와 다르게 개별 트랜잭션으로 나눠지지 않고 기존 트랜잭션에 참여하게 된다고 합니다.
(물론, OSIV가 false라면 영속성 컨텍스트가 서비스까지만 이어지기 때문에 컨트롤러까지 영향을 미치지 않을 것 같다.)
따라서 memberService.updateRewardPoint() 에서 propagation 옵션을 REQUIRES_NEW로 설정해주는 작업을 진행하였습니다.
또한, paymentFacade.createPayment() 하나의 메서드에서 공통 처리로 변경하였습니다.
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateRewardPoint(String email, Integer totalPrice) {
...
member.updatePoint(reward);
PointHistory pointHistory = PointHistory.builder()
.member(member)
.pointHistoryPoint(reward)
.pointHistoryReason("주문 적립")
.pointHistoryTime(LocalDate.now())
.build();
pointHistoryRepository.save(pointHistory);
}
이해하기 어려울 수 있어, 아래의 사진도 첨부합니다 !
물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위


참고
[Spring] 스프링의 트랜잭션 전파 속성(Transaction propagation) 완벽하게 이해하기
[Spring] @Transactional 옵션 알아보기 + 트랜잭션 전파