Projects/[Final] Shopping Mall Project

[Day1~10] 프로젝트 진행상황 중간점검

montmer27 2026. 4. 17. 00:15
 

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 // 실제 결제 승인 처리(업데이트) 담당