2.20

View in English

2.20 오류 처리와 복원력 패턴

개요와 동기

여러분이 쓰는 모든 프로그램은 실패할 것입니다. 디스크가 차고, 네트워크가 끊기고, 서비스가 시간 초과되고, 호출자가 쓰레기 값을 넘기고, 의존성이 문서에 없던 무언가를 돌려줍니다. 질문은 실패가 일어나느냐가 아니라, 코드가 그 실패를 계획으로 맞이하느냐 놀람으로 맞이하느냐입니다. 오류 처리는 세상이 협조하지 않을 때 코드가 무엇을 할지 줄마다, 함수마다 결정하는 기술입니다. 구축에서 가장 화려하지 않은 부분이면서, 어떤 기능보다도 사람들이 시스템을 신뢰하는지를 결정하는 부분입니다.

이 장은 코드와 구성 요소 수준의 복원력, 즉 함수, 모듈, API 안의 선택을 다룹니다. 시스템 수준의 복원력(로드 밸런싱, 복제, 서비스 간 페일오버)을 다루는 3.5장을 보완합니다. 3.5장은 한 리전이 꺼져도 전체 플랫폼을 서 있게 하고, 이 장은 단일 요청이 데이터를 손상시키거나 흔적 없이 사라지지 않게 합니다. 둘은 서로를 강화합니다. 뒤의 코드가 예외를 삼키면 아키텍처의 서킷 브레이커는 별 의미가 없고, 주변 시스템에 중복성이 없으면 방어적인 함수도 여러분을 구하지 못합니다. 이 장은 오류 처리가 여러 규율 중 하나였던 소프트웨어 구축(2.9장)에 이어, 그것을 주제 전체로 삼습니다.

큰 팀에서 일관성이 상입니다. 수백 명의 엔지니어가 오류를 수백 가지 다른 방식으로 처리하면 모든 서비스가 퍼즐이 되고 모든 인시던트가 발굴 작업이 됩니다. 기업 환경에서 그 불일치는 모든 감사와 통합의 비용을 높입니다. 정부와 다른 고위험 시스템에서는 이해관계가 더 날카롭습니다. 정확성, 안전한 실패, 분명한 감사 추적은 나중에 더하는 기능이 아니라 첫 커밋부터 시스템이 가져야 하는 속성입니다. 조용히 잘못 계산하는 급여 시스템이나 실패를 기록하지 않고 잃는 기록 시스템은 단지 버그가 있는 것이 아닙니다. 그 배후의 기관을 침식하는 방식으로 신뢰할 수 없습니다.

핵심 원칙

  • 오류(error), 결함(fault), 실패(failure)를 구분하고 각각을 알맞은 계층에서 처리하십시오.
  • 빠른 실패와 안전한 실패를 맥락마다 의도적으로 선택하고, 우연에 맡기지 마십시오.
  • 모든 함수와 API의 오류 처리 계약을 명시적이고 정직하게 만드십시오.
  • 경계에서 검증하고, 경계 안에서는 신뢰하고, 편집증 없이 방어하십시오.
  • 오류를 조용히 삼키지 마십시오. 드러내거나, 감싸거나, 의도적으로 처리하십시오.
  • 멱등성, 타임아웃, 백오프, 지터로 재시도를 안전하게 만드십시오.
  • 오류 경로에 정상 경로와 같은 설계 주의를 기울이십시오.

권장 사항

오류, 결함, 실패를 구분한다

엉성한 어휘는 엉성한 처리를 낳으므로, 분명한 용어로 시작하십시오. 결함(fault)은 시스템의 흠입니다. 버그, 잘못된 설정, 다운된 의존성입니다. 오류(error)는 결함이 낳는 잘못된 내부 상태입니다. 값이 있어야 할 곳의 null, 더는 맞지 않는 잔액입니다. 실패(failure)는 바깥 관찰자가 보는 것입니다. 요청이 틀린 답을 돌려주거나 답을 돌려주지 않습니다. 하나의 결함이 많은 오류를 일으킬 수 있고, 많은 오류가 어느 것도 눈에 보이는 실패가 되기 전에 잡힐 수 있습니다. 오류 처리의 요점은 그 사슬을 끊는 것, 즉 오류가 사용자나 감사자가 겪는 실패가 되기 전에 잡는 것입니다.

이 어휘는 어디서 행동할지도 알려 줍니다. 결함은 리뷰, 테스트, 설정에서 다룹니다. 오류는 런타임에 이 장의 패턴으로 다룹니다. 실패는 관측 가능성(9.2장)과 3.5장의 시스템 수준 복원력으로 다룹니다. 팀이 이 말을 공유하면 인시던트 리뷰가 날카로워집니다. “그 버그”가 무엇이었는지 다투는 대신, 사슬이 어디서 끊어졌어야 했는데 끊기지 않았는지 정확히 말할 수 있습니다.

맥락마다 빠른 실패와 안전한 실패를 고른다

