HTTP 상태 코드와 응답 데이터를 활용한 API 오류 처리 및 모니터링 전략

작성 : 2024-04-28수정 : 2024-04-28

목차 펼치기

머리말

사람은 실수하고 사용자는 예상치 못한 방식으로 서비스를 사용합니다. 좋은 서비스란, 예외적인 상황도 적절히 대응해 UX를 해치지 않고 사용자 이탈을 방지하는 서비스일 것입니다. 돌발적 상황과 예상치 못한 요구를 들어주고 나도 모르는 내가 필요한 것을 제공해 주는 집사처럼요. 이 글에서는 사용자를 위한 서비스에서 클라이언트-서버 통신 간 오류를 처리하고 모니터링하는 방법으로 HTTP 상태 코드를 활용하는 방법과 응답 데이터를 활용하는 방법에 대해 살펴보겠습니다.


HTTP 상태 코드란

 출처: freecodecamp - HTTP Networking in JavaScript – Handbook for Beginners

출처: freecodecamp - HTTP Networking in JavaScript – Handbook for Beginners

우선 HTTP 상태 코드에 대해 살펴보겠습니다. HTTP 상태 코드는 HTTP 프로토콜로 클라이언트-서버가 통신할 때 서버의 처리 결과를 클라이언트에 효율적으로 알려주기 위해 사용하는 코드입니다.


크롬 개발자 도구의 네트워크 탭에서도 확인할 수 있으며, 정의 된 코드별 의미는

MDN HTTP 상태 코드 문서

에서 자세히 확인할 수 있습니다. 이 규칙에 의해 클라이언트는 응답 데이터를 확인하지 않고 상태 코드만으로 상황을 파악할 수 있기 때문에 RESTful API 설계 원칙에서도 준수하기를 요구합니다. 200번 대는 성공, 300번 대는 리다이렉션, 400번 대는 클라이언트 오류, 500번 대는 서버 오류로 분류됩니다.


서버 상태를 확인하기 위해 헬스 체크를 할 때 200번 대를 반환하지 않으면 서버가 다운되었다고 판단할 것이고, 페이지 URL이 변경되었을 때 300번 대를 통해 크롤러에 알려주지 않으면 SEO 관점에서 문제가 발생해 검색 결과 순위가 유지되지 않을 수 있습니다.


이 외에도 적절한 HTTP 상태 코드를 사용하면 빠르게 문제를 파악해서 디버깅할 수 있고, 효율적으로 자원을 관리할 수 있으며, 클라이언트가 오류 상황에 대응할 수 있습니다.


오류와 상태

이제 오류 처리에 관해서 이야기하기 전에 먼저 서비스의 오류와 상태에 대해서 정의해보겠습니다.



먼저

오류

란 실행 중에 발생하는 비정상적인 예외 상황으로 사용자에게 예상치 못한 실패 경험을 주는 상황입니다. 여기서 오류를 문법적 오류, 비즈니스적 오류, 서비스적 오류 세 가지로 분류하겠습니다. 문법적 오류는 프로그래밍 언어 문법과 맞지 않을 때 발생하는 오류이고, 비즈니스적 오류는 의도와 다른 결과가 도출되는 오류입니다. 마지막으로 서비스적 오류는 사용자가 기대한 동작을 제공하지 못하는 오류입니다. 각 오류에 대해 상품 구매 기능으로 예를 들면 다음과 같습니다.

  • 문법적 오류의 예시 : 구매 개수를 필수로 넘겨주어야 하는 함수를 인수 없이 호출할 때

  • 비즈니스적 오류의 예시 : 사용자가 10개 이상 구매 불가능한 상품을 10개 초과하여 구매했을 때

  • 서비스적 오류의 예시 : 구매할 수 없다는 걸 인지하지 못한 사용자의 상품 구매 요청이 정상적인 유효성 검사에 의해 거부될 때


서비스적 오류에서 중요한 점은 사용자가

실패할 것이라 인지하지 못한 채 실패할 요청을 보내는 것

입니다. 사용자는 이 상황을 비정상적인 예외 상황이라 생각할 것입니다. 가장 좋은 것은 실패할 요청은 요청도 이뤄지지 않는 것입니다. 배고픈 고객이 고심 끝에 메뉴판에 있는 음식을 골라서 주문했는데, 주문한 음식은 존재하지 않는다는 대답을 듣는다면 이게 과연 좋은 서비스일까요? 하지만 경험상 정상적인 처리의 결과이기 때문에 오류가 아니라고 생각하시는 분들도 계셨습니다. 이 부분에 대해서는 무엇이 정답이라고 말씀드리기는 어렵습니다. 서비스의 성격이나 구성원 간 합의에 따라 달라질 수 있어 판단은 각자의 몫으로 남겨두겠습니다. 하지만 여기서는 오류라고 생각하고 글을 이어가겠습니다.


