리뷰에서 "여기서부터 DB를 직접 건드리지 마세요"라고 계속 말하지 않기 — 설계 규칙을 코드에 내장하여 AI에게도 지키게 하기
요약
본문은 소프트웨어 설계 규칙이 문서(README 등)에만 존재할 경우 쉽게 위반됨을 지적합니다. API가 리포지토리를 직접 호출하는 등의 의존성 위반은 아키텍처의 핵심 문제입니다. 따라서 이러한 구조적 규칙은 테스트 케이스로 내장하여 기계적으로 강제해야 한다고 주장합니다.
핵심 포인트
- 설계 규칙은 문서화만 할 것이 아니라 코드 자체에 내장되어야 합니다.
- API는 서비스 레이어를 거쳐 리포지토리를 사용해야 하는 의존성 흐름이 중요합니다.
- AI가 코드를 작성하는 시대에는 수동적인 리뷰나 기억에 의존할 수 없습니다.
- 아키텍처 규칙을 테스트 케이스로 옮겨 자동화된 검증 시스템을 구축해야 합니다.
AI에게 "주문 목록 API에 건수(件数)도 반환해 줘"라고 요청하면, API 핸들러에서 리포지토리(Repository)를 직접 호출하는 코드가 돌아오고, 테스트까지 제대로 통과합니다.
'API에서는 services를 거쳐야 한다'는 규칙은 README에도 있고 AI용 지시 파일에도 적혀 있음에도 불구하고 말입니다.
- 설계 규칙은 코드에 내장되어야 지켜진다
- 위반하면 테스트가 실패하고, 실패 메시지에 수정 방법을 명시한다
- 기계로 판별할 수 있는 규칙부터 내장한다
README에 적힌 규칙이 코드 상에서 지켜지지 않는 것은, 규칙을 어디에 두느냐(배치)에 원인이 있었습니다.
예를 들어, EC 사이트의 주문 관련 Web API를 가정해 보겠습니다.
코드는 3개의 레이어(층)로 나뉘어 있습니다.
app/api
: HTTP 요청을 받아 응답을 반환하는 부분
app/services
: 권한 체크와 비즈니스 규칙 처리app/repositories
: DB 접근
설계로 결정된 규칙 중 하나는 'API는 리포지토리를 직접 사용하지 않는다'입니다.
이처럼 어떤 레이어가 어떤 레이어를 사용할 수 있는지(import 할 수 있는지)에 대한 정해진 순서를, 본문에서는 **의존성 규칙(Dependency Rule)**이라고 부릅니다.
여기서 허용되는 것은 api → services → repositories의 일방향 흐름뿐입니다.
권한 체크나 비즈니스 규칙을 services에 모아두었기 때문에, api에서 repositories를 직접 호출하면 그 체크 과정을 건너뛸 수 있게 됩니다.
하지만 이 화살표(흐름)를 알고 있는 것은 설계도를 그린 사람뿐인 경우가 많습니다.
Python에서는 app/api/orders.py에 다음 1줄이 추가되어도 아무 말 없이 작동합니다.
from app.repositories.order_repository import count_orders
모든 코드가 하나의 파일에 몰려 있어도, 언어나 프레임워크는 불평하지 않습니다.
설계 규칙은 코드를 실행하는 입장에서 아무런 의미가 없습니다.
이러한 1줄의 위반은 작게 보일 수 있습니다. 하지만 매일 조금씩 쌓이면, 실제 의존성은 어느새 설계도와는 너무 달라집니다.
'그려진 패턴이 코드 상에서 보이지 않는다면, 그 패턴은 존재하지 않는 것과 같다'라는 문구를 읽고 크게 공감했습니다.
그렇다면 이 규칙은 지금 어디에 있을까요?
많은 팀에서는 README나 Wiki, AI용 지시 파일(CLAUDE.md 등), 그리고 코드 리뷰에서 찾을 수 있습니다.
배치 위치별로 위반했을 때 누가 언제 알아차리는지 비교해 보았습니다.
| 배치 위치 | 위반 시 누구가 알게 되는가 | 언제 알게 되는가 |
|---|---|---|
| README/Wiki | 읽고 기억하고 있던 사람 | 기억하고 있다면, 작성할 때 |
| ... |
위의 3가지 방법 모두 '그때 기억하는지, 알아차리는지'에 의존합니다.
AI가 코드를 작성하는 양이 늘어나면서, 리뷰에서 보는 차이점(diff)도 늘어납니다. 하루에도 몇 개의 PR을 보면, import 1줄의 위반은 놓치기 쉽습니다.
마지막 줄의 테스트만은 아무도 기억하지 못해도 매번 실패합니다.
그렇다면 규칙을 테스트 쪽으로 옮기면 됩니다.
옮기는 과정을 정리해 보니, 3단계가 있었습니다.
첫 번째 단계는 설계도에서 사용하는 용어와 디렉토리 이름을 일치시키는 것이었습니다. 설계상 'services'라고 부르는 것이 코드에서는 helpers/나 manager.py인 경우가 있습니다.
이렇게 되면 읽는 사람도, AI도 어떤 것이 설계도의 어느 상자에 해당하는지 연결하기 어렵습니다.
# Before: 설계도의 상자와 디렉토리가 대응하지 않음
app/
├── handlers/ # API의 입구?
...
After 쪽은 디렉토리만 봐도 설계도의 3가지 상자가 구분됩니다. 게다가 배치 위치가 설계 용어와 일치하면, 규칙을 그대로 테스트에 작성할 수 있는 형태가 됩니다.
'app/api' 파일은 'app.repositories'를 import 하지 않는다'라고 적기만 하면 되기 때문입니다.
다만, 눈에 보인다고 해서 위반하려는 의지가 생기면 위반할 수 있습니다.
의존성 규칙은 표준 라이브러리인 ast와 pathlib, 그리고 pytest만으로 테스트가 가능했습니다.
진행한 작업은 'app/api'
아래 파일의 import 문을 읽어보고, app.repositories가 포함되어 있다면 제거하기만 하면 됩니다.
import ast
from pathlib import Path
def imported_modules(path):
...
ast는 Python 코드를 구문 트리(syntax tree)로 읽는 표준 라이브러리로, 파일을 실행하지 않고도 import 문만을 추출할 수 있습니다.
직접 테스트해보니, 아까의 1줄은 제대로 제거되었고, 작성 방식을 바꿔도 결과는 동일했습니다.
from app import repositories
→ 제거됨 -
..repositories
로부터의 상대 import → 제거됨 -
from ..services import order_service
→ 통과
확인하는 것은 문자 그대로의 import 문만이기 때문에, importlib으로 문자열에서 읽어오는 것 같은 동적(dynamic) import까지는 잡아내지 못합니다.
AI 에이전트에게도 "변경하면 테스트를 실행하라"고 지시해 두면, 아까 그 1줄은 현장에서 제거됩니다.
규칙이 늘어나면, 언어별 전용 도구로 옮기는 것도 좋은 방법입니다.
Python이라면 import-linter, Java라면 ArchUnit, JS/TS라면 dependency-cruiser 등이 있습니다.
제거되도록 만들었음에도 불구하고, 아까 테스트가 실패했을 때 나오는 것은 다음 1줄입니다 (pytest 출력 발췌).
AssertionError: api에서 repositories를 직접 import하고 있습니다: app/api/orders.py: app.repositories.order_repository.count_orders
어디가 위반인지는 알 수 있지만, 왜 안 되는지 혹은 어떻게 고쳐야 하는지는 적혀있지 않습니다.
예외 메시지는 "오류가 발생했습니다"와 같은 일반적인 문구보다, 깨진 전제(assumption)를 작성하는 것이 훨씬 유용합니다.
이러한 생각을 읽고, 테스트 실패 메시지에도 그대로 적용할 수 있겠다는 생각이 들었습니다.
테스트의 message
내용만 다음과 같이 교체합니다.
message = (
"[의존 규칙 위반] app.api에서 app.repositories를 직접 import하고 있습니다.\n"
"이유: 권한 체크와 업무 규칙을 app.services에 모아두었기 때문입니다.\n"
...
)
교체한 후 같은 위반으로 실패하면, 이렇게 나옵니다 (역시 발췌).
AssertionError: [의존 규칙 위반] app.api에서 app.repositories를 직접 import하고 있습니다.
이유: 권한 체크와 업무 규칙을 app.services에 모아두었기 때문입니다.
수정 방법: app.services에 함수를 추가하여, api에서는 그것을 호출해 주세요.
...
이렇게 되면 PR을 낸 사람도 AI도 메시지를 읽는 것만으로 services를 거치도록 수정할 수 있습니다.
"느슨하게 하지 않는다"라는 규칙은, 실패한 테스트 케이스 쪽을 고쳐서 통과시키려는 AI가 있기 때문에 넣었습니다.
직접 실행하는 것을 잊더라도, PR을 내는 시점에 CI(Continuous Integration)가 같은 테스트로 막아줍니다.
리뷰에서 "여기는 services를 거치세요"라고 코멘트했던 내용을, 테스트가 먼저 알려주는 것입니다.
하나의 규칙이 테스트가 되면, 다른 모든 규칙도 전부 테스트로 만들고 싶어집니다.
하지만 팀에서 자주 나올 법한 규칙들을 나열해 보면, 테스트할 수 없는 것도 꽤 있었습니다.
결정적인 차이는 코드의 문자 그대로의 형태만으로 판별할 수 있느냐 여부였습니다.
| 설계 규칙 | 기계로 판별 가능 여부 | 어디서 지킬 것인가 |
|---|---|---|
| api는 repositories를 import 하지 않는다 | ○ import 문으로 알 수 있음 | 테스트 |
| ... |
위의 3가지 항목은 import의 방향과 장소에 관한 것이므로, 아까와 같은 형태의 테스트로 작성할 수 있습니다.
아래 2가지는 코드의 의미를 읽어야 판단이 가능합니다.
설계와 코드 사이의 간극은 테스트로 줄일 수는 있어도, 완전히 없애기는 어렵습니다.
게다가, 테스트로 만든 규칙에도 하나의 구멍이 남습니다.
테스트 파일 자체를 수정하면, 위반을 통과시킬 수 있습니다.
따라서, 의존성 테스트에 대한 변경은 사람이 확인하도록 해둡니다.
GitHub의 경우, CODEOWNERS 기능을 사용해 테스트 파일에 담당자를 지정합니다.
게다가 브랜치 보호(Branch Protection) 설정에서 "Code Owners의 리뷰 필수"로 지정해 놓으면, 테스트가 느슨한 PR은 해당 사람의 승인 없이는 병합(merge)할 수 없습니다.
모든 규칙을 한 번에 테스트로 만들 필요는 없습니다.
리뷰에서 같은 지적이 두 번째로 나온 규칙이나, AI가 반복적으로 위반하는 규칙이 테스트로 옮길 후보가 됩니다.
테스트로 옮긴 규칙만큼, 리뷰에서 import의 방향을 눈으로 추적하는 시간이 줄어듭니다.
그 시간을 "이 처리는 정말 services에 두어야 하는가"와 같이, 읽고 판단하지 않으면 결정할 수 없는 부분에 사용할 수 있습니다.
리뷰 자체가 필요 없어지는 것이 아니라, 보는 장소가 바뀐다는 점이 저에게는 가장 와닿았습니다.
먼저, 이번 주 리뷰 코멘트를 되돌아보면서 같은 지적이 두 번째로 나온 것을 하나 골라봅니다.
고른 후, 아까의 테스트를 복사해서 금지할 import와 실패 메시지의 두 곳만 수정합니다.
다음으로 AI가 같은 import를 작성했을 때는, 리뷰보다 테스트가 먼저 막아주고 수정 방법까지 알려줄 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기