본 프로젝트는 **클린 아키텍처(Clean Architecture)**와 도메인 주도 설계(Domain-Driven Design, DDD) 원칙을 적용하여 구현되었습니다.
- 의존성 역전 원칙 (Dependency Inversion): 외부 레이어가 내부 레이어에 의존
- 관심사의 분리 (Separation of Concerns): 각 레이어는 명확한 책임을 가짐
- 테스트 가능성 (Testability): 비즈니스 로직을 독립적으로 테스트 가능
- 프레임워크 독립성: 비즈니스 로직이 FastAPI에 종속되지 않음
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (API Routes - HTTP 처리) │
│ app/api/routers/ │
└─────────────────────────────────────────────────────────┘
↓ depends on
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Use Cases - 비즈니스 흐름) │
│ app/usecases/ │
└─────────────────────────────────────────────────────────┘
↓ depends on
┌─────────────────────────────────────────────────────────┐
│ Domain Layer │
│ (Services - 도메인 비즈니스 로직) │
│ app/services/ │
└─────────────────────────────────────────────────────────┘
↓ depends on
┌─────────────────────────────────────────────────────────┐
│ Infrastructure Layer │
│ (Repository, DB, External Services) │
│ app/repository/, app/db/, app/core/ │
└─────────────────────────────────────────────────────────┘
위치: app/api/routers/
책임:
- HTTP 요청/응답 처리
- 입력 검증 (Pydantic 스키마)
- UseCase 호출
- HTTP 상태 코드 및 예외 매핑
규칙:
- ❌ 비즈니스 로직 포함 금지
- ❌ 데이터베이스 직접 접근 금지
- ✅ UseCase만 호출
- ✅ HTTP 관련 처리만 수행
예시: app/api/routers/auth.py
@router.post("/register")
async def register(user_data: UserRegister, db: AsyncSession = Depends(get_db)):
try:
# Use Case 호출만 수행
use_case = RegisterUserUseCase(db)
user = await use_case.execute(...)
return user
except ValueError as e:
# HTTP 예외로 변환
raise HTTPException(status_code=400, detail=str(e))위치: app/usecases/
책임:
- 애플리케이션의 비즈니스 흐름 정의
- 여러 Service를 조합하여 사용
- 트랜잭션 경계 정의
- 각 Use Case는 하나의 사용자 의도(User Story)를 나타냄
규칙:
- ✅ 여러 Service를 조합하여 흐름 구성
- ✅ 각 Use Case는 단일 책임 원칙 준수
- ❌ HTTP 관련 처리 금지
- ❌ 직접적인 데이터베이스 접근 금지
예시: app/usecases/auth_usecases.py
class LoginUseCase:
"""
로그인 Use Case
User Story: 사용자가 로그인하여 액세스 토큰을 받는다
Flow:
1. UserService를 통해 자격 증명 검증
2. 사용자 활성화 상태 확인
3. TokenService를 통해 액세스 토큰 생성
"""
def __init__(self, db: AsyncSession):
self.user_service = UserService(db)
self.token_service = TokenService()
async def execute(self, username: str, password: str) -> dict:
# 1. 자격 증명 검증
user = await self.user_service.verify_user_credentials(username, password)
if not user:
raise ValueError("자격 증명 오류")
# 2. 활성화 상태 확인
if not await self.user_service.is_user_active(user):
raise ValueError("비활성화된 사용자")
# 3. 토큰 생성
token = self.token_service.create_user_access_token(username)
return {"access_token": token, "token_type": "bearer"}위치: app/services/
책임:
- 단일 도메인 엔티티에 대한 비즈니스 로직
- 도메인 규칙 검증
- Repository와 상호작용
규칙:
- ✅ 도메인 규칙 검증
- ✅ 단일 책임 원칙 (하나의 도메인 엔티티만 관리)
- ❌ HTTP 관련 처리 금지
- ❌ 다른 도메인 서비스와의 복잡한 조합 금지 (UseCase에서 수행)
예시: app/services/auth/user_service.py
class UserService:
"""사용자 도메인 서비스"""
def __init__(self, db: AsyncSession):
self.repository = UserRepository(db)
async def create_user(self, username: str, email: str, password: str) -> User:
"""
사용자 생성
비즈니스 규칙:
- 사용자명 중복 불가
- 이메일 중복 불가
"""
# 도메인 규칙 검증
if await self.repository.get_by_username(username):
raise ValueError("사용자명 중복")
if await self.repository.get_by_email(email):
raise ValueError("이메일 중복")
# 엔티티 생성
return await self.repository.create(username, email, password)위치: app/repository/, app/db/, app/core/
책임:
- 데이터베이스 접근
- 외부 서비스 연동
- 기술적 구현 세부사항
구성요소:
- Repository: 데이터 접근 계층 (
app/repository/) - Database: DB 연결 및 세션 관리 (
app/db/) - Security: 암호화, JWT 등 보안 관련 (
app/core/security.py) - Config: 설정 관리 (
app/core/config.py)
1. [Presentation] POST /api/v1/auth/register
↓
2. [Application] RegisterUserUseCase.execute()
↓
3. [Domain] UserService.create_user()
├─ UserService.check_duplicate_username()
├─ UserService.check_duplicate_email()
└─ UserRepository.create()
↓
4. [Infrastructure] UserRepository.create()
└─ PostgreSQL INSERT
1. [Presentation] POST /api/v1/auth/login
↓
2. [Application] LoginUseCase.execute()
├─ UserService.verify_user_credentials()
│ └─ UserRepository.get_by_username()
├─ UserService.is_user_active()
└─ TokenService.create_user_access_token()
↓
3. [Domain] UserService + TokenService
↓
4. [Infrastructure] Repository + Security (JWT)
- 타입: Integration Test
- 도구: TestClient (FastAPI)
- 검증: HTTP 요청/응답, 상태 코드
def test_register_user():
response = client.post("/api/v1/auth/register", json={...})
assert response.status_code == 201- 타입: Unit Test
- 도구: pytest + Mock
- 검증: 비즈니스 흐름, Service 호출 순서
async def test_login_use_case():
# Mock Services
user_service = Mock(UserService)
token_service = Mock(TokenService)
use_case = LoginUseCase(user_service, token_service)
result = await use_case.execute("user", "pass")
assert result["access_token"]
user_service.verify_user_credentials.assert_called_once()- 타입: Unit Test
- 도구: pytest + Mock Repository
- 검증: 도메인 규칙, 비즈니스 로직
async def test_create_user_duplicate_check():
# Mock Repository
repo = Mock(UserRepository)
repo.get_by_username.return_value = existing_user
service = UserService(repo)
with pytest.raises(ValueError, match="사용자명 중복"):
await service.create_user("duplicate", "email", "pass")- 타입: Integration Test
- 도구: pytest + Test Database
- 검증: 데이터베이스 CRUD
async def test_user_repository_create(test_db):
repo = UserRepository(test_db)
user = await repo.create("user", "email", "pass")
assert user.id is not None
assert user.username == "user"Router (HTTP)
↓ depends on
UseCase (Application Logic)
↓ depends on
Service (Domain Logic)
↓ depends on
Repository (Data Access)
↓ depends on
Database (Infrastructure)
핵심: 의존성은 항상 외부에서 내부로 흐릅니다.
- Router는 UseCase를 알지만, UseCase는 Router를 모릅니다.
- Service는 Repository를 알지만, Repository는 Service를 모릅니다.
# app/models/pii_detection.py
class PIIDetection(Base):
__tablename__ = "pii_detections"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
text: Mapped[str]
has_pii: Mapped[bool]
detected_at: Mapped[datetime]
# app/repository/pii_detection_repo.py
class PIIDetectionRepository:
async def create(self, user_id: int, text: str, has_pii: bool):
# DB 저장 로직# app/services/pii_service.py
class PIIDetectionService:
def __init__(self, db: AsyncSession):
self.repository = PIIDetectionRepository(db)
self.detector = get_pii_detector()
async def detect_and_analyze(self, text: str) -> PIIDetectionResult:
# 도메인 규칙: 텍스트 길이 검증
if len(text) > 10000:
raise ValueError("텍스트가 너무 깁니다")
# PII 탐지
result = await self.detector.detect_pii(text)
return result
async def save_detection(self, user_id: int, text: str, has_pii: bool):
return await self.repository.create(user_id, text, has_pii)# app/usecases/pii_usecases.py
class DetectAndSavePIIUseCase:
"""
PII 탐지 및 저장 Use Case
Flow:
1. PIIDetectionService로 텍스트 분석
2. PIIDetectionService로 결과 저장
3. 결과 반환
"""
def __init__(self, db: AsyncSession):
self.pii_service = PIIDetectionService(db)
async def execute(self, user_id: int, text: str):
# 1. 탐지
result = await self.pii_service.detect_and_analyze(text)
# 2. 저장
await self.pii_service.save_detection(user_id, text, result.has_pii)
# 3. 반환
return result# app/api/routers/pii.py
@router.post("/detect")
async def detect_pii(
request: PIIDetectionRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
):
try:
use_case = DetectAndSavePIIUseCase(db)
result = await use_case.execute(current_user.id, request.text)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))새로운 코드를 작성할 때 다음을 확인하세요:
- HTTP 요청/응답 처리만 수행하는가?
- UseCase를 호출하는가?
- 비즈니스 로직이 없는가?
- Repository를 직접 호출하지 않는가?
- 하나의 사용자 의도를 나타내는가?
- 여러 Service를 조합하여 흐름을 만드는가?
- HTTP 관련 코드가 없는가?
- 도메인 규칙은 Service에 위임하는가?
- 단일 도메인 엔티티에 집중하는가?
- 도메인 규칙을 검증하는가?
- Repository를 통해 데이터에 접근하는가?
- HTTP나 프레임워크에 의존하지 않는가?
- 데이터 접근만 담당하는가?
- 비즈니스 로직이 없는가?
- SQL/ORM 쿼리만 포함하는가?
- Clean Architecture by Robert C. Martin
- Domain-Driven Design by Eric Evans
- Hexagonal Architecture (Ports and Adapters)
작성일: 2025-10-11
버전: v1.1.0