빌드 타임 강제

빌드 타임 강제

빌드 타임 강제는 가장 싸고, 가장 일관되며, 우회가 가장 어렵다. 실행되기 전에 차단되는 문제는 런타임 비용이 없다.


빌드 타임 강제의 원리는 단순하다. 잘못된 코드가 작성되는 순간, 또는 늦어도 빌드 단계에서 실패로 전환한다. 실행까지 가지 않는다.

이 단계에서 AI와 사람 모두 같은 피드백을 받는다. 즉각적이고, 구체적이며, 어느 파일의 몇 번째 줄인지 정확히 가리킨다.


Atlas — Roslyn Analyzer

Atlas(.NET / C#)에서는 Roslyn Analyzer가 빌드 타임 강제의 핵심이다.

Roslyn Analyzer는 C# 컴파일러 파이프라인에 통합되어 코드를 정적으로 분석한다. 위반이 감지되면 컴파일 자체가 실패한다. 경고가 아니라 오류다.

주요 규칙과 적용

레이어 경계 규칙 (ATL001)

Application 레이어가 Infrastructure 구현체를 직접 참조하는 패턴을 탐지한다.

// 위반: Application에서 Infrastructure 직접 참조
public class DocumentService
{
    public async Task OpenAsync(string docId)
    {
        var repo = new FileDocumentRepository(); // ← 탐지
        var doc = await repo.LoadAsync(docId);
    }
}
error ATL001: Application layer must not instantiate Infrastructure types directly.
              Use the declared interface: IDocumentRepository
              → harness/atlas/backend-layer-bypass

검증 누락 규칙 (ATL401)

UseCase나 Handler의 진입점에서 Validator 호출이 없는 경우를 탐지한다.

// 위반: validator 없이 실행
public async Task<Result> ExecuteAsync(OpenDocCommand command)
{
    // _validator.Validate(command); 가 없음
    var doc = await _repository.LoadAsync(command.DocId);
    return new Result(doc);
}
error ATL401: Execution flow must validate input before continuing.
              Add: _validator.Validate(command);

실패 은닉 규칙 (ATL402)

빈 catch 블록이나 validation 실패를 success로 반환하는 패턴을 탐지한다.

// 위반: 빈 catch
try
{
    await _repository.SaveAsync(doc);
}
catch { }  // ← 탐지
error ATL402: Empty catch block hides execution failure.

severity 설정

<!-- .editorconfig -->
[*.cs]
dotnet_diagnostic.ATL001.severity = error
dotnet_diagnostic.ATL002.severity = error
dotnet_diagnostic.ATL401.severity = error
dotnet_diagnostic.ATL402.severity = error

warning으로 두면 무시된다. 모두 error로 설정한다.


Glif — ESLint

Glif(React / TypeScript)에서는 ESLint가 빌드 타임 강제의 핵심이다.

ESLint는 코드 작성 시점(IDE)과 빌드 시점 모두에서 작동한다. CI에서 eslint --max-warnings=0으로 설정하면 warning 하나도 없어야 빌드가 통과된다.

주요 규칙과 적용

직접 컴포넌트 import 차단

Page/Screen 레이어에서 UI 블록을 직접 import하는 패턴을 차단한다.

// 위반: Page에서 컴포넌트 직접 import
import { HeroBlock } from "@/ui/blocks/HeroBlock";

function LandingPage() {
  return <HeroBlock title="Hello" />;
}
{
  "no-restricted-imports": ["error", {
    "patterns": [{
      "group": ["@/ui/blocks/*"],
      "message": "Pages must not import UI blocks directly. Use registry resolver."
    }]
  }]
}
error: Pages must not import UI blocks directly. Use registry resolver.
       → harness/glif/component-registry-bypass

raw context 접근 차단

useContext를 직접 사용하는 것을 금지하고 typed hook 사용을 강제한다.

// 위반: raw context 직접 접근
const store = useContext(StoreContext);
{
  "no-restricted-syntax": ["error", {
    "selector": "CallExpression[callee.name='useContext']",
    "message": "Direct context access is forbidden. Use typed hook (e.g. useStore)."
  }]
}

global fallback 금지

?? globalValue 패턴이 global singleton으로의 우회를 만든다.

// 위반
return useContext(StoreContext) ?? globalStore;
{
  "no-restricted-syntax": ["error", {
    "selector": "LogicalExpression[operator='??'][right.type='Identifier'][right.name=/global/]",
    "message": "Global fallback is forbidden."
  }]
}

Infrastructure 직접 접근 차단

{
  "no-restricted-imports": ["error", {
    "patterns": [{
      "group": ["@/infra/*"],
      "message": "UI layer must not access infrastructure directly."
    }]
  }]
}

CI 설정

// package.json
{
  "scripts": {
    "lint": "eslint . --max-warnings=0",
    "build": "npm run lint && tsc && vite build"
  }
}

--max-warnings=0은 warning 하나도 없어야 빌드가 통과된다는 의미다.


공통: 오류 메시지의 형태

빌드 타임 강제가 효과적으로 작동하려면 오류 메시지가 세 가지를 포함해야 한다.

  1. 무엇이 위반됐는가 — 규칙 명칭과 설명
  2. 어디서 위반됐는가 — 파일과 줄 번호
  3. 합법 경로는 무엇인가 — 올바른 방향 링크

방향이 없는 오류 메시지는 다시 추측을 만든다. AI도, 사람도 무엇을 해야 하는지 모른 채 다른 방식으로 시도한다.


빌드 타임 강제 체크리스트

  • 위반이 warning이 아닌 error인가?
  • CI에서 강제가 동일하게 작동하는가?
  • 오류 메시지에 합법 경로 안내가 있는가?
  • disable 주석이 차단되어 있는가?
  • 규칙이 IDE에서도 즉시 표시되는가?