ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WIL - 장애 허용 시스템 구축
    Develop/WIL 2025. 8. 22. 21:52

     

    주간 학습 회고 (Weekly I Learned)

    이번 주의 핵심 미션은 "결제 시스템의 안정성을 극한으로 끌어올리는 것"이었습니다. 외부 PG(Payment Gateway) 연동 시 발생할 수 있는 예측 불가능한 장애에 효과적으로 대응하기 위해, 서킷 브레이커 패턴을 중심으로 재시도(Retry) 로직과 상황별 폴백(Fallback) 전략을 시스템에 체계적으로 적용했습니다.


    🧠 이번 주 학습 성과

    1. 재시도와 서킷 브레이커: 2중으로 구성한 안전망

    단순한 재시도를 넘어, 장애가 지속될 때 시스템 전체를 보호하는 서킷 브레이커를 함께 구현하며 2단계 방어 체계를 구축했습니다.

    // 1차 방어선: @Retry로 일시적인 오류 극복 시도
    // 2차 방어선: @CircuitBreaker로 지속적인 장애 전파 차단
    @CircuitBreaker(name = "pgClient", fallbackMethod = "requestFallback")
    @Retry(name = "pgClient")
    @Override
    public PaymentInfo.transaction request(PgClientDto.PgPaymentRequest request) {
        // PG 호출 로직
    }
    
    
    • 학습 포인트: Resilience4j를 활용해 어노테이션만으로 선언적인 장애 대응 로직을 구현했습니다. 재시도가 모두 실패했을 때 서킷 브레이커가 동작하는 흐름을 명확히 이해하게 되었습니다.

    2. 똑똑한 예외 처리: 모든 실패는 같지 않다

    어떤 예외를 '실패'로 간주할지 명확히 정의하는 것이 서킷 브레이커의 오작동을 막는 핵심임을 깨달았습니다.

    # 실패로 간주하여 서킷 카운트를 올릴 예외들
    recordExceptions:
      - HttpServerErrorException  # 5xx 서버 오류
      - TimeoutException         # 타임아웃
    # 실패로 간주하지 않고 무시할 예외들
    ignoreExceptions:
      - CoreException           # 의도된 비즈니스 로직 예외
    
    
    • 핵심 인사이트: 일시적인 기술적 장애(5xx, Timeout)와 예측 가능한 비즈니스 예외(4xx, 유효성 검사)를 분리하여, 정말 필요한 순간에만 서킷 브레이커가 동작하도록 정교하게 설계했습니다.

    3. 비즈니스를 이해하는 폴백: 상황에 맞는 최적의 플랜 B

    장애 발생 시 무조건 에러를 반환하는 대신, 각 API의 비즈니스적 특성에 따라 최적의 대체 응답(Fallback)을 제공하는 전략을 수립했습니다.

    [결제 요청 실패 시] 사용자에게 명확한 실패를 알려야 합니다.

    return new PaymentInfo.transaction(
        // ...
        TransactionStatus.FAILED, "PG 통신 실패"
    );
    
    

    [거래내역 조회 실패 시] 실제 상태를 알 수 없으므로, 재조회 여지를 남겨둡니다.

    return new PaymentInfo.transaction(
        // ...
        TransactionStatus.PENDING, "PG 통신 실패"
    );
    
    
    • 설계 원칙: 사용자 경험과 데이터 정합성을 최우선으로 고려하여, 각기 다른 비즈니스 맥락에 맞는 폴백 로직을 설계하는 원칙을 체득했습니다.

    4. 최종 일관성 확보: 스케줄러를 이용한 상태 동기화

    폴백 로직으로 PENDING 상태가 된 결제 건들을 그대로 두지 않고, 스케줄러를 통해 주기적으로 PG사에 상태를 재확인하여 최종적인 데이터 일관성을 확보하는 메커니즘을 구현했습니다.

    public void verifyPendingPayments() {
        List<Payment> pendingPayments = paymentRepository.findByStatus(PENDING);
        for (Payment payment : pendingPayments) {
            try {
                // 스케줄러가 주기적으로 PG사에 상태를 재문의하여 동기화
                verifyAndSyncPaymentStatusByScheduler(payment);
            } catch (Exception e) {
                // 개별 결제 건의 동기화 실패가 전체 스케줄러에 영향을 주지 않도록 격리
            }
        }
    }
    
    
    • 핵심 학습: 실시간 처리가 실패하더라도, 배치(Batch) 작업을 통해 시스템의 상태를 결국 올바른 상태로 맞춰가는 최종 일관성(Eventual Consistency) 패턴의 중요성을 이해했습니다.

    🤔 아쉬웠던 점과 개선할 부분

    • 콜백 비동기 처리 및 결제 비즈니스 도메인 학습 미숙
      • 문제점: 결제 시스템의 비동기 콜백 처리 방식과 복잡한 비즈니스 로직에 대한 이해도가 부족하여 초기 설계에 어려움을 겪었습니다.
      • 개선 방안: 관련 기술 문서와 실제 결제 성공/실패 사례를 분석하여 도메인 지식을 보강하고, 비동기 처리에 대한 학습을 별도로 진행할 계획입니다.
    • 다양한 장애 시나리오에 대한 회복 전략 부족
      • 문제점: 현재는 기본적인 폴백과 재시도 전략에 집중했지만,  더 복잡한 시나리오에 대한 회복 전략은 충분히 고려하지 못했습니다.
      • 개선 방안: 다음에는, 다양한 시나리오에 대응할 수 있는 견고한 회복 전략을 수립하겠습니다.
    • 학습 내용 정리 미완료
      • 문제점: 배운 내용이 방대해 블로그 포스팅을 완료하지 못했습니다. 지식의 체계화와 공유가 이루어지지 않아 아쉬웠습니다.
      • 개선 방안: 학습과 동시에 핵심 포인트를 메모하는 습관을 들이고, 주제별로 작은 단위로 나누어 포스팅을 꾸준히 진행하겠습니다.

     

    🚀 다음 주 도전 과제

    • 애플리케이션 이벤트 기반으로 유스케이스 분리
      • 현재 강하게 결합된 로직들을 애플리케이션 이벤트를 통해 분리하여, 시스템을 더 느슨하고 확장 가능하게 리팩토링하는 작업을 진행할 예정
    •  

    댓글

Designed by Tistory.