본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 05. 20:39

생성형 AI (GenAI) 개발의 오류 #7: 명세(Specification)는 새로 만들어야 하는 새로운 산출물이다

요약

생성형 AI 개발 시 명세(Specification) 작성을 과도한 작업으로 치부하는 오류를 지적합니다. 과거의 실패 사례와 달리, 현대의 명세는 기존 코드베이스의 타입 시그니처처럼 이미 존재하며 이를 활용한 검증 중심의 접근이 필요함을 강조합니다.

핵심 포인트

  • 명세를 완전한 동작 기술로 보는 과거의 관점은 오류임
  • 명세는 별도의 문서가 아닌 타입 시그니처 등 이미 존재함
  • 검증 없는 AI 코드 생성은 기술 부채를 양산함
  • 지속 가능한 명세는 구현과 함께 유지 가능한 수준이어야 함

이 글은 생성형 AI (Generative AI)를 사용하여 구축할 때 팀들이 범하는 잘못된 가정에 관한 8개의 포스트 시리즈 중 일곱 번째 글입니다. 오류 #1부터 #5까지는 왜 AI 속도의 개발이 검증(Verification) 없이는 무너지는지를 다루었습니다. 오류 #6은 왜 생성된 코드가 자산이 아닌 부채(Liability)가 되는지를 다루었습니다. 이 포스트는 대부분의 팀이 검증을 추가하는 것을 막는 가정, 즉 새로운 명세(Specification)를 처음부터 새로 작성해야 한다는 믿음을 다룹니다.

오류 (The Fallacy)

"명세 우선 개발 (Specification-first development)을 채택하려면 모든 것에 대해 명세를 작성해야 합니다. 그것은 너무 많은 작업입니다."

이것이 유혹적인 이유

'명세 (Specification)'라는 단어는 많은 짐을 안고 있습니다. 업계에 충분히 오래 몸담았던 엔지니어들은 다음을 기억합니다:

1980년대: 형식 기법 (Formal methods). Z 표기법 (Z notation). VDM. B 방법론 (B method). 구현만큼이나 복잡했던 완전한 동작 명세 (Behavioral specifications). 명세를 작성하는 데 코드를 작성하는 것만큼 시간이 걸렸습니다. 둘 다 유지 관리하는 것은 두 배의 작업이었습니다. 마감 압박이 닥치면 명세가 가장 먼저 버려졌습니다.

1990년대: IEEE 830 스타일의 요구사항 문서 (Requirements documents). 수백 페이지에 달했습니다. 전담 요구사항 엔지니어에 의해 코드와 함께 유지 관리되었습니다. 문서와 코드는 몇 달 안에 서로 어긋나기 시작했습니다. 요구사항 문서는 아무도 읽지 않는 허구가 되었습니다.

2000년대: 모델 주도 개발 (Model-driven development). UML 다이어그램이 코드를 생성했습니다. 다이어그램이 곧 명세였습니다. 다이어그램을 유지 관리하는 것은 전업 업무였습니다. 생성된 코드를 수동으로 수정해야 할 때, 다이어그램과 코드는 영구적으로 분리되었습니다.

각 시도는 동일한 이유로 실패했습니다. 명세(Specification)가 시스템이 수행하는 '모든 것'을 설명하려 했기 때문입니다. 완전한 동작 명세(Behavioral specification)는 구현(Implementation)만큼이나 복잡합니다. 두 가지를 모두 유지 관리하는 것은 지속 불가능합니다. 명세는 부패합니다. 팀은 이를 포기합니다. 노력은 낭비되었습니다.

엔지니어들은 명세라는 말을 들으면 이러한 실패 사례들을 떠올립니다. 즉각적인 반응이 나옵니다. "그거 해봤어요. 안 됐습니다. 오버헤드(Overhead)가 너무 커요. 다시는 안 합니다."

왜 틀렸는가

오류는 명세가 1980년대에 의미했던 것, 즉 시스템에 대한 완전한 동작 기술을 의미한다는 가정에서 비롯됩니다.

