byteforce

CPN 한국어 자습서 · Claude Certified Developer — Foundations Prep

4-4 · 실패 처리와 모델 선택

실패를 견디는 시스템, 그리고 알맞은 모델 고르기

Failure Handling & Model Selection

시제품에서는 잘 돌아가던 코드도, 실제 트래픽을 만나면 개발 중에는 보이지 않던 실패를 마주하는 경우가 많습니다. 요청 한도 초과, 시간 초과, 잘못된 응답 같은 것들이죠. 이 레슨에서는 실패가 일어나는 순간 시스템이 무엇을 하는지부터 살펴보고, 마지막에는 작업에 알맞은 모델을 고르는 기준까지 이어 갑니다.

이 장에서 배우는 것What you'll learn

약 38분
1

실패를 재시도 가능영구로 가르는 질문 하나

2

상태 코드로 실패 종류 판별하기 — 429·529·5xx400·401·403·404

3

SDK가 이미 해 주는 재시도와 retry-after 헤더

4

도구 오류를 Claude에 다시 알려 주는 is_error

5

개발에서는 한 번도 안 났던 실패가 프로덕션에서 터지는 이유

6

모델 계열과 등급 — Opus · Sonnet · Haiku의 트레이드오프

7

기본 모델에 신호로 오버라이드를 두는 라우팅

8

언제 등급을 올리고 내릴지 — 평가(eval)를 기준으로

먼저 짚고 갈 용어
재시도 가능 · 영구 오류 (retriable · terminal)
같은 요청을 잠시 뒤 그대로 다시 보냈을 때 성공할 가망이 있으면 재시도 가능, 몇 번을 보내도 똑같이 실패하면 영구 오류.
지수 백오프 (exponential backoff)
재시도 간격을 회차마다 점점 늘려 가는 방식. 여기에 약간의 무작위 값(지터)을 더해 요청이 한꺼번에 몰리는 것을 막는다.
retry-after 헤더
429·529 응답에 함께 담겨 오는 값. 다시 시도하기 전에 얼마나 기다리면 되는지를 서비스가 직접 알려 준다.
is_error
도구 실행 결과를 Claude에 돌려줄 때 "이 도구는 실패했다"를 표시하는 값.
거절 (refusal)
모델이 안전상의 이유로 요청에 답하지 않기로 한 것. HTTP로는 정상 응답(200)으로 오기 때문에 상태 코드만으로는 걸러지지 않는다.
평가 (eval)
정해 둔 사례로 모델의 품질을 점수로 재보는 절차. 모델을 바꿔도 될지 판단하는 계기가 된다.

재시도할 수 있나, 없나

Is the failure retriable or terminal?

프로덕션(실서비스) 트래픽은 개발할 때는 좀처럼 보이지 않던 실패를 만들어 냅니다. 요청 한도 초과, 시간 초과, 잘못된 도구 결과, 일시적인 연결 끊김 같은 것들이죠. 이런 실패를 만났을 때 가장 먼저 판단할 것은, 잠시 뒤 다시 시도하면 성공할 가망이 있는가입니다.

판단은 질문 하나로 끝납니다. 같은 요청을 잠깐 기다렸다가 그대로 다시 보내면 이번에는 통할까요? 통할 것 같으면 재시도 가능(retriable), 아니면 영구(terminal) 오류입니다. 요청 한도는 시간이 지나면 풀리지만, 형식이 잘못된 요청은 그 요청 자체를 고치기 전까지 몇 번을 보내도 똑같이 실패합니다. 인증 실패도 마찬가지고요.

Anthropic API에서는 상태 코드가 이 갈림길을 알려 줍니다. 429는 요청 한도에 걸린 것, 529는 서비스가 잠시 과부하 상태라는 것으로, 둘 다 재시도 가능합니다. 400은 잘못된 요청, 401은 인증 실패로 둘 다 영구 오류고요. 500이나 504 같은 5xx 서버 오류도 재시도 가능한데, Anthropic 쪽에서 생긴 문제라 다시 시도하면 대개 풀리기 때문입니다.

