만들면서 배우는 클린아키텍쳐 내용 메모

Dae-Hwa Jeong·2024년 7월 17일
0

책 내용 단순 나열에 가까운 기록용도의 글입니다. 특별한 내용은 거의 없습니다

https://github.com/wikibook/clean-architecture

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

계층형 아케텍쳐는 선택의 폭을 넓히고 변화하는 요구사항과 외부요인에 빠르게 적응할 수 있게 해준다.

문제점은 코드에 나쁜 습관들이 스며들기 쉽게 만들고 시간이 지날수록 소프트웨어를 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.

데이터베이스 주도 설계를 유도한다

  • 도메인 계층은 영속성 계층에 의존하기 때문에 모든 것이 영속성 계층을 토대로 만들어진다.
  • 비즈니스 관점에서는 도메인 로직을 먼저 만들 수 있어야 한다.
  • ORM을 사용하다보면 도메인 계층과 영속성 계층 사이 강한 결합이 생긴다. 도메인 계층에서 ORM 엔티티에 접근할 수 있기 때문

지름길을 택하기 쉬워진다

  • 같은 계층에 있는 컴포넌트나 아래 계층에만 접근하도록 하는 규칙만 있어 우회하기가 쉬워진다.
  • 예를 들어, 상위 계층의 컴포넌트에 접근하고 싶으면 해당 컴포넌트를 아래 계층으로 내려버리면 된다. 이러면 영속계층이 비대해진다.
  • 한번 망가지기 시작한다면?... 깨진 유리창 이론

테스트하기 어려워진다

  • 영속성 계층 형태 그대로 웹계층에서 사용하니 필드 하나만 바뀌어도 전체에 영향을 미친다. 책임이 섞이고 도메인 로직이 퍼져나갈 확률이 높아짐
  • 웹 계층 테스트를 하는데 영속성 계층도 모킹해야 한다. 테스트 설정이 복잡해지면 테스트를 전혀 작성하지 않는 방향으로 갈 확률이 높아짐
  • 실제 테스트를 작성하는 시간보다 설정시간이 더 오래걸림...

유스케이스를 숨긴다

  • 도메인 로직이 여러 계층에 존재할 확률이 높으니 새로운 기능이 어느 계층에 들어가야 할지 모호해진다.
  • 넓은(여러 컨트롤러를 담당하는) 서비스 클래스들이 많아진다.

동시 작업이 어려워진다

  • 인터페이스를 먼저 따서 개발하면 상관 없지만, 대부분 영속 계층에 의존한 개발을 하기 때문에 불가능

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

  • 엄격한 제약이 없으므로 잘못 흘러갈 가능성이 높다.
  • 계층형 단점을 염두에 두고 만들어야 한다.

2. 의존성 역전하기

단일 책임 원칙

  • 컴포넌트를 변경하는 이유는 한 가지여야 한다.
  • 하지만 변경할 이유는 컴포넌트 간의 의존성을 통해 전파된다.

부수효과에 관한 이야기

  • 핵심적인 컴포넌트를 건드렸을때 발생하던 부수효과때문에 그걸 회피하는 이상한 방식을 클라이언트에서 요구하더라는 썰풀

의존성 역전 원칙

  • 단일 책임 원칙을 고수준에서 적용하면 상위 계층이 하위 계층에 비해 변경할 이유가 더 많다
  • 따라서 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 바꿀때마다 도메인 계층도 변경해야 한다.
  • 하지만 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 변경의 영향을 최소화 하고 싶다.
  • 이럴 때 의존성을 역전시키면 된다.

    레포지토리의 인터페이스를 도메인 계층에 넣어두고 구현은 영속 계층에서 하도록

클린 아키텍처(로버트 마틴)

  • 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 하는 것(의존성을 역전시켜서)
  • 클린 아키텍처에서 유스케이스는 서비스에 대응된다. 단일 책임을 갖기 위해 세분화 되는데 이를 이용해 넓은 서비스 문제를 해결할 수 있다.
  • 도메인 코드에서 어떤 영속 프레임워크나 UI 프레임워크가 사용됐는지 알지 못한다. 따라서 특정 프레임워크에 종속된 코드를 가질 수 없고 비즈니스 규칙에 집중할 수 있다.
  • 따라서 순수한 형태의 DDD를 적용할 수 있다.
  • 트레이드 오프로 각 계층에서 엔티티에 대한 모델을 만들어야 한다. 그래서 계층을 넘나들때마다 DTO 변환작업이 필요해진다.
  • 다소 추상적

헥사고날 아키텍쳐(알리스테어 콕번)

  • 육각형은 크게 의미 없다. 다른 시스템이나 어댑터와 연결되는 4개 이상의 면을 가질 수 있음을 보여주기 위해 사용했다 한다.
  • 육각형에서 외부로 향하는 의존성이 없다. 따라서 클린아키텍쳐를 만족한다.
  • 코어와 어댑터들간의 통신을 위해 코어가 포트를 제공해야 한다.
    • driving 어댑터에게는 코어에 있는 유스케이스 클래스들에 의해 구현되고 호출되는 인터페이스가 포트

      내가 구현하면서 느껴본 바로는 의존방향과 제어 방향이 일치하므로 의존 역전이 필요 없을 수 있음

    • driven 어댑터에게는 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 포트
  • 이런 패턴때문에 포트와 어댑터 아케턱쳐라고 불리기도 함
  • 유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?
    • 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 해서 코드를 변경할 이유의 수를 줄일 수 있다. 이러면 유지보수성이 좋아진다.
    • 도메인 코드는 비즈니스 문제에 딱 맞도록 모델링 될 수 있다. 다른 영역은 고려하지 않아도 된다.

3. 코드 구성하기

