런타임 강제
빌드 타임에서 잡지 못한 것을 런타임에서 잡는다. 잘못된 상태는 한 발짝도 진행하지 못한다.
빌드 타임 강제는 강력하지만 완전하지 않다. 동적 데이터, 런타임 조건, 외부 입력, 타입 캐스팅으로 우회된 코드는 정적 분석을 통과할 수 있다.
런타임 강제는 최후의 방어선이다. 실행이 시작되는 순간, 잘못된 상태면 즉시 실패한다.
Guard를 진입점 앞에 배치한다
핵심 원칙은 하나다. 내부가 아니라 입구에서 막는다. 진입점 안쪽 어딘가에서 실패하는 것보다 진입점에서 바로 실패하는 것이 수정 범위가 좁고 오류 메시지가 명확하다.
Atlas (C#)
public async Task<OpenDocResult> ExecuteAsync(
OpenDocCommand command,
CancellationToken ct = default)
{
_validator.Validate(command); // 진입점 첫 번째 줄
var doc = await _repository.LoadAsync(command.DocId, ct);
return new OpenDocResult(doc.Id, doc.Content);
}Validator가 실패하면 즉시 예외가 던져지고 이후 코드는 실행되지 않는다.
Glif (TypeScript)
function useTheme() {
const value = useContext(ThemeContext);
if (!value) {
throw new Error(
"ThemeProvider is required but not found in component tree. " +
"Wrap the component with <AppProviders>."
);
}
return value;
}Provider 없이 hook을 사용하면 즉시 명확한 오류가 발생한다. ?? defaultTheme 같은 silent fallback은 문제를 숨긴다.
Silent fallback을 제거한다
// 위반: 문제를 숨기는 fallback
const theme = useContext(ThemeContext) ?? defaultTheme;
// 합법: 문제를 드러내는 guard
const theme = useContext(ThemeContext);
if (!theme) throw new Error("ThemeProvider required");Fallback은 “대체"가 아니라 “중단"이어야 한다. AI가 생성하는 코드에서 ?? defaultValue 패턴은 항상 위험하다. guard로 대체한다.
Assertion으로 전제 조건을 강제한다
Atlas (C#)
private static void AssertValidated(Task task)
{
if (task.State != TaskState.Validated)
{
throw new InvalidOperationException(
$"Task must be in Validated state before execution. " +
$"Current: {task.State}. Call Validate() first.");
}
}
public async Task RunAsync(Task task)
{
AssertValidated(task);
// 이 아래는 task가 Validated 상태임이 보장됨
}Glif (TypeScript)
function invariant(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function runTask(task: Task) {
invariant(
task.state === "validated",
`Task must be validated before running. Current: ${task.state}`
);
// 이 아래는 task.state === "validated"가 보장됨
}invariant 패턴은 두 가지를 동시에 달성한다. 런타임 검증과 TypeScript type narrowing. asserts condition으로 선언하면 이후 코드에서 타입이 좁혀진다.
실행 컨텍스트를 검증한다
Planner를 거쳐야만 실행 가능한 코드에서, 직접 호출이 들어오면 차단한다.
function executeInternal(
plan: ExecutionPlan,
ctx: ExecutionContext
) {
if (ctx.source !== "planner") {
throw new Error(
"Execution must originate from Planner. " +
"Direct executor calls are forbidden."
);
}
// planner를 통한 호출만 이 아래로 진행
}Side-effect 전에 반드시 검증한다
async function publishDocument(doc: Document) {
// side-effect 전에 먼저 검증
invariant(doc.status === "reviewed", "Document must be reviewed before publishing");
// 검증 통과 후에만 side-effect 실행
await storage.save({ ...doc, status: "published" });
await eventBus.emit("document.published", { id: doc.id });
}DB write, 네트워크 호출, 파일 쓰기 같은 side-effect는 되돌리기 어렵다. 검증은 항상 그 앞에 위치한다.
오류 메시지의 형태
런타임 강제의 오류 메시지는 빌드 타임보다 더 중요하다. 이미 실행이 진행된 상태이기 때문이다. 무엇이 잘못됐는지, 어떻게 해야 하는지를 즉시 알 수 있어야 한다.
{
"error": "TransitionViolationError",
"message": "Cannot transition document from 'draft' to 'published'",
"current_status": "draft",
"requested_transition": "published",
"allowed_transitions": ["review", "archived"],
"legal_path": "submitForReview() → approve() → publish()",
"trace_id": "abc-123"
}legal_path 필드가 합법 경로를 직접 가리킨다. AI 작업 흐름에서 이 형식은 수정으로 수렴하게 만든다.
런타임 강제 체크리스트
- Guard가 진입점의 첫 번째 코드인가?
- Silent fallback(
?? default)이 guard로 교체됐는가? - Assertion이 전제 조건을 명시적으로 표현하는가?
- Side-effect 전에 검증이 위치하는가?
- 오류 메시지에 현재 상태, 위반 내용, 합법 경로가 포함되는가?
- 런타임 실패가 trace ID와 함께 기록되는가?