
TDD → Clean Architecture → 동적 언어의 제어 ── AI가 안전하게 자율 주행할 수 있는 기반을 타입이 없는 레거시
요약
Perl 기반의 대규모 레거시 시스템에서 AI를 활용한 개발 효율을 극대화하기 위한 품질 보증 전략을 다룹니다. TDD, Clean Architecture, CI 환경 구축을 통해 AI가 생성한 코드의 안정성을 확보하는 방법을 제시합니다.
핵심 포인트
- AI 활용 시 운영 부채를 방지하기 위한 테스트 코드 가드레일 구축 필수
- TDD와 Clean Architecture를 통한 테스트 용이한 구조 설계
- 정적 분석이 어려운 동적 언어에서 결정론적 검증(테스트)의 중요성
- 설계 문서(md)와 CI를 연동한 AI 자율 주행 개발 루프 구현
전제
Perl + 10년 이상 운용된 레거시 시스템
거대한 코드베이스 + 테스트 기반도 없음
결론
- AI 기반 구축이 AI 개발에서의 품질 보증에 효과적임
- 기반 = TDD · Clean Architecture · 동적 언어의 제어 + 지시서 정비
- 엔지니어의 설계 능력이 AI 개발의 효율을 극대화함
저의 경우, 개발 체제에 비해 상당히 큰 규모의 개발이 필요하게 된 점,
AI 활용을 추진하려는 방침이 있었다는 점 등으로 인해 AI를 팀에서 운용하여 공수를 크게 절감할 필요가 있었습니다.
AI 활용을 통한 바이브 코딩(Vibe Coding)은 공수를 파괴적으로 절감할 수 있지만,
운영 부채를 남기기 쉬운 단점이 있어, 이대로는 업무에 채택할 수 없습니다.
그래서 AI에 대해 GitHub나 Anthropic의 웨비나 등을 통해 공부했습니다.
결과적으로 제가 특히 공감했고 또한 공통적이었던 것은 테스트 코드에 의한 가드레일(Guardrail)이 중요하다는 이야기였습니다.
Claude의 베스트 프랙티스
https://code.claude.com/docs/ja/best-practices#give-claude-a-way-to-verify-its-work -
GitHub Copilot의 튜토리얼
위에도 기재되어 있는 것처럼, 정적 분석이나 테스트 코드(결정론적 분석 · 검증)를 확실히 정비하는 것이 AI 활용의 대전제입니다.
특히 Perl에는 타입(Type)이 없기 때문에, 정적 분석으로 방지할 수 있는 범위도 한정적입니다.
결정론적인 검증을 테스트 쪽으로 돌릴 필요가 더욱 강하게 있었습니다.
덧붙여, 기존 시스템에는 테스트 기반 자체가 없었습니다.
Mock의 메커니즘, 커버리지(Coverage) 측정, CI 정비를 포함하여 모두 처음부터 준비해야 했습니다.
결정론적인 분석 · 검증의 이점을 극대화함으로써 AI 활용도 극대화할 수 있습니다
그것이 제가 생각한 AI 개발 기반의 사고의 근저입니다.
먼저 큰 틀인 TDD (Test-Driven Development)에 대하여
이 개발 기반에서는 테스트(결정론적 검증)가 가장 중요하므로, 반드시 테스트가 작성되는 개발 기법이기 때문에 선택했습니다.
그림1 TDD의 기본 개발 사이클
AI의 지시서(Agents.md)에서는 위 내용을 철저히 하도록 지시하고 있습니다.
또한, 테스트의 망라성은 커버리지로 정량화하여 AI가 자기 개선(리팩토링)할 수 있도록 합니다.
작업별 PR에 대해서도 GitHub Actions를 통한 CI로 리그레션(Regression)을 구조적으로 감지할 수 있게 합니다.
CI에는 코딩 규칙을 테스트화한 것이나 모든 Unit 테스트 등을 실행합니다.
(동적 언어의 제어에서 설명할 테스트도 여기서 실행됩니다)
테스트 중심이 되기 때문에, md 파일로 테스트 계약표(Domain, Repository) 또는 플로우차트(Usecase, Driver)를 작성합니다.
테스트 계약표는 번잡한 Domain 등에서 경계값이나 정상계 · 이상계 관점을 망라하고, 플로우차트는 오케스트레이션하는 계층의 유스케이스(Usecase) 망라를 가시화합니다.
이를 통해 사전에 테스트의 정확도를 높이면서 인간이 핸들링하기 쉽게 개발을 진행할 수 있게 됩니다.
그림2 설계 문서를 기점으로 한 AI 자율 주행 루프와 CI 게이트
다음은 아키텍처입니다.
관심사 분리(Separation of Concerns)를 통해 테스트하기 쉬운 구조를 구조적으로 담보하고 싶습니다.
심장부가 되는 로직을 도메인(Domain)으로 하여 의존성을 최소한으로 하고, 엄격한 테스트를 고속으로 수행할 수 있는 아키텍처를 고민한 결과, Clean Architecture에 도달했습니다.
또한, 이번에 제가 도입하는 시스템은 오랫동안 레이어드(Layered) 아키텍처가 채택되어 왔기 때문에, 그로부터 심리스(Seamless)하게 이 기반으로 갈아타기 쉽도록 레이어드 구조(Page/Logic/Model)에 Clean Architecture를 도입하는 결과가 되었습니다.
구체적으로는 기존 레이어 자체는 거의 그대로 유지하면서, DIP (의존성 역전)를 통한 관심사 분리를 도입하여 Repository 계층을 추가하고, 그곳을 이번에 구축하는 기반과 외부의 유일한 교류점으로 만들었습니다.
마지막으로, 의존의 방향을 모두 Domain을 향하도록 통일했습니다.
녹색 = 결정론적 · 고속 (AI가 자율 주행 가능) / 적색 = 비결정론적 · 저속 (인력 필요)
그림3 레이어드 기반의 Clean Architecture 구성
이 아키텍처도 독자적인 것이므로 중요하기 때문에, Agents.md에서 준수하도록 합니다.
특히 처음에는 지시서의 비중을 무겁게 하고, 샘플 코드도 있는 편이 좋습니다.
어느 정도 개형이 완성된 단계에서는 지시서와 함께 AI가 거의 오해 없이 처리를 작성해 주게 됩니다.
마지막으로 이번의 핵심인 동적 언어의 제어입니다.
구체적으로는 기저 클래스(Base Class)의 작성, Mock 작성의 스크립트화, 덕 타이핑(Duck Typing)의 테스트, use 체커 등입니다.
이를 통해 AI에게도 품질 측면에서 문제가 발생하기 쉬운 동적 언어의 특성(비결정론적인 AI × 동적 언어)을 결정론적(Deterministic)으로 검출합니다.
이러한 요소들을 이번 사례처럼 기저 클래스(Base Class)나 테스트로 만들지 않고 컨텍스트(Context)로 구성할 수도 있지만, 컨텍스트로 두면 비결정론적이며 품질 저하 및 리뷰 부하로 이어지기 때문에, 컨텍스트가 아닌 철저히 결정론적인 메커니즘으로 구조화하여 대응합니다.
각 제어 기구는 아래 표와 같이 각각의 계층(Layer)에 적용됩니다.
표 1 계층과 제어 기구의 대응
◎=순수성의 정적 보증까지 포함 ○=적용 −=대상 외
| 계층 | 기저 클래스 | Mock 생성 | 덕 타이핑 (Duck Typing) | use 체커 |
|---|---|---|---|---|
| Page (Driver) | − | ○ 이용 측 | ○ Page → Logic | ○ |
| ... |
최하단의 외부(Model·Legacy)가 모두 「−」인 것처럼, 이러한 제어가 작동하는 곳은 새로 만든 내부뿐입니다.
Perl에서는 아래와 같은 상황이 흔히 발생할 수 있습니다.
# 해시(Hash)에 원하는 키를 채워 넣기만 하면 됨
# pet => { id => undef, name => undef, hunger => undef } 이것을 상정함
my $pet = { id => 1, name => '포치', hunger => 5 };
...
이를 $pet라는 객체로 만들고, 기저 클래스를 통해 존재하지 않는 필드에 대한 데이터 쓰기 및 참조를 에러로 처리합니다.
# 기저 클래스를 상속한 객체로 만들면
# ATTRIBUTES를 id, name, hunger로 설정
my $pet = Domain::Pet->new({ id => 1, name => '포치', hunger => 5 });
...
유닛 테스트(Unit Test)용으로 Mock(외부 객체 대신 모의 외부 객체)을 작성합니다.
이는 Mock을 결정론적으로 생성하겠다는 의도입니다.
상수 계열 + Domain은 그대로 테스트에 사용하기 때문에(지시서의 규칙), 대상은 Repository 계층과 Logic 계층이 됩니다.
수동으로 작성한 Mock은 실제 객체가 변경되어도 따라가지 못하므로, 테스트는 통과(Green) 상태를 유지하면서 실제 코드와 괴리가 발생합니다.
실제 클래스의 정의로부터 기계적으로 생성하면, 괴리가 발생하는 즉시 생성물이 바뀌어 테스트가 실패(Fail)하게 됩니다.
또한, 이 차이를 검출하는 테스트를 작성함으로써 이러한 생성 누락도 검출할 수 있습니다.
동적 언어는 호출할 인터페이스(Interface)가 없을 때 런타임 에러(Runtime Error)로 발견되지만, 이 테스트를 통해 테스트 실행 시 및 CI 단계에서 검출할 수 있게 됩니다.
특히 이번에는 DIP(의존 역전 원칙) 활용 + 동적 언어 조합이므로, Logic(Usecase)에서 호출할 때 인터페이스가 맞는지 체크하지 않으면, 테스트가 모두 통과(All Green)하더라도 통합 테스트를 수행하기 전까지 에러가 발생하는 사고가 일어날 수 있습니다.
이를 미연에 방지할 수 있습니다.
이는 Mock 작성 스크립트 + 유닛 테스트를 통해 담보됩니다.
Mock은 실제 클래스의 정의로부터 생성되므로, 호출 측의 유닛 테스트가 그대로 인터페이스 일치 여부의 검증이 됩니다.
Logic → Repository, Page → Logic의 각 호출에 대해, 유닛 테스트 시점에서 덕 타이핑(Duck Typing)의 안전성이 보장되는 형태입니다.
사전에 use 누락 및 불필요한 use를 체크하여, 문제가 있으면 에러로 처리합니다.
이를 통해 런타임 시의 use 에러와 AI의 수정 누락으로 인한 불필요한 use를 결정론적으로 제어합니다.
또한, Domain 계층 경계 위반을 기계적으로 검출할 수 있습니다.
이를 통해 Domain 계층의 순수성을 정적으로 지킬 수 있습니다.
use에 나타나지 않는 DI 경계 부분은 덕 타이핑 테스트로 담보하고 있습니다.
타입이 없기 때문에 발생하는 런타임 에러를 결정론적으로 사전에 검지할 수 있도록 하여, AI가 스스로 수정할 수 있게 만드는 것이 핵심입니다.
-
AI에게 맡길 수 있는 범위가 넓어졌습니다 (신규 코드의 거의 전부)
-
이를 통한 공수 절감 (수동 작업 시의 공수 산정 대비, 실제 공수를 약 50% 정도 절감하는 데 성공했습니다)
-
목표치가 공수 산정 시의 약 50% 절감이었으므로 달성
-
이를 통한 공수 절감 (수동 작업 시의 공수 산정 대비, 실제 공수를 약 50% 정도 절감하는 데 성공했습니다)
-
리뷰의 관점이 "코드가 올바른가"에서 "테스트 관점이 타당한가"로 옮겨갔습니다.
-
런타임 시에만 알 수 있었던 에러가 CI에서 멈추게 되었습니다.
-
운영 구현 시 멤버 간의 숙련도 차이를 억제할 수 있었습니다.
-
AI에게 물어보면 알 수 있으므로 캐치업(Catch-up) 리드 타임도 단축되었습니다.
솔직히 말하자면, 이 기반은 만능이 아닙니다.
의식(Ritual)은 무겁습니다
설계 문서, 테스트, Mock 생성 등 기능당 작업량은 확실히 늘어납니다.
하지만 AI가 테스트를 작성하기 때문에, 의식(Ritual)의 무게는 실용적인 범위 내에 머물고 있습니다.
이번에는 기존 레거시 시스템에 대해 큰 기능을 추가하는 경우를 가정하여 글을 쓰고 있습니다.
규모가 작더라도 신규 기능이라면 이 아키텍처(Architecture)를 적용할 가치가 있다고 생각합니다.
기반으로서 여러 기능에서 재사용하며, 초기 비용을 회수한다는 전제의 투자가 됩니다.
테스트는 작성된 만큼만 보호한다
커버리지(Coverage)가 100%라도, 테스트 관점이 틀렸다면 초록색(Pass) 상태로 틀린 결과를 내놓습니다.
즉, 인간의 업무는 코드를 작성하는 것에서 테스트 관점을 설계하는 것으로 옮겨갔다고 할 수 있습니다.
=> 이 부분에 대해서는 레이어(Layer)별 테스트의 철저한 분담 및 테스트 케이스의 플로우차트(Flowchart)화를 통한 가시화, 별도의 AI를 통한 리뷰로도 대응해 나갈 것입니다.
외부는 여전히 수작업
그림 3의 빨간 영역(기존 레거시·DB)은 Playwright와 수동 테스트에 의존하고 있습니다.
이 기반이 결정론적(Deterministic)으로 보호할 수 있는 것은 어디까지나 새로 만든 내부뿐입니다.
언어나 프레임워크에 구애받지 않고 사용할 수 있다고 생각하는 점은 다음 3가지입니다.
- 타입(Type)이 없다면, 결정론적으로 실패하는 메커니즘을 만든다.
- AI를 위한 가드레일(Guardrail)은 컨텍스트(Context, 조언)가 아니라 실행되는 메커니즘(판정)에 둔다.
- 결정론적으로 검증할 수 있는 범위를 넓히는 것이 곧 AI에게 맡길 수 있는 범위를 넓히는 것이다.
결국, 새로운 것은 아무것도 없었습니다.
TDD도 Clean Architecture도 수십 년 전부터 있었던 정석입니다.
하지만 정석을 지키려 한 것이 아니라, AI 활용의 극대화를 도모하다가 이곳에 다시 착륙했다는 것이 이번의 발견입니다.
AI에게 맡길 수 있는 범위는 당신이 만든 구조의 강도로 결정됩니다
그렇기에 AI 시대에 엔지니어의 설계 능력의 가치는 더욱 높아지고 있다고 생각합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기