본문 바로가기
Loopers

WIL - 6주차 회고

by Soono991 2025. 8. 24.

이번 주에는 외부 결제 시스템(PG) 과의 연동 시, 외부 API 장애나 지연 상황에서도 우리 서비스의 안정성을 보장하기 위한 Resilience를 사용하여 장애 대응 전략을 직접 설계해 봤다.

"장애"란 무엇인가

서비스에서 장애란 사용자가 기대한 기능을 정상적으로 이용하지 못하는 모든 상태를 의미한다.
이는 단순히 서버가 다운되는 경우뿐만 아니라, 외부 시스템(PG)에서의 응답 지연, 일시적인 오류, 네트워크 단절 등 사용자 경험에 영향을 주는 모든 문제를 포함한다.

왜 장애를 예방하고 대응해야 할까

우리는 지금까지 다음과 같은 고민들을 하고 적용해 보면서 우리 서비스의 안정성을 높이려고 했다.

1.다양한 상황을 커버하는 테스트 코드 작성
2.동시성 제어를 통해 경쟁 상태(race condition) 방지
3.멱등성 보장으로 중복 처리 문제 방지
4.예상치 못한 이슈에 대한 예외 처리와 롤백 설계

 

덕분에 우리 서비스 내부에서 발생할 수 있는 장애는 최소화했지만, 외부 API(ex: 결제 PG) 하나를 연동하는 순간 상황이 달라진다.
외부 API에서 장애가 발생하면, 지금까지 우리가 쌓아온 모든 안정성 설계가 무너질 수 있다.

“우리가 아무리 잘 만들어도 외부 의존성 하나로 사용자 경험이 무너질 수 있는” 상황이 발생할 수 있다.
그렇기 때문에 단순히 기능을 구현하는 것을 넘어, 외부 시스템 장애에 대비한 Resilience 설계가 필수적이다.

외부 API 의존성 문제

결제 처리에서 외부 PG API를 호출하는 경우, 외부 API 상태에 따라 우리 서비스의 성공 여부가 결정되는 문제가 있다.
그럴 경우 만약 외부 API에 장애나 지연이 발생하면 결제 요청 API가 계속 대기하거나 타임아웃으로 실패할 수 있고,
이로 인해 사용자 경험 악화, 결제 지연, 시스템 부하까지 이어질 위험이 존재한다.

안정성을 확보하기 위해선 외부 시스템의 장애를 우리 서비스로 전파하지 않도록 설계해야 한다.

pg-simulator를 활용한 실패 시나리오 구현

pg-simulator 모듈을 활용해 외부 API 장애 상황을 재현해 보고 그에 맞는 대응 전략을 설계해 봤다.

// 장애 상황 경우
1. Timeout: PG 응답이 지연되는 상황
2. 4xx Error: PG 요청 실패 (유효성 검증 실패, 잔액 부족 등)
3. 5xx Error: PG 서버 장애
4. 네트워크 단절: 일시적 연결 불가

 

Resilience4j를 활용한 회복성 패턴 적용

CircuitBreaker

외부 API에서 일정 횟수 이상 오류 발생 (지속적인 오류 발생) 시 회로를 열어(open) 추가 호출을 차단
불필요한 재시도를 줄여 시스템 부하 완화

 

Retry

일시적 장애 시, 설정된 횟수만큼 재시도하여 성공 가능성 확보

 

더보기
    @Retry(name = "pg-request", fallbackMethod = "requestPaymentFallback")
    @CircuitBreaker(name = "pg-request")
    override fun requestPayment(userId: Long, command: PaymentGatewayCommand.Request): PaymentGatewayResult.Requested {
        return pgSimulatorFeignClient.createPayment(userId, command).data!!
    }

    private fun requestPaymentFallback(
        userId: Long,
        command: PaymentGatewayCommand.Request,
        ex: Throwable,
    ): PaymentGatewayResult.Requested {
        log.error("PG createPayment failed for userId: $userId, command: $command", ex)
        return PaymentGatewayResult.Requested(
            null,
            PaymentStatusType.FAILED,
        )
    }

 

