코드 리팩토링을 통해 내부 API 응답 구조 개선, 환경별 URL 분리, 환불 멱등성 처리를 적용했다.
1. 내부 호출 APIResponse 제거
기존에는 내부 서비스 간 통신에도 ApiResponse로 감싸서 응답했다.
// 기존
@PostMapping
public ResponseEntity<ApiResponse<PaymentResponse>> createPayment(
@RequestBody @Valid PaymentCreateRequest request) {
return ApiResponse.success(PaymentSuccessCode.PAYMENT_CREATED, response);
}
Booking 서비스가 Feign Client로 호출할 때 ApiResponse를 벗겨서 써야 하는 번거로움이 있었다.
내부 API는 바로 데이터만 반환하도록 변경했다.
// 변경 후
@PostMapping
public ResponseEntity<PaymentResponse> createPayment(
@RequestBody @Valid PaymentCreateRequest request) {
PaymentResponse response = PaymentResponse.from(
paymentCommandService.createPayment(request.toCommand())
);
return ResponseEntity.ok(response);
}
외부 API는 ApiResponse를 유지하고 내부 API만 제거했다.
2. 결제 성공/실패 URL 환경별 분리
토스 결제창의 successUrl, failUrl이 하드코딩되어 있었다.
// 기존 - 하드코딩
successUrl: "http://localhost:8084/api/v1/payments/confirm-redirect",
failUrl: "http://localhost:8084/fail"
로컬, 운영 환경마다 주소가 달라서 배포할 때마다 코드를 수정해야 했다.
config-repo에서 환경별로 관리하도록 변경했다.
# payment-service-local.yml
payment:
success-url: http://localhost:8084/api/v1/payments/confirm-redirect
fail-url: http://localhost:8084/fail
# payment-service-prod.yml
payment:
success-url: ${PAYMENT_SUCCESS_URL}
fail-url: ${PAYMENT_FAIL_URL}
@Value("${payment.success-url}")
private String successUrl;
@Value("${payment.fail-url}")
private String failUrl;
운영 환경에서는 환경변수로 주입하니까 코드 변경 없이 배포할 수 있다.
3. 재결제 시간 하드코딩 제거
결제 실패 시 재시도 가능 시간이 하드코딩되어 있었다.
// 기존
payment.fail(300); // 하드코딩
config-repo 공통 파일에서 관리하도록 변경했다.
# payment-service.yml (공통)
payment:
retry-ttl-seconds: 300 # 추후 Booking 담당자 협의 후 조정 예정
@Value("${payment.retry-ttl-seconds}")
private int retryTtlSeconds;
payment.fail(retryTtlSeconds);
추후 좌석 선점 시간과 맞춰서 값만 바꾸면 되니까 코드 수정이 필요없다.
4. 환불 멱등성 처리
booking.payment.refund 이벤트가 중복 수신될 경우 환불이 두 번 처리될 수 있었다.
별도 인박스 테이블을 만드는 대신 Payment 상태로 간단하게 처리했다.
// 2. 본인 확인 (먼저 수행)
if (!payment.getUserId().equals(command.userId())) {
throw new PaymentException(PaymentErrorCode.PAYMENT_FORBIDDEN);
}
// 3. 이미 환불된 결제면 그냥 반환 (멱등성)
if (payment.getStatus() == PaymentStatus.REFUNDED) {
log.info("이미 환불된 결제 - paymentId: {}", command.paymentId());
return PaymentResult.from(payment);
}
코드래빗이 중요한 순서 문제를 지적했다.
처음에 멱등성 체크를 본인 확인보다 앞에 뒀는데 다른 사용자의 paymentId를 알면 PAYMENT_FORBIDDEN 없이 결제 정보를 받을 수 있는 보안 취약점이 있었다.
본인 확인을 먼저 하고 그 다음에 멱등성 체크하도록 순서를 바꿨다.
마무리
리팩토링의 핵심은 작은 것들을 제대로 고치는 것이다.
- 하드코딩 → 설정값으로
- 내부/외부 API 응답 구조 분리
- 보안 순서 고려
코드래빗 리뷰 덕분에 보안 취약점을 잡을 수 있었다.
'Spring 개발일지' 카테고리의 다른 글
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - Dockerfile 및 CI workflow 작성 (0) | 2026.05.27 |
|---|---|
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 결제 만료 스케줄러 구현 (0) | 2026.05.26 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - booking.payment.refund 이벤트 수신 후 환불 처리 (1) | 2026.05.22 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 결제 최종 실패 정책 설계 (0) | 2026.05.21 |
| [팀프로젝트] MSA 기반 티켓팅 프로그램 - 결제 실패 이벤트 발행 (0) | 2026.05.20 |