계층으로 구성

  • domain, persistence, web 으로 패키지를 나누기.
    • 저자는 domain에 persistence의 인터페이스를 두고 영속 영역에서 이를 구현하도록 했음
    • 그럼에도 최적 구조가 아니라고 하는데,
      • 애플리케이션의 기능 조각(functional slice)이나 특성(feature)을 구분 짓는 패키지 경제가 없다. 이러면 서로 연관없는 클래스들끼리 부수효과를 발생시킬 가능성이 높아진다.(예시에서는 Account와 관련된 기능만 존재하던 프로젝트에 User 관련 기능이 추가되면 User와 관련된 컨트롤러, 서비스, 도메인, 레포지토리의 한 벌이 같이 섞이게 되는 것을 얘기함)
      • 애플리케이션이 어떤 유스케이스를 제공하는지 파악할 수 없다.(아마 테이블 명이 Account니까 이를 관성적으로 따라가지 않았을까...)

        이건 그냥 클래스 여러개 만들어서 관리하면 되는거 아닌가? 하는 생각도 든다

      • 육각형 아키텍쳐를 따랐는지 확인하려면 세부 구조를 확인해야한다.(인커밍 포트와 아웃고잉 포트가, 각 어댑터들이 어디에 존재하고 있는지 직관적으로 파악하기 힘들다)
  • 기능으로 구성하기
    • 기능으로 나누면 package private 접근 수준으로 패키지간 경계를 강화할 수 있다.
    • AccountService는 이미 account 패키지 아래에 있으니 SendMoneyService로 이름을 바꾼다.

      애플리케이션의 기능을 코드를 통해 볼 수 있게 만드는 것을 로버트 마틴은 소리치는 아키텍쳐라고 명명한다.

    • 그런데 이렇게하면 아키텍쳐의 가시성을 더 떨어뜨린다. 계층에 대한 구분이 없기 때문에

아키텍처적으로 표현력 있는 패키지 구조

  • 육각형 아키텍처에서 구조적으로 핵심적인 요소는 엔티티, 유스케이스, 인커밍/아웃고잉 어댑터다.
  • 아래는 구조의 각 요소들을 패키지에 매핑 시킨 구조
    buckpal
    - account
      - adapter
        - in
          - web
            - AccountController
        - out
          - persistence
            - AccountPersistenceAdapter
            - SpringDataAccountRepository
    - domain
      - Account
      - Activity
    - application
      - SendMoneyService
      - port
        - in
          - sendMoneyUseCase
        - out
          - LoadAccountPort
          - UpdateAccountStatePort 

    Adapter들은 packege private으로 둬도 된다. 어차피 소통은 port의 in/out 인터페이스로만 하니까

  • 구조가 생소하다면 복잡해 보일 수 있지만, 의사소통에 크게 도움이 될 수 있다.
  • 예를 들어, 만약에 서드파티 API 클라이언트를 변경해야 하면 adapter.out 에 있는걸 찾아서 바꾸면 된다.
  • 이 구조로 아키텍처-코드 갭, 모델-코드 갭을 다룰 수 있다.

    TODO: https://www.georgefairbanks.com/software-architecture/model-code-gap/

  • 또한 DDD 개념에 직접적으로 대응 시킬 수 있다.
  • account 같은 상위 레벨 패키지는 다른 바운디드 컨텍스트와 통신할 전용 진입접과 출구(포트)를 포함하는 바운디드 컨텍스트에 해당한다.
  • 완벽한 구조는 아니다. 지켜야 할 규칙이 있고, 패키지 구조가 적합하지 않아서 어쩔 수 없이 아키텍처-코드갭을 넓히고 아키텍처를 반영하지 않는 패키지를 만들어야 할 수도 있다.
  • 그럼에도 표현력 있는 패키지구조는 아키텍처-코드 갭을 줄일 수 있게 도와주는 도구다
  • 의존성 주입의 역할
    • 클린 아키텍처의 본질적인 요건은 애플리케이션 계층이 인커밍/아웃고잉 어댑터에 의존하지 않는 것이다.
    • 제어의 흐름과 의존성 방향이 다르다면 의존성을 역전 시켜야 한다.
      • 제어의 흐름 : AccountController -> <I>SendMoneyUseCase -> SendMoneyService -> <I>LoadAccountPort -> AccountPersistenceAdapter
      • 의존성 방향 : AccountController -> <I>SendMoneyUseCase -> SendMoneyService <- <I>LoadAccountPort <- AccountPersistenceAdapter

        책의 구현은 SendMoneyUseCase를 SendMoneyService가 구현하도록 하고, SendMoneyService에서 LoadAccountPort를 사용하도록 한다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

  • 이제 코드에서 아키텍처의 특정 요소를 찾으로면 패키지 구조를 보면 된다.
    이러면 의사소통, 개발, 유지보수가 수월해진다.

4. 유스케이스 구현하기

  • 서론?
    • 이제 애플리케이션, 웹, 영속성 계층이 느슨하게 결합되어 있기 때문에 도메인 코드는 편하게 모델링 하면 된다.
    • DDD를 해도 되고, 리치 도메인, 어니믹 도메인(빈약한 도메인) 모델을 구현하거나 자기 나름의 방식을 사용해도 된다.
    • 그럼에도 아래부터는 책에서 제시하는 방법을 설명한다.
    • 육각형 아키텍처는 도메인 중심의 아키텍처에 적합하다. 따라서 유스케이스를 도메인 엔터티 중심으로 구현한다.

도메인 모델 구현하기

  • 한 계좌에서 디른 계좌로 송금하는 유스케이스를 구현한다.
  • 객체지향적으로 모델링하는 한 가지 방법은 입금과 출금의 책임을 가진 Account 엔티티를 만들고 출금 게좌에서 돈을 출금해서 입금 계좌로 돈을 입금하는 것

    https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/domain/Account.java

    • Account 엔티티는 실제 계좌의 현재 스냅샷을 제공한다.
    • 입금과 출금은 Activity 엔티티에 포착된다.
    • 한 계좌의 모든 Activity를 미리 메모리에 올려두는 것은 비효율적이므로 ActivityWindow VO 에서 포착한 지난 며칠 혹은 몇 주간의 범위에 해당하는 활동만 보유한다.
    • 현재 잔고 계산을 위해 baselineBalance가 있다. 현재 총 잔고는 baselineBalance에 활동창의 모든 활동들의 잔고를 합한 값이 된다.
      Money.add(
      				this.baselineBalance,
      				this.activityWindow.calculateBalance(this.id)
            );
    • 입금과 출금은 각각 새로운 활동을 활동창에 추가하는 것을 의미한다. (withdraw() 와 deposit() 메서드)
    • 출금하기 전에는 잔고를 초과하는 금액은 출금할 수 없도록 검사하는 비즈니스 규칙이 있다.

