검증 우회
개요
정의된 검증 규칙이 있음에도 불구하고 이를 생략하거나 우회하고 결과만 맞추려는 문제는 시스템의 불변조건을 무너뜨리며, 이는 하네스를 통해 차단되어야 한다.
이 문서의 질문
이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 fallback & backdoor에 가장 가깝다.
핵심 질문은 다음과 같다.
입력을 통과시키는 것이 아니라 유효한 입력만 허용하는 경로가 유지되는가
문제
실행 흐름도 존재하고, 상태 변경도 Store를 통한다.
하지만 실제 코드에서는 다음과 같은 우회가 발생한다.
if (string.IsNullOrWhiteSpace(command.DocId))
{
return;
}또는
var docId = command.DocId?.Trim() ?? "temp";또는
try
{
await _repository.SaveAsync(doc);
}
catch
{
}또는
if (!validator.Validate(command))
{
return Result.Success();
}겉으로 보면 코드가 더 유연해 보일 수도 있다.
실패를 줄이는 것처럼 보이기도 한다.
하지만 이 코드는 공통적으로 같은 문제를 가진다.
입력은 허용되었지만
검증 규칙은 존중되지 않았다
문제의 본질
이 문제는 단순한 validation 누락이 아니다.
핵심은 다음이다.
시스템은 입력을 처리하는 것이 아니라
유효한 입력만 통과시켜야 한다
검증의 목적 왜곡
검증은 에러를 만들기 위한 장치가 아니다.
허용 가능한 입력의 범위를 정의하고
내부 시스템이 기대하는 형태를 보장하며
이후 단계가 안전하게 동작하도록 만든다
하지만 사람과 AI는 자주 검증을 다음처럼 오해한다.
귀찮은 전처리
실패를 유발하는 장애물
가능하면 줄여야 할 코드
그 결과 검증은 “통과시키기 위한 형식 절차"로 전락한다.
결과 중심 우회
사람과 AI는 자주 다음 선택을 한다.
invalid input을 조용히 보정
실패를 숨기고 성공처럼 반환
예외를 삼켜서 흐름 유지
검증 실패를 warning 수준으로 축소
이 방식은 겉으로는 부드럽다.
하지만 실제로는 시스템의 핵심 규칙을 흐린다.
즉 문제는 에러가 아니라
잘못된 성공이다.
불변조건 붕괴
검증이 무력화되면 시스템 내부에서 다음 전제가 깨진다.
DocId는 비어 있지 않다경로는 workspace 내부다
상태 전이는 허용된 순서만 가진다
저장 요청은 유효한 문서를 대상으로 한다
이 전제가 깨지면 이후 단계는 불안정해진다.
그리고 오류는 검증 지점이 아니라 훨씬 뒤에서 터진다.
AI의 선택 패턴
사람과 AI는 다음 상황에서 검증 우회를 선택한다.
빠르게 기능을 동작시켜야 할 때
validation이 복잡해 보일 때
실패 처리를 설계하기 어렵다고 느낄 때
테스트를 먼저 통과시키려 할 때
즉 사람과 AI는 다음 질문으로 코드를 만든다.
“에러 없이 계속 진행하려면?”
하지만 하네스 관점의 질문은 다르다.
“어떤 입력은 반드시 여기서 멈춰야 하지?”
목표 구조
핵심은 다음이다.
검증은 선택적 보조 단계가 아니라
실행 가능 여부를 결정하는 관문이어야 한다
사례 규칙 요약
외부 입력은 지정된 Validator를 반드시 통과해야 함
validation 실패를 성공으로 변환하는 코드 금지
예외 삼키기 금지
기본값 삽입으로 invalid input을 숨기는 처리 금지
validate 없이 repository, engine, bridge 호출 금지
자세한 rule/validator 설계 원칙은 규칙 설계와 검증기 설계를 따른다. 이 케이스에서는 위 금지 표면과 합법 경로가 바로 검사 대상이다.
하네스 적용 위치
이 케이스의 하네스는 validator contract, entrypoint, failure representation 세 지점에 놓인다.
계약 레벨
검증은 helper가 아니라 별도 validator나 valid type factory로 분리한다.
진입점 레벨
UseCase나 Handler 초입에서 validate가 실행되지 않으면 다음 단계로 갈 수 없어야 한다.
결과 레벨
validation failure는 success로 포장하지 않고 failure와 result, type level에서 그대로 남겨야 한다.
실제 구현
validator를 독립 계약으로 분리한다
검증이 메서드 안의 보조 코드로 남아 있으면 가장 먼저 빠진다. 그래서 validator는 별도 계약과 명확한 실패 의미를 가진 구성 요소가 되어야 한다.
public interface IOpenDocValidator
{
void Validate(OpenDocCommand command);
}검증은 진입점 첫 단계여야 한다
public sealed class OpenDocUseCase : IOpenDocUseCase
{
public async Task<OpenDocResult> ExecuteAsync(OpenDocCommand command, CancellationToken cancellationToken = default)
{
_validator.Validate(command);
var doc = await _repository.LoadAsync(command.DocId, cancellationToken);
return new OpenDocResult(doc.Id, doc.Content);
}
}중요한 것은 validator의 존재가 아니라, 합법 경로가 validator를 지나지 않으면 다음 단계로 갈 수 없게 만드는 것이다.
normalize는 허용되지만 fallback은 검증을 대체할 수 없다
_validator.Validate(command);
var normalizedDocId = command.DocId.Trim();허용되는 것은 검증 이후의 정규화다. 반대로 null이면 기본값을 넣거나, 이상한 값을 조용히 보정하는 코드는 validation bypass다.
실패를 success로 감싸지 않는다
검증 우회는 종종 에러를 없애는 코드가 아니라 잘못된 성공을 만드는 코드로 나타난다. ValidationException을 잡고 기본 결과를 success로 돌려주는 순간 시스템은 invalid input을 정상 흐름으로 학습한다.
타입 경계로 한 번 더 잠글 수 있다
public sealed record ValidDocId(string Value);
public interface IDocRepository
{
Task<Document> LoadAsync(ValidDocId docId, CancellationToken cancellationToken = default);
}repository가 검증 완료 타입만 받도록 만들면 validator는 관례가 아니라 다음 단계 진입 조건이 된다.
다른 언어에서도 기준은 같다
Rust에서도 검증은 가장 앞단에서 실패를 즉시 노출해야 한다. 핵심은 언어가 아니라 validator를 지나지 않으면 흐름이 진전되지 않는 구조다.
위반 예시와 실패 결과
다음 코드는 반드시 실패해야 한다.
var docId = command.DocId?.Trim() ?? "temp";
try
{
await _repository.SaveAsync(doc);
}
catch
{
return Result.Success();
}실패 결과:
error ATL401: Execution flow must validate input before continuing.
error ATL402: Empty catch block hides validation or execution failure.검증 우회의 본질은 에러가 아니라 잘못된 성공을 만드는 데 있다.
보조 강제 수단
이 케이스는 validator 호출, failure 은닉 금지, valid type contract를 함께 둬야 한다. 하나만 두면 fallback 값, empty catch, success wrapping이 다시 우회로가 된다.
진입점에서 validator 누락을 잡는다
ExecuteAsync 같은 진입점 안에 Validate(...) 호출이 없는 패턴을 잡는 규칙이 가장 먼저 필요하다. 이 규칙의 목적은 정교한 순서 추론이 아니라 “모든 합법 경로에는 검증이 있다”는 사실을 고정하는 데 있다.
실패를 숨기는 catch와 success wrapping을 금지한다
빈 catch, ValidationException 이후 Result.Success(...), fallback 결과 반환 같은 패턴은 모두 검증 실패를 성공처럼 위장한다. 그래서 failure 은닉은 별도 규칙으로 끊어야 한다.
타입 계약으로 검증 순서를 강화한다
분석기만으로 불안하면 repository와 downstream API가 ValidDocId 같은 타입만 받게 만들면 된다. 그러면 validator는 문서가 아니라 호출 전제 조건이 된다.
빌드 실패로 고정한다
[*.cs]
dotnet_diagnostic.ATL401.severity = error
dotnet_diagnostic.ATL402.severity = error이제 validation bypass는 스타일 문제가 아니라 즉시 막히는 구조 위반이 된다.
결과
하네스 적용 이후 변화는 명확하다.
검증의 위치 복원
검증은 더 이상 부수 코드가 아니라
실행의 첫 관문이 된다.
실패의 의미 회복
실패를 숨기지 않고 명확하게 드러낸다.
그 결과 시스템의 불변조건이 유지된다.
AI 행동 안정화
이제 사람과 AI는 더 이상 다음을 쉽게 선택할 수 없다.
fallback으로 invalid input 숨기기
catch로 실패 삼키기
validation 없이 실행 계속하기
실패를 success처럼 포장하기
대신:
먼저 validator를 통과시키고
실패를 실패로 다루는 방향으로 코드를 생성하게 된다
실무 적용 팁
- normalize와 fallback을 validation보다 먼저 두지 않는 편이 안전하다.
- validation failure를 success payload로 감싸는 Result 패턴은 초기에 금지하는 편이 낫다.
- repository가 validated type만 받게 만들면 validator 누락을 훨씬 강하게 줄일 수 있다.
요약
validation 우회 문제는 하네스를 통해서만 안정적으로 제어된다.
validator를 독립 계약으로 분리
UseCase 초입에서 검증 고정
fallback과 예외 삼키기 금지
Roslyn Analyzer로 누락과 실패 은닉 차단
타입 설계로 validation 이후만 다음 단계 진입 허용
빌드 에러로 강제
이 구조를 통해
시스템은 더 이상 “어떻게든 통과하는 입력"을 받지 않고,
정의된 규칙을 통과한 입력만 처리하게 된다.