1. 프로젝트 개요: 규약 중심 개발의 실천 사례
최근 GitHub에서 주목할 만한 프로젝트를 발견했다. izzymsft/spec-driven-dev-backend-apis라는 이름의 이 프로젝트는 FastAPI를 활용해 고객 및 주소 관리 시스템을 구현한 백엔드 REST API이다. 핵심 기능은 간단한 CRUD 연산이며, 데이터 저장에는 Azure Cosmos DB를 사용한다. 하지만 진정한 가치는 그 기술적 구현보다는 개발 방식에 있다. 바로 규약 중심 개발(Specification-Driven Development, SDD)의 적용이다.
이 방법론은 단순히 코드를 먼저 작성하는 것이 아니라, 먼저 명확한 '계약서'인 API 규약을 정의하고, 이를 바탕으로 자동화 도구와 대규모 언어 모델(LLM)이 코드 생성, 테스트, 배포 설정까지 지원하는 방식이다. 특히 미세 서비스 아키텍처에서 빈번하게 통신이 이루어지는 환경에서는 개발 속도와 일관성 확보에 큰 도움이 된다. 본 글에서는 이 프로젝트를 통해 어떻게 실제 업무에 규약 중심 개발을 도입할 수 있는지, 그리고 내가 겪었던 문제점과 해결책을 공유한다.
2. 규약 중심 개발의 핵심 원칙과 도구 선택
2.1 규약 중심 개발란?
규약 중심 개발은 API 설계 문서를 프로젝트의 유일한 사실 기준(One Source of Truth)으로 삼는 접근법이다. 전통적인 개발 방식은 코드를 먼저 작성한 후, 그 결과물을 기반으로 문서를 생성하는 경우가 많다. 이로 인해 문서와 실제 구현이 다를 수 있으며, 지연 발생이 일반적이다.
SDD는 반대로, 먼저 규약을 정의하고, 이후에 이를 충족하는 코드를 구현한다. 여기서 사용되는 규약은 보통 OpenAPI Specification (이전에는 Swagger)이다. 하나의 YAML 또는 JSON 파일로, 경로(/api/customers), HTTP 메서드(GET/POST 등), 요청/응답 본문 구조, 상태 코드 등을 명확하게 정의한다. 이는 다른 팀(프론트엔드, 모바일, 외부 서비스)과 체결하는 기술 계약과 같다.
- 병렬 개발 가능: 백엔드 코드가 없더라도 프론트엔드는 규약 기반의 가짜 데이터로 시작할 수 있다.
- 일관성 유지: 코드가 규약과 다르면 테스트가 실패하므로, 문서와 구현이 분리되는 문제가 발생하지 않는다.
- 자동화 가능성: 머신 리더블한 규약을 바탕으로 클라이언트 SDK, 서버 코드 스크립트, 테스트 케이스, API 게이트웨이 설정 등을 자동 생성할 수 있다.
주의할 점은, SDD가 모든 코드를 자동 생성하는 것을 의미하지는 않는다. 핵심은 계약서 우선라는 것이다. 계약서를 잘 설계하면, 구현 코드는 개발자가 직접 작성하거나, LLM이 보조적으로 생성할 수 있다. 본 프로젝트는 후자의 방식을 보여주고 있다.
2.2 최신 개발 도구 생태계: IDE와 LLM의 통합
본 프로젝트에서 언급된 cursor, vscode, llm-development, mcp, vibe-coding는 현재 규약 중심 개발을 효율적으로 수행하기 위한 핵심 도구들이다. 과거의 swagger-codegen 같은 독립형 생성기보다는, 대규모 언어 모델을 개발 환경 내에 깊이 통합하는 것이 트렌드다.
- IDE 선택:
Cursor는 AI 기능을 내장한 편집기로, 코드 작성을 혁신적으로 바꾼다. 만약 VS Code를 선호한다면,Continue플러그인을 설치하면 비슷한 경험을 할 수 있다. 두 도구 모두 내부에서 직접 LLM과 상호작용하며, 열린OpenAPI파일과 기존 코드를 분석하여 코드를 생성하거나 수정할 수 있다. - LLM의 역할: 이 프로젝트에서의
llm-development은 모델 자체를 훈련하는 것이 아니라, 대규모 언어 모델의 이해력과 생성 능력을 개발에 활용하는 것을 의미한다. 예를 들어, "이 OpenAPI 규약을 기반으로, FastAPI와 Pydantic을 사용해 데이터 모델(models.py)과 경로 처리 파일(customers.py,addresses.py)을 생성해줘"라고 요청하면, 모델은 구조, 타입, 유효성 검사 규칙 등을 정확히 파악하고 코드를 생성한다. - MCP: Model Context Protocol은 LLM이 외부 도구나 데이터 소스(코드베이스, 데이터베이스 스키마, 프로젝트 규칙)와 안전하고 구조적으로 연결되도록 하는 새로운 표준이다. 복잡한 작업 흐름에서 여러 소스를 통합해야 할 때 매우 중요하다. 이 프로젝트에서는 직접 표현되지 않았지만, 미래의 확장성에 핵심 요소가 될 것이다.
- Vibe Coding: 자연어로 의도를 묘사하는 개발 방식. 예를 들어 "이 쿼리 매개변수는 필터링 옵션을 포함하고, 응답은 중첩된 객체 구조를 가져야 해"라고 말하면, AI가 규약을 보완해 준다. 규약 중심 개발에서 이는 복잡한 구조를 쉽게 정의하는 데 유용하다.
내 작업 흐름은 다음과 같다. 먼저 Stoplight Studio 또는 직접 VS Code에서 OpenAPI 규약 초안을 작성한 후, Cursor의 AI가 규약의 정합성 검토, 예시 추가, 그리고 Pydantic 모델과 FastAPI 라우팅 스켈레톤을 자동 생성해 준다. 이 과정으로 초기 설정 시간을 크게 줄일 수 있다.
3. 프로젝트 아키텍처 분석 및 핵심 모듈 구현
3.1 명확한 계층 구조 설계
이 프로젝트는 유지보수 용이성을 위해 명확한 계층 구조를 따르고 있다.
spec-driven-dev-backend-apis/ ├── contoso_api_backend/ │ ├── __init__.py │ ├── models.py # 데이터 검증 및 직렬화 (Pydantic 모델) │ ├── database.py # 데이터 접근 (Cosmos DB 래퍼) │ ├── customers.py # 비즈니스 로직 및 라우팅 (FastAPI 경로) │ └── addresses.py # 비즈니스 로직 및 라우팅 (FastAPI 경로) ├── tests/ # 테스트 폴더 ├── main.py # 애플리케이션 구성 및 실행
models.py- 규약의 구체화:Customer,CustomerAddress등의Pydantic모델을 정의한다. 이는OpenAPI규약의 요청/응답 구조를 직접 반영한다. 예를 들어,accountCategory필드에Literal["Free", "Standard", "Premium"]를 사용하면, 문서상에서 하드코딩된 드롭다운 목록이 나타난다.database.py- 데이터 접근 추상화:Azure Cosmos DB와의 상호작용을 전담하는 래퍼.CosmosClient를 단일 인스턴스로 관리하고,get_container함수를 통해 컨테이너 접근을 제공한다. 이로써 연결 문자열이나 컨테이너 이름이 코드 곳곳에 흩어지는 것을 방지한다.customers.py&addresses.py- 비즈니스 로직 중심:Pydantic모델로 검증된 입력을 받아database.py를 호출하고, 필요한 경우 비즈니스 규칙을 적용한 후 응답을 반환한다. 복잡한 로직은 별도의services또는managers모듈로 분리하는 것이 좋다.main.py- 애플리케이션 조립:FastAPI애플리케이션을 생성하고, 모든 라우터를 포함하며, 종속성, 미들웨어, 이벤트 처리기를 설정한다.
3.2 Azure Cosmos DB 연동의 핵심 세부사항
- 연결 관리:
database.py에서는 싱글톤 패턴이나 의존성 주입을 통해CosmosClient를 관리하는 것이 좋다. 새로 생성하는 것은 비효율적이며,CosmosClient는 스레드 안전하고 초기화 비용이 크다. 애플리케이션 시작 시 한 번만 생성하고 재사용하는 것이 이상적이다.@lru_cache(maxsize=1) def get_cosmos_client(): connection_string = os.getenv("COSMOS_CONNECTION_STRING") if not connection_string: raise ValueError("COSMOS_CONNECTION_STRING 환경 변수가 설정되지 않았습니다.") return CosmosClient.from_connection_string(connection_string) def get_container(database_name: str, container_name: str): client = get_cosmos_client() database = client.get_database_client(database_name) container = database.get_container_client(container_name) return container - 데이터 모델링 및 파티셔닝:
Cosmos DB의 성능은 파티션 키에 달려있다.Customer모델에서id는 기본 문서 식별자지만, 파티션 키는 신중하게 선택해야 한다. 예를 들어, 조회가 계정 카테고리별로 빈번하다면/accountCategory를, 점검 중심이라면/id를 선택하는 것이 좋다. 파티션 키는 변경 불가이므로 설계 시 신중해야 한다.CustomerAddress는/customerId를 파티션 키로 설정하는 것이 효율적이다. - 자동 초기화: 프로젝트는 처음 실행 시 데이터베이스와 컨테이너를 자동 생성한다고 했지만, 생산 환경에서는 Infrastructure as Code(Terraform, Bicep, ARM 템플릿) 또는 CI/CD 파이프라인을 통해 미리 생성하는 것이 권장된다. 자동 생성은 성능 설정(루트 단위), 인덱스 전략, 고유 키 제약 등 세부 설정을 제어할 수 없기 때문이다.
4. 규약 기반 개발, 테스트, 배포 절차
4.1 규약 → 실행 가능한 코드로의 워크플로
- OpenAPI 규약 설계:
openapi.yaml파일을 생성한다.Stoplight Studio같은 도구를 사용하면 시각적으로 설계하기 쉽다.paths,components/schemas,parameters등을 정의하면서, 프론트엔드, 모바일 팀과 충분히 논의해야 한다. 이 단계가 성공 여부를 결정한다. - 코드 스크래핑: 규약이 준비되면 다음 두 가지 방식 중 하나로 코드를 생성할 수 있다.
openapi-generator: 다양한 언어와 프레임워크의 서버/클라이언트 코드를 생성할 수 있다. 예:openapi-generator generate -i openapi.yaml -g python-fastapi -o ./generated-server- LLM 보조:
Cursor또는VS Code + Continue에서 규약 파일을 열고, "이 규약을 기반으로, FastAPI와 Pydantic을 사용해 전체 프로젝트를 생성해줘.main.py,models.py,database.py, 라우터 파일 등을 포함하고,Azure Cosmos DB를 사용한다고 가정해."라고 요청하면, 거의 사용 가능한 코드를 얻을 수 있다.
- 비즈니스 로직 및 데이터 접근 채우기: 생성된 스켈레톤은 기본 구조만 제공한다. 실제 구현을 위해 다음과 같이 작업한다.
database.py에 실제로Cosmos DB연결, 쿼리, 삽입, 업데이트, 삭제 로직을 구현한다. 오류 처리(예:CosmosHttpResponseError)도 고려해야 한다.customers.py등의 라우트 함수에서 데이터 접근 계층을 호출하고, 이메일 중복 확인 같은 비즈니스 규칙을 추가한다.pydantic-settings또는python-dotenv를 사용해 환경 변수를 관리한다.
- 반복 및 업데이트: API 요구사항이 변경되면, 먼저 규약 파일을 수정한다. 그런 다음, LLM에게 "새로운
email필드가Customer모델에 추가되었으며, 필수입니다.models.py와 관련 라우터 함수를 업데이트해주세요."라고 요청하면, 변화에 맞춰 코드를 자동으로 수정할 수 있다.
4.2 테스트 전략: 계약 테스트와 통합 테스트
규약 중심 개발은 계약 테스트에 이상적이다.
- 단위 테스트:
pytest를 사용해 각 함수나 클래스를 독립적으로 테스트한다. 예를 들어,models.py의Pydantic모델이 유효한 데이터는 수용하고, 무효한 데이터는 거부하는지 확인한다. - 통합 테스트:
httpx또는requests를 사용해 실제API엔드포인트를 테스트한다.OpenAPI규약에 따라 요청/응답의 구조, 상태 코드, 데이터 형식이 일치하는지 확인한다.schemathesis처럼 규약 기반으로 자동으로 다양한 테스트 케이스를 생성하는 도구도 사용할 수 있다. - fixture 사용:
tests/conftest.py에서TestClient와Cosmos DB모킹을 정의하면, 테스트 간에 상태가 영향을 받지 않도록 보장할 수 있다.@pytest.fixture def client(): with TestClient(app) as test_client: yield test_client @pytest.fixture(autouse=True) def mock_cosmos(): with patch('contoso_api_backend.database.get_container') as mock_get_container: mock_container = Mock() mock_get_container.return_value = mock_container yield mock_container
4.3 배포 및 구성 관리
- 환경 설정: 민감한 정보(예:
COSMOS_CONNECTION_STRING)는 코드에 하드코딩해서는 안 된다..env파일(개발용)과 환경 변수(생산용)를 사용한다.pydantic-settings를 사용하면 환경 변수,.env파일로부터 자동으로 설정을 로드하고 타입 검사를 수행할 수 있다.class Settings(BaseSettings): cosmos_connection_string: str cosmos_database_name: str = "contoso_customer_db" api_host: str = "0.0.0.0" api_port: int = 8000 class Config: env_file = ".env" settings = Settings() - 생산 서버: 개발용
uvicorn main:app --reload은 사용하지 않고,gunicorn+uvicorn을 사용한다. 워커 수는 CPU 코어 수의 2~4배로 설정한다.Docker로 컨테이너화하면 환경 일관성을 확보할 수 있다.FROM python:3.12-slim WORKDIR /app COPY pyproject.toml . RUN pip install uv && uv sync --frozen COPY . . CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] - 건강 상태 점검 및 모니터링:
/health엔드포인트를 추가해 데이터베이스 연결 상태 등을 점검할 수 있다. 또한 Azure Monitor, Datadog, OpenTelemetry 같은 APM 도구를 통합해 지연, 에러율 등을 모니터링한다.
5. 일반적인 문제, 디버깅 팁, 고급 고민
5.1 개발 및 디버깅 시 흔한 문제
- Cosmos DB 연결 실패
- 원인: 연결 문자열 오류, 네트워크 차단, 권한 부족.
- 해결:
.env파일의 연결 문자열을 다시 확인하고, Azure 포털에서 복사해 붙여넣기. 회사 네트워크라면 프록시 설정을 확인.get_cosmos_client초기화 후 간단한 쿼리(client.list_databases())를 실행해 연결을 테스트한다.
- CORS 오류
- 원인: 브라우저에서 CORS 정책 위반.
- 해결:
main.py에CORSMiddleware를 추가하고, 프론트엔드 주소를 허용 목록에 포함한다.app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
- Pydantic 검증 오류 메시지 불명확
- 원인:
422 Unprocessable Entity응답이 너무 기술적이고 사용자에게 친절하지 않음. - 해결: 커스텀 예외 처리기로 오류 메시지를 구조화된 형태로 변환한다.
@app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): errors = [] for error in exc.errors(): field = " -> ".join([str(loc) for loc in error["loc"]]) if error["loc"] else "body" errors.append({ "field": field, "message": error["msg"], "type": error["type"] }) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": "유효성 검사 실패", "errors": errors}, )
- 원인:
5.2 규약 중심 개발의 도전과 대응
- 규약 변경 관리: 중요한 변경이 있을 때마다 URL 버전(
/api/v1/customers) 또는 헤더 버전(Accept: application/vnd.contoso.v1+json)을 사용해 새 버전을 도입하고, 일정 기간 동안 구버전을 유지한다. - 생성 코드의 유연성 부족: 자동 생성된 코드는 일반적이고 복잡한 비즈니스 로직이나 최적화에는 부족할 수 있다. SDD는 스켈레톤과 일관성 보장 도구일 뿐, 모든 코드를 대체하지 않는다는 점을 기억하자.
- 팀 협업 흐름: 규약 중심 개발은 사전 협의가 필요하다.
OpenAPI규약 파일을 Git에 포함하고, Pull Request를 통해 리뷰하는 방식을 도입하는 것이 좋다.
5.3 고급: 규약 중심 개발의 전체 수명 주기 확장
이 프로젝트는 개발 단계에 집중하지만, 규약 중심 개발은 더 넓게 확장될 수 있다.
- API 게이트웨이 통합:
OpenAPI규약을 Azure API Management, Kong, Apigee 등에 직접 임포트하면, 자동 라우팅, 제한, 인증 정책 설정이 가능하다. - 소비자 SDK 생성:
openapi-generator를 이용해 프론트엔드(TypeScript), 모바일(Swift, Kotlin), 다른 백엔드(C#, Java, Go)용 강타입 클라이언트 SDK를 자동 생성할 수 있다. - 자동화 테스트 및 모니터링:
schemathesis를 통한 속성 기반 테스트, CI/CD 파이프라인에서 규약 또는 코드 변경 후 자동 테스트 실행 등이 가능하다.
이 izzymsft/spec-driven-dev-backend-apis 프로젝트는 규약 중심 개발의 훌륭한 시작점이다. FastAPI, Cosmos DB, Pytest와 같은 기술과 LLM, 규약 중심 개발이라는 철학을 결합해, 빠르고 안정적이며 문서화된 백엔드 API를 구축하는 방법을 보여준다. 개인적으로 이 방식에 익숙해지면, 기존의 코드 작성 후 문서화 방식으로 돌아가는 것이 어렵다. 일관성과 협업의 원활함은 전통적 방식이 달성하기 어려운 수준이다.