RETRIABLE / TERMINAL — 상태 코드로 나누는 분류기

python
RETRIABLE = {429, 529, 500, 502, 503, 504}   # 요청 한도·과부하·일시적 오류
TERMINAL  = {400, 401, 403, 404}             # 잘못된 요청·인증·없는 리소스

def is_retriable(status):
    return status in RETRIABLE   # 그 외에는 즉시 실패

이 구분 하나가 이렇게 중요한 이유는, 기다리는 것이 도움이 되는지를 결정하기 때문입니다. 재시도 가능한 오류는 원인이 일시적입니다. 서비스가 잠깐 몰렸거나, 연결이 끊겼거나, 분당 한도를 잠시 넘긴 경우죠. 시간이 지나면 풀리니 조금 뒤에 다시 시도하면 됩니다. 영구 오류는 원인이 요청 자체에 있습니다. 본문이 잘못됐거나, 키가 만료됐거나, 존재하지 않는 모델 이름을 부른 경우입니다. 이런 요청은 몇 번을 보내도 같은 오류가 나기 때문에, 다시 시도하는 것은 재시도 예산만 쓰면서 진짜 문제를 가려 버립니다. 불필요한 재시도 하나하나가 예산을 갉아먹고, 정작 그 예산이 필요했을 다른 실패에 쓸 여유를 줄입니다.

몇몇 상태는 경계에 걸쳐 있어 따로 짚어 둘 만합니다. 시간 초과는 대체로 재시도 가능합니다. 클라이언트가 기다린 시간보다 작업이 조금 더 오래 걸렸을 뿐일 수 있으니까요. 다만 비싼 요청에서 시간 초과가 계속 난다면, 다시 시도하기보다 요청 자체를 손볼 때가 된 것입니다. 서비스가 낸 500은 재시도 가능하지만, 403은 권한 문제라 다시 시도해도 풀리지 않는 영구 오류입니다. 어느 쪽인지 확실하지 않을 때는 영구 오류로 보고 그대로 드러내는 편이 안전합니다. 재시도 가능한 것을 영구로 잘못 분류하면 요란하게 실패해 눈에 띄고 고쳐지지만, 영구적인 것을 재시도 가능으로 잘못 분류하면 서비스를 계속 두드리면서 진짜 문제를 재시도 더미 뒤에 가려 버리기 때문입니다.

쉽게 말하면

재시도 가능한 오류는 조금 기다리면 될 일이고, 영구 오류는 요청 자체를 고쳐야 할 일입니다. 그래서 실패를 만나면 어느 쪽인지부터 가려 놓는 것이 먼저입니다.

SDK가 이미 하는 재시도부터 확인하기

The SDK already retries some failures

재시도 반복문을 직접 짜기 전에, SDK가 이미 해 주는 일부터 확인해 두면 좋습니다. Anthropic 클라이언트 라이브러리는 일시적인 실패를 자동으로 다시 시도합니다. 재시도 간격을 점점 늘려 가며 정해 둔 횟수까지 시도하죠.

이걸 알아 두는 이유는, SDK가 이미 돌리고 있는 재시도 위에 내 재시도를 또 얹지 않기 위해서입니다. 같은 호출을 감싼 재시도 반복문이 두 겹이 되면, 요청 한도에 맞선 시도 횟수가 제한되기는커녕 곱절로 늘어납니다. 그래서 재시도를 어디에 둘지 미리 정해 두는 것이 좋습니다. 일시적인 경우는 SDK에 맡기고 내 코드는 애플리케이션에 맞는 대비책만 담당하거나, 아니면 SDK 재시도를 낮추고 전체 경로를 직접 가져가는 식입니다. 두 층이 서로 모른 채 같은 실패를 각자 재시도하는 상황만 피하면 됩니다.