빠른 실패는 무언가 잘못된 순간 멈추는 것으로, 나쁜 상태에서 진행하기를 거부해 문제가 크게, 그리고 원인 가까이에서 드러나게 합니다. 안전한 실패는 알려진 무해한 상태로 성능을 낮추고 안전하게 제공할 수 있는 것을 계속 제공하는 것입니다. 어느 쪽도 보편적으로 옳지 않으며, 기술은 맥락마다 고르는 것입니다. 개발 중과 내부 경계에서는 빠른 실패가 친구입니다. 불변식 위반에서 멈추는 프로그램은 긴 수수께끼 대신 짧은 스택 트레이스를 줍니다. 프로덕션에서 사용자와 마주하는 시스템의 가장자리에서는 안전한 실패가 흔히 이깁니다. 아무것도 돌려주지 않는 추천 패널이 로드되지 않는 결제 페이지보다 낫습니다.

각 경계에 대해 의도적으로 결정하고 그 결정을 적어 두십시오. 비행 제어나 의료 기기 구성 요소는 손상된 데이터로 계속하면 누군가를 다치게 할 수 있으므로 정의된 상태로 안전하게 실패합니다. 원장 기장은 잘못된 항목을 기장하는 것이 기장하지 않는 것보다 나쁘므로 빠르게 실패합니다. 잘못된 짝짓기는 양방향으로 위험합니다. 빠른 실패가 필요한 곳의 안전한 실패는 손상을 숨기고, 안전한 실패가 필요한 곳의 빠른 실패는 외관상의 딸꾹질을 장애로 바꿉니다.

오류 신호 메커니즘을 고르고 일관되게 쓴다

언어는 무언가 잘못되었음을 알리는 크게 두 가지 방법을 줍니다. 예외 처리는 어떤 핸들러가 잡을 때까지 호출 스택 위로 객체를 던져 오류 경로를 주 로직과 분리합니다. 대안은 명시적 오류 값입니다. 함수가 결과와 오류를 둘 다 돌려주고 호출자가 둘 다 검사해야 합니다. 많은 현대 언어는 후자를 Result 타입(흔히 Result나 Either)으로 형식화하여, 호출자가 값을 쓰기 전에 성공이나 실패를 풀도록 강제합니다. 각 접근에는 비용이 있습니다. 예외는 정상 경로를 깔끔하게 유지하지만 제어 흐름을 숨길 수 있고, 정보를 지우는 포괄적 catch 블록으로 개발자를 유혹합니다. 명시적 결과는 모든 실패를 타입 시그니처에서 보이게 하지만 의례가 늘고, 언어가 검사를 강제하지 않으면 무시될 수 있습니다.

옳은 답은 어느 메커니즘이냐보다 일관성과 정직성에 가깝습니다. 언어와 생태계가 선호하는 관용구를 고르고 서비스 전반에 균일하게 적용해, 독자가 실패가 어떻게 이동하는지 항상 알게 하십시오. 예외는 진짜 예외적인 조건에 남겨 두고, “사용자를 찾을 수 없음” 같은 일상적 제어 흐름에는 쓰지 마십시오. 그것은 정상 결과로 모델링하는 편이 낫습니다. 무엇을 고르든 실패가 보이지 않게 두지 마십시오. 확인하지 않은 오류 값은 빈 catch 블록만큼 위험합니다. 큰 코드베이스에서는 서면 관례와 무시된 오류를 표시하는 린터가 개인의 선호를 이깁니다.

오류 처리 계약을 명시한다

모든 함수와 API에는 아무도 적어 두지 않았더라도 오류 처리 계약이 있습니다. 여기서 무엇이 잘못될 수 있는지, 어떻게 알게 되는지, 그때 상태에 대해 무엇이 보장되는지에 답합니다. 그 계약을 명시하십시오. 함수가 어떤 오류를 돌려주거나 던질 수 있는지 문서화하고, 복구 가능한 오류(호출자가 합리적으로 재시도하거나 대체할 수 있음)와 복구 불가능한 오류(호출자가 고칠 수 없으니 전파하거나 중단해야 함)를 구분하고, 실패 시 함수가 상태를 바꾸지 않은 채 두는지 밝히십시오. 이 마지막 속성은 강한 예외 보장이라고도 하며, 실패한 호출이 일어나지 않은 것과 같다는 뜻으로, 바로 호출자가 안전하게 재시도하게 해 줍니다.

공개 또는 팀 간 API에서 이 계약은 매개변수 타입만큼 실재하는 인터페이스의 일부입니다. 작고 안정적인 오류 분류 체계를 설계하십시오. 검증 오류, 찾을 수 없음, 충돌, 권한 없음, 의존성 사용 불가, 내부 오류 같은 한정된 범주 집합입니다. 그러면 호출자는 문자열을 파싱하지 않고 범주로 분기할 수 있습니다. 분명한 분류 체계는 오류 처리를 많은 서비스에 걸쳐 조합 가능하게 하고, 모든 실패가 알려진 이름 붙은 종류에 대응하므로 실패를 감사 가능하게 합니다.

