채널톡 Go 백엔드에서 AI 코딩 도구가 아키텍처 규칙을 지키지 않는 문제를, CLAUDE.md에 규칙을 더 적는 대신 DDD 리팩터링과 커스텀 아키텍처 테스트로 푼 기록이다. "이 모듈은 저 모듈을 import하면 안 된다"고 문서에 적어도 AI는 종종 무시했고, 결국 사람이 리뷰에서 잡아야 하니 AI를 쓰는 의미가 반감됐다. 결론은 CLAUDE.md는 부탁이고 아키텍처 테스트는 강제라는 것이다.
핵심 포인트- 컨텍스트 파일을 열심히 써도 성능은 거의 개선되지 않고 비용만 오르며, 의미 있는 개선을 보인 유일한 경우는 문서화가 전혀 없는 레포뿐이라는 연구를 인용한다.
- 자연어 규칙이 실패하는 이유는 강제성이 모호하고("지양해주세요"가 절대 금지인지 권장인지 판단 불가), 읽는 것과 따르는 것이 다르며, 규칙이 많아지면 서로 모순되기 때문이다.
- God Object가 모든 것을 import할 수 있는 구조에서 "import하지 마세요"라고 적는 것은 컴파일러가 허용하는 것을 자연어로 금지하겠다는 뜻이다.
- 리팩터링 자체가 목적이 아니라 아키텍처 테스트를 걸 수 있는 구조를 만드는 것이 목적이었다.
- 좋은 아키텍처 테스트가 있으면 AI가 스스로 검증할 수 있고, 검증할 수 있는 AI는 사람 없이도 일할 수 있다.
상세 정리- 리팩터링 전 상태: 한 패키지가 app, command, widget, exposure, role 등 거의 모든 모듈을 의존하는 전형적인 God Object였고, 200개 이상의 파일이 internal import로 얽혀 하나의 변경이 어디까지 영향을 미칠지 추적이 불가능했다.
- 이 구조가 AI에 불리한 이유는 영향 범위를 추적할 수 없다는 점이다. 하나를 바꾸면 200개 이상 파일에 영향이 갈 수 있다.
- 결정론적 제어 넷: Go AST로 프로젝트 고유 규칙을 강제하는 커스텀 아키텍처 테스트, interface와 private impl 패턴으로 잘못된 의존을 컴파일 에러로 전환, 그리고 pre-commit에서 pre-push, CI로 이어지는 가장 빠른 단계 검출이다.
- 구조 원칙: "하나를 알면 전부를 안다"를 구조로 만드는 것으로, repo·svc·handler·infra·saga가 전부 같은 패턴이라 AI가 하나의 예시만 보면 나머지를 예측할 수 있다.
- 디렉터리 레이아웃: 도메인마다 core(다른 서브도메인에 의존하지 않는 핵심)와 선택적 서브도메인들이 있고, 외부에서 접근하는 유일한 창구인 Public Service와 유일한 진입점인 alias.go를 둔다.
- alias.go 패턴: Go의 type alias를 활용해 도메인 외부에서는 여기 정의된 타입만 import할 수 있게 해, 도메인 내부 구현을 완벽히 캡슐화한다. Saga나 다른 도메인이 서브도메인 internal을 직접 import하는 것이 사실상 불가능해진다.
- 핸들러를 도메인 안에 두면 도메인을 통째로 추출할 때 핸들러도 함께 따라간다.
- Saga 패턴: 여러 도메인에 걸친 작업을 조율하며, 앱 설치처럼 세 도메인에 걸친 작업이면 각 도메인의 Public Service를 순서대로 호출하고 하나가 실패하면 이전 단계를 롤백한다. Saga는 Public Service만 의존하고 internal에 절대 접근하지 않는다.
- Package Stutter 금지: 패키지명이 이미 컨텍스트를 주므로 exported 이름에서 반복하지 않는다. stutter가 없으면 인터페이스 이름에 정답이 하나뿐이라 AI의 예측 정확도가 올라간다.
- 실행 순서: 1차에서 God Object를 해체하고 Saga를 신규 도입하며 Public Service와 alias.go 패턴, domain 구조를 만들었고, 2차에서 규칙을 강제했다.
- 1차 과정에서 팀 피드백 10건을 받아 서브도메인 간 호출 전략, Saga의 실패 시 롤백 복구, Context timeout 처리를 구체적으로 논의했다.
- 확장 방식: 하나의 도메인에 v2 컨벤션을 시험 적용해 검증한 뒤, 단순한 도메인부터 서브도메인이 10개인 가장 복잡한 도메인까지 점진적으로 7개 도메인과 9개 Saga 전체로 넓혔다. 핸들러 35개도 의존성을 분석해 재배치했다.
- 이 리팩터링을 다른 피처 개발과 병행하면서 Claude Code에게 맡길 수 있었던 이유가 바로 아키텍처 테스트다. "파일을 옮겨라"는 간단하지만 "옮긴 후 의존성 방향이 맞는지" 판단하려면 규칙이 코드로 존재해야 한다.
- 도구 선택: 기존 아키텍처 테스트 도구들을 검토했지만 프로젝트의 세부 규칙을 표현하기 어려웠고, Go AST 패키지로 소스를 직접 파싱해 컨벤션에 100% 맞는 규칙을 구현했다.
- 규모: 총 약 105K 라인의 커스텀 분석 코드로, 적지 않은 양이지만 자연어로 규칙을 나열하는 것보다 확실한 투자라고 본다.
- 테스트 카테고리: 가장 중요한 것은 도메인 간과 서브도메인 간 의존성 방향 강제이고, 위반 시 어느 서브도메인이 어느 것을 직접 import했는지 에러 메시지로 즉시 드러난다.
- 그 외에 모든 계층에 exported interface와 unexported impl, constructor를 강제하는 카테고리, package stutter와 impl 네이밍·파일 네이밍·handler 프로토콜 임베딩 같은 네이밍 규칙 카테고리가 있다.
- 점진 전환: 처음부터 전체를 ERROR로 두면 기존 코드가 모두 실패하므로 WARNING에서 ERROR로 단계적으로 옮겼고, 지금은 7개 도메인과 9개 Saga가 모두 enforced 상태다.
- 시나리오 비교 1 새 기능 추가: 이전에는 AI가 어디에 만들지 몰라 결과가 불확실했지만, 이제는 기존 도메인 구조를 보고 동일하게 생성하고 테스트가 패턴 위반을 즉시 감지한다.
- 시나리오 비교 2: Saga가 도메인 internal을 직접 import하면 이전에는 AI가 무시하고 코드 리뷰에서 발견해 수동 수정했지만, 이제는 테스트가 실패하며 남기는 메시지를 보고 AI가 자동 수정한다.
- 교훈 셋: DDD의 명확한 경계와 일관된 패턴, 모듈의 자체 완결성은 AI에게도 그대로 적용되며 예측 가능한 구조가 코드 생성 정확도에 직접 영향을 준다. God Object가 있으면 아키텍처 테스트를 걸 수 없어 리팩터링이 먼저다. 한 번에 모든 것을 바꾸려 하면 실패한다.
- 다음 계획: 도메인 구조가 정리돼 서비스 간 영향도를 자동 파악하는 지식 시스템 구축이 쉬워졌고, 자체 완결적 모듈이 에이전트 단위 작업 위임의 기반이 된다고 밝힌다.
왜 읽나AI가 프로젝트 규칙을 자꾸 어긴다는 문제를 프롬프트로 풀려던 팀에게, 구조를 먼저 정리하고 규칙을 테스트로 강제하는 순서와 그 투자 규모를 실제 수치로 보여준다.