API는 응답마다 요청 한도 관련 헤더도 함께 돌려줍니다. 한도가 얼마나 남았는지, 언제 다시 채워지는지를 알려 주죠. 이 중 가장 쓸모 있는 것이 retry-after입니다. 429529 응답에 담겨, 다시 시도하기까지 얼마나 기다리면 되는지를 알려 줍니다. 서비스가 정확히 언제 여유가 생기는지 직접 말해 주는 값이라, 백오프로 어림하는 것보다 정확하고요. 그래서 이 헤더가 있으면 그 값을 기준 대기 시간으로 삼고, 없을 때만 직접 계산한 지수 백오프재시도 간격을 회차마다 두 배씩 늘리고 약간의 무작위 값(지터)을 더하는 방식.로 넘어가는 것이 좋습니다. 헤더 이름과 한도 값은 버전마다 다를 수 있으니, 실제 값은 사용 시점의 공식 안내에서 확인하는 편이 안전합니다.

도구 오류는 Claude에게 그대로 돌려준다

Return tool errors to Claude explicitly

코드가 도구를 실행했는데 그 도구가 실패하면, 그 결과를 Claude에 돌려줄 때 is_error 값을 명시적으로 true로 표시해서 보냅니다. 아무것도 없는 빈 결과로 돌려보내지 않는 것이 중요합니다.

오류가 함께 돌아오면 모델은 거기에 반응할 수 있습니다. 다른 방법을 시도하거나, 다시 물어보거나, 멈추는 식으로요. 반대로 도구가 자기 오류를 삼키고 아무것도 돌려주지 않으면, 모델은 그 빈 결과를 정상 데이터로 받아들이고 그 위에서 계속 추론합니다. 그 결과로 자신 있게 틀린 답이 다음 단계로 흘러가게 되죠. 눈에 보이는 실패가, 빠진 데이터 위에 쌓아 올린 그럴듯한 오답보다 훨씬 잡아내기 쉽습니다.

TOOL RESULT — is_error로 실패를 드러내는 형태

python
def run_tool(tool_use):
    try:
        result = execute(tool_use)
        return {"type": "tool_result", "tool_use_id": tool_use.id,
                "content": result}
    except Exception as e:
        # 오류를 그대로 돌려줘 Claude가 반응하게 한다 — 빈 결과를 돌려주지 말 것
        return {"type": "tool_result", "tool_use_id": tool_use.id,
                "is_error": True, "content": f"Tool failed: {e}"}

한 가지 더 짚어 둘 것은 거절(refusal)입니다. 모델이 안전상의 이유로 요청에 답하지 않기로 하면 stop_reasonrefusal로 돌아오는데, 이건 HTTP로는 정상 응답(200)이라 앞의 상태 코드 분류기가 걸러내지 못합니다. 그래서 거절은 따로 확인해서 호출한 쪽으로 올려 보내는 편이 좋습니다.

python
# 거절은 HTTP로는 200이라, 재시도 분류기가 걸러내지 못한다
if response.stop_reason == "refusal":
    raise ValueError("Model refused the request. Review input before retrying.")

실패 종류가 손에 잡히면, 종류마다 어떻게 대응할지 표 하나로 옆에 펼쳐 두고 만들 수 있습니다.

ERROR HANDLING — 오류 종류별 대응표

오류 종류재시도 / 즉시 실패백오프 방식대응
요청 한도 (429)재시도지터를 섞은 지수 백오프, retry-after를 지키고, 시도 횟수에 상한.상한을 넘기면 깔끔한 오류를 내거나, 미리 저장해 둔 결과·더 단순한 경로로 넘긴다.
과부하 (529)재시도백오프. 529는 Anthropic 쪽 부하라 요청 한도 신호와는 다르다.계속되면 대비 경로로 넘어가거나 매끄러운 오류를 돌려준다.
잘못된 요청 (400)즉시 실패재시도 없음. 같은 요청은 또 실패한다.입력을 고치거나 거절하고, 오류를 호출한 쪽에 드러낸다.
도구 결과 오류도구에 따라 다름원인이 일시적일 때만 재시도.오류 표시를 Claude에 돌려줘 모델이 반응하게 한다. 조용히 삼키지 않는다.
거절 (200, stop_reason: refusal)즉시 실패재시도 없음. 일시적 오류가 아니라 모델이 내용을 두고 내린 판단이다.거절을 호출한 쪽에 올리고 기록한다. 조용히 재시도하거나 정상 출력으로 다루지 않는다.