경계에서 검증하고 편집증 없이 방어한다

신뢰 경계를 넘는 데이터(네트워크 요청, 파일, 사용자 입력, 다른 서비스의 메시지)는 검증될 때까지 적대적인 것으로 다루고, 경계에서 한 번 철저히 검증하십시오. 이것은 판단과 함께 적용된 방어적 프로그래밍입니다. 입력을 이미 검증한 모듈 안에서는 모든 줄의 중복 검사가 로직을 가리고 보고 싶은 바로 그 실패를 억누릅니다. 규율은 가장자리에서 강하게 방어하고 안에서는 신뢰하는 것입니다. 데이터가 들어오는 곳에서 구조, 범위, 불변식을 검증하고, 불법 상태를 표현할 수 없게 하는 타입으로 변환하고, 내부 코드는 깨끗한 데이터로 일한다고 가정하게 하십시오.

편집증에는 실제 비용이 있습니다. null 검사와 방어적 분기로 뒤덮인 코드는 읽기 어렵고, 더 나쁘게는 분명한 실패를 조용한 어깨 으쓱으로 바꾸어 경보를 울려야 할 곳에서 기본값을 돌려주는 경우가 많습니다. 버그를 가리는 방어는 안전이 아니라 미루기입니다.

재시도를 안전하고, 한정되고, 예의 바르게 만든다

많은 결함은 일시적입니다. 순간적인 네트워크 끊김, 재시작하는 서비스, 짧은 락 경합입니다. 모든 분산 시스템(3.3장)에서 이런 부분 실패는 예외가 아니라 정상 사례입니다. 재시도가 자연스러운 대응이지만 순진한 재시도 루프는 장전된 총입니다. 첫째, 재시도하는 연산을 멱등하게 만드십시오. 두 번 수행해도 한 번 수행한 것과 같은 효과를 낸다는 뜻입니다. 멱등성이 없으면 타임아웃 후 재시도가 카드에 두 번 청구하거나 레코드를 두 개 만들 수 있습니다. 첫 시도가 실패했는지 확인 응답만 잃었는지 알 수 없기 때문입니다. 쓰기에는 멱등성 키를 써서 수신자가 반복을 알아보고 중복 제거할 수 있게 하십시오.

둘째, 매 원격 호출에 타임아웃을 두어 멈춘 의존성이 여러분을 멈추게 하지 못하게 하십시오. 셋째, 매 시도 후 대기를 두 배로 하는 지수 백오프로 재시도 간격을 두고, 지터(작은 무작위 지연)를 더해 한꺼번에 복구하는 천 개의 클라이언트가 동기화되어 복구 중인 서비스를 다시 쓰러뜨리는 쇄도를 만들지 않게 하십시오. 넷째, 재시도 횟수와 전체 시간에 상한을 두고 품위 있게 포기하십시오. 한도, 백오프, 지터, 멱등성이 없는 재시도는 작은 딸꾹질이 자초한 장애가 되는 가장 흔한 방법 중 하나입니다.

코드에 서킷 브레이커, 벌크헤드, 우아한 성능 저하를 더한다

의존성이 정말 다운되었을 때 재시도는 노력을 낭비하고 구멍을 깊게 할 뿐입니다. 서킷 브레이커는 의존성 호출의 실패율을 지켜보다가 실패가 임계값을 넘으면 “열려” 쿨다운 기간 동안 망할 호출을 기다리지 않고 즉시 실패합니다. 쿨다운 후에는 시험 호출을 통과시키고 의존성이 복구되었다면 다시 닫힙니다. 이는 호출자(쌓인 타임아웃 대신 빠르고 예측 가능한 실패)와 어려움을 겪는 의존성(복구할 숨 돌릴 틈)을 모두 보호합니다. 선박의 방수 격벽에서 이름 붙인 벌크헤드 패턴은 자원을 격리해 포화된 의존성 하나가 모든 스레드나 연결을 소비해 프로세스 전체를 침몰시키지 못하게 합니다. 각 의존성에 자체 한정된 풀을 줍니다.

이 패턴은 코드 수준의 우아한 성능 저하와 짝을 이룹니다. 필수가 아닌 의존성을 쓸 수 없을 때 오류 대신 줄어들었지만 쓸모 있는 결과를 돌려주십시오. 오래됨을 알리는 메모와 함께 캐시된 데이터를 보여 주고, 개인화 패널을 숨기고, 쓰기를 나중을 위해 큐에 넣습니다. 이것은 3.5장의 시스템 수준 복원력의 지역적 보완입니다. 아키텍처는 기계 간 중복성을 제공하고, 코드는 한 부분이 없을 때의 온당한 동작을 제공합니다.

맥락과 함께 오류를 감싸고 절대 삼키지 않는다