상태

란 현재 프로그램이 실행되는 상황입니다. 사용자가 정상적인 요청을 하면 200이라는 상태가, 클라이언트가 잘못된 요청을 하면 400이라는 상태가, 서버 내부에서 오류가 발생하면 500이라는 상태가 되는 것입니다.


이제 오류와 상태의 개념을 결합하여 다음과 같은 상황을 생각해 봅시다.

  1. 200번 성공 상태가 되어야 할 요청이 400번 오류 상태가 될 때

  2. 400번 오류 상태가 되어야 할 요청이 400번 오류 상태가 될 때 → 편의를 위해 ‘

    정상적인 오류 상태

    ’라고 정의하겠습니다.

1번은 더 이야기할 필요 없이 완벽한 오류입니다. 그렇다면 2번은 어떨까요? 기대한 오류 상태가 되었으니 정상일까요? 아니면 오류 상태가 되었으니 오류일까요? 위에서 말씀드렸던 것처럼 저는 2번이 오류가 아니라면, 서비스적 오류가 발생하여도 오류가 없는 상태를 유지하게 되기 때문에 2번도 오류라고 생각합니다.


HTTP 상태 코드를 통한 오류 처리

 출처: replit - 418: I’m a teapot

출처: replit - 418: I’m a teapot

API 응답이 적절한 HTTP 상태 코드를 사용하고 있다면 따로 설계하지 않아도 손쉽게 오류를 처리할 수 있습니다. 클라이언트에서는 상태 코드만 확인해서 추가로 조처할 수 있지만, 세부적인 대응이나 디버깅은 어려울 수 있습니다.



HTTP 상태 코드를 즉시 파악해서 대응할 수 있기 때문에 실시간 모니터링으로 상태 코드가 400 이상인 경우를 확인해서 시스템의 상태를 추적하고 대응하기에 유리합니다. 하지만 여기서는 ‘정상적인 오류 상태’도 오류로 같이 집계됩니다. 즉, 실제 예상치 못한 오류가 얼마나 발생했는지 세부적으로 확인해보지 않는 이상 알 수 없기 때문에 실시간 모니터링의 장점이 퇴색될 수 있습니다.


예상치 못한 오류만 모니터링하기 위해서는 예상 가능한 오류는 API 상태 코드로 정상을 반환해야 합니다. 이를 위한 방법으로 응답 데이터를 통한 오류 처리 방법에 대해서 살펴보겠습니다.


응답 데이터를 통한 오류 처리

 출처: Revisiting HTTP status code

출처: Revisiting HTTP status code

API 상태 코드는 200번을 사용하면서도 발생한 오류 내용을 내부 응답 데이터를 통해 클라이언트에게 제공하는 방식으로 오류를 처리할 수도 있습니다.


발생할 수 있는 오류를 코드로 관리하고 서버는 세분된 오류 유형 중 적절한 오류 코드를 반환합니다. 필요하다면 디버깅에 도움이 될 정보도 추가로 제공할 수 있습니다. 클라이언트는 내부 데이터를 확인해서 요청이 어떻게 처리되었는지 확인하고 적절히 대응할 수 있습니다. 즉, 세분된 오류 유형을 통해 사용자에게 상황에 맞게 적절히 조처할 수 있고, 빠르게 원인을 파악할 수 있습니다. 하지만 다양한 오류 상황을 관리해야되고, 표준화된 API 응답 구조가 필요하기 때문에 관리의 부담이 커질 수 있습니다.


일관성 있는 통신을 위해 서버 응답 데이터 구조에서 실제 응답 데이터를 감싸고 처리에 대한 메타 데이터를 함께 전달해 주는 구조를 사용할 수 있습니다. 회원가입 시 아이디를 입력하는 상황을 예시로 들어보겠습니다.

json
1// API 상태 코드 : 400
2{
3  "status": "error",
4  "message": "이미 존재하는 아이디입니다. 비밀번호 찾기로 이동할까요?",
5  "errorCode": "ID_CONFLICT",
6  "data": {}
7}
json
1// API 상태 코드 : 400
2{
3  "status": "error",
4  "message": "아이디는 최소 6글자 이상이어야합니다.",
5  "errorCode": "ID_TOO_SHORT",
6  "data": {}
7}

