나의 개발일지
[SpringBoot] WebClient 비동기 흐름 내 Transaction 비활성화 트러블슈팅 - 2 ( 문제 발생 및 해결 ) 본문
[SpringBoot] WebClient 비동기 흐름 내 Transaction 비활성화 트러블슈팅 - 2 ( 문제 발생 및 해결 )
사각분무기 2024. 2. 22. 16:29앞서 WebClient를 도입한 배경과 결과를 말씀드렸습니다.
아래와 같이 Service 클래스를 작성해뒀는데요.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class TossService {
private final WebClient webClient;
private static String authorization;
@Value("${custom.tossPayments.widget.secretKey}")
private void setTossPaymentsWidgetSecretKey(String tossPaymentsWidgetSecretKey) {
String encodedKey = Base64.getEncoder().encodeToString((tossPaymentsWidgetSecretKey + ":").getBytes(UTF_8));
authorization = "Basic " + encodedKey;
}
@Transactional
public Mono<TossPaymentRequest> confirmTossPayment(final TossConfirmRequest tossConfirmRequest) {
return webClient.post()
.uri("https://api.tosspayments.com/v1/payments/confirm")
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", authorization)
.bodyValue(tossConfirmRequest)
.retrieve()
.onStatus(
HttpStatusCode::isError,
clientResponse -> Mono.error(new PaymentException(PAYMENT_API_CALL_FAILED)))
.bodyToMono(TossPaymentRequest.class);
}
}
비동기가 가지는 장점을 여럿 알아본 저는 최대한 비동기 흐름을 통해 코드를 이어나가고 싶었는데요.
처음 작성할 땐 문제가 없다고 생각하고 있었습니다.
@PostMapping("/payByToss/{reserveId}")
public Mono<ResponseEntity<ResponseDto<CashLogIdResponse>>> payByTosss(
@RequestBody final TossConfirmRequest tossConfirmRequest,
@PathVariable final Long reserveId
) {
return tossService.confirmTossPayment(tossConfirmRequest)
.flatMap(tossPaymentRequest -> {
Reservation reservation = reservationService.findUnpaidById(reserveId).orElseThrow(() -> new ReservationException(NOT_FOUND_RESERVATION_ID));
if (!cashLogService.canPay(reservation, Long.parseLong(tossConfirmRequest.getAmount())))
return Mono.error(new PaymentException(INSUFFICIENT_DEPOSIT));
Long cashLogId = cashLogService.payByTossPayments(tossConfirmRequest, reservation).getId();
CashLogIdResponse cashLogIdResponse = cashLogService.getCashLogIdById(cashLogId);
return Mono.just(ResponseEntity.ok(
new ResponseDto<>(
HttpStatus.OK.value(),
"토스페이먼츠 결제가 완료되었습니다.", null,
null, cashLogIdResponse)
));
});
}
이와 같이 코드를 작성할 경우 CRUD 의 네가지 작업 중 생성과 읽기는 가능하지만 수정과 삭제가 불가능합니다.
분명 Transactional 어노테이션을 Service 딴에 붙였는데 왜 동작하지 않는걸까요?
JPA 에서 데이터의 입력과 수정은 더티 체킹을 통해서 이루어집니다.
더티 체킹이 이루어지기 위해선 쓰레드에 트랜잭션을 연결하고 작업 후 커밋을 하는 과정을 거쳐야합니다.
메소드 단위로 생성된 트랜잭션이 여러개 호출될 경우 보통 트랜잭션 전파를 통해 통합됩니다.
문제는 트랜잭션 전파가 한 쓰레드 안에서만 이루어진다는 부분입니다.



WebClient를 활용한 비동기 흐름을 사용할 경우 위와 같이 쓰레드가 나뉘게 됩니다.
비동기 흐름 내부에서 트랜잭션을 활용하는 작업은 할 수 없다는 것이죠.
알아보니 비동기 환경에선 NoSQL 을 활용하는 MongoDB 과 같은 비동기를 위한 db들을 활용해야 된다고 합니다.
결국 비동기 흐름을 생성하기 전에 모든 작업들을 마친 후 마지막에 비동기 흐름을 사용하는 방식으로 코드를 사용했습니다.
@PostMapping("/payByToss/{reserveId}")
public Mono<ResponseEntity<ResponseDto<CashLogIdResponse>>> payByToss(
@RequestBody final TossConfirmRequest tossConfirmRequest,
@PathVariable final Long reserveId
) {
Reservation reservation = reservationService.findUnpaidById(reserveId).orElseThrow(() -> new ReservationException(NOT_FOUND_RESERVATION_ID));
if (!cashLogService.canPay(reservation, Long.parseLong(tossConfirmRequest.getAmount())))
throw new PaymentException(INSUFFICIENT_DEPOSIT);
Mono<TossPaymentRequest> tossPaymentResponseMono = tossService.confirmTossPayment(tossConfirmRequest);
Long cashLogId = cashLogService.payByTossPayments(tossConfirmRequest, reservation).getId();
return tossPaymentResponseMono
.flatMap(tossPaymentRequest -> {
CashLogIdResponse cashLogIdResponse = cashLogService.getCashLogIdById(cashLogId);
return Mono.just(ResponseEntity.ok(
new ResponseDto<>(
HttpStatus.OK.value(),
"토스페이먼츠 결제가 완료되었습니다.", null,
null, cashLogIdResponse)
));
});
}
이 과정 중에서 WebClient의 api 호출에서 오류가 발생할 경우에 다른 트랜잭션과 다른 쓰레드에서 발생한 오류이기 때문에 분명 오류가 났음에도 불구하고 롤백이 이루어지지 않는 등 당시엔 그저 당황스럽던 헤프닝도 있었네요
추가로 이를 해결하기 위한 다른 방법으로는 JdbcTemplate을 이용한 벌크 연산을 활용하는 방법이 있지만 이 경우 데이터 무결성을 해친다거나 @CreatedDate, @LastModifedDate 와 같은 어노테이션이 적용되지 않기 때문에 사용하지 않았습니다.
'Language > SpringBoot' 카테고리의 다른 글
| [트러블 슈팅] AssertJ 를 통해 CustomException 내에 추가로 정의한 필드 테스트 (1) | 2024.04.19 |
|---|---|
| [트러블 슈팅] Spring 환경에서 @WebMvcTest를 활용한 컨트롤러 테스트 진행하기 - 403 forbidden 오류 해결 (0) | 2024.04.05 |
| [SpringBoot] WebClient 비동기 흐름 내 Transaction 비활성화 트러블슈팅 - 1 ( 토스페이먼츠 WebClient 로 구현 ) (0) | 2024.02.22 |
| [SpringBoot] Page와 PageImpl (0) | 2024.02.21 |
| [SpringBoot] hiddenmethod를 이용한 타임리프 환경에서 PutMapping, DeleteMapping 활용법 (1) | 2024.01.14 |