외부 API 장애에 대한 트러블슈팅 경험을 공유드립니다.

안녕하세요, 사람인의 빌링 서비스를 개발하는 이현재입니다. 프로젝트를 진행하면 API를 활용할 일이 많습니다. 어떻게 해야 API와의 의존성을 줄일 수 있는지 고민해보셨나요? 모든 전말은 1년 전으로 거슬러 올라갑니다.

이야기를 풀기 위해 잠깐 사람인 개발 문화에 대해 소개해드릴게요. 사람인 IT연구소는 개발자의 역량 향상을 위한 지원을 아끼지 않습니다. 그중 하나가 바로 컨퍼런스 지원입니다. 컨퍼런스 참여 시 100% 비용 지원은 물론이고, 외부 교육 활동으로 인정하여 개인 연차를 소모하지 않습니다. 코로나 팬데믹 이전만 하더라도 저는 오프라인 외부 세미나를 즐겨 듣는 개발자였습니다. 이 모든 이야기는 바로 여기서 시작됩니다.

사건 발생!

2019년 11월 27일이 어떤 날인지 기억하는 개발자들도 있을 겁니다. NHN에서 공개 컨퍼런스로 전환한 NHN FORWARD가 열린 날입니다. 다양한 개발자들의 경험담을 업무에 활용하기 위해 참석했습니다. 밖으로 나가 있던 그 날, 담당 도메인 중 하나인 위치정보 서비스에서 문제가 발생합니다.

2019년 11월 27일, 외부 API 장애 발생

우편번호 정보를 활용하기 위한 외부 API인 다음 우편번호 서비스에서 장애가 발생했습니다. 당시 외부에서 사내 인프라망을 접근할 방법이 없었기에 두 손을 놓고 다음 측에서 문제를 해결하길 기다릴 수밖에 없었습니다. 1시간가량 흐른 뒤 서비스는 정상으로 돌아왔지만, 아무런 대비가 되어 있지 않아 자책할 수밖에 없었습니다.

사건 발생 전까지

사람인은 2017년에 도로명주소를 본격적으로 도입하였습니다. 기획부터 시작하여 개발까지 홀로 진행한 프로젝트였습니다. 외부 우편번호 서비스를 활용하고자 할 때 ‘장애 대응을 할 수 있는가?’는 회의 당시 나온 이슈 중 하나였습니다. 다만 입사한지 채 1년이 지나지 않았을 시기에 홀로 사이트 분석부터 시작하여 DB 설계하고 데이터 마이그레이션까지 진행하다 보니 잊혀집니다. 물론 이는 핑계에 불가합니다. 예견된 사고를 방치했던 건 분명한 사실입니다. 심지어 도입하고자 했던 서비스의 운영 히스토리를 살펴보면 1년에 한 번씩 이슈가 발생했습니다.

외부 API가 미친 파급력은 대단했다.

채용플랫폼인 사람인에서 주소는 E-커머스 못지않게 중요합니다. 구직자가 일자리를 찾을 때 근무 위치는 중요한 정보입니다. 이를 위해 기업의 회원 가입부터 시작하여, 공고를 등록하고자 할 때 주소를 입력하길 권장합니다. 그럴 뿐만 아니라 기업도 구직자의 통근 시간은 채용의 당락을 결정 지을 수 있는 요소이므로 이력서 등록과 함께 입사 지원 시 주소 정보를 받습니다. 지금껏 나열한 모든 서비스는 채용 플랫폼의 핵심 비즈니스 도메인입니다. 심지어 결제 서비스의 세금계산서 발급을 위한 부분까지 외부 API 장애가 전파되어 문제가 발생했습니다. 세상에나! 이 얼마나 끔찍한 일인가요? 이 문제를 다시 겪으면 안 됩니다. 소 잃고 외양간을 고치는 격이었지만, 더이상 방치할 수 없습니다. 이와 같은 경험은 한 번이면 충분하다 못해 넘칩니다. 당시 해결해야 했던 과제는 다음과 같습니다.

  • 서비스 안정성
    • 외부 API에 장애가 발생해도 사람인 서비스에 장애가 전파되지 않아야 한다.
  • 사용자 공지
    • 문제가 발생하면 사용자보다 먼저 장애를 인지하고, 안내를 즉각적으로 취할 수 있어야 한다.
  • 테스트 환경
    • 장애 발생 시나리오에 대한 QA는 코드를 변경하지 않아도 가능해야 한다.

