Chap2. 장애 전파를 막기 위한 방법들

장애를 막기 위해 이중화(Redundancy)나 복제(Replication) 같은 인프라 수준의 대책도 필수적입니다.

하지만 애플리케이션 코드 레벨에서도 장애가 전체 시스템으로 번지지 않도록 방어 로직을 구축해야 합니다.

장애 전파를 막기 위한 대표 방법

1. 재시도 (Retry)

가장 단순하고 직관적인 방법입니다. 요청이 실패했을 때 다시 한번 요청을 보내는 것입니다.

  • 언제 사용하나요?:

    • 네트워크가 순간적으로 불안정하거나, 배포 중이라 서버가 잠깐 재시작되는 경우처럼 '일시적인 오류(Transient Failure)' 상황에서 유용합니다.

  • 주의점:

    • 무한정 재시도하는 것은 피해야 합니다. 만약 일시적인 문제가 아니라 코드 버그나 영구적인 장애라면, 반복된 재시도가 오히려 트래픽을 폭증시켜 시스템을 더 악화시킬 수 있기 때문입니다.

2. 타임아웃 (Timeout)

요청을 보낸 후 응답이 올 때까지 기다리는 최대 시간을 설정하는 것입니다.

  • 예시 상황:

    • 만약 외부 API 호출이 30초 동안 응답이 없다고 가정해 봅시다. 클라이언트도 30초를 기다려야 하고, 그 요청을 처리하는 우리 서버의 스레드도 30초 동안 묶여 있게 됩니다.

  • Fail Fast:

    • 이런 요청이 쌓이면 결국 스레드 풀이 고갈되어 전체 시스템이 멈추는 장애로 이어집니다.

    • 어차피 실패할 요청이라면, 오랫동안 붙잡고 있는 것보다 타임아웃을 통해 빠르게 실패 처리하고 자원을 반납하는 것이 전체 시스템의 안정성에 유리합니다.

3. 서킷 브레이커

누전 차단기처럼 특정 서비스에 장애가 감지되면, 일시적으로 연결을 차단하는 패턴입니다.

  • 동작 원리:

    • 평소에는 정상적으로 통신하다가 오류율이 임계치를 넘으면 회로를 엽니다(Open).

    • 회로가 열려있는 동안에는 요청을 보내지 않고 바로 에러를 반환하거나 대체 로직을 수행합니다.

  • 효과:

    • 이미 과부하로 힘들어하는 서버에 계속 요청을 보내는 것을 막습니다.

    • 장애가 발생한 컴포넌트가 스스로 복구될 수 있는 시간을 벌어주는 중요한 역할을 합니다.

4. 폴백 (Fallback)

요청이 실패했을 때(타임아웃, 재시도 실패, 서킷 브레이커 차단 등) 수행할 대체 로직(Plan B)을 의미합니다.

  1. 조회 실패 시 (캐싱 활용):

최신 리뷰 데이터를 가져오는 저장소에 문제가 생겼다면, 에러를 띄우는 대신 미리 저장해 둔 캐시(Cache)된 리뷰 데이터를 반환하여 서비스를 유지할 수 있습니다.

  1. 비동기 처리 전환:

주문 후 '결제 완료 메일' 발송이 실패했다고 가정해 봅시다. 이때는 메일 발송을 포기하는 것이 아니라, DB에 '발송 대기' 상태로 저장해 두고 나중에 배치 프로그램이 재전송하도록 처리할 수 있습니다.

  1. 우아한 성능 저하 (Graceful Degradation)

강의 목록 페이지에서 핵심인 '강의 리스트'는 보여주되, 부가적인 '수강평'이나 '가격 정보' 서버가 죽었다면 해당 부분만 숨기고 페이지를 노출하는 방식입니다. 핵심 기능은 살리고 부가 기능을 포기하여 서비스 전체가 중단되는 최악의 상황을 막습니다.

5. Bulkhead와 Rate Limiter

시스템의 자원을 효율적으로 격리하고 관리하는 기법입니다.

  • Rate Limiter (유량 제어):

    • 특정 시간 동안 처리할 수 있는 요청 횟수를 제한하여, 외부의 과도한 트래픽으로부터 서버를 보호합니다.

  • Bulkhead (격벽 패턴):

    • 선박 내부를 여러 구획으로 나누는 것과 같습니다. 애플리케이션의 자원(Thread Pool 등)을 기능별로 격리해 둡니다.

    • 이렇게 하면 특정 기능에 장애가 발생해 해당 구역의 스레드를 모두 소모하더라도, 다른 기능에는 영향을 주지 않고 정상 동작하게 됩니다.

예외와 응답 상태 코드

장애 상황을 핸들링 하는데 예외와 응답 상대 코드가 무슨 상관이 있을까요?

장애 대응의 시작은 장애를 정확하게 인지하는 것입니다. 그리고 그 신호는 바로 예외와 응답 상태코드에서 확인할 수 있습니다.

1. Checked Exception

  • 특징:

    • Exception을 상속받는 클래스들입니다.

  • 강제성:

    • 컴파일러가 빨간 줄을 띄우며 try-catchthrows를 강제합니다.

    • 둘 중 하나라도 명시하지 않으면 문법적으로 컴파일 에러가 발생합니다.

  • 단점:

    • 람다(Lambda)나 스트림(Stream) 내부에서 사용하기 조금 까다롭습니다.

    • 불필요한 try-catch 블록으로 인해 코드 가독성이 떨어질 수 있습니다.

2. Unchecked Exception

  • 특징:

    • RuntimeException을 상속받는 클래스들입니다.

  • 유연성:

    • 예외 처리를 문법적으로 강제하지 않습니다. 개발자가 필요한 곳에서만 명시적으로 처리하면 됩니다.

예외 포장하기 (Exception Wrapping)

외부 서비스를 호출할 때는 "무조건 실패할 수 있다"라는 전제하에 try-catch로 감싸야 합니다. 그리고 라이브러리의 예외를 우리 비즈니스에 맞는 예외로 바꿔서 던지는 것이 좋습니다.

장점:

  • 명확한 의미:

    • "알 수 없는 에러"가 아니라 "메일 전송 실패"라는 명확한 에러가 됩니다.

  • 제어권:

    • 상위 호출자(Controller 등)에서 MailSendException을 잡아서 "잠시 후 다시 시도해주세요"라고 안내하거나, 다른 메일 서버로 우회하는 등의 복구 로직(Fallback)을 작성할 수 있습니다.

Last updated