유스케이스 둘러보기

  • 유스케이스가 하는 일?

    1. 입력을 받는다.
    2. 비즈니스 규칙을 검증한다.
    3. 모델 상태를 조작한다.
    4. 출력을 반환한다.

      저자는 유스케이스 코드가 도메인 로직에만 신경써야 한다고 생각한다. 유스케이스에 입력 유효성 검증이 들어가면 오염된다고 생각한다.

  • 비즈니스 규칙을 충족하면 유스케이스는 입력을 기반으로 어떤 방법으로든 모델의 상태를 변경한다.

    일반적으로 도메인 객체의 상태를 바꾸고 영속성 어댑터를 통해 구현된 포트로 이 상태를 전달해서 저장될 수 있게 한다. 그리고 또 다른 아웃고잉 어댑터를 호출 할 수도 있다.

  • 넓은 서비스 문제를 피하려면 각 유스케이스별로 분리된 각각의 서비스를 만든다.

  • https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/application/service/SendMoneyService.java

입력 유효성 검증

유스케이스 책임은 아니지만 애플리케이션 게층의 책임에는 해당한다.

어댑터가 유스케이스에 입력을 저날하기 전에 입력 유효성을 검증한다면? 이걸 모두 검증했다고 믿을 수 있나? 그리고 유스케이스는 하나 이상의 어댑터에서 호출되는데 유효성 검증이 각 어댑터마다 구현되어야 한다.

애플리케이션 게층에서 입력 유효성 검증 하는 이유

  • 애플리케이션 바깥쪽에서 유효하지 않은 입력값을 받으면 모델의 상태를 해치게 된다.

그래서 입력 모델(input model)을 만든다.

  • SendMoneyService 에서 받는 메소드가 sendMoney(SendMoneyCommand) 이런 식으로 생김
  • in port에 위치하므로 애플리케이션 계층임
  • 쉽게 말하면 컨트롤러에서 서비스로 넘기는 DTO를 별도로 잡고 여기서 유효성 검증을 할 수 있도록 한 것.
  • 이렇게 하면 유스케이스 구현체 주위에 오류 방지 계층(anti corruption layer; 하나의 바운디드 컨텍스트를 다른 바운디드 컨텍스트와 격리시키는 계층)을 만든 것

생성자의 힘

SendMoneyCommand 는 생성자에 많은 책임을 부여했다. 클래스가 불변이고 유효성 검증까지 하고 있으니 유효하지 않은 상태를 만드는게 불가능

매개변수가 많으면 빌더로 만들어도 됨. 어차피 유효성 검사 하니까 빌더를 써도 잘못된 객체가 만들어지기 어려움.

그럼에도 빌더는 런타임에 감지가 되고 컴파일 타임에 감지를 하기 힘듦.

감지하려면 테스트 코드가 있어야 할 것이다

그래서 책에서는 생성자 사용을 권장

유스케이스마다 다른 입력 모델

서로 다른 유스케이스에 같은 입력 모델을 사용하고 싶을 수 있다.

그런데 이건 코드 스멜일 가능성이 생긴다.

예를 들어, 같은 불변 커맨드 객체를 사용하지만 특정 유스케이스에서는 허용해야 하는게 다른 유스케이스에서는 불허해야 하는 경우가 있으니(e.g. nullable, not null)

따라서 유스케이스 전용 입력 모델을 만들어야 불필요한 부수효과가 생기지 않는다고 설명한다.

대신 입력 모델에 매핑하는 비용이 생긴다.

비즈니스 규칙 검증하기

입력 유효성과 비즈니스 규칙 검증을 실용적으로 구분할 수 있는 방법은 도메인 모델의 현재 상태에 접근해야 하는지 여부

입력 유효성 검증은 @NotNull 같은거 붙이면 땡일 가능성이 높다.

책에서는 입력 유효성은 구문상의(syntactical) 유효성 검증이라 하고 비즈니스 규칙 검증은 의미적인(semantical) 유효성 검증이라 표현한다.

비즈니스 규칙 검증의 예시로 출금 계좌는 초과 출금 되어서는 안 된다는 조건을 얘기한다. 초과 출금 되지 않으려면 출금 계좌의 현재 상태에 접근해야 하므로 비즈니스 규칙이다.

반대로 송금되는 금액은 0보다 크다라는 조건은 모델에 접근하지 않아도 해결된다. 이러면 유효성 검증이다.

관점 따라 논쟁이 될 수 있다고 하는데 책에서는 코드의 일관성 측면에서 유지보수 하기 쉬운 방법이라 설명한다.

컨트롤러에서 받는 Request 모델에서 검증해야 하면 틀린 말일 수 있는데, 책에서 처럼 커맨드 객체를 만들어 사용하는거면 이게 맞는 것 같다.

그러면 비즈니스 규칙검증은 어떻게 하나? 도메인 객체에 넣어준다. 이러면 비즈니스 로직 옆에 존재하니 위치 고민도 없고 추론도 쉽다.

이게 애매하면 유스케이스 코드(서비스 구현체)에서 도메인 사용 전에 해준다.

풍부한 도메인 모델 vs 빈약한 도메인 모델

풍부한 도메인 모델

풍부한 도메인 모델에서는 엔티티에서 가능한 많은 도메인 로직을 구현한다.

  • 유스케이스는 도메인 모델의 진입점이다.
  • 유스케이스에는 사용자의 의도만 표현하고 실제 작업은 도메인 엔티티 메서드에서 수행한다.
  • 비즈니스 규칙이 유스케이스 구현체가 아닌 엔티티에 위치하게 된다.

빈약한 도메인 모델

엔티티 자체가 굉장히 얇다.

상태를 표현하는 필드와 getter, setter 메서드만 포함되고 도메인 로직을 가지지 않는다.

다른 말로 하면 도메인 로직이 유스케이스 클래스에 구현되어 있는 것

뭐가 좋은가?

상황과 필요에 따라서~

굳이 객체 상태를 다룰 필요성이 없는 아주 단순한 도메인이라면 리치 도메인이 없어도 된다. 개인적으론 그렇더라도 디미터 법칙은 지켰줬으면 하는 생각...