이 문제를 어떻게 해결하지?

API에 대한 의존성을 끊어야 했습니다. 끊을 수 없다면 의존성을 줄여야 했습니다. 어떻게 하면 이 문제를 해결할 수 있는지 고민하다가 한 소프트웨어를 떠올리게 됩니다. 바로 히스트릭스입니다. Netflix OSS 중 하나인 히스트릭스는 Java로 만들어진 서킷브레이커 패턴 구현체입니다. 이를 참고하여 클라이언트, 브라우저에서 발생하는 이슈를 제어할 방법은 없을지 고민했습니다. 한 편으로 외부 API는 단일 체계를 유지할 경우 장애에 대해 취약해질 수밖에 없어서 API 이중화를 진행했습니다. 국내 우편번호 서비스는 대표적으로 행정안전부 도로명주소 서비스와 다음 우편번호 서비스가 존재합니다.

동시에 비상대응 관리자를 통하여 즉각적인 대응이 가능한 체계를 쌓았습니다. 빠른 사내 공유는 기존에 활용 중이던 텔레그램 봇으로 해결하고자 했습니다. 이 중에서 개발자가 알아두면 좋을 서킷브레이커 패턴에 대해 간단하게 알아볼까요?

문제 해결을 위한 네 가지 도구

서비스 안정성을 강화하는 디자인 패턴, 서킷브레이커

서킷브레이커 패턴은 MSA에서 안정성을 높이기 위한 디자인 패턴 중 하나입니다. 국내에 번역서가 존재하는 Michael Nygard의 저서, 『Release It!』에서 처음 등장한 패턴이죠. 영문명 그대로 회로 차단기를 차용한 디자인 패턴으로 주식을 하신다면 흔히 들어봤을 법한 단어이기도 합니다. 서로 분리된 서비스 간 통신 시 언제 어떤 문제가 발생할지 알 수 없기에 이를 통제할 수 있는 수단으로 서킷브레이커 패턴을 도입합니다.

Michael Nygard의 저서, Release It

MSA는 내부 서버 간 통신으로 인하여 일부 도메인 서비스에 대한 장애가 크게 퍼져 나갑니다. 서비스 간 트랜잭션 처리가 제대로 되어 있지 않을 경우 데이터 정합성이 깨지기도 합니다. 이때 서킷브레이커 패턴을 활용하면 그 상황을 미연에 방지할 수 있습니다. 절전 스위치를 내리면 더 이상 전류가 흐르지 않듯이, 서킷브레이커 패턴은 문제가 발생하면 프로세스를 차단합니다. 안전장치를 애플리케이션에 심어 둠으로써 특정 도메인 장애가 다른 서비스에 악영향을 줄 수 있는 여지를 막고자 서킷브레이커 패턴을 도입하게 됩니다. 이를 기반으로 문제가 발생할 상황을 고려하여 로직을 구성하죠. 궁극적으로 일부 도메인 서비스가 동작하지 않지 않더라도 사이트는 무리없이 운영할 수 있게 됩니다. 프로그래밍 세계의 서킷브레이커는 아래와 같은 특징을 가집니다.

  • API 요청 시 성공 및 실패 횟수를 기록한다.
  • 설정된 실패 임계치에 도달하면 API를 차단하고 지정된 예외처리 로직을 실행시킨다.
  • 이후에도 요청이 지속 될 경우 API를 호출하지 않는다.
  • 지정된 예외 처리 시간이 흐르면 다시 API 요청을 시도한다.
  • API 요청이 성공했다면 서킷을 닫고, 서비스를 정상화 시킨다.

