3교시 · 클린 아키텍처 핵심 이해
1일차 13:00–14:00 · 이론/실습 선행: 2교시(F1~F7 기능 분해)
🎯 학습 목표
- 클린 아키텍처의 4계층과 의존성 규칙을 정확한 용어로 설명할 수 있다.
- "흔한 한 파일 앱"과 비교해 무엇이 좋아지는지 구체적으로 말할 수 있다.
- 의존성 역전(DIP) 이 왜 필요하고 어떻게 동작하는지 이해한다.
- 2교시의 F1~F7을 4계층에 정확히 배치한다.
📝 본 교시는 이 과정에서 가장 중요한 이론입니다. 코드는 적지만, 여기서 잡은 개념이 5~13교시 구현 전체를 지탱합니다.
📖 개념 1 — 나쁜 구조의 냄새: 모든 것이 한 파일에
많은 데이터 앱이 이렇게 시작합니다. (아래는 따라 하지 말아야 하는 예시입니다.)
# app.py — 모든 책임이 뒤섞인 한 파일 (안티패턴)
import streamlit as st
import pandas as pd
import openai
file = st.file_uploader("CSV")
if file:
df = pd.read_csv(file) # ① 데이터 적재
df = df[df["quantity"] > 0] # ② 검증/정리
total = (df.quantity * df.unit_price).sum() # ③ 분석(계산)
st.bar_chart(...) # ④ 시각화
openai.chat.completions.create(...) # ⑤ AI 요약
st.download_button(...) # ⑥ 출력
한 줄 한 줄은 멀쩡합니다. 문제는 여섯 가지 책임(①~⑥)이 한 덩어리로 섞여 있다는 것입니다. 이로 인해 생기는 실제 고통은 다음과 같습니다.
| 하고 싶은 일 | 이 구조에서 벌어지는 일 |
| 분석 로직(③)만 테스트 | Streamlit과 OpenAI까지 띄워야 함. 느리고 불안정 |
| AI 모델을 다른 것으로 교체(⑤) | 분석·화면 코드까지 영향을 받음 |
| 검증 규칙(②)을 찾기 | 화면 코드 사이를 뒤져야 함 |
| 새 팀원이 구조 파악 | 200줄을 처음부터 끝까지 정독해야 함 |
과정안내서가 말하는 "유지보수와 확장이 가능한 시스템" 은 바로 이 고통을 없애는 것이 목표입니다. 그 방법이 클린 아키텍처입니다.
📖 개념 2 — 클린 아키텍처의 4계층
핵심 발상은 "자주 바뀌는 것과 거의 안 바뀌는 것을 분리하라" 입니다.
- 비즈니스의 핵심 규칙(매출 = 수량 × 단가)은 거의 바뀌지 않습니다 → 안쪽에 둡니다.
- 화면 프레임워크나 AI 모델은 자주 바뀝니다(Streamlit→FastAPI, OpenAI→다른 모델) → 바깥에 둡니다.
이를 동심원으로 그리면 다음과 같습니다. 안쪽일수록 핵심(잘 안 바뀜), 바깥일수록 부품(갈아 끼움) 입니다.
┌───────────────────────────────────────────────────────────┐
│ [4계층] Frameworks & Drivers (프레임워크·드라이버) │
│ = 우리 폴더의 infra/ + app.py │
│ ┌───────────────────────────────────────────────────┐ │
│ │ [3계층] Interface Adapters (인터페이스 어댑터) │ │
│ │ = 우리 폴더의 adapters/ │ │
│ │ ┌───────────────────────────────────────────┐ │ │
│ │ │ [2계층] Use Cases (유스케이스) │ │ │
│ │ │ = 우리 폴더의 usecases/ │ │ │
│ │ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ │ [1계층] Entities (엔티티) │ │ │ │
│ │ │ │ = 우리 폴더의 domain/ │ │ │ │
│ │ │ │ SalesRecord, AnalysisResult │ │ │ │
│ │ │ └───────────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
위 그림에서 표준 용어(Entities/Use Cases/Interface Adapters/Frameworks & Drivers)와 이 프로젝트 폴더 이름(domain/usecases/adapters/infra)을 나란히 적었습니다. 가이드에서는 두 이름을 같은 뜻으로 섞어 씁니다.
각 계층의 책임을 표로 정리합니다.
| 계층 | 표준 용어 | 책임 | 우리 프로젝트의 예 | 외부(프레임워크)를 직접 쓰는가? |
| 1 (안쪽) | Entities | 핵심 데이터와 규칙 | SalesRecord, AnalysisResult | ❌ 전혀 안 씀 |
| 2 | Use Cases | 기능 흐름 조율 | AnalyzeSalesUseCase | ❌ 추상(포트)에만 의존 |
| 3 | Interface Adapters | 외부 ↔ 도메인 통역 | CsvSalesRepository, OpenAiInsightGateway | △ 외부와 도메인 양쪽을 안다 |
| 4 (바깥) | Frameworks & Drivers | 실제 프레임워크·화면 | app.py(Streamlit), openai_client | ✅ 직접 사용 |
📖 개념 3 — 의존성 규칙 (이 과정의 대원칙)
모든 소스 코드 의존성은 바깥에서 안쪽으로만 향한다. 안쪽 계층은 바깥 계층을 절대 알지 못한다.
먼저 화살표의 의미를 다시 못 박습니다(README 표기 규약과 동일).
A → B = "A가 B를 안다(= A가 B를 import 한다, A가 B에 의존한다)". 화살표는 의존 방향이지 데이터 흐름이 아닙니다.
이 표기로 의존성 규칙을 그리면:
Frameworks/Drivers ──▶ Interface Adapters ──▶ Use Cases ──▶ Entities
(infra/, app.py) (adapters/) (usecases/) (domain/)
바깥 안쪽·핵심
화살표 의미: "왼쪽이 오른쪽을 import 한다(안다)".
즉, 바깥은 안쪽을 알지만, 안쪽은 바깥을 모른다.
🔍 관찰 포인트 — 도메인 파일에는 외부 import가 없어야 합니다. domain/models.py 안에는 import streamlit도, import openai도, import pandas도 없습니다. 도메인이 바깥을 전혀 모르기 때문에 다음이 가능합니다.
- 화면을 Streamlit → FastAPI로 바꿔도 → 도메인은 한 줄도 안 바뀜.
- AI 모델을 OpenAI → 다른 모델로 바꿔도 → 분석 로직은 그대로.
- 도메인·유스케이스를 외부 도구 없이 단독으로 테스트 가능 (그래서 TDD가 쉬움).
한 문장 요약: "바깥은 갈아 끼우는 부품, 안쪽은 지켜야 할 핵심."
📖 개념 4 — 의존성 역전(DIP): "안이 바깥 기능을 써야 할 때"
여기서 자연스러운 의문이 생깁니다.
"분석 유스케이스(2계층, 안쪽)는 결국 CSV에서 데이터를 읽어와야 하잖아요? CSV 읽기는 어댑터(3계층, 바깥)의 일인데, 안쪽이 바깥쪽을 호출하면 의존성 규칙 위반 아닌가요?"
정확한 지적입니다. 이 문제를 푸는 기법이 의존성 역전 원칙(DIP, Dependency Inversion Principle) 입니다. 핵심 아이디어는 이렇습니다.
안쪽이 "필요한 기능의 모양(인터페이스)"을 직접 정의하고, 바깥쪽이 그 모양에 맞춰 구현한다.
이렇게 하면 안쪽은 "CSV"라는 구체적 존재를 모른 채, 자기가 정의한 추상적 약속(포트) 에만 의존합니다. 바깥쪽 어댑터가 그 약속을 구현하므로, 의존 화살표가 바깥→안쪽으로 유지(역전) 됩니다.
용어를 정리합니다.
- 포트(Port): 안쪽(유스케이스)이 정의하는 추상 인터페이스. "나는 load() 라는 기능이 필요하다"는 약속. 파이썬에서는 Protocol로 표현.
- 어댑터(Adapter): 바깥쪽이 그 포트를 실제로 구현한 것. "내가 CSV를 읽어 load()를 제공하겠다."
📝 용어 노트 — "포트/어댑터"는 Clean Architecture 고유 용어인가? 정확히 말하면, "Ports & Adapters"라는 짝 명칭의 원조는 Hexagonal Architecture(육각형 아키텍처, Alistair Cockburn, 2005년경) 입니다. Clean Architecture(로버트 마틴)는 4계층 중 하나로 "Interface Adapters" 를 두고(그래서 어댑터는 Clean의 용어이기도 합니다), 유스케이스 경계를 "Input/Output Port" 라고 부릅니다. 두 아키텍처는 형제지간으로 의존성 역전(DIP) 이라는 같은 원리를 공유하며 실무에서 자주 함께 쓰입니다. 본 과정은 "Clean의 4계층 구조 + Hexagonal의 포트/어댑터 어휘 + DDD의 Repository(CsvSalesRepository)" 를 실용적으로 섞은 형태인데, 이는 실제 코드베이스에서 가장 흔한 조합입니다. 핵심은 이름이 아니라 "안쪽이 추상을 정의하고 바깥이 구현한다" 는 원리입니다.
코드로 보면(자세한 구현은 6·7교시):
# usecases/ports.py — [안쪽] 유스케이스가 '필요한 기능의 모양'을 직접 정의(약속=포트)
from typing import Protocol
from sales_agent.domain.models import SalesRecord
class SalesRepository(Protocol):
"""매출 레코드를 어딘가에서 적재한다는 '약속'. 구현 내용은 비어 있다."""
def load(self) -> list[SalesRecord]: ... # 시그니처(모양)만 선언, 구현은 어댑터가
# usecases/analyze_sales.py — [안쪽] 유스케이스는 '약속'에만 의존(구체 구현은 모름)
class AnalyzeSalesUseCase:
def __init__(self, repository: SalesRepository): # 타입이 '포트(추상)'다
self._repo = repository # 어떤 구현이 들어올지 모른 채 보관
# adapters/csv_repository.py — [바깥] 어댑터가 '약속'을 실제로 구현
class CsvSalesRepository: # load()를 갖추면 SalesRepository 포트를 만족
def load(self) -> list[SalesRecord]:
... # 여기서 pandas로 CSV를 읽어 SalesRecord 목록을 반환(실구현은 6교시)
🔍 관찰 포인트 — 유스케이스는 CsvSalesRepository라는 이름을 모릅니다. analyze_sales.py 어디에도 CsvSalesRepository가 등장하지 않습니다. 오직 SalesRepository라는 약속만 압니다. 그 결과:
- 실제 실행 시에는 CsvSalesRepository를 끼워 넣고,
- 테스트 시에는 가짜(Fake) 리포지토리를 끼워 넣을 수 있습니다(파일 없이 분석만 검증). 이 한 가지 기법이 "테스트하기 쉬움"과 "교체하기 쉬움"을 동시에 가능하게 합니다. 4·6·7교시에서 계속 쓰게 됩니다.
✏️ 미니 실습 — "이 import는 어느 계층의 것?"
다음 중 domain/models.py(1계층)에 있으면 안 되는 것을 모두 고르세요.
(a) from dataclasses import dataclass
(b) import streamlit as st
(c) import pandas as pd
(d) from datetime import date
(e) import openai
🔒 진행 게이트
- 제출물(기록): 고른 보기와 그 이유 한 문장.
- 이 결과가 있어야 다음 교시로 넘어갑니다. (정답을 베껴 적는 것이 아니라 직접 골라 본 결과여야 합니다.)
- 정답·해설(별도 파일): 직접 풀어 본 뒤 solutions/미니실습정답_03.md에서 확인하세요.
💻 실습 — F1~F7을 계층에 확정 배치
VS Code에서 docs/architecture.md를 만들고 작성합니다.
# 계층 배치 결정
| 기능 | 계층 | 파일(예정) |
|---|---|---|
| F1 SalesRecord | domain(엔티티) | domain/models.py |
| F3 분석 결과 구조 | domain(엔티티) | domain/models.py (AnalysisResult) |
| F3 분석 로직 | usecase | usecases/analyze_sales.py |
| 데이터 적재 약속(포트) | usecase | usecases/ports.py (SalesRepository) |
| F2 CSV 적재 구현 | adapter | adapters/csv_repository.py |
| F4 차트 생성 | adapter | adapters/chart_renderer.py |
| F5 LLM 요약 | adapter | adapters/llm_gateway.py |
| F6 리포트 조립 | usecase | usecases/generate_report.py |
| F7 웹 UI | infra/UI | app.py |
| OpenAI 클라이언트 | infra | infra/openai_client.py |
## 의존성 규칙 자가 점검
- [ ] domain은 다른 어떤 계층도, 외부 라이브러리도 import 하지 않는다.
- [ ] usecase는 domain과 ports(추상)에만 의존한다(구체 어댑터 import 금지).
- [ ] adapter는 외부 라이브러리(pandas/matplotlib/openai)를 사용해도 된다.
- [ ] infra/UI(app.py)가 구체 어댑터를 생성해 유스케이스에 끼워 넣는다(조립).
✅ 직접 점검: 위 표를 보며 "각 화살표가 바깥→안쪽을 향하는가"를 눈으로 확인하세요. 예컨대 "usecase가 adapter를 import 한다"가 있으면 방향이 거꾸로(안쪽이 바깥을 앎)이므로 위반입니다. 11교시(구조 안정화)에서는 이 점검을 자동 테스트로 만들어, 규칙 위반 시 pytest가 빨간불을 켜게 합니다.
✅ 체크포인트
- [ ] 4계층의 표준 용어와 우리 폴더 이름을 짝지어 말할 수 있다
- [ ] "도메인은 바깥을 모른다"의 의미를 import 예로 설명할 수 있다
- [ ] 화살표 A → B가 "A가 B에 의존한다"는 뜻임을 안다
- [ ] 의존성 역전(포트/어댑터)이 왜 필요한지 한 문장으로 말할 수 있다
- [ ] docs/architecture.md에 F1~F7 배치를 확정했다
🛠️ 자주 하는 오해
| 오해 | 바로잡기 |
| "계층이 많아 복잡해진다" | 아주 작은 앱엔 과할 수 있다. 단, 테스트·교체·확장이 필요한 시스템에선 곧 본전을 뽑는다 |
| "도메인에 pandas 쓰면 편한데?" | 당장은 편하나, 그 대가로 테스트·교체가 어려워진다. 도메인은 순수하게 유지 |
| "포트/어댑터는 자바 같은 무거운 패턴 아닌가?" | 파이썬에선 Protocol 한 줄로 가볍게 구현된다 (4교시 실습) |
| "화살표는 데이터 흐름 아닌가?" | 클린 아키텍처 도식의 화살표는 의존 방향이다. 데이터 흐름과 다를 수 있다 |
🔑 핵심 정리
- 클린 아키텍처 = 안쪽일수록 핵심, 의존(화살표)은 안쪽으로만.
- 도메인이 외부를 모르기 때문에 테스트·교체·확장이 쉬워진다.
- 안쪽이 바깥 기능을 써야 할 땐 포트(추상)를 정의하고 어댑터가 구현한다(의존성 역전, DIP).
📚 더 읽기: 클린 아키텍처의 개념·정의·목적과, AI 에이전트 시스템 개발 / AI 코딩 에이전트(바이브 코딩) 개발에서 이 아키텍처가 주는 이점을 더 깊이 정리한 부록 A — 클린 아키텍처 개념과 이점을 참고하세요.
다음 교시: 이 설계를 실제 폴더·코드 골격으로 옮기고, AI Agent의 데이터 흐름을 도식으로 그립니다.
'AI System > 클린 아키텍처와 바이브 코딩을 활용한 AI Agent 시스템의 설계와 구' 카테고리의 다른 글
| d01 - 6. 데이터 처리 로직 구현 (0) | 2026.06.22 |
|---|---|
| d01 - 5. 첫번째 TDD 사이클 (0) | 2026.06.22 |
| d01 - 4. AI Agent 구조 설계 (0) | 2026.06.22 |
| d01 - 2. 문제정의와 구현전략 (0) | 2026.06.22 |
| d01 - 1. 개발환경 구축 (0) | 2026.06.22 |