유스케이스마다 다른 출력 모델

출력도 각 유스케이스에 맞게 구체적인이 좋다고 한다. Account를 직접 반환하는게 좋은 경우가 생길 수도 있긴 한데, 이건 계속 고민해야 하는 문제. 의심스러우면 가능한 적게 반환한다.

유스케이스들 간에 같은 출력 모델을 공유하면 유스케이스들도 강하게 결합된다.(단일 책임원칙 위반. 공유모델은 장기적으로 보면 무조건 커지게 되어있다.)

같은 이유로 도메인 엔티티를 출력 모델로 사용하는 것도 지양해야 한다. 도메인 엔티티를 변경해야 할 이유가 늘어나게 된다.

읽기 전용 유스케이스는 어떨까?

책에서 하는 방법은 인커밍 전용 포트를 만들고 쿼리 서비스에서 구현하는 것.

CQRS 처럼 구현할 수도 있다.

조회 전용은 여러 계층에서 같은 모델을 사용한다면 지름길을 써보는 것을 고려해볼 수도 있다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

입출력 모델을 독립적으로 모델링하면 원치 않는 부수효과를 피할 수 있다.

대신 작업은 늘어난다. 매핑해줘야 하니까.

대신에 장기적으로 보면 유스케이스 이해도를 높일 수 있고 유지보수성도 높아진다. 서로 작업중인 유스케이스를 건드리지 않을 수 있으니.

지속 가능한 코드.

5. 웹 어댑터 구현하기

의존성 역전

웹 어댑터는 인커밍(주도하는) 어댑터.

웹 어댑터는 애플리케이션 계층에 있는 포트를 호출. 여기도 의존성을 역전시켜놨다.

제어의 흐름 : 컨트롤러 -> 포트 -> 서비스
의존성 방향 : 컨트롤러 -> 포트 <- 서비스

컨트롤러에서 서비스 직접 호출을 막은 이유는 포트를 외부와 통신하는 명세로 사용하기 때문

웹소켓은 인 아웃 포트에 모두 의존하도록 그림을 그려놨다.

웹소켓 컨트롤러 -> 인 포트 <- 서비스
-> 아웃포트 <- 서비스

카프카 스트림즈 앱 구현하면서 난관?에 봉착했던 부분인데, in/out을 겸해야 하는 상황이다. 저렇게하면 순환 의존성은 확실히 피할 수 있을 것 같긴 한데, 스트림즈에 종속적인 코드가 어쩔 수 없이 나와야 하는 부분들은 어떻게 해야할지 아직도 고민이다.

웹 어댑터의 책임

  1. HTTP 요청을 자바 객체로 매핑
  2. 권한 검사
  3. 입력 유효성 검증
  4. 입력을 유스케이스의 입력 모델로 매핑
  5. 유스케이스 호출
  6. 유스케이스의 출력을 HTTP로 매핑
  7. HTTP 응답을 반환

유스케이스 입력모델과는 별개의 유효성 검증을 해야 한다고 한다. 웹 어댑터의 입력 모델을 유스케이스 입력 모델로 변환할 수 있는지를 확인하고, 이걸 방해하는 모든 것이 유효성 검증 에러라고 한다.

HTTP와 관련된 것이 애플리케이션 계층으로 침투하면 안된다. 만약 HTTP가 아닌 다른 방식을 사용한다면 동일한 도메인 로직을 수행할 수 있는 선택지를 잃게 된다.

도메인과 애플리케이션 계층부터 개발하면, 어댑터 보다 유스케이스를 먼저 구현하면 경계가 명확해진다고 한다.

컨트롤러 나누기

컨트롤러도 너무 적은 것 보다는 너무 많은게 낫다고 한다(넓은 서비스 문제와 동일하게).

같은 리소스라고 하나의 컨트롤러에 묶어놓으면 아무리 메소드를 잘 나눠놓는다 해도 파악이 힘들어진다. 테스트도 비대해진다.

같은 dto 를 공유하게 되는 경우도 생기는데, 이게 진짜 해당 연산에서 필요한 것인지 생각해봐야 한다.

책에서 예시든건, Account와 User 객체가 1:N 관계일때 계좌를 생성하거나 업데이트 할때 User 객체도 필요한지에 관한 것. list 연산에 사용자 정보도 포함 시켜야 하는가?

전용 모델들을 패키지 private으로 선언하면 다른 곳에서 재사용 될 위험에서도 벗어나게 된다.

이렇게 나누면 동시 작업도 수월해진다.

갈무리

웹 어댑터는 어떠한 도메인 로직도 수행하지 않는다. 애플리케이션 계층은 HTTP와 관련된 작업을 해서는 안 된다.

세분화된 컨트롤러는 처음에 공수가 더 들지만 유지보수하기 훨씬 좋다.

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

의존성 역전

영속성 어댑터는 아웃고잉(주도되는) 어댑터다.

어댑터에서 애플리케이션을 호출하지 않는다. 애플리케이션에게 호출 당한다.

코어의 서비스가 포트를 사용한다. 여기서 포트는 애플리케이션 레이어와 영속성 레이어 사이의 간접적 계층이다. 서비스에서 영속성 계층에 대한 코드 의존성을 없앨 수 있다.

영속성 어댑터의 책임

  1. 입력을 받는다.
  2. 입력을 데이터베이스 포맷으로 매핑한다.
  3. 입력을 데이터베이스로 보낸다.
  4. 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다.
  5. 출력을 반환한다.

포트 인터페이스에 의해 입력 메세지를 받으면 인터페이스가 지저한 특정 도메인 엔티티나 데이터베이스 연산 전용 객체가 입력 모델이 될 것이다.

영속성 어댑터는 이걸 데이터베이스에 쿼리 날리거나 변경하는데 사용할 수 있는 포멧으로 입력 모델을 매핑한다.

JPA를 사용하면 입력 모델을 JPA 엔티티로 변환시킬 것이다. 그런데 이건 가성비가 떨어질 수 있어서 매핑하지 않는 전략을 선택할 수도 있다.

영속성 어댑터의 입력 모델이 애플리케이션 코어에 존재한다. 따라서 영속성 어댑터 내부를 변경해도 코어에 영향을 주지 않는다.