서킷브레이커 동작 원리

CLOSE

서킷브레이커는 애플리케이션 요청에 대한 라우팅을 관리합니다. 호출이 실패하면 최근 실패 횟수를 기록합니다. 지정된 시간 내에 실패 횟수가 설정한 임계치를 초과하면 OPEN 상태로 변경합니다. 이 시점에서 타이머를 시작하고, 이 타이머가 만료되면 HALF_OPEN 상태로 변합니다.

close 다이어그램

타이머는 애플리케이션에서 발생한 문제를 해결하기 위한 시간을 시스템적으로 제공하기 위해 존재합니다.

OPEN

애플리케이션 요청이 즉시 실패하고, 예외를 반환합니다.

open 다이어그램

HALF_OPEN

애플리케이션의 일부 요청을 허용하여 프로세스를 진행합니다. 이러한 요청이 성공하면 문제 상황이 해결 되었다고 간주하여 서킷브레이커가 CLOSE 상태로 전환 됩니다. 만약 요청이 실패했다면 여전히 오류가 존재한다고 가정하여 다시 OPEN 상태로 되돌립니다. 이 때 타이머를 다시 시작하여 오류를 복구 할 수 있는 추가 시간을 제공합니다.

HALF_OPEN 상태는 서비스 복구 중 갑자기 요청이 몰리지 않도록 막는 역할을 수행합니다. 복구가 진행 되는 동안 수많은 요청으로 인해 서비스가 timeout 되거나, 다시 실패할 수 있는 상황을 방지합니다.

half_open 다이어그램

장애대응을 위한 고민들

서킷브레이커 패턴은 서버 간 통신에서 혹시 모를 장애를 대비하기 위한 패턴입니다. 하지만 이번에 작업해야 하는 영역은 서버가 아니라 클라이언트 영역이었습니다. 우편번호 API는 클라이언트 단에서 ECMAScript로 제어하기에 이에 대한 응용법이 필요하였습니다. 클라이언트 영역을 위한 서킷브레이커가 있다는 소리는 들어보지 못하였고, 공통적인 데이터를 활용하기 위해서는 결국 서버가 있어야 합니다. 아무리 제한 없는 외부 서비스일지라도 불필요한 호출은 피해야 하고, 배치는 꼭 필요한 상황이 아니라면 파편화되기 쉬워 고려 대상이 아니었습니다. 또한 배치는 실시간성을 보장해주지도 않습니다. 고민 끝에 외부 API 서버<->클라이언트<->서버로 연결고리를 구성했습니다.

이 고민으로 끝났다면 말 그대로 Happy end입니다. 사실 가장 큰 문제는 CORS 정책입니다. 서킷브레이커를 구현하기 위해서는 장애 판단 기준이 명확해야 하는데, CORS 정책으로 인하여 제약 사항이 존재합니다. 클라이언트에서 크로스도메인에 요청하는 경우 직접적인 에러 핸들링이 힘듭니다. 따라서 우회적인 방법을 취해야만 합니다. 바로 timeout이죠. 응답속도가 문제 있다는 건 장애로 판단할 수 있는 충분한 근거로 활용할 수 있습니다. 물론 사용자의 네트워크 속도에 따라 편차가 존재하므로 크롬 개발자 도구를 통해 최대 허용치를 미리 파악해야 합니다.

이 외에도 IE9에 대한 이슈도 몇 가지 존재했지만, 개발자라면 충분히 해결할 수 있는 문제였습니다.

서킷브레이커를 활용한 주소 검색 서비스 이중화

직접 구현한 서킷브레이커 패턴

우편번호 API flow 레디스를 제외한 주황색 박스가 클라이언트 영역입니다. 편의상 플로우차트를 간소화한 부분도 있습니다. 세부적인 수치도 이해를 돕기 위한 것으로 실제와는 다르지만, 기본적인 구조는 동일합니다. 주소 검색 서비스는 꼭 필요한 서비스이기에 이중으로 구성하였고, 서킷브레이커는 다음과 같이 구성되어 있습니다.