그렇지 않습니다. 지금 바로 여러분의 코드베이스(Codebase)를 보십시오. 여러분은 이미 명세를 가지고 있습니다. 단지 그것을 그렇게 부르지 않을 뿐입니다.

여러분이 이미 유지 관리하고 있는 명세들

타입 시그니처 (Type signatures). 타입이 지정된 언어(Typed language)의 모든 함수는 명세를 가지고 있습니다. 즉, 무엇을 입력받고 무엇을 반환하는지에 대한 명세입니다. func ParsePolicy(path string) (*PolicyDocument, error) — 이것이 바로 명세입니다. 이 함수는 문자열을 입력받아 문서 또는 에러를 반환한다고 말합니다. 잘못된 타입으로 호출하는 모든 코드는 컴파일 타임(Compile time)에 실패합니다. 여러분은 이미 이것들을 작성하고 있습니다. 이미 유지 관리하고 있습니다. 또한 컴파일러(Compiler)라는 강제 관문(Enforcement gate)을 통해 기계적으로 이를 강제하고 있습니다.

여러분은 이미 형식 방법론(Formal methods)을 사용하고 있습니다. 타입 체크(Type checking)는 역사상 가장 성공적인 형식 검증(Formal verification) 도구입니다. 이는 컴파일할 때마다 여러분의 코드에 대한 수학적 속성인 타입 안전성(Type safety)을 증명합니다. 이는 빠르고 결정론적(Deterministic)이며, 사람이 확인하기 전에 오류를 잡아냅니다. 우리는 컴파일러의 이러한 성공 사례를 여러분의 다른 경계(Boundaries)에도 적용해야 합니다.

API 계약 (API contracts). 여러분의 REST API에는 OpenAPI 명세(Spec)가 있습니다. gRPC 서비스에는 프로토콜 버퍼(Protocol Buffer) 정의가 있습니다. GraphQL API에는 스키마(Schema)가 있습니다. 각각은 엔드포인트(Endpoints), 필드(Fields), 타입(Types), 필수 파라미터(Required parameters)가 무엇인지 명시합니다. 계약을 위반하는 클라이언트는 에러를 받게 됩니다. 여러분은 이미 이것들을 작성하고 버전 관리(Version)하고 있습니다.

데이터베이스 스키마 (Database schemas). 여러분의 마이그레이션 파일 (migration files)은 다음과 같은 사항들을 명시합니다: 이것들은 테이블이고, 이것들은 컬럼이며, 이것들은 제약 조건 (constraints)이고, 이것들은 외래 키 (foreign keys)입니다. NOT NULL, UNIQUE, FOREIGN KEY — 각각은 데이터베이스 엔진 (database engine)이 모든 쓰기 작업 시 강제하는 명세 (specification)입니다. 여러분은 이미 이것들을 작성하고 있습니다.

설정 스키마 (Configuration schemas). 여러분의 Kubernetes 매니페스트 (manifests)는 리소스 제한 (resource limits), 상태 확인 (health checks), 복제본 수 (replica counts)를 명시합니다. 여러분의 Terraform 파일은 인프라 상태 (infrastructure state)를 명시합니다. 여러분의 CI 파이프라인 (CI pipeline) 파일은 빌드 단계 (build steps)와 그 의존성 (dependencies)을 명시합니다. 각각은 기계가 읽고 강제하는 명세입니다.

인터페이스 경계 (Interface boundaries). 여러분의 Go 패키지 (packages)에는 공개된 (exported) 함수와 공개되지 않은 (unexported) 함수가 있습니다. 여러분의 Java 클래스 (classes)에는 public 및 private 메서드 (methods)가 있습니다. 여러분의 Python 모듈 (modules)에는 __all__ 리스트가 있습니다. 각 경계는 다음과 같이 명시합니다: 이것이 인터페이스이며, 그 외의 모든 것은 숨겨져 있다. Parnas는 1972년에 여러분에게 이것들을 만들라고 말했습니다. 여러분은 그렇게 했습니다.