발생한 곳에서 열 계층 위에서 “connection refused”로 읽히는 오류는 거의 쓸모가 없습니다. 오류가 전파될 때 맥락으로 감싸십시오. 무엇을 하려던 중이었는지, 어느 엔티티나 요청인지, 어느 의존성인지를 담되, 근본이 사라지지 않도록 원래 원인을 보존하십시오. 좋은 언어와 라이브러리는 이런 오류 체이닝을 직접 지원합니다. 목표는 로그 한 줄이 호출 대기 엔지니어에게 무엇이, 어떤 연산 중에, 어떤 입력으로 실패했는지 알려 주는 것입니다. 이것이 9.2장의 관측 가능성과 2.15장의 디버깅의 원료입니다.

가장 큰 죄는 오류를 삼키는 것입니다. 빈 catch 블록, 무시된 반환 값, 디버그 수준으로 로그를 남기고 아무 일 없다는 듯 계속하는 catch입니다. 삼킨 오류는 사라지지 않고 나중에 손상된 데이터나 설명할 수 없는 결함으로 다시 나타나며, 이제 원인과 분리되어 있습니다. 모든 오류는 세 가지 운명 중 하나를 맞아야 합니다. 처리하거나(복구 또는 성능 저하), 감싸서 전파하거나, 스택의 맨 위에서 전체 맥락과 함께 로그를 남기고 실패하는 것입니다. 오류를 잡고서 이 중 어느 것도 하지 않았다면, 미래의 자신에게서 미래의 인시던트를 숨기기로 선택한 것입니다.

장단점

접근 방식장점단점
예외깔끔한 정상 경로. 검사하지 않으면 무시하기 어려움숨은 제어 흐름. 포괄적 catch로 지우려는 유혹
명시적 오류 값 / Result 타입시그니처에서 실패가 보임. 처리를 강제의례가 더 늘어남. 강제가 없으면 무시될 수 있음
빠른 실패버그를 크게, 원인 가까이에서 드러냄가장자리에서 쓰면 사용자 경험이 나쁨
안전한 실패계속 제공. 사용자와 데이터를 보호빠른 실패가 필요한 곳에서 쓰면 손상을 가릴 수 있음
백오프가 있는 재시도일시적 결함을 자동으로 넘김멱등성 없이는 부하를 증폭하고 이중 쓰기를 일으킴
서킷 브레이커빠른 실패. 의존성이 복구하게 함상태와 튜닝이 추가됨. 지속적 문제를 가릴 수 있음
경계에서의 방어적 검증나쁜 데이터를 일찍, 한 번, 크게 잡음지나치면 로직을 어지럽히고 실제 실패를 숨김

핵심 긴장은 가시성과 소음 사이에 있습니다. 오류를 너무 조용히 처리하면 비싸질 때까지 문제를 숨기고, 너무 시끄럽게 모든 곳에서 처리하면 신호를 의례 속에 빠뜨리고 중요한 실패를 가립니다. 위치와 의도로 해결하십시오. 나쁜 데이터와 의존성 실패가 들어오는 경계에서는 시끄럽고 엄격하게 하십시오. 입력이 이미 깨끗한 내부에서는 조용히 신뢰하십시오. 경계마다 빠른 실패 대 안전한 실패를 결정하고 적어 두십시오. 목표는 모든 실패에 정확히 하나의 분명한 소유자와 하나의 분명한 운명이 있고, 아무것도 틈새로 조용히 떨어지지 않는 코드입니다.