출력 모델도 애플리케이션 코어에 위치해야 한다.

잘 생각해보면 당연한 얘기인데, 코어에서는 영속성 레이어를 몰라야 하기 때문에 애플리케이션 레이어에 있는걸 가져온 뒤 변환하거나, 좀 느슨하게 만들어서 그게 @Entity 객체라면 바로 사용하거나 하는 식으로 진행 될것이다.

포트 인터페이스 나누기

특정 엔티티가 필요로 하는 모든 데이터베이스 연산을 하나의 리포지토리 인터페이스에 넣어두는게 일반적인 방법이다. 그런데 이러면 넓은 포트 인터페이스가 된다. 애플리케이션 레이어 입장에서 필요한 메소드 이외의 메소드도 알게 된다. 이러면 불필요한 의존성이 생긴다.

이러면 코드를 이해하고 테스트하기 힘들어진다. 어떤 메소드가 필요했던건지, 모킹을 어디까지 해야하는지 처음 보는 사람이 보면 모호해진다.

로버트 마틴 : 필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 발생할 수 있다.

이걸 해결하는 방법이 인터페이스 분리 원칙(Interface Segregation Principle; ISP)

포트(인터페이스)를 여러개 만들고 영속 어댑터에서 이걸 다 구현하도록 한다. 책에서는 플러그 앤 플레이로 비유한다.

굳이 이렇게까지? 싶긴 하다. 뒤를 보면 알겠지만 어차피 어댑터에서 레포지토리를 다시 호출해야 한다. 그렇지 않은 경우는 효과적일듯

영속성 어댑터 나누기

영속성 어댑터는 도메인 클래스(혹은 DDD의 어그리거트) 하나당 하나의 영속성 어댑터를 구현하는 방식이 되어도 된다.

class AccountPersistenceAdapter implements LoadAccountPort, UpdateAccountStatePort {
  // ...
}

이렇게 하면 영속성 어댑터들은 도메인 경계를 따라 자동으로 나눠진다. 나중에 여러개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기도 좋아진다.

포트가 여러개면 경우에 따라 영속 레이어 구현 기술을 필요에 따라 선택해 사용하면 된다.

도메인 코드는 영속성 포트의 구현이 어떻게 됐는지에 대해 관심이 없기 때문이다. 포트 구현만 할 수 있으면 영속성 계층에서 하고싶은대로 하면 된다.

스프링 데이터 JPA 예제

https://github.com/wikibook/clean-architecture/blob/main/src/main/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapter.java

updateActivities(Account account) 를 보면 도메인 엔티티인 Account를 그대로 가져온다. 그 뒤 JPA 엔티티로 바꿔준다.

for (Activity activity : account.getActivityWindow().getActivities()) {
    if (activity.getId() == null) {
        activityRepository.save(accountMapper.mapToJpaEntity(activity));
    }
}

이게 불필요하다고 판단해 매핑하지 않기 전략을 취할 수도 있다. 그런데 현재 Account 도메인의 요구사항을 지키려면 기본 생성자가 있으면 안 된다. 특정 기술을 위해 도메인 모델을 타협해야 하는 상황이고 책에서는 풍부한 도메인 모델을 원하기 때문에 매핑을 시켜줬다.

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

영속성 어댑터는 본인이 어떤 유스케이스에 포함되는지 알지 못한다. 따라서 언제 트랜잭션을 열고 닫을지 결정할 수 없다. 따라서 트랜잭션에 관한 책임은 서비스에 위임해야 한다.

가장 쉬운 방법은 유스케이스를 구현한 서비스 클래스에 @Transactional을 붙이는 것인데, 이게 싫으면 aop를 이용해 위빙하면 된다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

영속성 어댑터를 플러그인처럼 동작하게 만들면 도메인 코드가 영속성 계층과 분리되어 풍부한 도메인 모델을 만들 수 있게 된다.

좁은 포트 인터페이스를 사용하면 포트 구현이 유연해진다.

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

테스트 피라미드

테스트피라미드

출처 https://martinfowler.com/bliki/TestPyramid.html

시스템 테스트 -> 통합 테스트 -> 단위 테스트 순으로 이루어진다

비용이 적고 유지보수하기 쉽고 빨리 실행되고 안정적인 작은 크기의 테스트들에 대해 높은 커버리지를 유지해야 한다는 것

경계를 넘거나 결합하는 테스트는 만드는 비용이 비싸지고 실행이 느리고 깨지기 쉬워진다.

테스트 비용이 높으면 커버리지 목표를 낮게 잡아야 한다.

  • 단위 테스트

    • 피라미드의 토대
    • 테스트 중인 클래스(Class Under Test -> CUT 라고 한다고 한다)가 다른 크래스에 의존하면 해당 클래스들은 모킹
  • 통합 테스트

    • 연결된 여러 유닛을 인스턴스화 하고 시작점이 되는 클래스의 인터페이스로 데이터를 보낸 후 잘 동작하는지 검증
    • 특정 시점에서는 목을 대상으로 수행해야 한다.
  • 시스템 테스트

    • 애플리케이션을 구성하는 모든 객체 네트워크를 가동시켜 특정 유스케이스가 전 계층에서 잘 동작하는지 검증

단위 테스트로 도메인 엔티티 테스트하기

https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/domain/AccountTest.java

단위테스트는 만들고 이해하는 것도 쉽고 아주 빠르게 실행된다. 비즈니스 규칙을 검증하기에 가장 적절한 방법이다. 도메인 엔티티는 다른 클래스에 거의 의존하지 않기 때문

단위 테스트로 유스케이스 테스트하기

https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java

클래스 모킹 같은 세팅해야 될게 생기면서 가독성이 떨어지니 예제에서는 BDD 패턴으로 작성

테스트 대상이 외부 서버라 모킹을 했을 경우 상호작용 했는지 여부를 검증한다. 그런데 이러면 코드의 행동 변경 뿐만 아니라 구조 변경에도 취약해진다. 리팩토링 하면 테스트도 변경 될 확률이 높아진다.

따라서 모든 동작을 검증하기보단 꼭 필요한 상호작용만 테스트하는게 좋다. 그리고 이런 테스트는 사실상 통합테스트에 가깝다.

통합 테스트로 웹 어댑터 테스트하기

