Skip to content

ADR-02: 클린 아키텍처 레이어 도입

🇺🇸 English Version

날짜작성자리포지토리
2024-12-18@KubrickCodecollector

배경

초기 아키텍처 문제점

초기 모놀리식 구조는 코드베이스가 성장하면서 여러 문제를 드러냈다:

도메인-인프라 결합:

  • 비즈니스 로직이 데이터베이스 쿼리 및 큐 작업과 뒤섞임
  • 인프라 세부사항(예: PostgreSQL 쿼리, River 태스크 처리) 변경이 핵심 비즈니스 로직 수정 필요

제한된 테스트 용이성:

  • 구체 구현에 대한 직접 의존으로 인해 단위 테스트가 어려움
  • 대규모 리팩토링 없이는 Mock 주입 불가능
  • 단순한 비즈니스 규칙 검증에도 통합 테스트 필요

변경 영향:

  • 외부 라이브러리 전환(예: 다른 큐 시스템)이 비즈니스 로직 재작성 필요
  • 관심사 간 명확한 경계 부재로 코드 탐색 어려움

목표

  1. 관심사 분리: 비즈니스 로직을 인프라 세부사항으로부터 격리
  2. 테스트 용이성: Mock 의존성을 통한 단위 테스트 가능
  3. 유연성: 비즈니스 규칙에 영향 없이 인프라 변경 허용
  4. 확장성: 공유된 비즈니스 로직으로 다중 진입점(Worker, Scheduler, CLI) 지원
  5. 코드 가독성: 명확한 레이어 경계를 통한 코드 탐색 및 리뷰 효율성 향상
  6. 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 레이어에 정의:

인터페이스위치
Repositorydomain/ 패키지
VCSdomain/ 패키지
Parserdomain/ 패키지
TokenLookupdomain/ 패키지

이를 통해:

  • Domain 레이어는 인프라 의존성이 없음
  • 구현체 변경이 도메인 수정 없이 가능
  • 모든 어댑터에 대한 명확한 계약

DI 컨테이너 전략

진입점별 별도 컨테이너:

컨테이너목적특별한 의존성
WorkerContainer큐 태스크 처리암호화 키 (토큰 복호화)
SchedulerContainerCron 작업 스케줄링분산 락

근거:

  • 다른 진입점은 다른 의존성 요구사항을 가짐
  • Scheduler는 암호화 불필요 (private 저장소 접근 안 함)
  • Worker는 분산 락 불필요 (큐가 동시성 처리)

동시성 제어 위치

비즈니스 수준의 동시성 결정(예: 최대 동시 clone)은 Adapter가 아닌 UseCase 레이어에 속함:

  • 세마포어 기반 스로틀링은 리소스 할당에 관한 비즈니스 결정
  • Adapter 레이어는 정책이 아닌 기술적 실행에 집중

결과

긍정적

테스트 용이성:

  • Mock 없이 도메인 로직 테스트 가능
  • 간단한 인터페이스 Mock으로 UseCase 테스트 가능
  • 비즈니스 규칙 검증에 데이터베이스/큐 불필요

유지보수성:

  • 명확한 경계로 인지 부하 감소
  • 한 레이어 변경이 다른 레이어에 거의 영향 없음
  • 잘 정의된 책임으로 온보딩 용이

유연성:

  • 데이터베이스 마이그레이션: 어댑터 레이어만 변경
  • 큐 시스템 전환: 인프라/핸들러 레이어만 변경
  • 새 진입점 (예: HTTP API): 핸들러 추가, UseCase 재사용

확장성:

  • Worker와 Scheduler 독립적 스케일링
  • 공유된 UseCase 로직으로 일관성 보장
  • 컨테이너 분리로 배포 유연성

부정적

초기 복잡도:

  • 모놀리식 접근보다 많은 패키지와 파일
  • 의존성 흐름 이해에 문서화 필요

간접 참조:

  • 진입점과 비즈니스 로직 사이에 더 많은 레이어
  • 디버깅이 여러 패키지 추적 필요할 수 있음

단순 작업의 오버헤드:

  • 단순한 CRUD도 전체 레이어 순회 필요
  • 직관적인 기능에 과도하게 느껴질 수 있음

참고 자료

Open-source test coverage insights