그럼 이제 우리는 다음과 같은 고민을 해볼 수 있다.
모든 예외 상황에 대해 서킷 브레이커와 Retry를 적용하는 것이 과연 옳을까?

예를 들어,
• 사용자가 카드 번호를 잘못 입력했거나
• 사용자의 카드 잔액이 부족한 경우

이런 상황에서도 Retry를 하는 것이 과연 의미가 있을까?

또한, 사용자의 잘못된 요청으로 인해 서킷 브레이커가 OPEN 상태가 되면
정상적으로 요청하는 다른 사용자들까지 서비스 이용에 영향을 받을 수 있는데,
그렇다면 과연 서킷 브레이커를 무조건 OPEN하는 것이 최선일까?

결국, 우리는 상황에 따른 장애 대응 전략을 세밀하게 구분하고 케이스별로 적절한 대응 방안을 논의해야 한다.

 

1. 4xx Error(카드번호 오류, 잔액 부족 등)가 발생할 경우 Retry, 서킷 브레이커는 적용하지 않는다.
2. Timeout, 5xx Error, 네트워크 단절은 Retry, 서킷 브레이커는 적용한다.

 

장애를 이런 식으로 분류하기 되면 나중에 장애 알림까지도 연결해 볼 수 있다.

4xx Error는 우리가 장애 발생 알림을 받을 필요가 없기 때문에 제외하고, 나머지 예외에 대해서만 장애 알림을 받도록 하여 모니터링해 볼 수 있다.

 

믿지 마라

Alen 멘토님께서 해주신 말이다.
우리가 왜 장애 대응 시스템을 구축해야 하는지, 왜 Resilience 설계가 필요한지를 한 마디로 설명해 주는 말 같아서 크게 공감했다.


우리가 외부 API가 항상 정상적으로 동작할 것이라고 믿는다면 사실 장애 대응 시스템을 구축할 필요가 없다.
“어차피 정상적으로 응답을 줄 테니까”라는 생각으로 안일하게 접근할 수도 있다.

하지만 현실은 다르다.
“정상적으로 줄 거라고 믿었는데” 어느 날 갑자기 장애가 발생한다면, 그 피해와 책임은 외부 시스템이 아니라 우리 서비스가 지게 된다.
사용자는 우리 시스템만 바라보고 있기 때문이다.

따라서 정상 응답을 준다고 약속한 외부 시스템이라도 ‘혹시 모르는 상황’을 대비해야 한다.

 

이번 주를 돌아보며

단순히 라이브러리 및 외부 API 사용법을 배우는 것을 넘어, 시스템 설계 관점에서의 장애 대응 설계의 중요성을 체감했다.
이제까지는 사실 외부 API 호출 후 예외 발생 여부만 try-catch로 처리했을 뿐, 이처럼 다양한 장애 상황까지 깊게 고민해보지는 않았다.
이번 과제처럼 추후 외부 API를 연동해야 하는 상황에서는 외부 환경에서 발생할 수 있는 장애를 더 세분화하고,
그에 맞는 대응 전략을 사전에 설계하는 시야를 길러야겠다고 느꼈다.

 

다음에 더 해보고 싶은 것

  • 서킷 브레이커, Retry 적용에 따른 모니터링 시스템 구축
  • 서킷 브레이커 OPEN 시 장애 알림 발송 (ex: slack...)
  • PG API 장애 발생 시 fallback에서 다른 PG API로 요청하도록 PG 이중화 구성

'Loopers' 카테고리의 다른 글

WIL - 8주차 회고  (0) 2025.09.05
WIL - 7주차 회고  (1) 2025.08.29
WIL - 5주차 회고  (2) 2025.08.17
WIL - 4주차 회고  (5) 2025.08.10
WIL - 3주차 회고  (2) 2025.08.03

댓글