Parnas가 말한 것

David Parnas의 1972년 논문 "시스템을 모듈로 분해할 때 사용되는 기준에 대하여 (On the Criteria To Be Used in Decomposing Systems into Modules)"는 인간이 전체 시스템을 머릿속에 담아둘 수 없다고 주장했습니다. 해결책은 정보 은닉 (information hiding)입니다. 각 모듈은 인터페이스 (interface, 무엇을 하는가)를 노출하고 구현 (implementation, 어떻게 하는가)을 숨깁니다.

이 논문은 모든 모듈형 프로그래밍 언어 (modular programming language), 모든 API 경계 (API boundary), 모든 마이크로서비스 아키텍처 (microservice architecture), 모든 패키지 시스템 (package system)의 토대입니다. 여러분이 공개된 함수를 가진 Go 패키지를 작성할 때, 여러분은 Parnas를 구현하고 있는 것입니다. 여러분이 Protocol Buffer 서비스를 정의할 때, 여러분은 Parnas를 구현하고 있는 것입니다. 여러분이 외래 키 제약 조건이 있는 데이터베이스 스키마를 생성할 때, 여러분은 Parnas를 구현하고 있는 것입니다.

여러분은 커리어 내내 명세를 작성해 왔습니다. 단지 그것들을 인터페이스, 스키마, 계약 (contracts), 타입 (types)이라고 불렀을 뿐입니다.

무엇이 빠졌는가

명세는 존재합니다. 빠져 있는 것은 단 한 가지입니다: 모든 변경 사항에 대해, AI의 속도로, 이 모든 명세들을 관통하는 기계적 강제 (mechanical enforcement).

당신의 타입 시스템(type system)은 타입 명세(type specifications)를 강제하지만, 이는 단 하나의 언어와 하나의 컴파일 단위(compilation unit) 내에서만 유효합니다. TypeScript 클라이언트가 Go 서버의 API 계약(API contract)과 일치하는지는 강제하지 못합니다.

당신의 데이터베이스 엔진은 스키마 제약 조건(schema constraints)을 강제하지만, 이는 오직 쓰기 시점(write time)에만 해당됩니다. 애플리케이션 코드가 모든 제약 조건 위반을 올바르게 처리하는지는 강제하지 못합니다.

당신의 API 계약은 존재하지만, 대부분의 팀은 생성된 코드가 OpenAPI 명세(OpenAPI spec)에서 벗어날 때 빌드를 실패시키는 CI 체크(CI check)를 갖추고 있지 않습니다.

당신의 모듈 경계(module boundaries)는 존재하지만, AI 에이전트가 리플렉션(reflection), 타입 캐스팅(type casting), 또는 동적 임포트(dynamic imports)를 통해 경계를 넘어가는 코드를 생성하는 것을 막을 수 있는 것은 아무것도 없습니다.

명세는 존재합니다. 하지만 강제(enforcement)는 인간의 몫입니다 — 코드 리뷰(code review), 수동 테스트(manual testing), "리뷰할 때 확인해 볼게요

Layer 2는 부분적으로 갖추고 있습니다. 컴파일러(compiler)는 타입(types)을 위한 Layer 2입니다. 데이터베이스 엔진(database engine)은 스키마(schemas)를 위한 Layer 2입니다. 당신에게 부족한 것: 현재 사람들의 머릿속이나 문서 속에만 존재하는 아키텍처 제약 조건(architectural constraints), 보안 속성(security properties), 그리고 서비스 간 계약(cross-service contracts)을 위한 Layer 2입니다.

Layer 3는 거의 갖추고 있지 않습니다. 아마도 몇몇 아키텍처 결정 기록(Architecture Decision Records, ADR)이 있을 수도 있습니다. 코드 내에 "왜(why)"를 설명하는 몇몇 주석이 있을 수도 있습니다. 하지만 이들이 설명하는 경계(boundaries)와 연결되는 경우는 드뭅니다. 강제(enforced)되는 경우도 드뭅니다. 인터페이스에 대한 제안된 변경 사항이 그것을 형성했던 근거(rationale)를 위반하는지 아무도 확인하지 않습니다.

