LostCatBox

(책읽기) 만들면서 배우는 클린 아키텍처

Word count: 1.2kReading time: 7 min
2025/01/03 Share

왜?

  • 클린 아키텍처는 무엇인지?
  • DDD에 대한 추가적인 학습.

용어정리

  • 계층형 아키텍처: 웹 -> 도메인 -> 영속성 등 각 계층을 책임에 따라 분리후 역할 기능만 수행한다.
  • 클린 아키텍처: 외부 애플리케이션이나 기술 등에 의존되지 않으며, 비즈니스 규칙 생성 및 테스트하기 좋은 구조

1. 계층형 아키텍처의 문제는 무엇일까?

  • 전통적인 계층 구조는 웹 → 도메인 → 영속성
    • 웹: 요청을 받아 도메인 혹은 비즈니스 계층에 있는 서비스로 요청 역할
    • 도메인: 필요한 비즈니스 로직을 수행하고, 도메인 엔티티의 현재 상태를 조회하거나 변경하기 위해 영속성 계층의 컴포넌트 호출
    • 영속성: DB와의 통신으로 조회 및 커밋
  • 계층형 아키텍처는 데이터베이스 주도 설계를 유도한다.
    • 도메인 모델 추출후, 행동 중심으로 모델링한다. → 하지만, ORM를 보통 사용하므로 결국 DB 테이블과의 밀접한 관계를 맺기때문에, 데이터베이스의 구조를 먼저 생각후, 도메인 로직을 구현한다.
  • 지름길을 택하기 쉬워진다.
    • 추가적인 기능 구현이 필요할 때 영속성 계층에 추가하는 것이 가장 쉬운길이기 때문에 비대해질 가능성이 크다. → 어느계정에도 속하지 않는 것처럼 보이는 헬퍼, 유틸리티도 영속성계층이 될확률이 높다
  • 테스트하기 어려워진다.
    • 웹계층 → 영속성 계층으로 바로 참조하여 개발한 경우에도, 도메인 계층을 거치지 않기때문에, 결국엔 도메인 로직드링 웹계층으로 분산된다.
    • 이제 웹 계층 테스트시, 도메인+영속성 계층도 모킹해야한다.
  • 유스케이스를 숨긴다.
    • 계층형 아키텍처는 도메인 로직이 여러 계층에 걸쳐있을수있어서, 유스케이스에 대응하는 로직이 정확한 위치를 찾는것이 어렵다. → 따라서 서비스가 비대해지기도한다.
  • 동시 작업이 어려워진다
    • 결국 영속성 계층이 만들어져야지만 → 서비스 → 웹계층을 개발할수있기때문에, 같은 서비스를 분산해서 개발할수없다.

2. 의존성 역전하기

  • 프로세스 진행방향(화살표)를 반대로 바꾸는 방법
  • 단일 책임 원칙
    • 하나의 컴포넌트는 하나의 일만수행
  • 의존성 역전 원칙
    • 계층형 아키텍처는 계층 간 의존성은 항상 다음 계층인 아래 방향을 가르킨다.
    • 이를 인터페이스 + 주입을 이용하여 방향 변경이 가능하다
      • [서비스 → 리포지토리 구현체] ⇒ [서비스 → 리포지토리 인터페이스 <- 리포지토리 구현체]
  • 클린 아키텍처란?
    • 아아키텍처에서는 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, 기술, 그밖의 외부 애플리케이션이나 인터페이스로부터 독립적일수있다.
    • 즉, 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야한다.
    • 의존성 역전 원칙의 도움으로 모든 의존성이 도메인 코드를 향해야한다.
  • 헥사고날 아키텍처는 클린 아키텍처에 속한다.
    • 코어(도메인계층+유스케이스[=서비스])으로 포트-어텝터 패턴 사용

