분류기에 unknown 레인 없으면 오분류가 조용히 통과한다
API 요청 타입 분류에서 매칭 실패를 else로 흘려보내면 장애가 로그 없이 사라져요. unknown 레인을 명시적으로 두는 백엔드 설계를 정리했어요.
백엔드에서 들어온 요청을 종류별로 나누는 코드, 다들 한 번씩 쓰죠. 결제 webhook 이벤트 타입을 보고 분기하거나, 업로드된 파일 확장자로 처리기를 고르거나, LLM이 뽑아준 라벨로 큐를 나누거나요. 저도 몇 번 만들어 봤는데 처음엔 항상 이렇게 생겼어요.
if (type === 'payment') return handlePayment(req)
if (type === 'refund') return handleRefund(req)
return handleDefault(req) // 여기가 함정마지막 줄이 문제예요. handleDefault는 "나머지 전부"를 받는데, 그 나머지 안에는 우리가 아직 모르는 타입이 섞여 있어요. 새로 생긴 이벤트, 오타 난 값, 상대 서버가 스펙 바꾼 것까지 전부 조용히 삼켜요. 200을 돌려주니 상대는 성공했다고 믿고, 우리 로그엔 아무것도 안 남죠.
조용한 실패가 위험한 이유
에러는 터지면 고칠 수 있어요. 무서운 건 안 터지는 쪽이에요. 직접 파이프라인 하나를 두 주 돌려 보니, 실패율 0%로 찍히던 구간에서 처리가 안 된 레코드가 뒤늦게 나왔어요. status=success는 "함수가 예외 없이 끝났다"는 뜻이지 "일이 됐다"는 뜻이 아니거든요.
else로 흘려보내기와 unknown 레인으로 격리하기의 차이
unknown을 값으로 만들기
고치는 방법은 단순해요. 매칭 실패를 기본값이 아니라 하나의 명시적인 결과로 올려요. 타입부터 바꿔요.
type Classified =
| { kind: 'payment'; data: Payment }
| { kind: 'refund'; data: Refund }
| { kind: 'unknown'; raw: unknown; reason: string }unknown이 유니온에 들어가면 컴파일러가 도와줘요. switch에서 이 갈래를 안 다루면 타입 에러가 나요. "나중에 처리하지 뭐"가 구조적으로 불가능해져요.
호출부는 이렇게 갈라요.
const c = classify(req.body)
if (c.kind === 'unknown') {
await db.insert('unclassified_events', {
payload: c.raw, reason: c.reason, received_at: new Date()
})
return res.status(202).json({ status: 'pending_review' })
}핵심은 세 가지예요. 원문을 통째로 저장하고, 왜 매칭 실패했는지 이유를 같이 남기고, 응답 코드를 200이 아니라 202로 돌려요. 202는 "받았지만 처리는 아직"이라는 뜻이라 상대 시스템에도 정직해요.
격리 테이블이 알림이 되게
테이블만 만들어 두면 아무도 안 봐요. 저는 이 테이블을 지표로 연결해요.
- 최근 24시간 unknown 건수가 0이 아니면 알림
- 같은
reason이 3건 이상 쌓이면 스펙 변경 의심 - 7일 이상 검토 안 된 행이 있으면 별도 표시
쿼리 한 줄이면 돼요.
SELECT reason, count(*) AS n
FROM unclassified_events
WHERE received_at > now() - interval '24 hours'
GROUP BY reason ORDER BY n DESC;실제로 이렇게 붙여 놓고 돌려 보니, 처음 한 주 동안 4건이 걸렸어요. 전부 우리가 예상 못 한 이벤트 타입이었고, 예전 구조였다면 그냥 사라졌을 값이에요. 규칙을 추가하는 데는 10분도 안 걸렸어요. 찾는 게 어려웠지 고치는 건 쉬웠어요.
어디까지 적용할까
모든 분기에 다 넣을 필요는 없어요. 제 기준은 이래요. 입력이 외부에서 오고, 우리가 스펙을 못 바꾸는 곳에만 unknown 레인을 둬요. 결제 webhook, 파일 업로드, 외부 API 응답, LLM 출력이 여기 해당해요. 반대로 우리 프론트가 보내는 내부 enum처럼 양쪽을 다 통제하는 값은 그냥 400으로 거절해도 충분해요.
플랜엘(plan-l)에서 법령 검색 결과를 조문 유형별로 나눌 때도 같은 구조를 썼어요. 국가법령정보센터 데이터는 우리가 형식을 정할 수 없으니, 예상 밖 형태는 억지로 분류하지 않고 따로 빼 둬요. 월 3건 무료로 쓰는 사용자에게 엉뚱한 결과를 자신 있게 보여주는 것보다, 모르면 모른다고 하는 쪽이 낫거든요.
정리하면 한 문장이에요. 매칭 안 된 입력을 조용히 버리지 말고, 이름 붙여서 눈에 보이는 곳에 쌓아 두세요. else는 편하지만, 편한 만큼 나중에 비쌉니다.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.