극한의 도메인들은 Layer 2를 추가했습니다

안전이 필수적인(safety-critical) 어떤 도메인도 Layer 1에서 멈추지 않았습니다. 모든 도메인은 강제성(enforcement)이 없는 경계는 대규모의 현실과 맞닥뜨렸을 때 살아남지 못한다는 것을 발견했습니다.

마이크로프로세서 설계(Microprocessor design): 인터페이스 명세(interface specifications)가 존재합니다 (버스 프로토콜(bus protocols), 타이밍 다이어그램(timing diagrams)). 형식 검증(formal verification) 도구들이 모든 회로를 명세에 따라 기계적으로 체크(CHECK)합니다. 검증 스위트(verification suite)를 통과하지 않고서는 칩을 테이프 아웃(tape out)할 수 없습니다.

원자력 운영(Nuclear operations): 운영 절차(operating procedures)가 존재합니다 (명세(specifications)). 물리적 인터록(physical interlocks)이 매개변수 제한(parameter limits)을 기계적으로 강제(ENFORCE)합니다. 운영자가 무엇을 하든 상관없이, 매개변수가 허용 범위(envelope)를 벗어나면 원자로가 자동으로 비상 정지(scram)합니다.

항공(Aviation): 비행 포락선(flight envelopes)이 존재합니다 (명세(specifications)). 플라이 바이 와이어(Fly-by-wire) 시스템이 이를 기계적으로 강제(ENFORCE)합니다. 조종사는 항공기를 실속(stall)시킬 수 없습니다. 컴퓨터가 입력값이 제어면(control surfaces)에 도달하기 전에 입력을 무효화(override)하기 때문입니다.

Google 모노레포(monorepo): API 계약(API contracts)이 존재합니다 (명세(specifications)). CI 게이트(CI gates)가 모든 커밋(commit)에 대해 이를 강제(ENFORCE)합니다. 수백만 줄의 코드에 영향을 미치는 대규모 변경은 영향을 받는 모든 계약 테스트(contract test)가 기계적으로 통과될 때만 병합(merge)됩니다.

각 도메인의 공통점: 명세는 존재했습니다 (Layer 1). 인간의 강제(human enforcement)는 따라갈 수 없었습니다 (Layer 2 부재). 그들은 기계적 강제(mechanical enforcement)를 추가했습니다 (Layer 2 추가). 그 시스템은 규모 확장(scaling) 과정에서 살아남았습니다.

당신의 코드베이스도 같은 상황에 처해 있습니다. Layer 1은 존재합니다. Layer 2는 부분적입니다. AI 속도의 생성(AI-speed generation)이 그 간극을 눈에 보이게 만들었습니다.

이것이 1980년대와 다른 이유

1980년대의 형식 기법 (Formal methods) 실패에는 특정한 원인이 있었습니다. 당시의 명세 (Specification)는 코드만큼이나 복잡한 '완전한 동작 기술 (COMPLETE BEHAVIORAL DESCRIPTION)'이었기 때문입니다. 이 글에서 제안하는 것은 그런 방식이 아닙니다.

1980년대 방식 (실패함):
    시스템이 수행하는 모든 것에 대한 전체 명세를 작성한다.
    명세 복잡도 ≈ 코드 복잡도.
...

func(string) (*Document, error)라는 타입 시그니처 (Type signature)는 해당 함수의 완전한 동작 명세가 아닙니다. 이것은 인터페이스 명세 (Interface specification)입니다. 즉, 무엇이 들어가고 무엇이 나오는지를 나타냅니다. 이는 코드보다 작습니다. 변경 빈도도 더 낮습니다. 컴파일러 (Compiler)가 이미 이를 강제하고 있습니다.