팀과 논의할 질문

  1. 서비스 전반에 걸쳐 공유된 하나의 오류 분류 체계와 오류 처리 관례가 있습니까, 아니면 팀마다 즉흥적으로 합니까? 큰 팀에서 이것은 조합되는 실패와 혼란스러운 실패의 차이입니다. 한 서비스가 검증 문제에 HTTP 500을 돌려주고, 다른 서비스는 타입 있는 예외를 던지고, 세 번째는 null을 돌려주면, 모든 통합이 협상이 되고 모든 인시던트가 번역 연습이 됩니다. “레코드를 찾을 수 없음” 같은 같은 논리적 실패가 세 서비스에서 어떻게 나타나는지 예를 가져와 얼마나 다르게 신호하는지 보십시오. 답은 서면 표준이 되어야 합니다. 한정된 오류 범주 집합, 그것을 신호하는 일관된 방법, 이를 시행하는 린터나 리뷰 체크리스트입니다. 여기서의 일관성은 모든 미래의 통합, 감사, 호출 대기 교대에서 보상받습니다.

  2. 각 핵심 경계에서 빠른 실패와 안전한 실패를 의도적으로 골랐으며, 코드가 그 선택과 일치합니까? 대부분의 팀은 이 결정을 명시적으로 내린 적이 없고, 이는 처음 코드를 쓴 사람이 일관성 없이 대신 내렸다는 뜻입니다. 상충하는 고려는 실제입니다. 안전하게 실패하면 사용자에게 계속 제공하지만 손상이 퍼지게 둘 수 있고, 빠르게 실패하면 데이터를 보호하지만 사소한 의존성 장애를 눈에 보이는 실패로 바꿀 수 있습니다. 인시던트 이력을 가져와 최악의 몇 건에 대해 코드가 미리 물었다면 선택했을 방식으로 실패했는지 물으십시오. 원하는 증거는 각 경계에 의도적인 라벨이 붙은 지도이며, 특히 돈, 안전, 시민 기록이 관련된 곳이 그렇습니다. 라벨과 코드가 어긋나는 곳이 다음 수정입니다.

  3. 마지막으로 오류 경로를 의도적으로 실행해 본 것은 언제이며, 설계대로 동작했습니까? 오류 경로는 대개 소유한 코드 중 가장 덜 테스트된 코드이지만 신뢰를 얻고 잃는 곳이며, 한 번도 지켜본 적이 없다면 “우리는 안전하게 실패한다”는 뒷받침할 수 없는 주장입니다. 멱등성 없는 재시도 루프, 임계값이 틀린 서킷 브레이커, 드물게 닿는 분기의 삼킨 예외는 실제 인시던트가 대신 찾아 줄 때까지 숨어 있습니다. 현실적인 환경에 의도적으로 실패를 주입한(죽인 의존성, 유발한 타임아웃, 잘못된 형식의 페이로드) 결과를 가져오십시오. 이어지는 행동은 실패 주입을 일상화하여 복구, 성능 저하, 안전한 실패 동작이 바라는 것이 아니라 지속적으로 검증되게 하는 것입니다. 한 번도 일으켜 보지 않은 오류 경로는 테스트하지 않은 약속입니다.

  4. 쓰기 연산 중 어느 것이 멱등하며, 확인 응답을 잃은 뒤의 재시도가 결제나 레코드 같은 실제 효과를 중복시킬 곳은 어디입니까? 재시도는 가장 흔한 복원력 반사이며, 부주의하게 하면 일시적 끊김이 돈이나 데이터의 중복으로 바뀌는 가장 흔한 방법입니다. 큰 팀에서 재시도 로직은 공유 클라이언트, 미들웨어, 개별 서비스에 동시에 있는 경우가 많아, 하나의 쓰기가 여러 계층에서 재시도될 수 있고 아무도 전체 동작을 소유하지 않습니다. 상충하는 끌림은 멱등성 키, 중복 제거, 저장된 요청 결과가 저장소와 코드를 더하며, 전달 압박 아래의 팀이 안전하다고 잘못 가정한 쓰기에서 이를 건너뛴다는 점입니다. 외부에서 보이는 쓰기의 목록을 가져와, 각각 멱등성 키를 갖는지 수신자가 반복을 어떻게 알아보고 제거하는지 표시하십시오. 기업과 정부 환경에서는 돈을 움직이거나 시민의 기록을 바꾸는 것을 먼저 표시하십시오. 이중 지급이나 중복된 급여는 단순한 결함이 아니라 감사 지적이고 때로는 법적 노출이기 때문입니다.

  5. 타임아웃, 서킷 브레이커, 벌크헤드가 하나의 공유되고 테스트된 라이브러리에서 오며, 아니면 팀마다 직접 만듭니까? 이 패턴들은 설명하기 쉽고 미묘하게 틀리기도 쉽습니다. 빠진 타임아웃, 결코 작동하지 않는 브레이커 임계값, 느린 의존성 하나가 프로세스 전체를 굶기도록 크기가 정해진 연결 풀입니다. 모든 팀이 다시 구현하면 약간씩 망가진 사본이 많이 쌓이고, 결함을 찾아도 한 번에 고칠 단일한 자리가 없습니다. 상충하는 고려는 공유 라이브러리가 공통 인터페이스와 업그레이드 주기를 강요하며, 특이한 런타임이나 지연 요건이 있는 팀이 불평하거나 우회할 수 있다는 점입니다. 프로덕션에서 실제로 도는 서로 다른 재시도·브레이커 구현이 몇 개인지, 어느 서비스가 아웃바운드 호출에 타임아웃이 아예 없는지 조사한 결과를 가져오십시오. 큰 기업이나 기관에서는 검증된 공유 라이브러리가 보안 리뷰어와 감사자에게 수십 개가 아니라 인증할 하나의 구성 요소를 주어 모든 리뷰의 비용을 낮추기도 합니다.

  6. 어젯밤 인시던트가 있었다면 호출 대기 엔지니어가 로그 한 줄로 추적할 수 있으며, 감사자가 나중에 시스템이 기록한 모든 실패를 볼 수 있습니까? 감싸지고, 분류되고, 잘 기록된 오류는 10분 진단과 한밤의 발굴 작업의 차이이고, 삼킨 오류는 스스로에게서 숨긴 미래의 인시던트입니다. 큰 팀에서 실패는 여러 서비스 홉을 가로지르므로 가치는 한 팀의 성실함이 아니라 그 홉을 살아남는 일관된 맥락과 상관 식별자에서 옵니다. 상충하는 긴장은 비용과 소음입니다. 모두 기록하면 신호를 빠뜨리고 저장 비용을 치르며, 너무 적게 기록하면 무슨 일이 있었는지 재구성할 수 없습니다. 최근의 실제 실패 하나를 가져와 그 흔적을 처음부터 끝까지 따라가며, 맥락이 떨어졌거나 오류가 잡혀 버려진 모든 홉을 기록하십시오. 규제 및 정부 시스템에서는 이를 컴플라이언스 속성으로 다루십시오. 감사할 수 없는 실패, 또는 몇 년 뒤 설명할 수 없는 결정은 단순한 운영상의 간극이 아니라 법적 노출이기 때문입니다.