https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/in/web/SendMoneyControllerTest.java

예제 코드에서는 MockMvc를 이용해 모킹했기 때문에 실제로 HTTP 프로토콜로 테스트한 것은 아니다. 대신 JSON에서 커맨드 모델 객체로 매핑하는 과정을 검증한다.

또한 기대한 응답을 반환하는지도 확인한다.

단위테스트처럼 보일 수 있지만, 내부적으로 스프링 프레임워크와 관련된 작업이 이루어진다(@WebMvcTest를 사용했기 때문에). 프레임워크와 강하게 묶여 때문에 프레임워크와 통합된 상태로 테스트하는 것이 합리적이다.

매핑, 유효성 검증, HTTP 입력 검증 등

통합 테스트로 영속성 어댑터 테스트하기

https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/account/adapter/out/persistence/AccountPersistenceAdapterTest.java

비슷한 이유로 영속성 어댑터도 통합 테스트로 진행하는게 합리적이다. 데이터베이스 매핑도 검증해야 하기 때문에 @DataJpaTest 를 사용한다.

이 테스트에서는 데이터베이스를 모킹하지 않았는데, 커버리지와 별개로 실제 데이터베이스에서 구문 오류나 매핑 에러 등이 생길 수 있기 때문이다. 인메모리 테스트도 실용적이긴 하지만 같은 문제점이 있다. 테스트컨테이너 같은걸 사용하면 실제 환경과 같은 환경으로 진행할 수 있어 유용하다.

시스템 테스트로 주요 경로 테스트하기

https://github.com/wikibook/clean-architecture/blob/main/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java

싹다 띄워야 하니까 SpringBootTest 어노테이션 사용한다. TestRestTemplate을 이용해 실제 HTTP 통신을 한다.

실제 영속성 어댑터도 사용한다. 만약 서드파티 시스템을 실행해야 할 경우 해당 출력 포트 인터페이스들을 모킹해준다.

테스트 가독성을 위해 헬퍼 메서드를 만들었는데 이 헬퍼 메서드는 DSL(도메인인 특화 언어)를 형성한다.

적절한 어휘를 사용하면 도메인 전문가가 테스트에 대해 생각하고 피드백을 줄 수도 있다(큐큠버?).

위에서 작성한 통합테스트와 겹치는 부분도 있을 수 있는데, 계층간 매핑 문제 같은 통합테스트만으로 알 수 없는 문제도 알 수 있게 해주기 때문에 의미가 있다.

사용자가 애플리케이션을 사용하면서 거쳐갈 특정 경로로 시나리오를 만들어 커버할 수 있다면 최신 변경 사항들을 배포할 준비가 되었다고 확신할 수 있다.

얼마만큼의 테스트가 충분할까?

답하기 어려운 문제

라인 커버리지가 중요한 부분을 커버했다는 것을 알려주지는 않기 때문에 100퍼센트가 아니면 의미가 없을 수 있다.

저자는 마음 편하게 소프트웨어를 배포할 수 있는 것을 기준으로 삼는다고 한다. 그리고 배포 주기가 짧으면 테스트도 자주 돌아가니까 테스트를 더 신뢰할 수 있다고 한다.

따라서 처음에는 신뢰의 도약이 필요하다. 문제가 생기면 수정하고 이걸로 배우는 것을 우선순위로 삼으면 맞는 방향성이다.

테스트가 버그를 잡지 못한 이유를 생각하고 커버할 수 있는 테스트를 추가한다.

아예 테스트를 정의하는 것도 좋다.

  • 도메인 엔티티, 유스케이스 -> 단위테스트
  • 어댑터 -> 통합테스트
  • 중요한 애플리케이션 경로 -> 시스템 테스트

테스트가 개발 중에 이뤄진다면 귀찮은 작업이 아닌 개발 도구로 생각해볼 수 있다.

새로운 필드를 추가할때마다 테스트를 고치는데 한 시간을 써야 한다면 뭔가 잘못된 것

테스트가 코드의 구조적 변경에 너무 취약한 것이므로 개선해야 한다. 이러면 테스트로서의 가치를 잃는다.

내 생각에 테스트의 큰 장점 중 하나가 믿음을 가지고 변경이나 리팩토링할 수 있게 해준다는 점인데, 이 장점이 사라지기 때문인 것 같음

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

육각형 아키텍처는 도메인 로직과 바깥으로 향한 어댑터를 분리해놔서 내부는 단위테스트, 외부는 통합테스트로 명확하게 전략을 정할 수 있다.

입출력 포트는 뚜렷한 모킹 지점이 된다. 모킹할지 사용할지만 정하면 된다. 포트의 크기가 작으면 모킹하기 쉽고 어떤 메소드를 모킹해야할지 더 명확해진다.

모킹하는 것이 버거워지거나 코드의 특정 부분을 커버하기 위해 어떤 종류의 테스트를 사용해야 할지 모호하면 경고 신호다. 이런 측면에서 테스트코드가 카나리의 역할도 한다고 할 수 있다.

8. 경계간 매핑하기

매핑 찬성 : 두 계층 간 매핑을 하지 않으면 같은 모델을 사용해야 하기 때문에 두 계층이 강하게 결합됨

매핑 반대 : 보일러 플레이트 코드가 너무 많아지고, 대부분의 유스케이스들은 CRUD만 수행하고 계층에 걸쳐 같은 모델을 사용하기 때문에 과하다

둘 다 맞다. 장단을 따지고 상황에 따라 적합한 방법을 사용해야 한다.

매핑하지 않기 전략

입출력 모델로 도메인 모델을 사용하는 방법

도메인에 영속 계층과 JSON 직렬화를 위한 로직도 포함되므로 단일 책임 원칙을 위반한다.

하지만 간단한 CRUD에는 굳이 매핑을 할 필요가 없다.

모든 계층이 정확히 같은 구조의 같은 정보를 필요로 한다면 매핑하지 않기 전략은 완벽한 선택지다.

대신 애플리케이션 계층이나 도메인 계층에서 웹과 영속성 문제를 다루게 되면 다른 전략을 취해야 한다.

양방향 매핑 전략

도메인 모델 외에 웹 모델과 영속성 모델을 별도로 둔다. 따라서 도메인 모델이 단일 책임 원칙을 만족한다.

