디버그 백도어

디버그 백도어

개요

디버그, 테스트, 임시 대응을 이유로 추가된 우회 경로는 처음에는 편의처럼 보이지만 결국 시스템에 영구적인 뒷문을 남기며, 이는 하네스를 통해 차단되어야 한다.

이 문서의 질문

이 문서는 하네스 개요에서 정리한 다섯 공통 패턴 중 fallback & backdoor에 가장 가깝다. 핵심 질문은 다음과 같다.

디버그와 예외용 경로가 사실상 두 번째 공식 경로로 굳고 있지 않은가

문제

구조도 있고, 상태 흐름도 있고, validation도 존재한다.

그런데 실제 개발 과정에서는 자주 다음과 같은 코드가 생긴다.

if (DebugMode)
{
    return Result.Success();
}

또는

if (_settings.SkipValidation)
{
    await _repository.SaveAsync(doc);
    return;
}

또는

#if DEBUG
    _docStore.Replace(fakeState);
#endif

또는

if (Environment.UserName == "dev")
{
    return await _engine.RunInternalAsync(raw);
}

또는

public async Task ForceOpenAsync(string path)
{
    var content = File.ReadAllText(path);
    CurrentContent = content;
}

이 코드는 대개 다음 같은 명분으로 들어온다.

  • 잠깐 테스트하려고

  • 디버깅 편의를 위해

  • 급하게 막기 위해

  • 개발 환경에서만 쓰려고

  • 나중에 지우려고

하지만 이 코드들은 공통적으로 같은 문제를 가진다.

정식 경로가 아닌
예외 경로가 시스템 안에 남는다

문제의 본질

이 문제는 단순한 임시 코드 문제가 아니다.
핵심은 다음이다.

시스템은 규칙으로 유지되지 않고
예외를 얼마나 허용하느냐에 의해 무너진다

임시 코드의 영구화

가장 흔한 패턴은 다음이다.

테스트용 추가 → 급한 상황에서 재사용 → 다른 코드가 의존 → 제거 불가

처음엔 일회성이다.
하지만 한 번 쓰이기 시작하면 점점 구조 일부처럼 굳어버린다.

이 순간부터 문제는 “임시 코드가 남았다"가 아니라
정식 구조 밖의 실행 경로가 생겼다는 데 있다.

규칙의 선택적 적용

정상 구조에서는 모든 요청이 같은 규칙을 통과해야 한다.

  • validation

  • execution flow

  • state transition

  • event publish

  • logging

  • permission

하지만 뒷문이 생기면 어떤 요청은 이 단계를 건너뛴다.

즉 시스템은 더 이상 하나의 규칙으로 동작하지 않는다.

  • 일반 경로

  • 우회 경로

두 개가 동시에 존재한다.

이 상태는 구조보다 더 위험하다.
겉보기에는 같은 기능처럼 보여도 실제 동작 규칙이 다르기 때문이다.

테스트 코드와 프로덕션 경계 붕괴

AI는 테스트 편의를 위해 구조를 많이 무시한다.

  • private API 노출

  • internal state 직접 변경

  • validation skip flag 추가

  • force method 추가

  • fake object를 실제 흐름에 삽입

이 자체는 테스트 목적일 수 있다.
문제는 그 경계가 코드 안에서 명확히 잠기지 않으면,
테스트용 우회가 그대로 실제 코드 경로가 된다는 점이다.

AI의 선택 패턴

사람과 AI는 다음 상황에서 뒷문을 아주 쉽게 만든다.

  • 기존 흐름이 길고 복잡할 때

  • 지금 당장 결과를 확인해야 할 때

  • 테스트를 빨리 통과시켜야 할 때

  • “잠깐만” 우회하면 될 것처럼 보일 때

즉 사람과 AI는 다음 질문으로 코드를 만든다.

“이 과정을 잠깐 건너뛸 방법은?”

하지만 하네스 관점의 질문은 다르다.

“이 우회 경로가 시스템에 남아도 되는가?”

목표 구조

핵심은 다음이다.

디버그와 테스트는 허용할 수 있어도
정식 구조를 우회하는 경로는 남아 있으면 안 된다

사례 규칙 요약

  • production 코드에서 skip flag 금지

  • debug 전용 direct path 금지

  • force, bypass, raw, internal-run 같은 우회 메서드 금지

  • 테스트 편의 API는 테스트 프로젝트 내부에서만 허용

  • #if DEBUG를 이용한 구조 우회 금지

  • 환경 변수, 사용자 이름, 로컬 조건으로 validation/authorization/flow를 생략하는 코드 금지

자세한 rule/validator 설계 원칙은 규칙 설계검증기 설계를 따른다. 이 케이스에서는 위 금지 표면과 합법 경로가 바로 검사 대상이다.

하네스 적용 위치

이 케이스의 하네스는 production code, debug branch, test assembly 세 곳에 동시에 걸린다.

production 코드 레벨

Skip*, Force*, Raw* 같은 우회 API가 public surface에 남지 못하게 해야 한다.

조립 레벨

테스트 편의는 fake, stub, builder로 해결하고 실행 절차 자체는 production과 같은 진입점을 통과하게 해야 한다.

검증 레벨

우회성 이름, #if DEBUG 분기, skip flag 조건을 정적 분석과 프로젝트 분리로 함께 막는다.

실제 구현

테스트와 디버그 편의를 production 흐름 밖으로 밀어낸다

이 패턴의 핵심은 간단하다. 테스트와 디버깅이 필요하다는 사실은 맞지만, 그 편의를 이유로 production 코드 안에 두 번째 실행 경로를 남기면 안 된다.