분야별 관점

스타트업. 몇 명의 엔지니어와 여유 없는 런웨이라면 오류 처리 예산을 실패가 고객이나 데이터를 잃게 하는 곳에 쓰십시오. 모든 아웃바운드 호출에 타임아웃을 두고, 돈을 움직이는 쓰기를 멱등하게 만들고, 무시된 오류에 대한 린트 규칙을 추가하십시오. 정교한 프레임워크는 건너뛰십시오. 핵심 함수의 Result 타입과 필수가 아닌 의존성의 우아한 성능 저하가 며칠 작업으로 안전의 대부분을 삽니다. 개발에서는 빠르게 실패하여 버그가 크게 드러나게 하고, 정당화하는 의존성이 실제로 생기기 전에 서킷 브레이커를 직접 만들려는 유혹을 이기십시오.

소기업. 복원력 전문가도 빠듯한 예산도 없으니, 패턴을 처음부터 만들지 말고 언어, 프레임워크, 클라우드 제공자가 이미 주는 것에 기대십시오. 관리형 큐, 제공자 측 재시도, 라이브러리 타임아웃이 대부분의 팀이 기대하는 것보다 많이 덮습니다. 결정을 사느냐 만드느냐로 보고, 성숙한 의존성이 재시도, 백오프, 멱등성을 대신 처리하는 곳에서는 사십시오. 희소한 주의를 잘못되거나 잃은 거래가 정말 아픈 한두 경계에 쓰고, 그것들이 안전하게 실패하고 흔적을 남기게 하십시오.

대기업. 많은 팀에 걸쳐 상은 일관성입니다. 하나의 공유 오류 분류 체계, 타임아웃, 재시도, 서킷 브레이커, 벌크헤드를 위한 공통 라이브러리, 파이프라인에서 이를 시행하는 린터와 리뷰 체크리스트입니다. 모든 오류를 상관 식별자와 함께 통합 관측 가능성 플랫폼에 공급해 서비스 홉을 가로질러 실패를 추적할 수 있게 하고, 감사가 지역 습관의 산재가 아니라 문서화되고 방어 가능한 패턴을 찾도록 경계마다 빠른 실패 대 안전한 실패 결정을 표준화하십시오. 공유 라이브러리를 진짜 제품으로 다스리십시오. 거기서 한 번 고친 결함은 모든 곳에서 고친 결함이기 때문입니다.

정부. 정확성, 안전한 실패, 내구성 있는 감사 추적은 선호가 아니라 의무입니다. 돈이나 자격에 닿는 위반된 불변식에는 빠르게 실패하고, 시민과 마주하는 모든 입력을 경계에서 검증하고, 몇 년 뒤 결정을 설명하고 검토할 수 있을 만큼의 맥락과 함께 각 실패를 불변 로그에 기록하십시오. 조달과 긴 시스템 수명은 원래 저자가 떠난 한참 뒤에도 공무원이 코드를 유지할 수 있도록 오류 계약이 문서화되어야 한다는 뜻이며, 모든 벤더 구성 요소는 실패 동작을 불투명한 인터페이스 뒤에 숨기지 말고 드러내야 합니다.

사례

스타트업. 네 명 규모의 스타트업이 제3자 결제 제공자와 이메일 서비스를 호출하는 앱을 출시합니다. 초기에 순진한 재시도 루프를 더했다가, 타임아웃이 성공한 청구를 가려 고객에게 즉시 이중 청구를 합니다. 해결은 교훈을 줍니다. 모든 쓰기에 멱등성 키를 추가하고, 모든 아웃바운드 호출에 타임아웃을 두고, 지터가 있는 지수 백오프로 바꿉니다. 실패가 시그니처에 보이도록 핵심 서비스 함수에 Result 타입을 채택하고, 린트 규칙이 무시된 모든 오류를 표시합니다. 이메일 발송이 실패하면 결제는 판매를 막지 않고 메시지를 큐에 넣어 우아하게 성능이 저하됩니다. 이 규율은 며칠이 들었고 환불과 신뢰에서 훨씬 더 큰 비용이 들었을 부류의 인시던트를 막아 줍니다.

