무결성을 지키려던 검증 규칙이 오히려 서비스를 멈춰 세웠다
10 views
# 검증의 역설: 무결성을 지키는 규칙이 서비스 중단을 부를 때
한 LLM 에이전트 시스템에서 도구 호출 한 건이 `BadRequestError: Error code: 400`으로 끝났다. 거기까지 가는 동안 시스템은 같은 요청을 열한 번 반복했다. 인자 스키마가 맞지 않는다는 것은 첫 호출에서 이미 드러난 사실이었는데도 그랬다.
엄격함이 늘 미덕인 것은 아니다. 어떤 클라이언트를 상대하느냐에 따라 같은 규칙이 방어막이 되기도, 단절의 원인이 되기도 한다. 이 장애는 API 경계에서 부딪치는 두 설계 철학, 엄격한 검증(Strict Validation)과 결함 허용 읽기(Tolerant Reader)의 긴장을 그대로 보여준다. 무결성을 지키려고 만든 규칙이 어쩌다 서비스를 멈추는가. 그 원인이 검증의 강도에 있는지, 아니면 검증 다음을 설계하지 않은 공백에 있는지 따져볼 만하다.
## 검증이라는 수문장
표층은 단순하다. 컨트롤러 레이어의 검증이 인자 스키마 불일치를 잡아내 요청을 막았고, 이상 데이터는 비즈니스 로직까지 가지 못했다. 검증은 설계대로 작동했다.
문제는 그 차단이 부른 연쇄다. 구문 불일치 한 건이 트랜잭션 전체의 실패로 번졌고, 출력이 일정하지 않은 클라이언트는 같은 오류를 계속 만들어냈다. 무결성을 지키는 장치가 동시에 흐름을 끊는 장치가 된 셈이다. 이 어긋남을 보려면 두 패턴을 따로 떼어 봐야 한다.
### 엄격한 검증의 논리
엄격한 스펙 준수는 대가와 보상이 분명한 거래다. 서버는 들어오는 데이터가 계약을 충족한다고 전제하므로 내부 예외 처리 부담이 줄고 데이터 정합성이 일정하게 유지된다. 악의적 입력은 경계에서 걸러진다. 금융이나 인증처럼 실수의 비용이 큰 도메인에서 이 패턴이 기본값이 된 이유다.
대신 클라이언트와 서버가 강하게 묶인다. 결합도가 올라간다는 뜻이다. 클라이언트가 필드명 하나를 빠뜨리거나 타입을 잘못 직렬화하면 그 작은 어긋남이 곧장 장애로 이어진다. 계약이 엄격할수록 계약을 어겼을 때 치르는 값도 커진다.
### 결함 허용 읽기의 논리
Tolerant Reader는 다른 전제에서 출발한다. 클라이언트는 바뀌고, 버전은 갈라지며, 출력은 일정하지 않다는 전제다. 서버는 이해할 수 있는 부분만 받아들이고 나머지는 흘려보내며, 복원할 수 있는 것은 복원한다. 이 유연함이 다양한 클라이언트를 받아내는 완충재 역할을 한다.
대가는 서버 쪽 복잡성이다. 관대함은 공짜가 아니다. 오염된 데이터가 핵심 로직까지 침투하지 못하도록 하위 레이어에 검증 비용이 더 붙고, 무엇을 받아들이고 무엇을 거부할지의 경계를 설계자가 직접 그어야 한다.
두 패턴에 우열은 없다. 보안과 무결성을 끌어올리면 결합도가 오르고, 유연성을 끌어올리면 복잡성이 오른다. 관건은 둘 중 하나를 고르는 일이 아니라 균형점을 어디에 두느냐다. 그 위치는 클라이언트의 성격이 정한다. 출력이 일정한 클라이언트에는 엄격함이, 그렇지 않은 클라이언트에는 관용이 어울린다.
## 재시도가 자원을 태우는 자리
열한 번의 반복은 검증 패턴 자체의 문제가 아니다. 검증 다음을 설계하지 않은 복구 아키텍처의 문제다.
재시도는 일시적 장애를 전제로 한 장치다. 네트워크 단절, 서버 과부하, 타임아웃 같은 5xx 계열의 일시적 실패는 시간이 지나면 풀릴 여지가 있으니 재시도가 합리적이다. 그러나 400 BadRequest는 4xx 계열의 영속적 실패다. 요청 자체가 계약을 어겼으므로 같은 요청은 천 번을 보내도 같은 결과로 끝난다.
이 시스템은 정적인 문법 오류를 일시적 오류로 잘못 판단했다. 에러 코드에 따라 경로를 나누는 처리가 없는 채로, 모든 실패를 재시도 큐에 밀어 넣었다. 열한 번의 호출은 처음부터 성공할 수 없는 시도였고, 그만큼 자원이 그냥 타들어갔다.
이런 무분별한 재시도가 여러 클라이언트에서 한꺼번에 터지면 분산 시스템 전반을 위협하는 천둥 무리 문제(Thundering Herd)로 커진다. 회복하려던 서버가 재시도 폭주에 눌려 회복이 더 늦어지는 식이다.
해법은 분명하다. 재시도 앞에 에러를 분류하는 단계를 둔다. 4xx는 즉시 멈추고 피드백 경로로 보내며, 5xx만 지수 백오프(Exponential Backoff)와 함께 다시 시도한다. 여기에 서킷 브레이커(Circuit Breaker)를 입출력 단계에 두면, 반복 실패가 임계치를 넘는 순간 회로를 끊어 연쇄 붕괴를 막을 수 있다. 재시도는 무조건 반복하는 동작이 아니라 실패의 성격을 읽고 경로를 가르는 판단이어야 한다.
## 출력이 일정하지 않은 클라이언트
LLM 에이전트는 전통적 클라이언트와 성격이 다르다. 같은 입력에도 출력이 흔들린다. 이 특성이 검증 패턴의 트레이드오프를 다시 쓰게 만든다.
출력이 일정한 클라이언트라면 엄격한 검증이 대체로 옳다. 클라이언트의 오류는 곧 코드의 버그이므로 경계에서 막아 일찍 드러내는 편이 낫다. 그러나 LLM 클라이언트의 사소한 구문 오류는 버그가 아니라 본래 성질에 가깝다. 따옴표 하나, 타입 하나의 어긋남은 없앨 수 있는 결함이 아니라 일정 확률로 반복되는 잡음이다. 이를 매번 장애로 처리하는 시스템은 오래 버티지 못한다.
### 자가 치유 어댑터
첫 번째 길은 검증 에러를 차단의 끝점이 아니라 피드백의 출발점으로 바꾸는 것이다. 검증이 실패하면 그 사유를 구조화된 메시지로 클라이언트에 돌려준다. 어떤 필드가 어떤 제약을 어떻게 위반했는지 알려주면, 클라이언트는 그 신호를 받아 스스로 데이터를 고쳐 다시 보낸다.
이 자가 치유 루프는 엄격한 검증의 무결성을 버리지 않으면서도 클라이언트에 회복 경로를 준다. 다만 보정 시도 횟수에 상한을 두지 않으면, 이 루프 역시 열한 번 재시도와 똑같은 함정으로 돌아간다.
### 중간 변환 계층
두 번째 길은 핵심 로직과 외부 입력 사이에 가벼운 완충 레이어를 두는 것이다. 빠진 따옴표를 채우고, 의도가 분명한 타입을 형변환하며, 사소한 구문 오류를 정규화하는 스키마 적응 계층(Schema Adaptation Layer)이다. 핵심 비즈니스 로직은 여전히 엄격한 계약 위에서 돌되, 그 앞단에서 복원 가능한 잡음을 흡수한다.
Tolerant Reader 철학을 시스템 전체가 아니라 한 곳에 가둔 적용이다. 시스템 전부를 관대하게 만드는 대신, 관용의 책임을 단일 레이어로 격리한다. 무결성은 안쪽에서 지키고 유연성은 바깥쪽에서 제공하는 분업이다.
## 남는 질문
열한 번의 재시도가 남긴 것은 어느 패턴이 더 낫느냐에 대한 답이 아니라, 그 질문이 처음부터 잘못 놓였다는 사실이다.
엄격한 검증은 출력이 일정한 환경에서 무결성을 지키고, 결함 허용 읽기는 그렇지 않은 환경에서 회복력을 준다. 문법 오류를 일시적 장애로 오판한 재시도는 검증이 강해서가 아니라 에러를 분별하는 단계가 없어서 생긴 일이었다. LLM 클라이언트를 상대하는 지금 필요한 것은 더 엄격한 규칙도, 더 관대한 규칙도 아니다. 규칙과 적응을 각각 어디에 둘지에 대한 판단이다.
그 판단은 한 번 내리고 끝나지 않는다. 마주한 클라이언트의 성격, 실패의 종류, 회복의 비용은 계속 바뀌고, 균형점도 그에 맞춰 다시 잡아야 한다. 결국 설계의 수명은 정답 하나를 고수하는 데서가 아니라 좌표가 움직일 때마다 균형추를 옮기는 데서 나온다.