11교시 · 구조 리팩터링과 안정화
2일차 13:00–14:00 · 이론/실습 선행: 10교시(동작하는 웹앱 완성)
🎯 학습 목표
- 전체 코드의 계층 구조를 점검하고 의존성 규칙 위반을 바로잡는다.
- 테스트 커버리지를 측정해 검증의 빈틈을 찾고, 가벼운 통합 테스트로 메운다.
- 1~10교시에 손으로 완성한 프로젝트를 믿을 수 있는 상태로 마무리한다.
📖 개념 1 — "동작한다"와 "안정적이다"는 다르다
10교시까지 앱은 동작합니다. 하지만 "동작하는 것"과 "안정적인 것"은 다릅니다. 안정적인 시스템은 다음을 추가로 갖춥니다.
- 변경에 강함: 한 곳을 바꿔도, 의존성 규칙 덕분에 파급이 작다.
- 회귀에 강함: 무언가 깨지면 테스트가 즉시 알려 준다.
- 재현 가능: 누가 받아 실행해도 같은 결과가 나온다.
이 교시는 새 기능을 만들지 않습니다. 이미 만든 것을 "믿을 수 있게" 만드는 시간입니다. 좋은 소프트웨어의 가치는 기능의 수가 아니라 변경을 견디는 힘에서 나옵니다.
📖 개념 2 — 아키텍처 규칙을 "문서"가 아니라 "테스트"로
지금까지 의존성 규칙(도메인은 외부를 import 하지 않는다)은 사람의 약속이었습니다. 약속은 깜빡하면 깨집니다. 누군가 급한 마음에 domain/models.py에 import pandas를 넣어도, 당장은 아무도 모릅니다.
그래서 규칙을 자동으로 검증되는 테스트로 바꿉니다. 규칙이 "지켜지길 바라는 것"에서 "어기면 빨간불이 켜지는 것" 으로 바뀝니다.
💻 실습 1 — 의존성 규칙 자동 점검 테스트
먼저 사람 눈으로 한 번 점검합니다 — domain/ 폴더의 파일들이 외부 라이브러리(pandas/openai/streamlit/matplotlib)나 바깥 계층(usecases/adapters/infra)을 import 하지 않는지, usecases/가 domain과 ports(추상)에만 의존하는지 확인합니다.
그리고 이 점검을 자동화하는 테스트를 추가합니다. tests/test_architecture.py:
import pathlib
# 검사 대상 폴더: 도메인(1계층) 소스가 모인 곳
DOMAIN_DIR = pathlib.Path("src/sales_agent/domain")
# 도메인 코드에 등장하면 '의존성 규칙 위반'으로 보는 import 대상 목록.
# 외부 라이브러리 + 바깥 계층(어댑터/인프라/유스케이스)을 모두 금지한다.
FORBIDDEN_IN_DOMAIN = [
"pandas", "openai", "streamlit", "matplotlib",
"sales_agent.adapters", "sales_agent.infra", "sales_agent.usecases",
]
def test_domain_has_no_outward_imports():
"""도메인(1계층)은 외부 라이브러리·바깥 계층을 import 하지 않는다."""
# rglob("*.py"): DOMAIN_DIR 아래 모든 하위 폴더의 .py 파일을 재귀적으로 찾는다.
for py in DOMAIN_DIR.rglob("*.py"):
text = py.read_text(encoding="utf-8") # 파일 내용을 통째로 읽어 문자열로
for forbidden in FORBIDDEN_IN_DOMAIN:
# "import pandas" 형태가 들어 있으면 위반. assert의 두 번째 값은 실패 메시지.
assert f"import {forbidden}" not in text, \
f"{py} 가 '{forbidden}' 를 import 함 → 의존성 규칙 위반"
# "from pandas import ..." 형태도 함께 막는다.
assert f"from {forbidden}" not in text, \
f"{py} 가 '{forbidden}' 에서 import 함 → 의존성 규칙 위반"
🔍 관찰 포인트 — 규칙을 코드가 지키게 만들었습니다. 이제 누군가 도메인에 pandas를 넣으면, uv run pytest가 빨간색으로 실패하며 정확한 파일·이유를 알려 줍니다. 사람의 주의력에 의존하던 규칙이, 기계가 강제하는 규칙으로 바뀐 것입니다. 이런 테스트를 "아키텍처 테스트"라 부릅니다.
uv run pytest tests/test_architecture.py -v
💻 실습 2 — 커버리지 측정
1교시에서 설치한 pytest-cov를 이제 씁니다. "어떤 코드가 테스트로 한 번이라도 실행됐는지"를 측정합니다.
uv run pytest --cov=sales_agent --cov-report=term-missing
출력 예시:
Name Stmts Miss Cover Missing
---------------------------------------------------------------------------
src/sales_agent/domain/models.py 28 0 100%
src/sales_agent/usecases/analyze_sales.py 24 0 100%
src/sales_agent/adapters/csv_repository.py 30 4 87% 45-48
...
TOTAL ... 92%
🔍 관찰 포인트 — 커버리지는 "목표"가 아니라 "지도"입니다. "100% 달성"이 목적이 아닙니다. 오른쪽 Missing 열이 가리키는 "한 번도 검증되지 않은 줄" 을 발견하는 도구입니다. 특히 어댑터의 예외 처리 경로(잘못된 인코딩, 빈 파일 등)가 빠지기 쉽습니다. 그 줄들이 정말 중요한 분기라면 테스트를 보강하고, 사소하다면 그냥 둡니다. 숫자를 채우는 것이 아니라 위험을 줄이는 것이 목적입니다.
✅ 직접 보강: Missing 열이 가리키는 줄(예: 잘못된 인코딩·빈 파일 처리)을 검증하는 테스트를 직접 추가해 보세요. 실제 파일은 tmp_path로 만들면 됩니다.
💻 실습 3 — 가벼운 통합 테스트
지금까지는 부품별(단위) 테스트였습니다. 이제 부품들이 함께 잘 맞물리는지 한 번에 확인하는 통합 테스트를 추가합니다. (LLM·차트는 Fake로 대체해 빠르고 과금 없이)
tests/test_pipeline_integration.py:
from pathlib import Path
# 통합 테스트: '진짜' CSV 어댑터와 '진짜' 유스케이스들을 함께 엮어 검증한다.
from sales_agent.adapters.csv_repository import CsvSalesRepository
from sales_agent.usecases.analyze_sales import AnalyzeSalesUseCase
from sales_agent.usecases.generate_report import GenerateReportUseCase
class FakeChart:
"""차트 포트를 만족하는 가짜(matplotlib 없이 더미 파일만 생성)."""
def render(self, result, out_dir):
p = Path(out_dir) / "c.png"
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(b"x") # 내용은 아무거나(존재만 하면 됨)
return [str(p)]
class FakeInsight:
"""LLM 포트를 만족하는 가짜(네트워크·과금 없이 고정 문장 반환)."""
def summarize(self, result):
return "통합 테스트 요약"
HEADER = "date,product,region,quantity,unit_price\n"
def test_csv_to_report_end_to_end(tmp_path):
"""CSV 적재 → 분석 → 리포트까지 '한 흐름'이 실제로 동작한다."""
csv = tmp_path / "s.csv"
# 정상 데이터 2행을 가진 임시 CSV 작성
csv.write_text(
HEADER
+ "2024-01-05,노트북,서울,3,1000000\n" # 3,000,000
+ "2024-02-06,마우스,부산,10,20000\n", # 200,000
encoding="utf-8",
)
# 진짜 CSV 어댑터 → 진짜 분석 유스케이스 → (가짜 차트·LLM을 끼운) 진짜 리포트 유스케이스
result = AnalyzeSalesUseCase(CsvSalesRepository(str(csv))).execute()
report, md_path = GenerateReportUseCase(FakeChart(), FakeInsight()).execute(
result, str(tmp_path / "out")
)
assert result.total_revenue == 3_200_000 # 3,000,000 + 200,000
assert Path(md_path).exists() # 리포트 파일이 생성됐는가
assert "총매출" in Path(md_path).read_text(encoding="utf-8") # 내용이 채워졌는가
🔍 관찰 포인트 — 통합 테스트는 "조립이 맞는가"를 봅니다. 단위 테스트는 부품 하나하나를, 통합 테스트는 부품들이 포트를 통해 맞물리는 전체 흐름을 검증합니다. 위 테스트는 사실상 10교시 app.py가 하는 일을, 화면 없이 코드로 검증한 것입니다. 단위 테스트와 통합 테스트는 경쟁 관계가 아니라 층위가 다른 안전망입니다.
uv run pytest -v # 전체 통과 확인
💻 실습 4 — 손으로 완성한 프로젝트 최종 점검
1~11교시까지 Copilot 없이 직접 만든 프로젝트가 처음부터 끝까지 동작하는지 확인하고 마무리합니다.
uv run pytest -v # 모든 단위·아키텍처·통합 테스트 통과 확인
uv run pytest --cov=sales_agent --cov-report=term-missing # 커버리지 최종 확인
uv run streamlit run app.py # 웹앱이 실제로 뜨는지 확인
🔍 관찰 포인트: 지금 손에 있는 것은 단순한 매출 앱이 아니라, 4계층으로 분리되고 / 테스트로 보호되며 / 외부 도구를 교체할 수 있는 시스템입니다. 그리고 이 모든 것을 여러분이 한 줄씩 직접 만들었기 때문에, 어디에 무엇이 있고 왜 그렇게 했는지 완전히 이해하고 있습니다. 이 이해가 바로 다음 12·13교시에서 바이브 코딩을 안전하게 쓰기 위한 자격입니다 — AI가 짠 코드가 맞는지 판단할 수 있어야 비로소 AI에게 맡길 수 있기 때문입니다.
📖 (심화) 언제 pandas/대용량을 고려하나
7교시에서 분석을 순수 파이썬으로 했습니다. "왜 pandas를 안 썼지?"의 답을 여기서 정리합니다.
- 지금 규모(수천~수만 행)는 순수 파이썬으로 충분하며, 테스트가 빠르고 의존이 적다는 이점이 큽니다.
- 수백만 행·복잡한 시계열 분석이 필요해지면, 어댑터 계층에 pandas/polars 기반 분석 어댑터를 새로 만들고, 유스케이스가 그 포트를 주입받게 바꾸면 됩니다.
- 핵심: 성능 최적화가 필요해도 도메인·유스케이스의 계약은 거의 그대로 두고, 바깥(어댑터)만 교체합니다. 이것이 클린 아키텍처가 주는 확장 여지입니다.
🔍 관찰 포인트: "지금 필요하지 않은 최적화를 미리 하지 않는다"는 것도 중요한 설계 판단입니다. 구조를 잘 잡아 두었기에, 나중에 필요할 때 안전하게 최적화할 수 있습니다.
✏️ 미니 실습 — 커버리지 점검 + 테스트 1개 보강
--cov-report=term-missing으로 커버리지를 측정하고, Missing이 가장 많은 파일을 하나 골라 검증 안 된 분기를 메우는 테스트 1개를 직접 추가해 보세요.
uv run pytest --cov=sales_agent --cov-report=term-missing
- 전체 커버리지 %와, Missing이 가장 많은 파일 이름을 기록한다.
- 그 파일의 빠진 분기(예: 빈 파일·잘못된 인코딩·필수 컬럼 누락)를 검증하는 테스트 1개를 추가한다.
- uv run pytest로 새 테스트 통과를 확인한다.
🔍 관찰 포인트: 목표는 커버리지 100%가 아니라 "검증 안 된 중요한 분기를 찾아내는 것" 입니다. 숫자보다 "무엇이 안 덮였는가"를 읽는 눈이 중요합니다.
🔒 진행 게이트
- 제출물(기록): 전체 커버리지 % + 가장 낮은 파일 1개 + 보강 테스트 1개 통과.
- 이 결과가 있어야 다음 교시로 넘어갑니다.
- 정답·해설(별도 파일): solutions/미니실습정답_11.md
✅ 체크포인트
- [ ] test_architecture.py가 의존성 규칙을 자동 검증한다
- [ ] 커버리지를 측정하고 빠진 분기를 1개 이상 메웠다
- [ ] CSV→분석→리포트 통합 테스트가 통과한다
- [ ] uv run pytest -v 전체가 통과(초록)한다
- [ ] 웹앱이 실제로 뜨고, 손으로 만든 프로젝트가 처음부터 끝까지 동작한다
🛠️ 트러블슈팅
| 증상 | 원인 | 해결 |
| --cov 옵션 인식 안 됨 | pytest-cov 미설치 | uv add --dev pytest-cov |
| 아키텍처 테스트 오탐 | 주석 속 단어까지 매칭 | 정규식으로 보완하거나 주석 라인 제외 |
| 통합 테스트만 느림 | 실 LLM/차트가 섞임 | Fake로 대체했는지 확인 |
| 커버리지 100% 집착 | 목적 혼동 | 100%는 목적이 아님. 중요한 분기 검증에 집중 |
🔑 핵심 정리
- 아키텍처 규칙을 테스트로 강제해 "어기면 깨지게" 만들었다.
- 커버리지로 검증 안 된 분기를 찾고, 통합 테스트로 조립을 검증했다.
- 1~11교시에 걸쳐 손으로 직접 만들고 테스트로 보호되는 완성 프로젝트를 갖게 됐다.
다음 교시: 지금까지 손으로 익힌 모든 것을 규칙(copilot-instructions.md)과 프롬프트로 박제하고, GitHub Copilot 바이브 코딩으로 핵심 줄기를 빠르게 재현해 봅니다.
'AI System > 클린 아키텍처와 바이브 코딩을 활용한 AI Agent 시스템의 설계와 구' 카테고리의 다른 글
| d02 - 13. 바이브 코딩으로 빠른 확장 (0) | 2026.06.23 |
|---|---|
| d02 - 12. 바이브 코딩 도입과 빠른 재현 (0) | 2026.06.23 |
| d02 - 10. 웹 인터페이스 연결 (0) | 2026.06.23 |
| d02 - 9. 결과생성 파이프라인 (0) | 2026.06.23 |
| d02 - 8. 시각화와 출력 설계 (1) | 2026.06.23 |