개념적으로 매핑하지 않기 전략 다음으로 간단하다.

단점은 보일러 플레이트 코드가 많이 생긴다는 점. 매핑 프레임워크를 사용해도 시간이 들고 디버깅도 어렵다.

도메인 모델이 계층 경계를 넘어 사용된다(각 포트에서 도메인 모델을 입출력 값으로 사용). 따라서 도메인 모델이 바깥 계층의 변경에 취약해진다.

완전 매핑 전략

커맨드 모델 같이 각 계층을 넘나들때도 별도의 모델을 사용하도록 한다.

더 많은 코드가 필요하다. 대신 여러 유스케이스의 요구사항을 함께 다뤄야 하는 매핑에 비해 구현 및 유지보수하기가 쉽다.

여러 유스케이스의 요구사항을 함께 다뤄야 한다 -> 매핑하지 않기나, 양방향은 하나의 도메인 모델이 여러 유스케이스의 입출력에 사용될 것

단방향 매핑 전략

모든 계층의 모델들이 같은 인터페이스를 구현하도록 한다. 도메인 계층을 바깥으로 전달하고 싶을때도 매핑 없이 할 수 있다. 인터페이스로만 주고 받으면 되니까.

DDD의 팩터리(어떤 특정한 상태로부터 도메인 객체를 재구성할 책임을 가진다) 개념과 잘 어울린다.

계층 간의 모델이 비슷할때 효과적이다. 예를 들어 읽기 전용 연산의 경우

단점은 개념적으로 어렵다.

언제 어떤 매핑전략을 사용할 것인가?

각 매핑 전략은 저마다 장단을 가지고 있다.

요구사항에 비해 과하게 복자한 전략은 개발을 불필요하게 더디게 만든다. 어떤 매핑 전략도 철칙처럼 여겨져서는 안된다.

필요하다면 섞어쓴다. 특정 전략이 전역 규칙일 필요는 없다.

어떤 매핑 전략을 선택했더라도 나중에 언제든 바꿀 수 있다. 지금은 최선의 전략처럼 보여도 시간이 지나면 아닐 수 있다.

따라서 어떤 상황에서 어떤 매핑전략을 최우선으로 고려해야 하는지에 대한 가이드라인을 정해둬야 한다.

책에서 제안하는 가이드라인

  • 변경 유스케이스
    • 웹 계층과 애플리케이션 계층 사이
      • 완전 매핑 전략을 먼저 고려한다.
      • 이러면 유스케이스별 유효성 검증 규칙이 명확해지고 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다.
    • 애플리케이션과 영속성 계층 사이
      • 매핑하지 않기 전략을 첫 번째 선택지로 둔다.
      • 매핑 오버헤드를 줄이고 빠르게 코드를 짤 수 있다.
      • 애플리케이션에서 영속성 문제를 다뤄야 하게 되면 양방향 매핑 전략으로 바꾼다.
  • 쿼리 작업
    • 매핑하지 않기 전략을 첫 번째 선택지로 둔다.
    • 애플리케이션 계층에서 영속성 문제나 웹 문제를 다뤄야 하게 양방향 매핑 전략으로 바꾼다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

좁은 포트를 사용하면 유스케이스마다 다른 매핑 전략을 사용할 수 있고, 다른 유스케이스에 영향을 미치지 않으면서 코드를 개선할 수 있다.

상황별로 매핑전략을 선택하면 더 어렵고 더 많은 커뮤니케이션을 필요로 할 것이다. 대신 코드가 정확히 해야하는 일만 수행하면서 더 유지보수하기 쉽게 될 것이다.

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

최상위의 설정 관련된 것. 의존성 관리 프레임워크의 원리에 대해 간단하게 설명해줌. 뒷 내용 빌드업용인 것 같아 메모 X

10. 아키텍처 경계 정하기

경계와 의존성

경계를 강제한다는 것

클린 아키텍처애서는 계층 경계를 넘는 의존성을 항상 안쪽으로 향하게 해야 한다. 이 규칙을 강제하는 것

접근 제한자

경계를 강제하기 위해 자바에서 제공하는 가장 기본적인 도구

package-private(default)을 이용한다.

예를 들어, 각 인터페이스만 public으로 두고 그 구현체들은 package-private으로 만든다.

의존성 주입은 보통 리플렉션을 이용하니 package-private이어도 의존성 주입이 가능하다는 점을 이용. 대신 클래스 패스 스캐닝을 이용해야 함(직접 설정할 경우 인스턴스화 하기 위해 public 선언이 필요)

작은 모듈에서 효과적이다. 하지만 클래스가 특정 개수를 넘어가기 시작해 하위 패키지를 사용하게 된다면 불가능한 방법

컴파일 후 체크

public 제한자를 사용하면 컴파일 단계에서 의존 방향이 달라졌는지 체크하기 힘들다.

컴파일 후 체크(post-compile check)를 사용할 수 있다. 테스트할때 런타임 체크를 해준다.

ArchUnit 이라는 도구가 있다. JUnit과 같은 단위 테스트 프레임워크 기반에서 잘 동작한다. 의존성 규칙을 위반하면 테스트가 실패한다.

단점은 리팩토링에 취약하다. 항상 코드와 함께 유지보수돼야 한다.

빌드 아티팩트

maven이나 gradle 같은 자동화된 빌드 프로세스 이용

빌드 도구의 주요한 기능 중 하나는 의존성 해결. 모든 아티팩트가 사용 가능한지 확인해준다.

각 계층을 모듈로 나누고 각 모듈에서 의존할 수 있는 모듈을 강제하면 컴파일 에러를 발생시킬 수 있다.

어댑터 계층은 서로 의존하는걸 엄격하게 막지는 않지만 대부분의 경우 서로 격리시켜 유지하는 것이 좋다.

영속 계층이 웹 영역에 영향을 미치거나, 반대거나 서드파티에 영향을 받거나 하는 일이 발생할 수 있으므로.

모듈을 세분화 할 수록 의존성을 더 잘 제어할 수 있다. 대신 매핑이 그만큼 많이 필요하다.