API 상태 코드는 400으로 동일하지만, 내부 오류 코드를 확인해서 클라이언트는 비밀번호 찾기 페이지로 이동하게 할 수도 있고, 아이디 입력 창에 다시 포커스를 이동시킬 수도 있습니다. 만약, API 상태 코드만으로 오류를 처리했다면 이런 세부적인 대응은 할 수 없었을 것입니다.


이 오류 처리 방식은 HTTP 상태 코드는 성공 대역을 사용하기 때문에 실시간 모니터링 도구에서는 제대로 감지되지 않기 때문에

console.error

또는 별도의 로거를 활용해 로그 기반 모니터링 해야 합니다. 데이터 기반으로 대응하기 때문에 문제를 발견하고 대응하기까지 시간이 소요될 수 있고 유용한 데이터를 추출하기 위한 처리 과정이 필요하지만, 이를 분석해 복잡한 문제의 원인도 파악할 수 있습니다.


사용자를 위한 서비스

서비스는 사용자를 위한 것입니다. 오류를 모니터링하고, 디버깅하고, 수정하는 과정도 결국 사용자를 위한 것입니다. 사용자 관점에서의 좋은 서비스를 위해 어떻게 오류 상황에 대응할 수 있을까요? 제가 내린 결론은 다음과 같습니다.


  1. 예측 불가능한 오류는 주로 중요한 문제일 수 있습니다. 빠른 대응을 위해 HTTP 상태 코드를 활용해 처리하며 실시간 모니터링으로 추적, 개선합니다.

  2. ‘정상적인 오류 상태’는 개선의 여지가 있지만 서비스 이용에 큰 문제가 되지는 않을 수 있습니다. 응답 데이터를 활용해 예외 케이스를 정리해서 세부적으로 대응하되 로그 기반 모니터링으로 꾸준히 추적, 개선합니다.

  3. ‘정상적인 오류 상태’가 발생할 수 있는 요청은 사용자도 예측할 수 있게 하며, 요청 자체를 방지해 사용자에게 안정적이고 성공적인 서비스 경험을 제공합니다.


잘못된 요청이 자주 들어온다면, 유효성 검사를 추가해서 요청 전에 미리 알 수 있게끔 하거나 기획적으로 기능을 보완해서 사용자가 적절한 요청을 보낼 수 있도록 유도할 수 있을 것입니다. 사용자가 자꾸 권한이 없는 기능을 요청한다면, 어떤 경로를 통해 요청하는 것인지 파악해서 요청할 수 없도록 숨기거나 관리자에게 권한을 요청하도록 처리할 수 있을 것입니다.


사용자의 요청에 대해서 ‘오류가 발생했습니다.’라는 메시지만 띄우는 것은 사용자에게 무책임한 모습만 보여줄 뿐입니다. 메시지에 로직 오류가 그대로 드러나는 것은 더욱 좋지 않습니다. 시멘트와 뼈대가 적나라하게 보이는 집에서 살고 싶은 사람은 없을 것이니까요. 적어도 어떤 오류가 발생해서 사용자가 어떤 상태가 되었는 지는 알려주어야 부족한 시스템을 사람이 보완할 수 있습니다. 넷플릭스는 시청 중 예상치 못한 오류로 재생이 중지되면 사용자한테 서비스를 재시작해 달라고 요청합니다. 오류가 발생했다며 서비스가 갑자기 종료되는 것보다는 더 나은 경험을 제공합니다. 아마 내부적으로도 로그를 보내어 적절한 조치가 진행될 것입니다.


꼬리말

오류 처리에 대한 방법과 그에 맞는 모니터링 방법은 각자의 장점과 단점을 가지고 있습니다. 양자택일해야 하는 문제도 아닙니다. ‘정상적인 오류 상태’를 오류라고 생각하지 않기로 했다면, 그래도 좋습니다. 서비스의 성격이나 가용 자원 등을 고려하고 구성원 간 합의를 통해 적절한 방식을 사용하면 됩니다.


HTTP 상태 코드와 오류 처리는 요기요에서도 고민하고 팀의 방향을 찾아나간 과정을 글로 작성한 적이 있으며, 해외에서도 많은 논의가 되는 주제인 것 같습니다. 사용자를 위한 서비스를 만들기 위해서 같이 고민하고 서로 지원하며 계속 서비스가 발전할 수 있었으면 좋겠습니다.


참고

Wanna get in touch?

All Icons byiconiFy