1. 주소 검색 이벤트 발생 시 1차 API 상태 확인
   레디스에 기록된 데이터를 토대로 1차 API 상태를 확인한다.
   1차 API 상태에 따라 어느 API를 호출할지 결정한다.

2. 1차 API 호출
   1차 API 호출이 가능한지 확인하며, 문제가 없다면 주소 검색 프로세스를 진행한다.
   만약 레디스에 기록된 상태와 달리 장애가 있다고 판단할 경우 2차 API를 호출한다.
   2차 API 호출할 때 1차 API 실패를 레디스에 기록한다.

3. 2차 API 호출
   2차 API가 호출 가능한지 확인하며, 문제가 없다면 주소 검색 프로세스를 진행한다.
   만약 2차 API를 호출할 수 없다면 장애 대응을 위한 프로세스를 진행한다.
   이와 동시에 2차 API 실패를 레디스에 기록한다.

4. 장애 대응 프로세스
   1차 API에서 2차 API로 전환되는 순간 내부 전파가 이루어진다.
   만에 하나 2차 API 또한 장애가 발생할 경우 비상대응 관리자로 사용자에게 양해를 구한다.

증명된 서킷브레이커와 장애대응 후일담

외부 서비스를 도입하기 위해서는 안정성이 확보되어야 합니다. 아니, 외부 서비스뿐만 아니라 API 통신이 이루어진다면 아무리 잘 만들어진 서비스일지라도 문제가 발생하지 않는다는 보장은 할 수 없습니다. 순간적인 네트워크 장애가 생길 수도 있고, 잘못된 배포로 인하여 오류가 발생할 수 있습니다. 그중에서도 외부 서비스는 직접적인 통제가 불가능한 영역이기 때문에 더더욱 장애 대응법을 갖추어야 합니다. 뒤늦게 준비하긴 했지만, 2020년 10월 23일에 발생한 외부 API 장애가 사람인 서비스에 영향을 미치지 않았습니다. 오히려 API 서비스의 공지보다 한발 빠르게 인지할 수 있었습니다.

먼저 도착한 텔레그램 알림 늦게 도착한 다음 측 공지사항

서비스 이중화는 외부 서비스의 장애 가능성을 해소하기 위한 선택지 중 하나입니다. 장애허용시스템을 만들기 위한 방법 중 하나로 유사한 외부 서비스를 다중으로 구축하면 통제 불가능 영역에 대한 관리가 가능해집니다. 일반적으로 서로 다른 주체의 외부 서비스가 동시에 문제가 발생할 확률은 지극히 낮다는 걸 이용한 방법입니다.

실제 사례 중 하나로 모 회사의 결제 서비스가 결제 모듈 n개를 랜덤하게 보여주고 있습니다. 이처럼 이중화를 한다면 어느 정도 대응이 가능하지만, 디자인이 다른 화면을 무작위로 노출하는 건 사용자 편의성을 떨어트립니다. 서비스를 만들 때 사용자 관점에서 생각해야 하기에 서킷브레이커로 엮었고, 사용성이 좋은 서비스를 메인 API로 선택했습니다. 물론 코드를 수정하지 않더라도 언제든지 순서를 뒤바꿀 수 있는 관리자는 기본이겠죠?

외부 API 장애대응을 위한 서킷브레이커는 클라이언트 단부터 시작하여 서버 단까지 전부 라이브러리에 의존하지 않고 직접 구현하였습니다. 하지만 이에 대한 공유는 힘드니 다음 글은 라이브러리를 활용한 서킷브레이커 구현 방법을 소개해드리겠습니다. 앞서 언급한 히스트릭스는 예제가 많으니 식상합니다. 그러니 다소 생소하게 느껴질 「PHP로 구현하는 서킷브레이커」를 알아보도록 합시다!

참고 자료