대기업. 한 글로벌 물류 회사가 수백 개 서비스를 운영하며 모두에 걸쳐 오류 처리를 표준화합니다. 모든 서비스가 실패를 공유 분류 체계(검증, 찾을 수 없음, 충돌, 의존성 사용 불가, 내부)로 매핑하여 호출자가 메시지를 파싱하지 않고 범주로 분기합니다. 공통 라이브러리가 서킷 브레이커, 백오프와 지터가 있는 한정된 재시도, 벌크헤드 연결 풀을 제공하여 아무도 이 패턴을 잘못 직접 만들지 않습니다. 모든 오류는 9.2장의 관측 가능성 플랫폼에 공급되는 상관 맥락과 함께 기록되어, 호출 대기 엔지니어가 한 줄로 서비스 홉을 가로질러 실패를 추적할 수 있습니다. 표준이 균일하고 파이프라인에서 시행되므로, 엔지니어는 낯선 서비스를 자신 있게 오가고 감사자는 모든 실패가 기록되고, 분류되고, 추적 가능함을 볼 수 있습니다.

정부. 한 국가 급여 기관이 잘못된 답이 누군가의 월세를 거부하거나 공적 자금에서 과지급할 수 있는 자격 및 지급 시스템을 만듭니다. 정확성과 안전한 실패는 협상 불가이므로, 코드는 위반된 모든 재무 불변식에서 빠르게 실패합니다. 조정되지 않는 계산은 틀린 수치를 기장하는 대신 기장을 거부합니다. 시민과 마주하는 모든 입력은 경계에서 검증되고, 불법 상태는 도메인 타입에서 표현할 수 없게 됩니다. 각 실패는 전체 맥락과 함께 불변 감사 로그에 기록되어, 결정이 몇 년 뒤에도 설명되고 검토될 수 있어야 한다는 법적 요건을 충족합니다. 문서 미리보기 같은 필수가 아닌 의존성이 다운되면 시스템이 우아하게 성능이 저하되어 담당자가 여전히 청구를 처리할 수 있습니다. 새 공무원은 오류 계약이 문서화된 코드를 물려받아, 원래 저자가 떠난 한참 뒤에도 안전하게 유지할 수 있습니다.

비즈니스 사례: 동기, ROI, TCO

규율 있는 오류 처리의 수익은 더 적은 인시던트, 더 짧은 인시던트, 더 싼 인시던트로 나타납니다. 대부분의 프로덕션 장애는 이국적이지 않습니다. 삼킨 예외, 빠진 타임아웃, 재시도 폭풍, 검증했어야 할 데이터를 신뢰한 경계로 거슬러 올라갑니다. 각각은 여기의 패턴으로 예방할 수 있고, 예방된 각 인시던트는 다운타임의 직접 비용뿐 아니라 긴급 대응, 고객 이탈, 조사의 누적 비용도 아낍니다. 감싸지고 잘 기록된 오류는 몇 시간이 아니라 몇 분 안에 진단할 수 있으므로 평균 복구 시간이 떨어지고, 엔지니어가 오류 경로를 두려워하지 않게 되면서 변경 실패율도 함께 떨어집니다.

도입 비용은 소소하고 대부분 일회성입니다. 오류 분류 체계를 적어 두고, 팀이 나쁘게 재발명하지 않도록 재시도와 서킷 브레이커의 공유 라이브러리를 제공하고, 무시된 오류에 대한 린트 규칙을 추가하고, 실패 주입의 습관을 만듭니다. 방치의 비용은 조용히 누적됩니다. 삼킨 오류는 되돌리기 비싼 손상된 데이터로 쌓이고, 일관성 없는 처리는 모든 통합과 감사의 비용을 늘립니다. 규제 및 정부 환경에서 감사할 수 없는 실패는 단순한 엔지니어링 문제가 아니라 컴플라이언스와 법적 노출입니다. 리더십을 설득하려면 오류 처리 규율을 그들이 이미 지켜보는 지표에 연결하십시오. 인시던트 빈도, 평균 복구 시간, 변경 실패율, 감사 지적입니다.

안티패턴과 함정

  • 조용한 삼킴: 실패를 지연되고 분리된 수수께끼로 바꾸는 빈 catch 블록과 무시된 반환 값.
  • 포괄적 catch 지우기: 일반적인 메시지를 기록하고 원래 오류와 그 맥락을 버리는 넓은 catch.
  • 멱등성 없는 재시도: 타임아웃 후 멱등하지 않은 쓰기를 다시 실행해 이중 청구하거나 레코드를 중복시키는 것.
  • 재시도 폭풍: 백오프도, 지터도, 상한도 없어 클라이언트가 동기화되어 복구 중인 의존성을 다시 쓰러뜨리는 것.
  • 타임아웃 없음: 멈춘 의존성 하나가 스레드를 소진하고 프로세스 전체를 얼리게 하는 무한 원격 호출.
  • 제어 흐름으로서의 예외: “찾을 수 없음” 같은 일상적 결과에 던지고 잡아 로직을 숨기고 코드를 느리게 하는 것.
  • 방어적 편집증: 로직을 묻고 실제 실패를 조용한 기본값으로 바꾸는 모든 줄의 검사.
  • 문자열로 된 오류: 분기할 안정적이고 분류된 체계가 없어 호출자가 오류 메시지 텍스트를 파싱하는 것.
  • 빠른 실패가 필요한 곳의 안전한 실패: 틀린 답이 답이 없는 것보다 나쁜 시스템에서 손상된 상태로 계속하는 것.

