오늘은 클린 아키텍처 관련 책인 만들면서 배우는 클린 아키텍처 를 읽고 정리해둔 내용을 포스팅하려고 합니다.
해당 책은 얇아서 진입장벽이 낮았던 책이었고, 예제 코드를 통해 헥사고날 아키텍처를 간접적으로 접해볼 수 있는 기회가 되었다고 생각합니다.
이번 포스팅에서는 1,2,3,4장 내용을 다루고자 합니다.
01장 계층형 아키텍처의 문제는 무엇일까?
02장 의존성 역전하기
03장 코드 구성하기
04장 유스케이스 구현하기
01장 계층형 아키텍처의 문제는 무엇일까?
계층형 아키텍처
- 웹계층이나 영속성 계층에 독립적으로 도메인 로직을 작성할 수 있다.
- 잘 만들어진 계층형 아키텍처는
선택의 폭을 넓히고, 변화하는 요구사항과 외부요인에 빠르게 적용
할 수 있게 해준다. - 코드에 나쁜 습관들이 스며들기 쉽게 만들고, 시간이 지날수록 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.
계층형 아키텍처는 데이터베이스 주도 설계를 유도
한다.
- 웹계층은 도메인 계층에 의존, 도메인 계층은 영속성 계층에 의존
- 데이터베이스 의존
- 상태가 아니라 행동을 중심으로 모델링한다.
- 상태가 중요한 요소이긴 하나, 행동이 상태를 바꾸는 주체
- 데이터베이스의 구조를 먼저 생각하고, 이를 토대로 도메인 로직을 구현
- 영속성 계층을 먼저 구현
- 의존성의 바향에 따라 자연스럽게 구현
- 비지니스 관점에서는 맞지 않다 (도메인 로직 -> 영속성, 웹 계층)
- ORM 에 의해 관리되는 엔티티들은 일반적으로 영속성 계층에 둔다.
- 영속성 계층과 도메인 계층간 강한 결합이 생긴다.
- 서비스는 영속성 모델을 비지니스 모델처럼 사용, 영속성 계층과 관련된 작업들을 해야 한다.
지름길을 택하기 쉬워진다.
- 특정한 계층에서는 같은 계층 또는 아래 계층에만 접근 가능(
유일한 규칙
)- 상위 계층에 위치한 컴포넌트에 접근해야 한다면?
- 컴포넌트 계층 아래로 내려버리면 된다.
- 영속성 계층은 컴포넌트를 아래 계층으로 내릴수록 비대해진다.
테스트하기 어려워진다.
- 일반적으로 나타나는 변화의 형태는 계층을 건너뛰는 것 (웹계층 -> 영속성 계층 접근)
- 도메인 로직을 웹계층에 구현하게 되는 것 (책임이 섞이고, 핵심 도메인 로직이 퍼져나간다)
- 웹계층 테스트에서 영속성 계층도 모킹해야 한다.
- 단위 테스트 복잡도가 올라감
유스케이스를 숨긴다.
- 계층형 아키텍처는 도메인 로직이 여러 계층에 걸쳐 흩어지기 쉽다.
- 새로운 기능을 추가할 적당한 위치를 찾는 일이 어렵다.
- 도메인 서비스의 ‘너비’에 관한 규칙을 강제하지 않는다.
- 넓은 서비스는 영속성 계층에 많은 의존을 갖게되고, 웹 레이어의 많은 컴포넌트가 이 서비스에 의존하게 된다.
동시작업이 어려워진다.
- 적절한 규모에서는 프로젝트에 인원이 더 투입될 경우 확실히 더 빨라진다고 기대할 수 있다.
- 아키텍처가 동시작업을 지원해야 가능
- 데이터 주도 설계는 영속성 로직이 도메인 로직과 너무 뒤섞여서 각 측면을 개별적으로 작업할 수 없기 때문이다.
- 넓은 서비스가 있다면 동시에 편집하는 상황이 발생하고, 이는 병합 충돌과 잠재적으로 이전 코드로 되돌려야 하는 문제를 야기
02장 의존성 역전하기
단일 책임 원칙(Single Responsibility Principle, SRP)
컴포넌트를 변경하는 이유는 오직 하나뿐
이어야 한다.- 많은 코드는 단일 책임 원칙을 위반하기 때문에 시간이 갈수록 변경이 어려워지고 변경 비용도 증가한다.
- 컴포넌트를 변경할 더 많은 이유가 쌓여가고, 다른 컴포넌트가 실패하는 원인으로 작용할 수 있다.
의존성 역전 원칙(Dependency Inversion Principle, DIP)
- 계층형 아키텍처에서 계층간 의존성은 항상 다음 단계인 아래를 가리킨다.
- 코드상의
어떤 의존성이든 그 방향을 바꿀 수 있다.
- 의존성의 양쪽 코드를 모두 제어할 수 있을 때만 의존성을 역전시킬 수 있다.
엔티티는 도메인 객체를 표현, 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 중심으로 하기 때문에 엔티티를 도메인 계층
으로 올린다.- 영속성 계층의 리포지토리가 도메인 계층에 있는 엔티티에 의존(순환 의존성 발생)
도메인 계층에 리포지토리에 대한 인터페이스를 만들고, 영속성 계층에 구현
클린 아키텍처
- 비즈니스 규칙의 테스트를 용이, 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 어플리케이션이나 인터페이스로 부터 독립적일 수 있다.
도메인 코드가 바같으로 향하는 어떤 의존성도 없어야 함
을 의미-
의존성 역전 원칙의 도움으로
모든 의존성이 도메인 코드를 향하고 있다.
- 이 아키텍처의 코어(core)에는 주변 유스케이스에서 접근하는 도메인 엔티티들이 있다.
- 도메인 코드에서는 어떤 영속성 프레임워크나 UI 프레임워크가 사용되는지 알 수 없다.
- 비즈니스 규칙에 집중, 도메인 코드를 자유롭게 모델링
도메인 계층이 외부 계층과 철저하게 분리
어플리케이선의 엔티티에 대한 모델을 각 계층에서 유지보수
- 도메인 계층은 연송석 계층을 모르기 때문에 두 계층에서 각각 엔티티를 만들어야 한다.
- JPA 에서는 ORM 이 관리하는 엔티티에 인자 없는 기본 생성자가 강제됨
- 프에임워크 특화된 결합의 예!
헥사고날 아키텍처
- 클린 아키텍처와 동일하게 육각형에서
외부로 향하는 의존성이 없고, 모든 의존성이 코어를 향한다.
- 육각형 바깥에는 어플리케이션과 상호작용하는 다양한 어댑터들이 있다.
- 어플리케이션 코어와 어댑터들 간의 통신이 가능하려면 어플리케이션 코어가 각각의 포트를 제공
- 포트가 코어에 있는 유즈케이스 클래스들에 의해 구현되고 호출되는 인터페이스
- 포트가 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스
- 포트와 어댑터 아키텍처
유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?
의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드에 의존하지 않고
,영속성과 UI에 특화된 문제로 부터 도메인 로직의 결합을 제거
,코드를 변경할 이유의 수를 줄일 수 있다
03장 코드 구성하기
계층으로 구성하기
- 웹, 도메인, 영속성 계층에 대한 web, domain, persistence 로 구성
- 기능 조각(functional slice)이나 특성(feature)을 구분짓는 패키지 경계가 없다.
- 어플리케이션이 어떤 유즈케이스들을 제공하는지 파악이 어렵다.
기능으로 구성하기
- 각 기능을 묶은 새로운 그룹은 account 와 같은 레벨의 새로운 패키지로 구성
- 패키지 외부에서 접근되면 안 되는 클래스들에 대해 package-private 접근 수준을 이용
- 각 기능 사이의 불필요한 의존성을 방지
- 패키지 외부에서 접근되면 안 되는 클래스들에 대해 package-private 접근 수준을 이용
- 유즈케이스를 구현한 코드는 클래스명만으로 찾을 수 있게 반영
아키텍처적으로 표현력 있는 패키지 구조
- 헥사고날 아키텍처에서 구조적으로 핵심인 요서
- entity, usecase, in-coming/out-going port, in-coming/out-going adapter
- 최상위에는 관련된 유즈케이스를 구현한 모듈임을 나타내는 account 패키지가 있다.
패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 점점 목표하던 아키텍처로 부터 멀어지게 될 것
이다.- adapter 패키지는 application 패키지에 내에 있는 포트 인터페이스를 통하지 않고는 바깥에서 호출되지 않는다.
- package-private 접근 수준으로 둬도 된다.
- application, domain 패키지 내의 일부 클래스들은 public 으로 지정해야 한다.
- adapter 에서 접근해야 하는 port 들
- domain 클래스들은 service, 잠재적으로 adapter 에서도 접근 가능하도록 public
- 서비스는 in-coming 포트 인터페이스 뒤에 숨겨질 수 있어 public 이 아님
의존성 주입의 역활
- 아웃고잉 어댑터에 대해서는 제어 흐름의 반대 방향으로 의존성을 올리기 위해 의존성 역전 원칙을 이용
- 어플리케이션 계층에 인터페이스를 두고 어댑터에 해당 인터페이스 구현한 클래스를 두면된다.
- 모든 계층에 의존성을 가진 중립적인 컴포넌트를 하나 도입
- 이 컴포넌트는 아키텍처를 구성하는 대부분의 클래스를 초기화하는 역활
04장 유스케이스 구현하기
풍부한 도메인 모델 VS 빈약한 도메인 모델
- 풍부한 도메인 모델
- 애플리케이션 코어에 있는 엔티티에서 가능한 한 많은 도메인 로직이 구현
엔티티들은 상태를 변경하는 메서드를 제공, 비즈니스 규칙에 맞는 유효한 변경만을 허용
- 빈약한 도메인 모델
- 엔티티 자체가 굉장히 얇다.
- 상태를 표현하는 필드와 getter, setter 메소드만 포함
유스케이스는 비즈니스 룰을 검증할 책임이 있다.
도메인 엔티티와 이 책임을 공유
한다.
호출하는 어댑터가 입력 유효성을 검증
- 유즈케이스가 하나 이상의 어댑터에서 호출시 각 어댑터(입력모델)마다 유효성 검증
유스케이스마다 다른 출력 모델
- 각 유스케이스에 맞게 구체적일 수록 좋다.
호출자에게 꼭 필요한 데이터만 들고 있어야 한다.
- 유스케이스를 가능한 한 구체적으로 유지하기 위헤서는 질문을 계속해야 한다.
- 만약 의심스럽다면 가능한 한 적게 반환하자.
- 유스케이스들 간에
같은 출력 모델을 공유하게 되면 유스케이스들도 강하게 결합된다.
- 단일 책임 원칙을 적용하고 모델을 분리해서 유지하는 것은 유스케이스의 결합을 제거하는데 도움이 된다.
- 도메인 엔티티를 출력 모델로 사용하고 싶은 유혹도 견뎌야 한다.
읽기 전용 유스케이스는 어떨까?
- 쿼리를 위한 인커밍 전용 포트를 만들고 이를 ‘쿼리 서비스’에 구현하는 것이다.
유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?
입출력 모델을 독립적으로 모델링 한다면 원치않는 부수효과를 피할 수 있다.
- 유스케이스간 모델을 공유하는 것 보다는 더 많은 작업이 필요
- 유스케이스별로 모델을 만들면 유스케이스를 명확하게 이해, 유지보수도 더 쉽다.
- 꼼꼼한 입력 유효성 검증, 유스케이스별 입출력 모델은 지속 가능한 코드를 만드는데 큰 도움이 된다.
마무리
유스케이서, 어댑터, 포트 등 생소한 용어들을 많이 접하게 되는 장이였던 것 같습니다.
다른 것 보다 실무를 하면서 느낀 패키지 구조의 중요성, 유스케이스마다 독립적인 입/출력 모델, 각 계층별 엔티티 모델 구현 등 내용이 기억에 남는 것 같습니다.
클린 아키텍처에서 가장 중요한 점은 모든 의존성 방향이 코어(도메인)를 행해야 한다는 것 잘 기억
해야 할 것 같습니다.
그럼 이만. 🥕👋🏼🖐🏼