만들면서 배우는 클린 아키텍쳐

01

  • 기본적인 구조는 웹 -> 도메인 -> 영속성

  • 이게 계층형 아키텍쳐

  • 이 구조의 문제는?

    • 데이터 베이스 중심 설계가 됨.

  • 데이터 베이스 중심 설계가 되는 가장 큰 원인은 ORM

  • ORM을 사용함으로써 비즈니스 규칙을 영속성에 섞게 됨. (이게 편하니)

    • 문제는 영속성 계층과 도메인 계층에서 생기는 강한 결합

    • 이 결합의 증거가 eager, lazy loading, transaction, cache flush등을 서비스 계층에서 진행하게 된다는 것.

  • 영속성 계층이 비대해지거나, 웹에서 직접 영속성 계층에 접근하게 되면.. 테스트하기 어려워짐.

    • 왜 테스트 하기가 어려워짐?

    • 테스트 코드 작성하는 것보다 종속성을 이해하고, mock을 주입하는 거에 시간이 더 오래 걸린다.

    • 맞는 말인듯.

  • UserService에 모든 유저관련된 로직이 있는 것보단, RegisterUserService와 같이 나누는게 낫다.

02

  • 의존성 역전

  • 단일 책임 원칙

    • 컴포넌트를 변경하는 이유 는 오직 하나 뿐이어야함.

    • 단일 변경 이유 원칙이 더 맞겠다.

  • JPA...와 같은 ORM을 사용하면 도메인 계층은 영속성 계층에 대해 의존하고 있음.

    • 그러면 영속성 계층이 변경되면?

    • 도메인 계층도 변경되어야함.

    • 이 의존성을 어떻게 없앨 수 있을까? -> 의존성 역전

    • 도메인이 영속성에 의존하게 하는 것이 아닌, 영속성이 도메인에 의존하도록 해야함.

  1. 도메인에 리포지토리 인터페이스를 만듬. (도메인에 대한 행동)

  2. 이 인터페이스를 기반으로 영속성 계층에서 구현체를 만듬

  3. 그리고 영속성 계층에서 ORM에서 관리하는 엔티티도 가지고 있고.

  • 위와 같이 하면, 도메인이 영속성 계층에 의존하는 것이 아닌, 영속성 계층이 도메인 (리포지토리 인터페이스) 에 의존하고 있다고 할 수 있음. -> (신기하고 적용해보면 좋을 것 같다고 느껴진다)

  • 이렇게 하면, 영속성 구현체가 바뀌어도 (jpa -> mybatis) 도메인 계층의 변경은 없고 영속성 계층에서 변경만 일어날 것.

  • 헥사고날 아키텍쳐

    • 왜 헥사고날?

    • 어플키에션은 다른 시스템 혹은 어댑터와 연결되는 면이 4개 이상 될 수 있다는 의미.

03

  • AccountService -> SendMoneyService

    • AccountService의 책임을 줄이기 위해서

  • 기능에 의한 패키징 방식은 계층에 의한 패키징 방식보다 가시성을 훨씬 떨어트린다.

  • adapter.in.web -> 어댑터 역할인데 web으로 들어오는걸 처리하는 녀석들.

    • 위와 같이 표현력 있는 구조는 코드와 아키텍쳐 간의 갭을 줄일 수 있게 해줌.

  • 클린아키텍쳐의 본질은 애플리케이션 계층이 인/아웃 어댑터에 대해 의존성을 갖지 않도록 하는 것.

04

  • ActivityWindow, 계좌의 활동 (입금이나 출금) 을 나타내는 도메인인듯.

  • 입력 유효성 검사는 어플리케이션 계층에서 해야함.

  • SelfValidating 괜찮은듯.

    • 입력 모델에서 유효성 검사하는 방법

  • 불변 커맨드 객체의 필드에 대해서 null 을 유효한 상태로 받아들이는 것 그 자체로 코드 냄새 (리팩토링...이나 수정의 필요성이 있는)

  • 입력 유효성 검증은 유스케이스 로직의 일부가 아님.

    • 그렇지만 비즈니스 규칙 검증은 분명히 유스케이스 로직의 일부

    • 비즈니스 검증 규칙은 어디에? -> 보통 도메인 내부에