TRADE-OFF — 이 방식이 주는 것과 무는 것

잘 해결하는 것실패 유형을 하나씩 이름으로 처리해, 나쁜 응답 하나가 장애로 번지지 않게 막습니다.
비용·복잡도가 느는 지점모든 실패 경로는 정상 경로 위에 따로 쓰고 시험하고 관리해야 하는 코드입니다.
다른 접근이 나을 때영구 오류는 재시도하지 않습니다. 400을 다시 시도하면 재시도 예산만 버립니다.

개발에서는 한 번도 실패하지 않던 호출

Watch Out: The call that never failed in development

프로덕션에 낼 준비가 다 된 것처럼 보이는 코드도, 개발 중에는 한 번도 실패해 본 적이 없다는 이유만으로 오류 처리 경로가 통째로 빠져 있는 경우가 있습니다.

개발할 때는 이 엔드포인트를 수십 번 호출했고 매번 깔끔하게 응답이 돌아왔습니다. 그러니 오류 경로를 굳이 쓸 이유가 눈에 띄지 않았죠. 여기에 함정이 있습니다. 개발 트래픽은 양이 적고 연결도 안정적이라, 호출이 실패할 만한 조건 — 요청 한도, 시간 초과, 일시적인 연결 끊김, 부하 상황의 잘못된 응답 — 을 거의 만나지 않습니다. 손으로 시험할 때는 그런 조건이 나타나지 않으니, 그것을 처리하는 코드도 쓰이지 않은 채 넘어가는 경우가 많습니다. 그 호출이 처음 실패하는 것은 프로덕션에서이고, 실패는 복구 가능한 오류가 아니라 처리되지 않은 예외의 모습으로 나타납니다.

실제로 있었던 이야기입니다. 한 개발자가 고객이 쓰는 기능을 만들면서 API를 반복문 안에서 호출했습니다. 개발 중에는 트래픽이 요청 한도 근처에도 가지 않아 매번 성공했고, 지금까지 한 번도 실패한 적이 없다는 이유로 오류 처리 없이 코드가 쓰였습니다.

SHIPPED — 오류 경로가 없는, 배포된 버전

python · shipped
results = []                    # 응답을 모은다

for item in batch:
    # 배포된 버전 — 오류 경로가 없다
    resp = client.messages.create(model=MODEL, max_tokens=MAX_TOKENS, messages=msg(item))
    results.append(resp.content)   # 모든 호출이 200을 돌려준다고 가정한다

기능은 배포됐습니다. 첫 트래픽 정점에서 API가 요청 한도 응답을 돌려줬고, 처리되지 않은 오류가 그대로 올라와 — 잠시 기다렸다 다시 시도하는 대신 — 요청 전체가 실패했습니다. 사용자 눈에는 그저 기능이 고장 난 것처럼 보였죠. 개발자가 처음 떠올린 방법은 촘촘한 반복문 안에서 곧바로 재시도를 넣는 것이었습니다. 그런데 이게 상황을 더 나쁘게 만들었습니다. 즉시 재시도가 저마다 같은 한도에 맞선 또 하나의 요청으로 세어져, 한도 초과가 더 심해졌기 때문입니다. 진짜 해법은 앞 화면에서 본 그 구분이었습니다. 요청 한도 응답은 재시도 가능하니, 시도 횟수에 상한을 둔 지수 백오프가 필요했고, 응답에 retry-after 값이 있으면 그 값을 지키는 재시도가 필요했습니다.

