밤빵's 개발일지
결제시스템구현: 멱등성 문제 (PUT & PATCH) 본문
결제기능을 구현하면서, 동시성문제와 멱등성에 대해서 알게됐는데 이름부터 너무 생소해서 이걸 어떻게 공부해야하나 걱정부터 앞서던 중 기술매니저님 께서 PUT과 PATCH를 생각하면 이해하기 쉬울거라고하셔서 연관지어 개발일지를 작성하게 되었다. 코드 예시는 내 서비스 코드를 사용했는데.. 기능 구현이 너무너무 안되고 있다😢
멱등성(Idempotency) 문제는 결제 시스템을 구현할 때 중요한 개념이다. 멱등성은 같은 요청을 여러 번 반복해서 보내더라도 결과가 변하지 않는 성질을 의미하는데, 특히 결제 시스템에서는 멱등성을 보장하는 것이 매우 중요하다. 같은 결제 요청이 여러 번 처리되면 고객에게 중복된 결제가 발생할 수 있기 때문이다. 이 멱등성을 이해하기 위해서 HTTP메서드인 PUT과 PATCH와 연관지어 이해해보려 한다🤓
▶ 멱등성(Idempotency)이란?
멱등성은 요청이 여러 번 호출되더라도 서버의 상태가 동일하게 유지되는 것을 보장하는 속성이다. 위에서 설명했 듯 결제 요청을 여러번 전송했을 때 결제가 한 번만 처리되어야 하는 것이 멱등성이다. 만약 멱등성이 보장되지 않는다면, 네트워크 오류로 인해 동일한 요청이 반복되어 중복 결제가 발생할 수 있다.
▶ 결제 시스템에서의 멱등성 문제
결제 시스템에서 멱등성을 보장하지 않으면 다음과 같은 문제가 발생할 수 있다.
→ 중복 결제 문제 :
사용자가 결제를 시도했을 때, 요청이 여러 번 전송되면 서버는 요청을 여러 번 처리하여 중복 결제가 이루어질 수 있다.
→ 데이터 무결성 문제 :
중복 요청이 동일한 리소스를 여러 번 수정하게 되면 데이터의 일관성이 깨질 수 있다.
따라서 결제 API를 설계할 때, 동일한 결제 요청이 여러번 호출되더라도 한 번만 처리되도록 멱등성을 보장해야 한다.
▶HTTP 메서드와 멱등성 : PUT vs PATCH
HTTP 메서드 중 PUT 과 PATCH는 멱등성과 관련된 중요한 개념을 가지고 있다. 이를 결제 시스템과 연결지어 설명하면
▷ PUT 메서드
→ 특징 :
PUT메서드는 리소스를 생성하거나 업데이트 하는 요청으로, 같은 PUT요청을 여러 번 보내더라도 결과는 동일하다.
→ 멱등성 보장 :
PUT 요청은 멱등성을 보장한다. 동일한 요청이 여러 번 전송되더라도 리소스의 상태는 요청이 처음 수행되었을 때와 동일하게 유지된다.
→ 결제 시스템에서의 사용 :
결제와 같은 시스템에서는 PUT요청을 통해 결제 상태를 "완료"로 설정할 수 있다. 예를 들어 결제 ID를 기반으로 PUT요청을 보내면, 해당 결제의 상태를 "완료"로 변경하고, 동일한 요청이 다시 들어와도 결과는 변경되지 않는다.
▼ 코드예시 (결제 상태를 업데이트하는 PUT 메서드)
▽Controller
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
// 결제 상태 업데이트 (PUT 메서드)
@PutMapping("/{impUid}")
public ResponseEntity<PaymentResponseDto> updatePaymentStatus(@PathVariable String impUid, @RequestBody PaymentUpdateRequestDto requestDto) {
PaymentResponseDto response = paymentService.updatePaymentStatus(impUid, requestDto);
return ResponseEntity.ok(response);
}
}
▽Service
@Service
@Transactional
public class PaymentService {
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
// 결제 상태 업데이트 (PUT 메서드)
public PaymentResponseDto updatePaymentStatus(String impUid, PaymentUpdateRequestDto requestDto) {
PaymentEntity paymentEntity = paymentRepository.findByImpUid(impUid)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 결제입니다."));
// 요청된 상태로 결제 정보 업데이트
paymentEntity.setStatus(PaymentStatusType.valueOf(requestDto.getStatus().toUpperCase()));
paymentRepository.save(paymentEntity);
PaymentResponseDto response = new PaymentResponseDto(0, "결제 상태가 업데이트되었습니다.", paymentEntity.toPaymentDetail());
return response;
}
}
→ PUT메서드가 결제 상태를 업데이트하는 데 사용되고, 같은 impUid로 여러 번 호출해도 항상 동일한 상태로 업데이트 된다. 멱등성을 보장하는 특성을 가졌다.
→ updatePaymentStatus 메서드 : 결제 상태를 업데이트하는 비즈니스 로직을 수행한다. impUid로 결제를 찾고, 요청된 상태로 결제 정보를 업데이트한다.
▷ PATCH 메서드
→ 특징 :
PATCH 메서드는 리소스의 일부를 수정하는 요청으로, 특정 필드나 속성만 수정되고, 요청이 여러 번 반복될 경우 일부 수정작엄이 반복될 수 있다.
→ 멱등성 부분 보장 :
PATCH 요청은 사용 방법에 따라 멱등성을 보장할 수도, 보장하지 않을 수도 있다. 올바르게 설계 된 경우, 같은 PATCH요청이 여러 번 수행되어도 최종 결과는 동일하게 유지될 수 있다.
→ 결제 시스템에서의 사용 :
결제 시스템에서는 PATCH요청을 통해 결제 정보의 일부 (예 : 배송지 정보, 고객 연락처 등)를 업데이트할 수 있다. 그러나 결제 상태를 업데이트 하는 경우, 멱등성을 신경 써서 설계해야 한다.
▼ 코드예시 (결제 정보 일부를 수정하는 PATCH 메서드)
▽Controller
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
// 결제 상태 업데이트 (PUT 메서드)
@PutMapping("/{impUid}")
public ResponseEntity<PaymentResponseDto> updatePaymentStatus(@PathVariable String impUid, @RequestBody PaymentUpdateRequestDto requestDto) {
PaymentResponseDto response = paymentService.updatePaymentStatus(impUid, requestDto);
return ResponseEntity.ok(response);
}
}
▽ Service
@Service
@Transactional
public class PaymentService {
private final PaymentRepository paymentRepository;
public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}
// 결제 정보 일부 수정 (PATCH 메서드)
public PaymentResponseDto updatePaymentDetails(String impUid, PaymentPartialUpdateRequestDto requestDto) {
PaymentEntity paymentEntity = paymentRepository.findByImpUid(impUid)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 결제입니다."));
// 요청된 필드만 업데이트
if (requestDto.getBuyerEmail() != null) {
paymentEntity.setBuyerEmail(requestDto.getBuyerEmail());
}
if (requestDto.getBuyerTel() != null) {
paymentEntity.setBuyerTel(requestDto.getBuyerTel());
}
paymentRepository.save(paymentEntity);
PaymentResponseDto response = new PaymentResponseDto(0, "결제 정보가 부분적으로 업데이트되었습니다.", paymentEntity.toPaymentDetail());
return response;
}
}
→ PATCH 메서드가 결제 정보의 일부만 수정하는 데 사용된다. 예를 들어 buyerEmail이나 buyerTel과 같은 특정 필드만 업데이트 한다. 여러 번 호출해도 요청된 필드만 업데이트 되므로 멱등성을 보장할 수 있다.
→ updatePaymentDetails 메서드: 결제 정보의 특정 필드를 수정한다. 각 필드의 값이 존재할 때만 업데이트되고, 데이터 무결성을 보장한다.
" PUT과 PATCH 메서드를 결제 시스템에서 사용하면, 각각의 요청이 여러 번 수행되더라도 항상 동일 결과를 보장하고 멱등성을 보장할 수 있다. PUT은 전체 리소스를 교체하거나 업데이트하는데 사용되고, PATCH는 리소스의 일부만 업데이트 할 때 사용된다. 이를 통해 결제 상태나 일부 결제 정보를 수정하는 작업에서 안전하게 멱등성을 유지할 수 있다. "
▶ 멱등성을 보장하는 방법
→ Idempotency Key 사용:
클라이언트가 결제 요청을 보낼 때 마다 고유한 멱등성 키를 생성하고, 서버는 이 키를 기반으로 요청을 식별해서 동일한 요청이 여러 번 들어와도 한 번만 처리되도록 한다.
→ 중복 요청 필터링:
서버에서 동일한 요청을 여러 번 수신했을 때, 첫 번째 요청만 처리하고 이후 요청은 무시하도록 로직을 구현 할 수 있다.
→ 트랜잭션 관리:
데이터베이스 트랜잭션을 통해 동일하 작업이 여러 번 수행되지 않도록 보장 할 수 있다.
▽ 코드예시 ( 결제 완료(Complete Payment) 시 멱등성 적용 )
public PaymentResponseDto completePayment(String accessToken, String impUid, PaymentRequestDto requestDto) {
String bearerToken = "Bearer " + accessToken;
// 포트원 API에서 결제 정보 조회
PaymentDetailResponseDto portOneResponse = portOneService.getPaymentDetails(bearerToken, impUid);
if (portOneResponse == null || !portOneResponse.getResponse().getStatus().equals("paid")) {
throw new IllegalArgumentException("유효하지 않은 결제 정보입니다.");
}
// 이미 처리된 결제인지 확인
Optional<PaymentEntity> existingPayment = paymentRepository.findByImpUid(impUid);
if (existingPayment.isPresent() && existingPayment.get().getStatus() == PaymentStatusType.COMPLETED) {
log.info("이미 완료된 결제입니다. imp_uid: {}", impUid);
return new PaymentResponseDto(0, "이미 완료된 결제", null);
}
// 새로운 결제 정보 저장 또는 업데이트
PaymentEntity paymentEntity = existingPayment.orElseGet(() -> PaymentEntity.builder()
.imp_uid(impUid)
.merchant_uid(portOneResponse.getResponse().getMerchant_uid())
.amount(portOneResponse.getResponse().getAmount())
.status(PaymentStatusType.COMPLETED)
.build());
paymentEntity.completePayment();
paymentRepository.save(paymentEntity);
PaymentResponseDto.PaymentDetail paymentDetail = new PaymentResponseDto.PaymentDetail(
paymentEntity.getImp_uid(),
paymentEntity.getMerchant_uid(),
paymentEntity.getStatus().name(),
paymentEntity.getAmount(),
paymentEntity.getPay_method().name()
);
return new PaymentResponseDto(0, "결제 완료", paymentDetail);
}
→ 중복 결제 방지 : impUid로 이미 완료된 결제가 있는지 확인하고, 있으면 "이미 완료된 결제"라는 메세지를 반환한다.
→ API 호출 및 데이터 검증 : 포트원 API를 호출해서 결제 정보의 상태를 확인하고, 결제가 "paid"상태가 아닐 경우 유효하지 않은 결제 정보로 간주한다.
→ 결제 상태 업데이트 : 결제가 유효한 경우에만 결제 정보를 저장 또는 업데이트 해서 상태를 관리한다.
"결제 시스템에서 멱등성을 보장하기 위해서는 중복 요청을 방지하는 로직과 Idempotency Key를 사용하는 것이 중요하다. 예시 코드처럼 결제 준비와 완료 시에 멱등성 키를 기반으로 결제를 관리하면, 중복 결제 문제를 방지하고 데이터 무결성을 유지할 수 있다."
▶ 결론
결제 시스템에서 멱등성은 매우 중요한 개념이다. HTTP 메서드인 PUT과 PATCH와 연관지어서 멱등성을 보장하는 설계를 한다면, 중복 결제와 데이터 무결성 문제를 방지할 수 있다. 따라서 결제 시스템을 구현할 때는 멱등성을 보장하는 방법을 고려하여 설계하고 개발해야 한다.
'단체개발일지' 카테고리의 다른 글
단체개발일지: 결제기능구현 흐름 (0) | 2024.09.15 |
---|---|
HikariPool...!! (0) | 2024.09.08 |
결제시스템구현: 동시성 문제&LOCK 개념 (1) | 2024.08.25 |
stateless 특성과 JWT의 관계 (0) | 2024.08.19 |
서버가 상태를 가지고 있으면 안된다? = Stateless (0) | 2024.08.12 |