OpenAPI 명세는 API 동작에 대한 완전한 설명이 아닙니다. 이것은 인터페이스 명세입니다. 엔드포인트 (Endpoints), 필드 (Fields), 타입 (Types), 필수 파라미터 (Required parameters)를 정의합니다. 이는 코드보다 작습니다. 변경 빈도도 더 낮습니다. 계약 테스트 (Contract testing)가 이미 이를 강제하고 있습니다.

데이터베이스 스키마 (Database schema)는 데이터 의미론 (Semantics)에 대한 완전한 모델이 아닙니다. 이것은 구조적 명세 (Structural specification)입니다. 테이블 (Tables), 컬럼 (Columns), 제약 조건 (Constraints)을 정의합니다. 이는 애플리케이션 코드보다 작습니다. 변경 빈도도 더 낮습니다. 데이터베이스 엔진 (Database engine)이 이미 이를 강제하고 있습니다.

이 각각의 방식이 1980년대의 방식이 실패한 지점에서 성공할 수 있었던 이유는, '동작 (Behavior)'이 아니라 '인터페이스 (Interfaces)'를 명세하기 때문입니다. 인터페이스는 작습니다. 동작은 큽니다. 작은 명세는 유지보수가 가능하지만, 거대한 명세는 부패합니다.

이번 주에 할 수 있는 일

1. 기존 명세 목록을 작성해 보세요. 코드베이스를 열어보세요. 다음을 세어보십시오: 타입이 지정된 함수 시그니처는 몇 개인가? API 계약 (API contracts: OpenAPI, Protobuf, GraphQL schemas)은 몇 개인가? 제약 조건이 포함된 데이터베이스 스키마는 몇 개인가? CI 파이프라인 정의는 몇 개인가? 예상보다 더 많은 명세를 발견하게 될 것입니다. 당신은 커리어 내내 이미 명세들을 작성해 오고 있었습니다.

2. 강제되지 않는 명세(unenforced specifications)를 식별하십시오. 발견한 명세들 중 어떤 것들이 기계적 강제(mechanical enforcement) 수단(컴파일러(compiler), 계약 테스트(contract test), 스키마 검증기(schema validator), CI 체크(CI check))을 갖추고 있습니까? 어떤 것들이 문서로는 존재하지만 모든 변경 사항마다 확인되지 않고 있습니까? 강제되지 않는 것들이 바로 당신의 레이어 2(Layer 2) 격차(gaps)입니다. 각 격차는 AI가 생성한 코드가 아무도 알아채지 못한 채 인터페이스(interface)를 위반할 수 있는 지점입니다.

3. 이번 주에 하나의 명세를 기계적으로 강제하십시오. 가장 중요한 강제되지 않는 명세를 하나 선택하십시오. 존재는 하지만 계약 테스트(contract test)가 없는 OpenAPI 계약(contract)이 있습니까? 하나를 추가하십시오. 경계는 존재하지만 경계를 넘나드는 임포트(import)를 막는 것이 아무것도 없는 모듈 경계(module boundary)가 있습니까? 린터(linter) 규칙을 추가하십시오. arch-go, depguard, 또는 eslint-plugin-import와 같은 도구들을 사용하여 internal 패키지를 public 패키지에서 임포트할 수 없도록 강제할 수 있습니다. 그것은 단 하나의 설정 파일로 구현된, 레이어 1(Layer 1) 경계를 위한 레이어 2(Layer 2) 게이트(gate)입니다. 문서로는 존재하지만 CI 체크(CI check)가 없는 아키텍처 결정(architecture decision)이 있습니까? 하나의 규칙에 대해 하나의 체크를 작성하십시오.

단 하나의 명세. 이미 작성되어 있는 것. 하나의 강제 게이트(enforcement gate). 이번 주에 새로 추가되는 것. 이것이 채택 경로의 전부입니다. "모든 것에 대해 명세를 작성하라"가 아닙니다. "정형 기법(formal methods)을 도입하라"도 아닙니다. "6개월짜리 프로세스 변화를 추가하라"도 아닙니다. 기존의 명세 하나. 새로운 CI 체크 하나. 이번 주에 말입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0