ADR-02: 클린 아키텍처 레이어 도입
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-18 | @KubrickCode | collector |
배경
초기 아키텍처 문제점
초기 모놀리식 구조는 코드베이스가 성장하면서 여러 문제를 드러냈다:
도메인-인프라 결합:
- 비즈니스 로직이 데이터베이스 쿼리 및 큐 작업과 뒤섞임
- 인프라 세부사항(예: PostgreSQL 쿼리, River 태스크 처리) 변경이 핵심 비즈니스 로직 수정 필요
제한된 테스트 용이성:
- 구체 구현에 대한 직접 의존으로 인해 단위 테스트가 어려움
- 대규모 리팩토링 없이는 Mock 주입 불가능
- 단순한 비즈니스 규칙 검증에도 통합 테스트 필요
변경 영향:
- 외부 라이브러리 전환(예: 다른 큐 시스템)이 비즈니스 로직 재작성 필요
- 관심사 간 명확한 경계 부재로 코드 탐색 어려움
목표
- 관심사 분리: 비즈니스 로직을 인프라 세부사항으로부터 격리
- 테스트 용이성: Mock 의존성을 통한 단위 테스트 가능
- 유연성: 비즈니스 규칙에 영향 없이 인프라 변경 허용
- 확장성: 공유된 비즈니스 로직으로 다중 진입점(Worker, Scheduler, CLI) 지원
- 코드 가독성: 명확한 레이어 경계를 통한 코드 탐색 및 리뷰 효율성 향상
- AI 기반 개발: 작업 범위를 특정 레이어로 한정하여 컨텍스트 크기를 제한, AI 기반 코딩의 효과적 활용 가능
결정
6개의 명확한 레이어로 구성된 Clean Architecture 레이어 구조 채택
레이어 구조
| 레이어 | 책임 |
|---|---|
| Domain | 비즈니스 로직 및 인터페이스 정의 |
| UseCase | 비즈니스 워크플로우 오케스트레이션 |
| Adapter | 인터페이스 구현체 (Repository, VCS, Parser) |
| Handler | 진입점 어댑터 (큐 핸들러, 스케줄러 작업) |
| Infrastructure | 기술 컴포넌트 (DB 풀, 큐 클라이언트, Config) |
| Application | 의존성 주입 컨테이너 |
의존성 규칙
의존성은 내부로만 흐른다:
Command (main) → Application → Handler → UseCase → Domain
↓ ↓ ↓
Infrastructure Adapter (의존성 없음)- Domain Layer는 외부 의존성이 없음
- UseCase Layer는 Domain 인터페이스에만 의존
- Adapter Layer는 Infrastructure를 사용하여 Domain 인터페이스 구현
- Handler Layer는 외부 요청을 UseCase 호출로 변환
- Application Layer는 의존성을 연결
레이어 세부사항
Domain Layer:
- 인터페이스 정의:
Repository,VCS,Parser,TokenLookup - 비즈니스 모델 및 Value Object 포함
- 도메인 특화 에러 정의
- 외부 패키지 import 없음
UseCase Layer:
- 비즈니스 워크플로우 오케스트레이션 (예: Clone → Parse → Save)
- Domain 인터페이스에만 의존 (생성 시 주입)
- 동시성 제한, 타임아웃 등 횡단 관심사 관리
Adapter Layer:
- 특정 기술로 Domain 인터페이스 구현
- 예: PostgreSQL 저장소, Git VCS 어댑터, Core 파서 어댑터
- 도메인 모델과 외부 데이터 포맷 간 매핑
Handler Layer:
- 외부 트리거(River 태스크, Cron 작업)의 진입점
- 요청 파라미터 추출 및 UseCase 호출
- 프레임워크 특화 관심사 처리 (페이로드 언마셜링, 에러 코드)
Infrastructure Layer:
- 데이터베이스 연결 풀 관리
- 큐 클라이언트/서버 설정
- 설정 로딩
- 분산 락 구현
Application Layer:
- DI 컨테이너 정의
- 진입점별 의존성 연결
- 생명주기 관리 (시작/종료)
검토한 옵션
옵션 A: Clean Architecture (선택)
설명:
엄격한 의존성 규칙을 가진 6-레이어 구조. 외부 의존성이 없는 Domain이 중심.
장점:
- 명확한 관심사 분리
- 인터페이스 주입을 통한 높은 테스트 용이성
- 기술 변경이 어댑터/인프라 레이어에 격리
- 공유된 비즈니스 로직으로 다중 진입점 지원
단점:
- 초기 설정 복잡도
- 관리할 파일과 패키지 증가
- 패턴에 익숙하지 않은 팀원의 학습 곡선
옵션 B: 모놀리식 구조 유지
설명:
직접 의존성을 가진 단일 패키지 구조 유지.
장점:
- 단순한 초기 구조
- 적은 간접 참조
- 작은 기능 구현이 빠름
단점:
- 테스트에 통합 설정 필요
- 변경이 관심사 전반에 파급
- 다중 진입점으로 확장 어려움
옵션 C: 육각형 아키텍처
설명:
덜 규정적인 내부 구조를 가진 포트와 어댑터 패턴.
장점:
- 유연한 내부 조직
- 포트(인터페이스)와 어댑터(구현체)에 집중
- 잘 문서화된 패턴
단점:
- 내부 레이어 구조에 대한 가이드 부족
- "애플리케이션 육각형" 내부가 정의되지 않음
- Clean Architecture가 이 프로젝트 규모에 더 실행 가능한 구조 제공
구현 원칙
인터페이스 정의 위치
인터페이스는 구현체가 아닌 Domain 레이어에 정의:
| 인터페이스 | 위치 |
|---|---|
| Repository | domain/ 패키지 |
| VCS | domain/ 패키지 |
| Parser | domain/ 패키지 |
| TokenLookup | domain/ 패키지 |
이를 통해:
- Domain 레이어는 인프라 의존성이 없음
- 구현체 변경이 도메인 수정 없이 가능
- 모든 어댑터에 대한 명확한 계약
DI 컨테이너 전략
진입점별 별도 컨테이너:
| 컨테이너 | 목적 | 특별한 의존성 |
|---|---|---|
| WorkerContainer | 큐 태스크 처리 | 암호화 키 (토큰 복호화) |
| SchedulerContainer | Cron 작업 스케줄링 | 분산 락 |
근거:
- 다른 진입점은 다른 의존성 요구사항을 가짐
- Scheduler는 암호화 불필요 (private 저장소 접근 안 함)
- Worker는 분산 락 불필요 (큐가 동시성 처리)
동시성 제어 위치
비즈니스 수준의 동시성 결정(예: 최대 동시 clone)은 Adapter가 아닌 UseCase 레이어에 속함:
- 세마포어 기반 스로틀링은 리소스 할당에 관한 비즈니스 결정
- Adapter 레이어는 정책이 아닌 기술적 실행에 집중
결과
긍정적
테스트 용이성:
- Mock 없이 도메인 로직 테스트 가능
- 간단한 인터페이스 Mock으로 UseCase 테스트 가능
- 비즈니스 규칙 검증에 데이터베이스/큐 불필요
유지보수성:
- 명확한 경계로 인지 부하 감소
- 한 레이어 변경이 다른 레이어에 거의 영향 없음
- 잘 정의된 책임으로 온보딩 용이
유연성:
- 데이터베이스 마이그레이션: 어댑터 레이어만 변경
- 큐 시스템 전환: 인프라/핸들러 레이어만 변경
- 새 진입점 (예: HTTP API): 핸들러 추가, UseCase 재사용
확장성:
- Worker와 Scheduler 독립적 스케일링
- 공유된 UseCase 로직으로 일관성 보장
- 컨테이너 분리로 배포 유연성
부정적
초기 복잡도:
- 모놀리식 접근보다 많은 패키지와 파일
- 의존성 흐름 이해에 문서화 필요
간접 참조:
- 진입점과 비즈니스 로직 사이에 더 많은 레이어
- 디버깅이 여러 패키지 추적 필요할 수 있음
단순 작업의 오버헤드:
- 단순한 CRUD도 전체 레이어 순회 필요
- 직관적인 기능에 과도하게 느껴질 수 있음