05

  • 어댑터와 유스케이스 사이에 왜 간접계층 (인터페이스...) 를 넣어야함?

    • 어플리케이션과 외부가 통신할 수 있는 명세가 필요하기 때문

    • 이게 포트이고 보통 인터페이스 정의함.

  • 웹 어댑터의 입력 모델을, 유스케이스의 입력보델로 변환할 수 있다는 것을 검증해야함.

  • 클래스마다 코드는 적을수록 좋다.

06

  • 영속성 어댑터의 역할을 데이터베이스로 데이터를 요청하거나, 받는 역할 그리고 데이터 포맷을 변경하는 역할.

    • AccountPersistenceAdapter

  • 도메인 - 영속성 모델 맵핑 방식을 이용하는게 낫다 왜?

    • 그러면 풍부한 도메인 모델을 생성할 수 있음.

    • 도메인에 기본 생성자를 안 만들 수도 있고..

07

  • 의존성과 상호작용하고 있으면 통합테스트에 가까움.

  • 영속성 어댑터 테스트는 실제 데이터베이스 (인메모리가 아닌) 대상으로 진행해야함.

    • testcontainers 같은 라이브러리 사용, 필요한 데이터베이스를 도커 컨테이너에 띄울 수 있음.

  • @SpringBootTest 어노테이션을 이용하는 테스트에서는 MovMvc 를 이용하지 않고, TestRestTemplate 으로 실제 프로덕션 환경에 더 가깝게 테스트할 수 있도록 함.

  • 도메인 엔티티, 유스케이스는 -> 단위 테스트로 커버

  • 어댑터는 통합 테스트

  • 사용자가 취할 수 있는 중요 어플리케이션 경로는 시스템 테스트 (@SpringBootTest와 같은 형식. 모든 필요한 리소스를 띄우고 실제 프러덕션 환경에 가깝게 테스트 하는 것.)

08

  • No mapping

    • port.in interface(SendMoneyUseCase)와, port.out interface(UpdateAccoutStatePort) 가 도메인인 Account에 직접 의존하게 되면...

    • Account class는 책임 지는 계층이 증가함. 그러면 port.in에서 비롯된 변화가, port.out까지 전파될 수 있음. 단일 책임 원칙에 어긋남

  • two way mapping

    • 웹 계층에서는 웹 모델을 인커밍 포트와 필요한 도메인 모델을 맵핑. 인커핑 포트에서 반환된 도메인 객체를 웹 모델로 맵핑

    • 즉 웹 계층이 웹 모델에 의존. 인커밍 포트는 도메인 모델에 의존. 그러면서 웹 계층은 인커밍 포트에 의존해서. 양방향인듯. (웹 -> 웹 모델, 웹 -> 인커밍 포트)

  • full mapping

    • SendMoneyCommand 처럼, 각 작업에 특화 모델을 각자 사용하는 것 (각 계층마다.. 행동에 맞는 DTO를 만들어주는 걸 의미하는 듯.)

  • one way mapping

    • 모든 계층의 모델이 같은 인터페이스를 구현하는 것.

  • 아직 이 부분에 대한 차이점은 잘 이해가 되지 않음.

09

  • 유스 케이스가 영속성 어댑터를 호출해야하고, 인스턴스화 한다면 코드 의존성이 잘못된 것.

    • 그 중간에 있는 입력 포트, 출력 포트를 호출해야함.

  • 객체 인스턴스를 생성할 책임은, 설정 컴포넌트에게 있음.

    • 스프링 IOC 컨테이너에서 DI를 해주는 부분이지 않을까 (설정 컴포넌트)

10

  • 아키텍쳐 경계를 강제하기 위한 도구는.. 접근 제한자

  • package-private (default)

    • 모듈 내에 있는 클래스끼리는 서로 접근 가능하지만, 패키지 밖에서는 접근할 수 없음.

    • 그러면 모듈 진입점으로 활용될 클래스만을 골라서 public으로 만들면 됨 (e.g. SendMoneyCommand 같은 클래스)

  • 이걸 사용하면 어떤 장점?

    • 의존성이 잘못된 방향을 가리켜서, 의존성 규칙 (안쪽을 바라봐야함..)을 위반할 위험이 줄어들게 됨.

  • ArchUnit , 의존성 방향이 기대한 대로 잘 설정되어 있는지 체크하는 API 제공

11

  • 지름길은 깨진 창문.

    • 한번 가기 시작하면 다시 고치기 힘들다.

    • 다른 부분에도 영향을 미친다.

12

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

Last updated