public sealed class OpenDocUseCase
{
    public async Task<OpenDocResult> ExecuteAsync(OpenDocCommand command)
    {
        _validator.Validate(command);
        var doc = await _repository.LoadAsync(command.DocId);
        return new OpenDocResult(doc.Id, doc.Content);
    }
}

반대로 SkipValidation, ForceOpenAsync, UnsafeLoad, #if DEBUG 분기 같은 경로가 들어오면 정식 경로와 우회 경로가 동시에 공개된다.

디버그는 실행 규칙이 아니라 관찰 장치로 지원한다

디버깅 때문에 필요한 것은 별도 실행 루트가 아니라 다음 같은 관찰 수단이다.

  • trace panel
  • state diff inspector
  • command log viewer
  • validation failure detail viewer

즉 디버깅은 같은 규칙을 더 잘 보게 만들어야지, 다른 규칙을 실행하게 만들면 안 된다.

테스트는 fake 조립으로 해결하고 진입점은 유지한다

public sealed class FakeDocRepository : IDocRepository
{
    private readonly Dictionary<string, Document> _docs = [];

    public void Seed(Document doc) => _docs[doc.Id] = doc;

    public Task<Document> LoadAsync(string docId, CancellationToken cancellationToken = default)
        => Task.FromResult(_docs[docId]);
}

허용되는 우회는 조립 계층의 대체 구현뿐이다. use case 진입점, validation 순서, 상태 전이 자체는 그대로 유지되어야 한다.

다른 언어에서도 기준은 같다

Rust에서도 cfg(debug_assertions)나 feature flag로 정식 실행을 건너뛰기보다, fake repository와 inspector 도구로 편의를 제공하는 편이 맞다. 핵심은 언어가 아니라 “production에서 공식 경로는 하나뿐이어야 한다”는 점이다.

위반 예시와 실패 결과

다음 코드는 반드시 실패해야 한다.

public bool SkipValidation { get; set; }

public async Task<OpenDocResult> ExecuteAsync(OpenDocCommand command)
{
    if (!SkipValidation)
    {
        _validator.Validate(command);
    }

    return await ForceOpenAsync(command.DocId);
}

실패 결과:

error ATL501: Member 'ForceOpenAsync' looks like a debug or bypass backdoor and must not exist in production code.
error ATL503: Conditional skip of validation, execution flow, or state transition is not allowed.

디버깅 편의가 구조 우회로 남는 순간 두 번째 공식 경로가 생긴다.

보조 강제 수단

이 케이스는 우회 이름 탐지, debug 분기 감지, test boundary 분리를 함께 둬야 한다. 하나만 두면 다른 이름이나 다른 계층으로 같은 우회가 다시 생긴다.

우회성 이름과 skip flag를 먼저 막는다

private static readonly string[] ForbiddenTokens =
[
    "Skip",
    "Bypass",
    "Force",
    "Unsafe",
    "Direct"
];

이 규칙은 의미 분석의 완성도보다, 뒷문이 제도화되기 전에 냄새를 빠르게 자르는 데 의미가 있다.

#if DEBUG 안의 흐름 분기를 잡는다

저장소, 엔진, 상태 전이를 debug 분기로 갈라버리는 코드는 디버깅 편의가 아니라 구조 분기다. 그래서 #if DEBUG 안에서 repository/store/engine을 직접 만지는 패턴은 별도 규칙으로 차단하는 편이 안정적이다.

테스트 경계는 프로젝트 구조에서도 분리한다

production이 test helper를 참조하기 시작하면 temporary backdoor는 오래 살아남는다. 테스트 편의는 아래에서 위로 주입되어야지, production API로 역류하면 안 된다.

빌드 실패로 고정한다

[*.cs]
dotnet_diagnostic.ATL501.severity = error
dotnet_diagnostic.ATL502.severity = error
dotnet_diagnostic.ATL503.severity = error

이제 debug backdoor는 나중에 지울 코드가 아니라 즉시 막히는 구조 위반이 된다.

결과

하네스 적용 이후 변화는 명확하다.

예외 경로 제거

정식 경로 외의 실행 루트가 사라진다.

그 결과 시스템은 다시 하나의 규칙으로 움직인다.

테스트와 프로덕션 경계 복원

테스트 편의는 조립 계층으로 밀려나고,
production 코드는 본래 구조를 유지한다.

AI 행동 안정화

이제 사람과 AI는 더 이상 다음을 쉽게 선택할 수 없다.

  • skip flag 추가

  • force API 생성

  • debug 전용 direct path

  • 환경 조건 기반 우회

  • 임시 뒷문 삽입

대신:

  • fake implementation

  • test fixture

  • logging / inspector

  • 정식 진입점 기반 테스트

같은 방향으로 유도된다.

실무 적용 팁

  • 테스트는 skip flag 대신 fake dependency와 fixture seed로 해결하는 편이 낫다.
  • 디버그 지원은 force API가 아니라 trace panel, inspector, log viewer 같은 관찰 도구로 제공해야 한다.
  • 예외 경로가 정말 필요하다면 주석이 아니라 만료 조건과 제거 시점을 함께 남겨야 한다.

요약

debug backdoor 문제는 하네스를 통해서만 안정적으로 제어된다.

  • skip flag와 force API 금지

  • #if DEBUG를 이용한 구조 우회 금지

  • 테스트 편의는 fake와 fixture로 분리

  • Roslyn Analyzer로 우회 패턴 차단

  • 프로젝트 경계로 test helper 역류 방지

  • 빌드 에러로 강제

이 구조를 통해
시스템은 더 이상 “잠깐만 쓰는 우회 코드"에 잠식되지 않고,
항상 동일한 규칙 아래에서 동작하게 된다.