소프트웨어 엔지니어링에서 소프트웨어 아키텍처로
요약
코딩 품질을 넘어 소프트웨어 아키텍처의 지속 가능성을 확보하기 위한 구조적 강제화 방안을 다룹니다. 단순한 린팅을 넘어 아키텍처 적합성 함수(Architectural Fitness Functions)를 통해 코드 구조와 모듈 간 경계를 자동 검증하는 방법을 제안합니다.
핵심 포인트
- 코드 품질은 린트와 같은 도구로 수렴 가능하지만, 아키텍처는 별도의 강제성이 필요함
- 산문 형태의 규칙보다 코드와 구조에 인코딩된 훅(Hook)이 더 지속 가능함
- 아키텍처 적합성 함수를 통해 모듈 DAG 및 레이어 경계를 자동 검증할 수 있음
- AI 에이전트의 코드 생성량이 늘어남에 따라 선언적이고 구조적인 검증이 필수적임
매우 주관적이며, 제가 직접 사용해 본 경험을 바탕으로 작성되었습니다.
이는 처방전이 아닙니다. 저는 아직 표면만 긁어보고 있으며 배울 것이 많습니다.
Part 0에서 저는 코딩은 해결되었으며, 여기에 도달하기 위해 네 가지 요소가 필요하다고 동의했습니다. 하지만 이 중 어느 것도 아키텍처가 '올바른지'를 보장하지는 않습니다. 이 글은 바로 그 간극에 관한 것입니다. 코드 품질은 이제 수렴하는 문제입니다. 기계적으로
| 단계 (Phase) | 훅 (Hooks) | 포착하는 것 (What it catches) |
|---|---|---|
| 0 · 안전성 (Safety) | merged-branch guard, main-clone guard, public-leak guard | 절대 발생해서는 안 되는 커밋 및 푸시 |
| ... | ||
| 두 가지는 사람들이 보통 설정하는 것보다 더 엄격하게 실행됩니다. ruff는 `lint.select = [ |
두 번째 훅(hook)은 린트(lint)가 아닌 구조(structure)를 대상으로 작동합니다: 500라인 파일 제한, 모듈 수준 함수 제한(클래스 내 메서드 선호), 타입이 지정되지 않은 dict[str, object] 금지(dataclass 또는 TypedDict 선호) 등이 이에 해당합니다. 두 가지 크기 제한은 모든 초과 파일을 한꺼번에 제한 범위 내로 강제하지는 않습니다. HEAD 시점에서 이미 제한을 초과한 파일은 그대로 유지되지만, 크기를 줄일 수만 있습니다. 반면, HEAD 시점보다 파일 라인 수가 늘어나거나 모듈 수준의 공개 함수가 늘어나는 커밋은 차단되며, 제한을 새로 넘어가는 행위는 즉시 차단됩니다. 따라서 구조적 제약은 테스트 커버리지(coverage)와 동일한 방식으로 점진적으로 개선(ratchet)됩니다. dict[str, object] 금지는 더욱 엄격하여, 커밋이 추가하거나 변경하는 라인만을 대상으로 플래그(flag)를 표시합니다.
지속 가능한 것은 규칙 그 자체가 아닙니다. 규칙이 어디에 존재하는가가 중요합니다. 산문(prose) 형태—기술, 메모, 혹은 내가 기억해야 할 무언가—로 유지되는 규칙은 산문이 일관성 없이 읽히기 때문에 반복적인 문제가 발생합니다. 훅(hook)으로서 작동하는 동일한 규칙은 반복되지 않습니다. 설계도(blueprint)는 이를 명확히 명시합니다: 지속 가능성은 부식되는 산문이 아니라, 코드와 구조에 인코딩된 강제성(enforcement)에서 옵니다. 품질 계층(quality layer)이 수렴하고 그다음 계층은 그렇지 못한 이유의 대부분이 바로 이것입니다.
최근 제가 더 많이 의존하고 있는 게이트(gate)의 부류는 구조적 게이트, 즉 '아키텍처 적합성 함수 (architectural fitness functions)'라고 불리는 것들입니다. 일반적인 린트 훅은 한 줄을 검사합니다. 적합성 함수는 전체 그래프의 속성을 검사합니다. tach는 모듈 DAG(Directed Acyclic Graph)를 강제합니다. import-linter 계약은 레이어(layer)가 넘지 말아야 할 경계를 넘어가는 것을 금지합니다. 작은 초크포인트 레지스트리(chokepoint registry)는 각 위험한 프리미티브(primitive)를 호출할 수 있는 단 하나의 모듈에 매핑하여, 그 외의 장소에서 발생하는 가공되지 않은 호출(raw call)은 검사를 통과하지 못하게 합니다. 제가 현재 이러한 방식들을 선택하는 이유는 볼륨(volume) 때문입니다. 탐지와 산문 기반의 "X를 하지 않도록 기억하세요" 방식은, 에이전트(agent)가 내가 읽을 수 있는 것보다 더 많은 코드를 생성할 때 한계에 부딪힙니다. 위반을 불가능하게 만드는 구조적 테스트는 누군가가 무엇인가를 읽어야 하는 것에 의존하지 않습니다. 또한 이는 선언적(declarative)입니다. 새로운 규칙은 단순히 눈앞의 인스턴스뿐만 아니라, 아직 존재하지 않는 코드까지 포함하여 해당 클래스 전체를 잡아내는 단 한 줄의 코드가 됩니다.
어떤 게이트도 결정하지 못하는 것
위의 모든 게이트는 코드를 검사합니다. 하지만 그 중 어떤 것도 설계가 올바른지는 검사하지 않습니다.
ruff는 함수가 너무 복잡하다고 알려줄 것입니다. 하지만 그 함수가 존재해서는 안 된다거나, 다른 모듈에 속해야 한다거나, 혹은 그 함수가 위치한 경계(boundary)가 잘못된 위치에 있다는 사실은 알려주지 않습니다. ty는 타입 오류(type error)를 잡아내지만, 우연히 타입 체크를 통과하는 잘못된 추상화(abstraction)는 그냥 통과시켜 버립니다. 커버리지(Coverage)는 코드가 실행되었는지를 알려줄 뿐, 실행해야 할 올바른 코드인지는 알려주지 않습니다. 여러분은 Part 0의 네 가지 요소인 모델(model), 하네스(harness), 결정론적 제약 조건(deterministic constraints), 기술(skills)을 모두 통과하고도, 잘못된 아키텍처를 가진 깨끗하고, 타입이 완벽하며, 커버리지가 완벽한 구현체를 배포할 수 있습니다.
아키텍처를 막아서는 게이트는 아무것도 없습니다. 제 설정에서 그나마 가장 유사한 것은 디자인 컴패니언(design companion)입니다. 이는 아무것도 결정하지 않으므로 게이트는 아니지만, CLI, 핵심 모델, 스캐너(scanners), 오버레이 베이스 클래스(overlay base class), 백엔드 프로토콜(backend protocol)과 같은 핵심 인터페이스(core surfaces)에 변경이 생기기 전에 작동합니다. 이는 다음 아홉 가지 체크리스트를 강제로 검토하게 합니다.
- 레이아웃 (Layout) — 청사진 정렬(blueprint alignment), 컴포넌트 경계(component boundaries), 의존성 방향(dependency direction)
- 계약 (Contracts) — FSM 단계 경계(FSM phase boundaries), 확장 지점 계약(extension-point contracts), 동작 보존(behavior preservation)
- 변경 사항 (Under change) — 테스트 표면(test surface), 회복력 불변량(resilience invariants), 식별자 및 키 정규화(identity and key normalization)
이는 코드 우선(code-first) 단계 이전에 설계 판단을 강제하는 기능(forcing function)입니다.
이 아홉 가지 체크리스트 중 정확히 하나만이 실제 게이트가 될 수 있습니다. 바로 의존성 방향(dependency direction)입니다. tach가 선언된 모듈 DAG(Directed Acyclic Graph)를 기계적으로 강제하기 때문입니다. 즉, 하위 수준 모듈이 상위 수준 모듈을 임포트(import)하면 체크에 실패합니다. 나머지 여덟 가지는 설계가 올바른지에 대해 인간의 추론이 필요합니다. 컴패니언은 체크리스트를 생성할 뿐이며, 에이전트(agent)가 코드 작성 전에 이를 검토하며, 컴패니언이 생성한 그 어떤 것도 무언가를 결정하지 않습니다. 아키텍처 계층화(Architectural layering)는 게이트로 작동할 만큼 충분히 기계적인 유일한 아키텍처 속성입니다. 나머지는 그렇지 않으며, 이는 제가 나중에 메울 수 있는 간극이 아닙니다. 그것이 문제의 본질입니다.
제 자신의 설정에 대해 제가 잘못 알고 있었던 부분이 바로 여기입니다. 저는 제가 아키텍처를 검증하는 사람이 될 것이라고, 즉 8개의 게이트가 없는(ungated) 체크 항목들이 저에게 전달될 것이라고 가정했습니다. 하지만 그렇지 않았고, 그 이유는 아주 평범합니다. 디자인 컴패니언(design companion)이 핵심 표면(core surface)의 모든 변경 사항에 대해 실행되기 때문에, 매 단계마다 승인하는 것은 에이전트가 몇 분마다 저를 방해한다는 것을 의미했습니다. 그것은 확장 가능하지(scale) 않습니다. 그래서 저는 기본적으로 에이전트가 결정을 내리도록 두었고, 에이전트는 자체적인 불확실성을 표시할 때만 저를 불러들입니다. 이는 에이전트가 이미 대부분의 아키텍처 결정을 내리고 있다는 것을 의미합니다. 게이트가 없는 계층이 인간의 영역으로 남지 않은 이유는 그것을 보호할 게이트가 없기 때문입니다. 그것 역시 모델로 미끄러져 들어가고 있으며, 다만 그 과정을 안전하게 만들 게이트가 없을 뿐입니다.
명세(spec)가 아닌 사용을 통해 발견됨
아키텍처가 올바른지 여부를 게이트로 제어할 수 없다면, 유일한 다른 방법으로 알아내야 합니다. 그것을 실행하고 망가지는 것을 지켜보는 것입니다.
제 자신의 README는 이 점에 대해 직설적입니다: 안정적인 제품이 아니며, 망가질 것으로 예상되고, 형태가 변할 것으로 예상되며, 실제 작업에서 매일 도그푸딩(dogfooded)됩니다. 모든 고장 난 엣지(edge)가 실제 티켓(ticket)을 중단시키기 때문에 버그가 빠르게 드러납니다. 마지막 절항이 바로 그 메커니즘입니다. 사용하지 않는 시스템의 설계 결함은 가설에 불과합니다. 하지만 매일 의존하는 시스템의 설계 결함은 중단된 티켓이며, 중단된 티켓은 무시하는 것이 불가능합니다. 배틀 테스팅(Battle-testing)은 설계 이후의 단계가 아닙니다. 그것이 곧 설계 프로세스입니다. 왜냐하면 그것이 아키텍처에 대해 제가 가질 수 있는 유일하게 정직한 신호이기 때문입니다.
오늘날의 형태는 사전에 명세(specced)된 것이 아닙니다. 그것은 여러 단계를 거치며 현재의 Django 프로젝트로 성장했습니다.
살아남은 것들의 순서입니다. 처음에는 ac-multitask라고 불리는 단일 기술로 시작했습니다. 이는 "티켓을 가져와서 처음부터 끝까지 실행한다"는 하나의 모놀리식 (monolithic) 작업이었습니다. 저는 이것을 약 8개의 라이프사이클 (lifecycle) 기술로 나누었으며, 각 단계(티켓 접수, 워크스페이스, 코드, 테스트, 리뷰, 배포, 디버그, 후속 조치)마다 하나씩 할당했습니다. 그리고 이것들이 t3-* 기술 시스템이 되었습니다. 그 후 상태 머신 (state machine)을 갖춘 통합 t3 CLI가 이들을 하나로 모았습니다. 그다음에는 모델 (models), 마이그레이션 (migrations), 실제 영속성 (persistence)을 갖춘 Django 확장 기능이 되었습니다. 그러다 반전이 일어났습니다. 전체 시스템이 Django 프로젝트 자체가 되었고, 기존의 오버레이 (overlays)들은 그 위의 가벼운 패키지로 격하되었습니다. 마지막으로 저는 이 모든 것을 Claude 플러그인으로 패키징했습니다.
사용 사례가 호출을 확정해 준 가장 명확한 사례는 대시보드였습니다. 저는 HTML 대시보드를 구축했습니다. 처음에는 정적으로 생성된 페이지였고, 그다음에는 패널들이 가득한 Django 기반의 웹 UI였습니다. 그리고 약 두 달 동안 매일 이를 실행했습니다. 상태 표시줄 (statusline)은 처음부터 그 옆에 함께 있었습니다. 두 가지를 모두 사용해 보니, 상태 표시줄에 제가 실제로 확인하는 모든 정보가 이미 담겨 있다는 것을 알게 되었습니다. 그래서 저는 대시보드를 떼어내고 상태 표시줄만을 유일한 영속적 UI로 남겼습니다. 그 어떤 게이트 (gate)도 저에게 무엇을 남겨야 할지 말해줄 수 없었습니다. 두 가지를 모두 사용해 보는 경험만이 그것을 알려주었습니다.
모든 형태의 변화는 벽에 부딪혔을 때 발생한 것이지, 벽을 예상한 계획에서 나온 것이 아닙니다. 청사진 (blueprint)은 현재의 형태가 기록되는 곳입니다. 하지만 그것은 첫 번째 코드를 작성하기 전에 규정된 명세 (spec)가 아니라, 사용을 통해 살아남은 것들에 대한 기록으로서, 사용이 확인된 이후에 기록됩니다.
한 단계 위로
우리가 예전에 소프트웨어 엔지니어링 (software engineering)이라고 불렀던 작업은 사라지지 않았습니다. 그것은 한 단계 위로 이동했습니다. 코드를 생성하는 부분 — 작성하고, 타이핑하고, 테스트하고, 깨끗하게 유지하는 것 — 은 이제 네 개의 다리와 게이트가 담당합니다. 남은 것은 아무런 제약이 없는 부분입니다. 즉, 설계가 옳은지 결정하고, 그 설계 속에서 직접 살아보며 그것을 알아내는 일입니다.
그리고 그 부분 또한 변화하고 있습니다. 아키텍처의 올바름을 결정하는 자동화된 검사(automated check)는 없습니다. 그것은 추론(reasoning)의 영역이지, 코드가 스스로 증명할 수 있는 것이 아니기 때문입니다. 에이전트(agent)는 이미 그러한 결정의 대부분을 내리고 있습니다. 왜냐하면 각 결정을 일일이 수동으로 검증하는 것은 확장성(scale)이 떨어지기 때문입니다. 따라서 솔직한 걱정은 아키텍처가 아무런 검증 장치 없이 인간의 업무로 남는 것이 아닙니다. 문제는 모델이 자신이 조용히 점유하고 있는 이 계층(layer)을 책임질 만큼 충분히 뛰어난가 하는 점입니다.
아직은 아닙니다. 모델은 여전히 초보적인 실수들을 저지릅니다. 잘못된 위치에 경계(boundary)를 설정하거나, 잘못된 방향으로 결정을 내리는 식입니다. 이는 숙련된 개발자라면 누구나 즉각적으로 알아채고 경고를 보냈을 법한 유형의 문제들입니다. 저는 에이전트가 진행하는 추론(reasoning)을 읽음으로써 이러한 실수 중 일부를 잡아냅니다. 이러한 인지 능력은 모델에 대한 영리함이나 직관이 아니라, 개발자 경험(developer experience)에서 비롯됩니다. 운이 좋다면 잘못된 결정이 지나갈 때 마침 제가 읽고 있는 것뿐입니다. 즉, 에이전트는 이제 아키텍처 결정의 '양(volume)'은 갖추었지만, 자신의 초보적인 실수를 잡아낼 '판단력(judgment)'은 아직 갖추지 못했습니다. 그리고 이를 잡아내는 도구는 여전히 개발자의 눈이며, 이는 여전히 희소하고 인간적인 영역입니다.
저는 모델이 그 격차를 줄일 것이라고 예상하며, 아마 제가 입 밖으로 내뱉기 불편할 정도로 빠른 시일 내에 그럴 것입니다. 하지만 아직은 아닙니다. 오늘날 모델이 틀린 것을 잡아내기 위해서는 여전히 경험이 필요하며, 그 경험이야말로 제가 검증 장치(gate)에 넘겨줄 수 없는 유일한 부분입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기