3. 코드 구성하기

  • 계층으로 구성하기
    • web, domain, persistence로 계층(패키지)인 폴더 트리
    • 기능(유저, 계좌 등등) 별로 서비스가 섞이기 쉬운 구조
  • 기능으로 구성하기
    • account 패키지 안에 Controller, Repository, RepositoryImpl, Service 등 존재
    • 계층보다 아키텍처의 가시성을 떨어트린다.
  • 표현력있는 패키지 구조
    • account라는 폴더 상위 폴더(기능별 폴더)
      • adapter(어뎁터)(코어X)
        • in
        • out
      • domain(도메인)(코어O)
      • application(애플리케이션, 서비스, 유스케이스)(코어O)
        • Service
        • port
          • in
          • out

4. 유스케이스 구현

  • 유스케이스는 비즈니스 로직
  • 단계
    • 입력을 받는다
    • 비즈니스 규칙을 검증한다(단순한 입력 유효성검증 X)
    • 모델 상태를 조작한다
    • 출력을 반환한다.
  • 입력 유효성 검증
    • 입력 유효성과 비즈니스 규칙 검증의 차이는 실제 모델에서 조회해서 검증해야하는지 여부의 차이다.
    • 입력 모델(INPUT)은 코어계층에 속하며, 생성자에서 검증을 추천한다.(SelfValidating)
  • 유스케이스마다 다른 입력 모델 추천
  • 비즈니스 규칙 검증하기
    • 유스케이스에서의 유효성 검증: “출금 계좌는 초과 인출되어서는 안된다” 처럼 모델에 접근한 후에 가능한 검증
    • 입력 모델에서의 입력 유효성 검증: 모델 접근 필요없는 경우
  • 풍부한 도메인 모델 vs 빈약한 도메인 모델
  • 유스케이스마다 다른 출력 모델
    • 출력 모델 또한 코어계층에 속한다.
  • 읽기 전용 유스케이스는 어떨까?
    • 개인적으로 쿼리 서비스 같은경우는 UseCase → Query로 하여, 간단하게 쿼리하여 출력

5. 웹 어댑터 구현하기

  • 의존성 역전

    • [컨트롤러 → 서비스] 외부요청에서 애플리케이션으로 요청이 흐른다.(의존성 방향 = 진행방향)
    • 의존성 역전을 통해 [컨트롤러(어뎁터) → 포트 ← 유스케이스(서비스)]로 위치시키면 코어를 지킬 수

    있다.

  • 웹 어댑터의 책임

    • HTTP 요청의 자바 객체로 매핑
    • 권한 검사
    • 입력 유효성 검증
    • 입력을 유스케이스의 입력 모델로 매핑
    • 유스케이스 호출
    • 유스케이스의 출력을 HTTP로 매핑
    • HTTP 응답을 반환
  • 웹 어뎁터의 입력 모델과 유스케이스의 입력모델 차이

    • 유스케이스의 입력 모델로 변환할 수있다는 것을 고려(검증필요)
  • 컨트롤러 나누기(테스트등의 용이성, SRP)