성숙도 모델

  • 1단계, 시작: 오류 처리가 즉흥적이고 반응적이며 개발자마다 결정됩니다. 빈 catch 블록과 무시된 반환이 흔하고, 재시도는 순진하고, 타임아웃이 없으며, 실패는 일관된 로깅 없이 손상된 데이터나 수수께끼 결함으로 드러납니다.
  • 2단계, 발전: 팀이 기본 실천을 채택하지만 일관되지 않습니다. 오류는 어느 정도의 맥락과 함께 기록되고, 명백한 삼킴은 리뷰에서 지양되며, 타임아웃과 단순한 재시도가 있지만, 관례는 서비스마다 다르고, 멱등성은 군데군데이며, 오류 경로는 거의 테스트되지 않습니다.
  • 3단계, 표준화: 공유 오류 분류 체계와 처리 관례가 조직 전체에 문서화되고 시행됩니다. 경계 검증, 백오프와 지터가 있는 멱등 재시도, 서킷 브레이커, 벌크헤드, 오류 감싸기가 표준이며 공통 라이브러리가 제공하고, 모든 오류가 통합 관측 가능성 파이프라인에 공급됩니다.
  • 4단계, 관리: 오류 처리 동작이 기준선에 대해 측정되고 데이터로 통제됩니다. 재시도 비율, 서킷 브레이커 작동, 타임아웃 횟수, 정적 분석의 삼킨 오류 발견, 평균 복구 시간, 변경 실패율이 서비스별로 추적되고, 서킷 브레이커 임계값과 타임아웃은 추측이 아니라 관찰된 지연과 실패 데이터로 조정되며, 실패 주입이 일정에 따라 실행되고, 팀이 이 지표를 검토해 회귀를 잡고 각 빠른 실패 또는 안전한 실패 선택을 증거로 유지합니다.
  • 5단계, 오케스트레이션: 복원력이 전달 및 위험 계획과 통합되어 지속적으로 개선됩니다. 분류 체계, 공유 라이브러리, 표준이 모든 인시던트에서 진화하고, 카오스 및 실패 주입 실험이 일상이며, 조직은 트래픽, 의존성, 위험 그림이 이동함에 따라 타임아웃, 브레이커 임계값, 성능 저하 전략, 경계 결정을 조정합니다.

논의를 위한 아이디어

  1. 코드베이스 어디에서 오류가 현재 삼켜지고 있으며, 그렇지 않다는 것이 틀렸을 때 어떻게 알겠습니까?
  2. 쓰기 연산 중 어느 것이 멱등하며, 확인 응답을 잃은 뒤 재시도가 발동하면 어느 것이 이중 실행됩니까?
  3. “사용자를 찾을 수 없음”은 예외여야 합니까, 오류 값이어야 합니까, 정상 결과여야 합니까, 그리고 팀이 이에 일관되게 답합니까?
  4. 검증이 일어나는 곳에 대한 실제 규칙은 무엇이며, 신뢰하지 말아야 할 데이터를 신뢰하는 경계를 짚을 수 있습니까?
  5. 서킷 브레이커의 임계값과 쿨다운을 어떻게 정하며, 현재 설정이 틀렸음을 어떻게 알겠습니까?
  6. 감사자가 지난달 시스템이 겪은 모든 실패를 보여 달라고 한다면, 분류하고 맥락과 함께 제시할 수 있습니까?

핵심 요점

  • 결함, 오류, 실패를 구분하고, 내부 오류가 눈에 보이는 실패가 되기 전에 사슬을 끊으십시오.
  • 경계마다 빠른 실패 또는 안전한 실패를 의도적으로 고르고, 모든 함수의 오류 처리 계약을 명시하십시오.
  • 신뢰 경계에서 강하게 검증하고 안에서는 신뢰하십시오. 실패를 가리는 방어는 안전이 아니라 미루기입니다.
  • 멱등성, 타임아웃, 지수 백오프, 지터로 재시도를 안전하게 만들고, 코드에 서킷 브레이커와 우아한 성능 저하를 더하십시오.
  • 맥락과 함께 오류를 감싸고, 관측 가능성에 공급하고, 절대 삼키지 마십시오. 모든 오류는 처리되거나, 전파되거나, 기록되고 드러나야 합니다.

참고 문헌과 더 읽을거리

  • Michael T. Nygard, Release It! Design and Deploy Production-Ready Software
  • Andrew Hunt and David Thomas, The Pragmatic Programmer
  • Steve McConnell, Code Complete: A Practical Handbook of Software Construction
  • Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy (eds.), Site Reliability Engineering: How Google Runs Production Systems
  • Marc Brooker, “Timeouts, Retries, and Backoff with Jitter,” Amazon Builders’ Library
  • Martin Fowler, “CircuitBreaker,” martinfowler.com
  • Nassim Nicholas Taleb, Antifragile: Things That Gain from Disorder