본문 바로가기
AI System/클린 아키텍처와 바이브 코딩을 활용한 AI Agent 시스템의 설계와 구

d01 - 3. 클린 아키텍처 핵심 이해

by Toddler_AD 2026. 6. 22.

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의 데이터 흐름을 도식으로 그립니다.