6. 영속성 어댑터 구현하기

  • 의존성 역전

    • [서비스 → 영속성계층] ⇒ [서비스 → 포트 ← 영속성 어댑터(아웃고잉 어뎁터)]
  • 영속성 어댑터의 책임

    • 입력을 받는다
    • 입력을 데이터베이스 포맷으로 매핑한다
    • 입력을 데이터베이스로 보낸다
    • 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
    • 출력을 반환한다.
  • 영속성 어댑터의 입력 모델은 반드시 애플리케이션 코어에 있어야한다. → 영속성 어댑터 내부변경이 코어에 영향이없다.

  • 포트 인터페이스 나누기

    • ISP 원칙에 따라 UseCase별로 인터페이스 분리해서 아웃 고잉포트를 적용해야한다
      • AccountRepository 에 Register, Update 등에 필요한 모든 메서드가 포함된경우, 각 UseCase에서는 AccountRepository하나에만 의존하게됨 → 지양
      • Repository하나로 넓은 곳에서 사용하는것은 코드에 불필요한 의존성이 생기는 방향
  • 영속성 어댑터 나누기

    • DDD영속성 연산에 필요한 도메인 클래스(애그리거트) 하나당 하나의 영속성 어댑터를 구현하는 방식을 선택할 수있다.

      • AccountPersistenceAdapter, UserPersistenceAdapter
    • 도메인 코드는 영속성 포트에 의해 정의된 명세를 어떤 클래스가 충족시키는지는 관심이없다. 관심이없다

    • 애그리거트당 하나의 영속성 어댑터 접근 방식은 나중에 여러 개의 바운디드 컨텍트(bounded context)의 영속성 요구사항 분리하기 위한 좋은 토대다.

    • 각 바운디드 컨텍스트는 영속성 어댑터를 하나씩 가지고있다. 경계 기준이된다.

      • account, billing 맥락의 영속성 어댑터에 서로 접근하지않는다.

      • 어떤 맥락이 다른 맥락의 무엇인가를 필요로 한다면 전용 인커밍 포트를 통해 접근해야한다

        접근해야한다

  • 데이터베이스 트랜잭션은 어떻게 해야할까?

    • 애플리케이션 계층(Service, UseCase)에서 에너테이션을 적용하는것이 바람직하다.

7. 아키텍처 요소 테스트하기

  • 테스트 피라미드(given, when, then)
    • 단위 테스트: 단 하나의 클래스가 제대로 동작 테스트(단 하나의 클래스와 의존성은 mock처리)
      • 도메인 엔티티
      • 유스케이스
    • 통합 테스트: 두 계층 간의 경계를 걸친 테스트
      • 컨트롤러
        • @WebMvcTest
      • 영속성 어댑터
        • @Import
        • @DataJpaTest
        • DB를 모킹하지않고, 실제로 DB접근까지 확인 한다.
    • 시스템 테스트: 애플리케이션을 구성하는 모든 객체 네트워크 가동시켜, 특정 유스케이스가 전 계층이 동작하는 테스트
      • 통합 유스케이스별 테스트(요청부터 json응답까지)
        • @SpringBootTest
  • 통합테스트로 웹 어댑터 테스트하기
  • 얼마만큼의 테스트가 충분할까?
    • 도메인 엔티티를 구현할 때는 단위 테스트를 커버하자
    • 유스케이스를 구현할 때는 단위 테스트를 커버하자
    • 어댑터를 구현할 때는 통합 테스트를 커버하자
    • 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버하자
    • 개발 중에 테스트를 짜야(TDD) 개발 도구로 느껴질것이다.
    • 최대한 작은 범위 테스트는 60%, 큰 범위 테스트는 핵심만 지향하자

8. 경계 간 매핑하기

  • “매핑하지 않기” 전략

    매핑하지않기

    • 모든 계층 입출력 모델을 도메인 모델 사용
    • (단점)웹 계층과 영속성 계층에서 요구하는 변경점이 생길경우, 도메인 모델 변경되어야함
    • 빠른 개발 후 넘어가는 부분적으로 양방향 전략으로 넘어가는 과정 필요
  • “양방향” 매핑 전략

    양방향

    • 도메인 모델 ↔ 각 계층 모델간의 매핑 필수
    • 장점
      • 각 계층의 변경사항이 있어도, 도메인 모델에 변경을 유발하지않는다.
    • 단점
      • 각 모델별 매핑 필요 → 보일러플레이트 코드가 다수 발생
      • 도메인 모델 계층이 인커밍 포트에서는 파라미터로, 아웃고잉 모델에서는 반환값으로 사용된다. 즉, 바깥쪽 계층의 요구에 따른 변경에 취약함
  • “완전” 매핑 전략

    완전매핑

    • 각 계층별 전용 모델 존재 및 계층 통과 시 필요한 모델(커맨드 혹은 리퀘스트라고 불림)을 정의하여 매핑
      • SendMoneyUseCase 포트의 입력 모델로 특화도니 SendMoneyCommand
    • 이러한 전용 모델들은 각자의 전용필드 및 유효성 검증이 가능하다.
    • 보통 코어 ↔ 영속성 계층 사이에서는 매핑 오버헤드 때문에 사용하지않는다.
  • “단방향” 매핑 전략

    단방향

    • 모든 계층의 모델을 단일 인터페이스로 정의
  • 언제 어떤 매핑 전략을 사용할 것인가?

    • 그때그때 다르다.