빌드 도구로 나눴을때 장점

  • 순환 의존성을 막을 수 있다.
  • 특정 모듈의 코드를 격리한 채로 변경할 수 있다. 예를 들어, 애플리케이션 계층에서 컴파일 에러가 발생해도 다른 계층에 영향이 없을 것
    • 극단적으로는 각 모듈을 서로 다른 레포지토리에서 관리할 수도 있음
  • 의존성을 새롭게 추가하려면 빌드 스크립트를 건드려야 하므로 의식적인 행동이 된다. 꼭 필요한건지 생각해보게 할 수 있다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

소프트웨어 아키텍처는 아키텍처 요소간의 의존성을 관리해주는 것.

의존성이 꼬이면 아키텍처 역시 꼬인것

의존성이 올바른 방향으로 가고있는지 확인

세가지 접근 방식을 조합해서 사용할 수도 있음

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

어떤 지름길이 있는지 알면 우발적으로 사용되는 지름길을 막고, 정당한 지름길이라면 그 효과를 택할 수도 있다.

왜 지름길은 깨진 창문 같을까

깨진 창문 실험? 차의 유리창에 깨지니 좋은 동네에서도 행인들이 차를 망가뜨리기 시작했다.

  • 품질이 떨어진 코드에서 작업할 때 더 낮은 품질의 코드를 추가하기가 쉽다
  • 코딩 규칙을 많이 어긴 코드에서 작업할 때 또 다른 규칙을 어기기도 쉽다
  • 지름길을 많이 사용한 코드에서 작업할 때 또 다른 지름길을 추가하기도 쉽다.

레거시라고 불리는 많은 코드의 품질은 시간이 가면서 심하게 낮아진다.

깨끗한 상태로 시작할 책임

코드 작성할때도 심리적 영향을 받기 때문에 가능한 지름길을 쓰지 않고 기술 부채를 만들지 않은 채로 깨끗하게 시작하는 것이 중요하다.

만약 프로젝트를 마무리하지 못하고 다른 이들이 인계 받는다면, 인계받는 입장에서는 레거시이기 때문에 깨진 창문을 만들어 내기가 더 쉽다.

비교적 중요하지 않은 부분이거나 프로토 타이핑 작업 중이거나 경제적인 이유가 결부된다면 지름길을 취하는 것이 더 실용 적일 수도 있다.

대신 의도적인 지름길에 대해서는 잘 기록해둬야 한다.

유스케이스 간 모델 공유하기

유스케이스간 모델을 공유하는 것은 유스케이스들이 특정 요구사항을 공유할때 괜찮다 -> 특정 세부사항을 변경했을때 두 유스케이스 모두에 영향을 주고 싶은 경우

여러 유스케이스가 같은 모델을 공유한다면 해당 모델은 변경할 이유가 여러개가 된다(SRP 위반).

따라서 두 유스케이스가 서로 독립적으로 진화해야 한다면 똑같은 모양을 사용하더라도 분리해서 시작해야 한다.

도메인 엔티티를 입출력 모델로 사용하기

도메인 모델에 존재하지 않는 값을 추가하고 싶을때 다른 도메인이나 다른 바운디드 컨텍스트에 포함 시켜야 하는 경우에도 도메인에 값을 추가하게 될 수 있다.

간단한 생성이나 업데이트 유스케이스에서는 괜찮을 수 있다.

도메인 로직을 복잡하게 구현해야 한다면 유스케이스 전용 입출력 모델을 만들어야 한다. 유스케이스의 변경이 도메인 엔티티까지 전파될 수 있기 때문에

인커밍 포트 건너뛰기

인커밍 포트는 의존성 역전에 필수 요소는 아니다.

인커밍 포트를 제거하면 추상화 계층을 줄일 수 있다.

대신 어떤 서비스 메서드를 호출해야 하는지와 같은 세부사항이나 내부 동작에 대해 잘 알아야 한다.

인커밍 포트를 유지하면 아키텍처를 강제할 수 있다. 인커밍 포트만 호출할 수 있도록 강제하면 인커밍 어댑터에서 호출하면 안 되는 메서드를 호출할 일이 없어진다.

애플리케이션 서비스 건너뛰기

도메인 로직이 어댑터에 추가 될 위험이 있다(한방쿼리?).이러면 도메인 로직이 흩어져서 도메인 로직을 찾거나 유지보수 하기 어려워진다.

단순한 전달만 하는 보일러플레이트를 줄일 수 있다. 이를 위해 간단한 CRUD 케이스에서는 애플리케이션 서비스를 건너 뛸 수도 있다.

대신 유스케이스가 단순 CRUD보다 더 많은 일을 해야 한다면 애플리케이션 서비스를 만들어야 한다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

경제적인 관점에서 지름길이 합리적일 때도 있다.

유스케이스가 단순한 CRUD에서 벗어나는 시점이 언제인지에 대해 팀이 합의하는 것이 매우 중요하다.

단순 CRUD 상태에서 벗어나지 않는 유스케이스는 이대로 두는게 더 경제적이다.

어떤 경우든 지름길을 선택한 이유를 기록해두고 나중에 다시 평가할 수 있도록 해야 한다.

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

도메인이 왕이다

외부의 영향을 받지 않고 도메인 코드를 자유롭게 발전시킬 수 있다는 점이 육각형 아키텍처 스타일의 가장 중요한 가치다.

도메인을 중심에 두고 의존성을 역전시키지 않은 구조에서는 DDD를 제대로 할 가능성이 없다. 설계가 항상 다른 요소들에 의해 주도되기 때문에

다르게 말하면 도메인 코드가 애플리케이션에서 가장 중요한 것이 아니라면 이 아키텍처 스타일은 필요하지 않을 것이다.

경험이 여왕이다

계층형을 자주 사용했으니 편하다. 육각형 아키텍처도 편해지면 뭐가 더 좋은지 결정내리기 쉬울 것이다.

육각형 아키텍처를 작은 모듈에 적용해보고 편하게 느껴지는 스타일을 찾아보면 다음번 결정에 도움이 될 것이다.

그때그때 다르다

어떤 소프트웨어를 만드느냐에 따라, 도메인 코드의 역할에 따라서도 다르다. 팀의 경험이나 내린 결정이 마음에 드느냐에 따라서도 다르다.

profile
대화로그

0개의 댓글