GitHub - all-in-market/mvp-admin-server: responsible for administrative apis
responsible for administrative apis. Contribute to all-in-market/mvp-admin-server development by creating an account on GitHub.
github.com
GitHub - all-in-market/mvp-api-server: multi-vendor ecommerce platform backend project (api server & scheduler)
multi-vendor ecommerce platform backend project (api server & scheduler) - all-in-market/mvp-api-server
github.com
프로젝트명 : ALL-IN-MARKET
목표 : 대용량 트래픽을 안정적으로 처리할 수 있는 멀티벤더 이커머스 쇼핑몰 백엔드 서버 구축
특징 : 관리자 서버(admin-server(와 클라이언트 (구매자, 판매자, api-server) 서버를 분리
현재 상황
- [x] mvp 개발 완료 (핵심 비즈니스 로직 전부 동작 확인)
- [x] 상품 조회(목록, 단건) 시나리오, 결제 시나리오(카트 담기, 주문 생성하기, 결제 승인하기) 대해 부하 테스트 실행 (K6 + Grafana)
- [x] 테스트 결과 바탕으로 Redis로 캐싱 적용 결과 상품 조회 시나리오에서 1000rps까지 안정적으로 처리 가능함 확인
응답 시간 : 48.9s에서 169.69ms로 개선
병목 발생 시점 : 57rps에서 발생하던 병목이 1310 rps에서도 발생하지 않음
- [x] 아래 api에 Redis 캐싱 적용
- 카테고리 목록 조회
- 상품 목록 조회
- 판매 상품 목록 조회
- 정산 내역 조회
- 판매자 대시보드
- [x] 아래 api에 락 적용
- [x] 결제 생성 : 비관적 락
- [x] 결제 승인 : 낙관적 락
- [x] 주문 생성 : 분산락
- [x] 환불 생성 : 비관적 락
- [ ] 환불 승인/거절 : 낙관적 락 -> Retryable을 Transaction안에서 호출하고 있어 별도 서비스로 분리 필요
- [x] 판매자 일일/기간 통계 조회 기능 구현 (기간 통계 조회 시 Querydsl 사용)
- [x] 판매자 대시보드 00시마다 초기화하는 스케줄러 구현
비고 : 테스트 코드는 통합 테스트 시 작성
[오늘 한 일]
1. 결제 승인 API에 낙관적 락 적용
[낙관적 락 재시도 로직 @Retryable 보장을 위한 서비스 클래스 분리 설계]
기존 흐름에서는 BuyerPaymentController가 BuyerPaymentFacade를 호출하고, 뒤이어 BuyerPaymentService가 confirmPayment()를 호출하는 구조였다.
하지만 BuyerPaymentFacade가 BuyerPaymentService를 직접 호출하지 않고, PaymentRetryService.retryConfirmPayement() 메서드를 통해 BuyerPaymentService.confirmPayment()를 호출하도록 했다.
그 이유는 낙관적 락을 구현하기 위해 추가한 @Retryable 어노테이션의 동작을 보장하기 위해서이다. Spring AOP는 프록시를 통한 외부 호출에만 어노테이션을 허용하기 때문에, 클래스를 분리한 것이다.
PaymentRetryService.retryConfirmPayment()에 @Retryable 어노테이션을 통해 재시도 로직을 구현한다. 이 어노테이션이 적용된 buyerPaymentService.confirmPayment는 실패할 때마다 해당 어노테이션의 적용을 받아 최대 3번까지, 100ms부터 시작해 2배씩 텀을 늘려가며 재시도한다.
@Service
@RequiredArgsConstructor
public class PaymentRetryService {
private final BuyerPaymentService buyerPaymentService;
@Retryable(retryFor = OptimisticLockingFailureException.class, maxAttempts = 3, backoff = @Backoff(delay = 100, multiplier = 2))
public PaymentDetailResponse retryConfirmPayment(Long currentUserId, String paymentId, PortOnePaymentResponse payment) {
return buyerPaymentService.confirmPayment(currentUserId, paymentId, payment);
}
// 지정된 횟수만큼 시도 후 모두 실패하면 전파된 OptimisticLockingFailureException을 받아 처리하여 500 응답 대신 지정된 에러 응답 반환
@Recover
public PaymentDetailResponse recoverConfirmPayment(OptimisticLockingFailureException e, Long currentUserId, String paymentId,
PortOnePaymentResponse payment) {
throw new BaseException(ErrorEnum.PAYMENT_FAILED); // 적절한 에러 응답
}
}
위 메서드에 의해 호출된 confirmPayment는 commit 전에 낙관적 락 적용 대상인 paymentRepository에 flush한다. 그 이유는 업데이트 실패로 인한 낙관적 락 실패 에러가 메서드 안에서 발생하도록 하기 위해서이다.
/**
* 결제 확인 및 상태 업데이트
*/
@Transactional
public PaymentDetailResponse confirmPayment(Long currentUserId, String paymentId, PortOnePaymentResponse payment) {
// 낙관적 락 적용
Payment dbPayment = paymentRepository.findByImpUidWithOrder(paymentId).orElseThrow(
() -> new BaseException(ErrorEnum.PAYMENT_NOT_FOUND)
);
validatePaymentOwner(currentUserId, dbPayment);
validatePaymentIdMatch(paymentId, payment);
PaymentDetailResponse idempotentSuccessResponse = handleAlreadySucceededPayment(dbPayment);
if (idempotentSuccessResponse != null) {
return idempotentSuccessResponse;
}
validatePaymentNonProcessableStatus(dbPayment);
validatePaymentResult(payment, dbPayment);
validatePaymentAmount(currentUserId, payment, dbPayment);
dbPayment.success(LocalDateTime.now());
dbPayment.getOrder().paid();
// todo: seller_dashboard 업데이트
// todo: transaction_histories 업데이트
// flush를 commit 직전에 발생하도록 하여 OptimisticLockingFailureException이 메서드 안에서 발생하도록 수정
paymentRepository.saveAndFlush(dbPayment);
return PaymentDetailResponse.from(dbPayment);
}
[최종 흐름]
1. BuyerPaymentController.processPayment() // 클라이언트 요청 검증
2. BuyerPaymentFacade.processPayment() // 외부 결제 API 연동 (지금은 Mock)
3. PaymentRetryService.retryConfirmPayment() // 실패 시 3회 재시도 로직 구현
4. BuyerPaymentService.confirmPayment // 실제 결제 승인 처리(업데이트) 담당
'Projects > [Final] Shopping Mall Project' 카테고리의 다른 글
| [QueryDsl] 기간별 판매 통계 조회 메서드 테스트 코드 작성 및 수정하기 (0) | 2026.04.22 |
|---|---|
| [트러블슈팅] TLS 비활성화 상태에서 환경별로 연결 설정 분리하기 (1) | 2026.04.22 |
| [테스트] contextLoads() 실패 이유 분석하기 (0) | 2026.04.22 |
| Code Review Bot 코드래빗 적용하기 (0) | 2026.04.22 |
| [부하 테스트] 오늘의 성과와 한계 (0) | 2026.04.21 |