9. 애플리케이션 조립하기

애플리케이션 조립하기

  • 평범한 코드로 조립하기
  • 스프링의 클래스패스 스캐닝으로 조립하기
    • @Component
    • 단점
      • 어떤 것들이 Bean으로 생성되었는지 관리 못할가능성있음
  • 스프링의 자바 컨피그로 조립하기
    • @Configuration
    • 장점
      • 내가 의도한 객체를 Bean으로 등록해줌

10. 아키텍처 경계 강제하기

  • 경계와 의존성

    아키텍처 경계 강제하기

  • 접근 제한자

    • package-private 접근 제어자+ 스프링을 사용하여, 계층의 강제성을 부여가능하다.

    계층의 강제성

  • 컴파일 후 체크(나중에 학습하기)

    • ArchUnit
  • 빌드 아티팩트

    • 여러 개의 분리된 빌드 아티팩트를 통해서, 의존성을 분리하여 각 계층의 영향도 낮춤

11. 의식적으로 지름길 사용하기

  • 왜 지름길은 깨진 창문 같을까?
  • 깨끗한 상태로 시작할 책임
  • 유스케이스 간 모델 공유하기
    • 유스케이스마다 다른 입출력 모델을 가져야 매우 독립적이다.
    • 하지만 변경할 이유가 동일하다면, 실제로 두 유스케이스에 모두 영향을 주고싶은게 의도되었다면 상관없다.
  • 도메인 엔티티를 입출력 모델로 사용하기
    • 간단한 개발시에는 요청사항이 엔티티 변경사항과 일치하여 입력모델 또한 도메인 엔티티를 사용하는것간단하지만, 시간이 지날수록 영향도가 생긴다면, 전용 모델로 교체해야한다.
  • 인커밍 포트 건너뛰기
    • 인커밍 어뎁터에서 포트없이 코어(서비스)에 붙는경우, 추상화 계층이 없으므로, 전용 모델을 쓰지도 못하고 구분점도 모호해진다. → 아키텍처 또한 무너지므로 지양
  • 애플리케이션 서비스 건너뛰기
    • 유스케이스에서 코어(서비스) 제외하고 영속성 어뎁터로 처리하는경우, 유스케이스를 판단하기어렵고, 아키텍처 또한 무너지기때문에 지양

12. 아키텍처 스타일 결정하기

  • 도메인이 왕이다
    • 도메인에 따라 다양한 도메인 로직 작성가능
    • 입력, 출력과 상관없이 비즈니스 로직에만 집중가능한 구조
  • 경험이 여왕이다.
    • 헥사고날 경험해봐라. 소규모라도 도입
  • 그때그때 다르다
    • 어떨때는 계층형, 어떨때는 헥사고날 선택하자.
CATALOG
  1. 1. 왜?
  2. 2. 용어정리
  3. 3. 1. 계층형 아키텍처의 문제는 무엇일까?
  4. 4. 2. 의존성 역전하기
  5. 5. 3. 코드 구성하기
  6. 6. 4. 유스케이스 구현
  7. 7. 5. 웹 어댑터 구현하기
  8. 8. 6. 영속성 어댑터 구현하기
  9. 9. 7. 아키텍처 요소 테스트하기
  10. 10. 8. 경계 간 매핑하기
  11. 11. 9. 애플리케이션 조립하기
  12. 12. 10. 아키텍처 경계 강제하기
  13. 13. 11. 의식적으로 지름길 사용하기
  14. 14. 12. 아키텍처 스타일 결정하기