주의 · WATCH OUT
  • 재시도 가능한 실패가 오류 경로가 없는 코드를 만났고, 거기에 한도 초과를 더 키우는 즉시 재시도까지 겹쳤습니다.
  • 개발 트래픽은 이 실패를 만들어 내지 못하니, 그것을 처리할 줄 아는 경로도 끝내 쓰이지 않았습니다.
  • 순서를 지키면 됩니다 — 먼저 재시도 가능한 오류로 분류하고, 상한을 둔 백오프로 물러섰다가 다시 시도합니다. 트래픽이 그 빈틈을 먼저 찾아내기 전에요.

스스로 점검 — 깨진 재시도 코드 고치기

Checkpoint · Repair the broken retry path

원문의 점검 과제는 직접 고쳐 쓰는 서술형입니다. 아래 코드에는 결함이 하나 있습니다. 무엇이 문제인지 먼저 짚어 보고, 고친 call_with_retry()를 직접 써 보세요.

BROKEN CODE — 결함이 하나 있는 재시도 함수

python · broken
def call_with_retry(make_call, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return make_call()
        except Exception:
            time.sleep(0)
    raise RetryBudgetExhausted()

이어서 객관식으로도 한 번 짚어 봅니다. 아래에서 하나를 골라 보세요. 맞히면 해설이 나옵니다.

Q1위 코드의 핵심 결함은 무엇이었을까요?

프로덕션에서 모델 고르기

Model selection in production

비용 관리는 하나의 모델 안에서 지출을 줄이는 일이고, 모델 선택은 그 최적화가 출발하는 기준선 자체를 정하는 일입니다. 앞 화면들이 모델을 고른 뒤 예산 안에 머무는 법을 다뤘다면, 이 화면은 그 예산을 처음 정하는 선택 — 어떤 Claude 모델로 작업을 실행할지 — 을 다룹니다.

Claude는 비용·지연·성능을 서로 맞바꾸는 모델들의 계열입니다. Fable은 가장 까다로운 추론·코딩·에이전트 작업까지 감당하는, 계열에서 가장 뛰어난 모델입니다. Opus는 Sonnet의 범위를 넘어서는 까다로운 작업을 맡고, Sonnet은 대부분의 프로덕션 작업을 위한 균형 잡힌 기본값입니다. Haiku는 그 범위에 맞는 작업에서 속도와 비용 효율에 맞춰 만들어졌고요. 같은 프롬프트가 이 중 어느 모델에서도 실행되기 때문에, 모델 선택은 작업마다 바꿀 수 있는 손잡이입니다. 애플리케이션을 다시 쓰지 않고도 바꿀 수 있죠. 현재 제공되는 모델과 정확한 이름은 사용 시점의 공식 안내에서 확인하는 것이 좋습니다.

MODEL FAMILY — 등급별 성격

Fable가장 까다로운 추론·코딩·에이전트 작업에 쓰는, 계열에서 가장 뛰어난 등급.
OpusSonnet의 범위를 넘어서는 까다로운 작업을 맡는 상위 등급.
Sonnet대부분의 프로덕션 작업을 위한 균형 잡힌 기본값.
Haiku그 범위에 맞는 작업에서 속도와 비용 효율에 맞춘 등급.

모델 등급을 올리면 성능을 얻는 대신 토큰당 비용이 오르고 대개 지연도 늘어납니다. 등급을 내리면 속도와 낮은 비용을 얻는 대신 성능이 떨어질 위험을 안고요. 다만 상위 모델이 더 적은 토큰으로 결론에 도달하면, 오히려 더 빠르고 싸게 처리하기도 합니다. 여기에 실수의 비용도 함께 계산에 넣습니다. 하위 모델로 하루 몇 달러를 아끼더라도, 그 성능 저하가 뒤에서 큰 비용을 부르는 오류를 만든다면 남는 장사가 아닙니다. 어디에나 맞는 정답은 없고, 특정 작업을 특정 품질 기준에서 봤을 때의 정답만 있습니다.

그래서 필요한 태도는, 습관적으로 가장 센 모델에 손이 가지 않도록 트레이드오프를 잴 수 있게 만드는 것입니다. 그렇게 습관적으로 고르는 것이 프로덕션에서 가장 흔하고 가장 비싼 모델 선택 실수인 경우가 많습니다. 기본 순서는 이렇습니다. Sonnet에서 시작하고, 평가에서 Sonnet이 품질 기준을 놓치는 것이 확인될 때만 Opus로 올리며, 평가에서 성능 저하가 그 작업에 받아들일 만하다고 확인될 때만 Haiku로 내립니다.

시스템이 모든 요청에 한 모델만 써야 하는 것은 아닙니다. 프로덕션에서 흔한 방식은 기본 모델에 오버라이드를 두는 것입니다. 트래픽 대부분은 균형 잡힌 기본 모델로 보내고, 특정한 요청 종류만 요청에서 읽어 낸 값싼 신호 — 작업 종류, 입력 길이, 난이도 분류 같은 것 — 에 따라 더 크거나 작은 모델로 보냅니다. 검색에서 쓰던 라우팅 아이디어를 모델 선택에 그대로 적용한 것으로, 더 센 모델의 값은 그것이 필요한 요청에만 치릅니다. 모든 요청이 같은 모양이라면 라우터는 건너뛰고 모델 하나를 고정하면 됩니다.

등급을 올릴 때는, 지금 모델이 트래픽 안의 가장 어려운 사례에서 실패하는 것을 평가가 보여 주고 오답의 비용이 클 때입니다. 등급을 내릴 때는, 더 싼 모델이 트래픽 대부분에서 품질 기준을 지키는 것을 평가가 보여 줄 때고요. 예산과 지연에 여유가 생기죠. 어느 방향이든 평가가 계기 역할을 합니다. 모델 변경은 내 사례에 대해 측정한 점수를 근거로 올리는 것이라, 앞에서 만들어 둔 평가가 곧 모델 결정의 관문이 됩니다.

핵심

가장 센 모델을 습관처럼 고르는 것이 가장 흔하고 비싼 실수입니다. Sonnet에서 시작해, 평가 결과가 요구할 때만 위아래로 옮기는 것을 기본으로 삼으면 됩니다.

TRADE-OFF — 라우팅이 주는 것과 무는 것

잘 해결하는 것각 작업을 품질 기준을 넘는 가장 싼 모델에 맞춥니다 — 짐작이 아니라 평가로 측정해서.
비용·복잡도가 느는 지점라우팅은 분류 단계 하나와 두 번째 모델 경로를 유지·관리해야 하는 부담을 더합니다.
다른 접근이 나을 때트래픽이 하나의 품질 기준으로 균일하다면, 모델 하나를 고정하고 라우터는 건너뜁니다.

스스로 점검 — 모델과 결정 제약 고르기

Checkpoint · Choose the model and the deciding constraint

이번엔 세 가지 상황에서 모델 등급을 고르고, 결정을 이끄는 제약 하나를 짚어 보는 점검입니다. 정답을 먼저 떠올려 본 뒤 골라 보세요. 맞히면 설명이 이어집니다.

Q1대량 분류 단계가 하루에 짧은 메시지 수백만 건에 라벨을 답니다. 평가에서는 Haiku가 품질 기준을 지키는 것으로 나왔습니다. 가장 나은 선택은?

Q2여러 단계짜리 에이전트가, 앞 단계가 틀리면 비용이 큰 의존적 리팩터링을 계획합니다. 평가에서는 Sonnet이 가장 어려운 사례에서 기준을 놓치는 것으로 나왔습니다. 가장 나은 선택은?

Q3트래픽이 섞여 있습니다. 대부분은 단순한 조회이고, 일부는 복잡한 종합입니다. 가장 나은 접근은?

기억할 점
MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 38개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.