AI를 탓하기 전에 아키텍처를 점검하세요
요약
AI 코딩 에이전트가 생성한 코드가 기능적으로 작동하더라도, 아키텍처적 결함은 발견하기 어렵습니다. AI 지원 개발에서는 단순히 코드 구현을 넘어, 프로젝트의 구조와 책임 분리(Separation of Concerns)에 대한 깊은 이해가 필요합니다. 명확한 도메인 모델과 사용 사례를 중심으로 코드를 재구성하는 것이 중요합니다.
핵심 포인트
- AI 에이전트 결과물은 기능적일 수 있으나 아키텍처적 결함이 있을 수 있다.
- 개발 시에는 책임 분리(SoC)와 명확한 도메인 모델링에 집중해야 한다.
- PHP, Symfony 등 타입 시스템을 활용하여 개발의 안정성을 높이는 것이 좋다.
- 비즈니스 로직은 컨트롤러가 아닌 도메인 객체나 사용 사례 계층에서 관리되어야 한다.
AI 코딩 에이전트에게 작은 작업을 부여해 봅시다: 기존 구독 시스템에 바우처 활성화 기능을 추가하는 것입니다.
생성된 코드는 그럴듯해 보일 수 있습니다. 엔드포인트는 작동하고, 응답은 정확하며, 성공 경로(happy-path) 테스트도 통과합니다. 하지만 바우처 규칙이 이제 컨트롤러 안에 존재하게 되고, 데이터베이스 쿼리와 결제 제공업체 호출 옆에 위치하게 됩니다.
모델이 나빴을까요? 어쩌면 그럴 수도 있습니다. 저는 또한 리포지토리가 제시한 예시들도 살펴볼 것입니다.
만약 유사한 규칙들이 이미 컨트롤러, 서비스, 영속성 콜백(persistence callbacks)에 존재한다면, “기존 아키텍처를 따르라”는 지침은 해석의 여지를 많이 남깁니다. 그 프로젝트에 합류하는 사람도 질문을 할 것입니다.
이것이야말로 AI 지원 개발에서 아키텍처가 더 많은 주목을 받아야 한다고 생각하는 부분입니다. 프로젝트의 구조는 다음 변경 사항에 대한 컨텍스트를 제공합니다. 명확한 책임(Clear responsibilities)은 그 변경 사항을 설명하기 쉽게 만들고, 실수(mistakes)를 감지하기도 더 쉽게 만듭니다.
PHP와 Symfony가 여기서 유용한 예시를 제시합니다. PHP는 논의가 시작되기도 전에 무시되는 경우가 있지만, 타입 지정 인터페이스(typed interfaces), 의존성 주입(dependency injection), 정적 분석(static analysis)은 우리가 활용할 것이 충분하도록 해줍니다.
하나의 기능, 여러 위치에 배치하기
구독 애플리케이션을 예로 들어 봅시다. 고객들은 이미 결제 제공업체를 통해 구독을 구매할 수 있습니다. 이제 우리는 다음 기능을 원합니다:
고객이 바우처를 사용하여 구독을 활성화할 수 있도록 허용한다.
이 문장은 비즈니스 결정들을 열어둡니다. 바우처가 특정 플랜을 커버하는 것일까요? 한 번 이상 사용할 수 있을까요? 고객이 이미 활성 구독을 가지고 있다면 어떻게 될까요?
이 예시를 위해, 명확한 규칙을 선택해 봅시다: 바우처는 적격한 하나의 구독을 활성화하며, 정의된 시간에 만료되고, 한 번만 사용될 수 있습니다. 이미 활성화된 구독은 거부되어야 합니다. 바우처 활성화 시 고객에게 요금이 청구되어서는 안 됩니다.
이것들은 예시를 위한 가상의 요구사항이며, 실제 운영 시스템의 보고서는 아닙니다.
관련 코드가 다음과 같다고 가정해 봅시다:
src/
Controller/SubscriptionController.php
Service/SubscriptionManager.php
...
이 디렉토리들 자체에는 본질적으로 잘못된 것이 없습니다. 어려움은 책임들이 중첩될 때 발생합니다. 컨트롤러가 자격 여부를 확인하고, 매니저가 상태를 변경하며, 결제 헬퍼도 구독 정보를 영속화(persist)하기 때문입니다.
이제 에이전트는 바우처 검증을 추가할 수 있는 몇 가지 그럴듯한 위치를 갖게 됩니다. 더 많은 파일을 읽어보면 의도된 관례(convention)가 드러날 수도 있습니다. 또는 세 가지 양립 불가능한 관례가 드러날 수도 있습니다.
프롬프트는 원하는 설계를 설명해 줄 수 있지만, 그 설명은 코드베이스가 도움을 줄 수 있는 작업을 수행하고 있는 것입니다.
변경 사항에 명확한 위치를 부여하세요
비즈니스 로직이 충분하여 이를 정당화할 수 있는 애플리케이션의 경우, 다음과 같은 구조를 고려해 볼 것입니다:
src/Subscription/
Domain/
Subscription.php
...
애플리케이션 사용 사례(use case)가 활성화를 조정합니다. 도메인 객체나 도메인 정책이 비즈니스 규칙을 강제합니다. 리포지토리 인터페이스는 이러한 규칙과 사용 사례에 필요한 영속화 작업을 설명합니다. Doctrine은 경계면(edge)에서 이를 구현합니다.
HTTP 컨트롤러는 요청을 처리하고, 사용 사례를 호출하며, 결과를 응답으로 매핑합니다. 이는 바우처 자격 여부의 두 번째 구현체가 되어서는 안 됩니다.
이는 Alistair Cockburn의 헥사고날 아키텍처(hexagonal architecture)가 제시하는 분리를 따릅니다. 즉, 애플리케이션은 포트(port)와 어댑터(adapter)를 통해 외부 시스템과 통신합니다. 폴더 이름은 우리의 선택이며, 유용한 속성은 비즈니스 동작이 HTTP 및 데이터베이스 어댑터와 독립적으로 실행될 수 있다는 점입니다.
우리의 예시에서 의도된 소스 코드 종속성(dependency)은 다음과 같습니다:
UI ------------> Application ------------> Domain
Infrastructure -------------------------> Domain
인프라스트럭처 클래스는 내부로 정의된 인터페이스를 구현합니다. 애플리케이션의 와이어링(wiring)이 이러한 인터페이스들을 구체적인 어댑터에 연결합니다.
런타임 시, 사용 사례는 인터페이스를 통해 Doctrine 지원 리포지토리를 호출할 수 있습니다. 그 소스 코드는 여전히 Doctrine을 가져올 필요가 없습니다.
Symfony의 autowiring과 인터페이스 별칭(interface aliases)이 이러한 와이어링을 지원합니다. 생성자에서 인터페이스를 요청할 수 있으며, 컨테이너가 구성된 구현체를 제공합니다. Symfony는 비즈니스 경계를 저희 대신 선택해주지 않습니다.
현재 상품권 활성화를 담당하는 에이전트는 검사해야 할 이름 있는 유스케이스와 따라야 할 의존성 규칙을 갖게 됩니다. 리뷰어들은 다음과 같은 구체적인 질문을 할 수 있습니다: 이 패치는 도메인에 HTTP 의존성을 추가한 이유가 무엇인가요? 아무에게도 청구해서는 안 되는 기능에 결제 게이트웨이가 왜 관련되어 있나요?
이름은 의미를 가져야 합니다
DDD(Domain-Driven Design)는 우리가 확립하도록 요구하는 공유 언어 덕분에 여기서 유용합니다.
이 예시에서, 상품권은 권한을 사용합니다. 결제는 금융 거래를 기록합니다. 둘 다 활성 구독으로 이어질 수 있지만, 규칙은 다릅니다. 두 작업 모두를 process()라고 부르는 것은 구현체가 보존해야 할 구분을 숨깁니다.
ActivateSubscriptionWithVoucher와 같은 이름은 그 구분을 개발자와 에이전트가 모두 접하게 하는 곳에 배치합니다. CommonService는 그들에게 다룰 것이 적습니다.
이름들 역시 정의가 필요합니다. 만약 “고객(customer)”이 한 모듈에서는 계정을 의미하고 다른 모듈에서는 청구 연락처를 의미한다면, 일관성 있어 보이는 클래스 이름이 불일치를 감출 수 있습니다. 짧은 용어집과 의도된 동작의 예시들은 코드에 유용한 보조 자료가 됩니다.
제 기대는 이것이 에이전트가 추론해야 하는 결정을 줄여줄 것이라는 것입니다. 저는 클래스 이름을 변경하거나 DDD를 도입하는 것만으로 모델 정확도의 측정 가능한 개선을 주장하는 것은 아닙니다.
위반 사항을 가시화하세요
Domain이라는 이름의 디렉터리가 누군가 그 안에 HTTP 클라이언트를 가져오는 것을 막을 수는 없습니다.
Deptrac은 CI를 포함하여 구성된 계층과 허용되는 관계에 따라 PHP 의존성을 확인할 수 있습니다. 위의 구조의 경우, 저는 도메인 종속성이 애플리케이션, 인프라스트럭처, UI 코드에 있는 것을 금지하고, 인프라스트럭처나 UI에서 애플리케이션으로 가져오는 것을 거부할 것입니다.
설정은 관련 네임스페이스와 외부 패키지를 포함해야 합니다. 분류되지 않은 코드는 누락을 초래할 수 있습니다. 녹색 아키텍처 검사(green architecture check)는 분석한 의존성이 우리가 설정한 규칙을 만족한다는 것만을 증명합니다.
행동에는 별도의 테스트가 필요합니다. 바우처 활성화의 경우, 만료된 바우처가 실패하는지, 적격한 바우처가 구독을 활성화하는지, 그리고 반복적인 사용이 또 다른 권한을 부여하지 않는지를 보여주는 테스트를 원할 것입니다. 애플리케이션 테스트는 또한 결제 게이트웨이가 호출되지 않음을 검증해야 합니다.
마지막 요구사항에 숨겨진 영속성(persistence) 문제가 있습니다. 두 요청 모두 어느 쪽도 결과를 기록하기 전에 동일한 미사용 바우처를 읽을 수 있습니다. isRedeemed()를 확인하는 도메인 메서드만으로는 그 경쟁 조건(race)을 막을 수 없습니다.
영속성 구현에는 적절한 원자적 업데이트(atomic update) 또는 잠금 전략이 필요하며, 구독 활성화와 사용은 일관되게 커밋되어야 합니다. 저는 경쟁적인 사용 시도를 위한 통합 테스트를 포함할 것입니다. 깔끔한 계층 구조가 트랜잭션 의미론(transaction semantics)을 결정해주지는 않습니다.
이러한 검사들은 에이전트에게 변경 과정에서 구체적인 피드백을 제공합니다. 개발자는 여전히 테스트가 올바른 동작을 표현하는지, 그리고 검사 자체의 변경 사항을 검사할지 결정해야 합니다. 실패하는 의존성 규칙을 약화시켜 “수정”하는 에이전트는 아키텍처 계약(architecture contract)을 변경한 것입니다.
저장소 지침은 간결하고 코드와 연결되어야 함
AGENTS.md 또는 CLAUDE.md 파일은 에이전트에게 어디서 시작해야 하는지 알려줄 수 있습니다. 이 예시의 경우, 유용한 지침은 구독 모듈을 가리키고, 의존성 방향을 설명하며, 검사를 실행하는 명령어를 식별할 것입니다.
또한 바우처 활성화가 고객에게 결제비를 청구하지 않는다는 규칙과 이를 검증하는 테스트 링크를 포함할 것입니다. 이는 “항상 깨끗한 코드를 작성하라”는 것보다 훨씬 더 실용적입니다.
그렇다면 저는 에이전트에게 기존 활성화 경로를 검사하고 편집하기 전에 변경할 것으로 예상되는 파일들을 제안하도록 요청할 것입니다. 그 계획은 사용 사례(use case), 도메인 규칙, 영속성 동작(persistence behavior) 및 테스트를 고려해야 합니다. 만약 그것이 전체 기능을 컨트롤러에 넣는다고 제안한다면, 패치가 커지기 전에 논의할 구체적인 내용이 있다는 뜻입니다.
연구는 여기서 유용하고 좁은 참고점을 제공합니다. Microsoft의 CodePlan 논문은 레포지토리 수준 코딩을 계획 문제로 다루며, 의존성 분석과 변경 사항이 다른 코드에 미치는 영향을 분석하는 것을 결합합니다. 그 평가는 C# 패키지 마이그레이션 및 Python 편집을 다룹니다. 이것이 육각형 PHP 애플리케이션(hexagonal PHP applications)이 자동으로 더 나은 AI 결과를 낸다는 증거는 아닙니다.
제가 실질적으로 추론하기로는, 의존성은 검사하고 검증할 만큼 충분히 명시적이어야 합니다. 이 기사의 바우처 예제는 그 아이디어를 보여주지만, 그것은 벤치마크가 아닙니다.
깨지기 쉬운 경계부터 시작하세요
저는 AI 에이전트를 수용하기 위해서만 이 전체 구조를 작은 CRUD 애플리케이션에 도입하지 않을 것입니다. 추가되는 인터페이스와 레이어는 이를 유지 관리하는 사람들에게 작업을 만듭니다.
기존 프로젝트의 경우, 반복되는 문제 하나부터 시작하세요. 컨트롤러가 계속해서 구독 규칙을 가져온다면, 하나의 일관된 사용 사례를 명확한 진입점(entry point) 뒤에 배치하세요. 그 비즈니스 개념에 이름을 붙이고, 규칙에 대한 테스트를 추가하며, 중요한 의존성 경계를 강제하세요.
다음 관련 변경 사항으로 에이전트를 시도해 보세요. 그것의 패치가 그 진입점을 사용하는지, 테스트가 우리가 신경 쓰는 실수를 잡아내는지, 그리고 리뷰에서 여전히 얼마나 많은 설명이 필요한지를 살펴보세요. 자리를 얻을 때만 구조를 확장하세요.
더 나은 모델이나 더 명확한 프롬프트(prompt)가 도움이 될 수 있습니다. 또한 레포지토리가 무엇을 가르치고 있는지 확인해 보세요: 동작이 어디에 속하는지, 어떤 의존성이 허용되는지, 그리고 잘못된 변경 사항이 어떻게 거부되는지 말입니다.
바우처 기능의 경우, 성공이란 비즈니스 규칙이 한 곳에 명확하게 존재하고, 경쟁 요청 하에서도 사용(redemption)이 안전하며, 다음 개발자가 모든 것을 찾을 수 있다는 의미입니다. 이는 에이전트가 패치(patch)를 작성할 때도 여전히 우리의 책임으로 남아 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기