01 계층형 아키텍처의 문제는 무엇일까?
데이터베이스 주도 설계를 유도한다
- 계층형 아키텍처에선 보통 DB를 모델링 하고, 이를 토대로 도메인 로직을 구현한다. 하지만 비즈니스 관점에서는 전혀 맞지 않는다. 다른 무엇보다도 도메인 로직을 먼저 만들어야 한다. 도메인 로직이 맞다는 것을 확인한 후에 이를 기반으로 영속성 계층과 웹 계층을 만들어야 한다.
- 우리가 만드는 애플리케이션은 보통 비즈니스 정책을 반영한 모델이다.
즉, 상태(state)가 아니라 행동(behavior)을 중심으로 모델링한다.
- 행동이 상태를 바꾸기 때문에 행동이 비즈니스를 이끌어간다.
- DB 중심 아키텍처가 만들어지는 가장 큰 원인은 ORM이다.
- 도메인 계층과 영속성 계층 사이 강한 결합이 생겨 서비스는 영속성 모델을 비즈니스 모델처럼 사용하게 되고 이로 인해 도메인 로직뿐만 아니라 즉시 로딩/지연 로딩, 트랜잭션, 캐시 플러시 등 영속성 계층 관련 작업을 해야만 한다.
깨진 유리창
- 누구 한 명이 망치기 시작하면 다른 사람들도 쉽게 망치기 시작한다.
- 영속성 계층에서는 모든 것에 접근이 가능하기 때문에 시간이 지나면서 점점 비대해진다.
테스트하기 어려워진다
- 이건 케이스가 웹 계층에서 바로 영속성 계층까지 조작하는 경우가 생기면 테스트 하기 어려워진다는 내용인데(…)
유스케이스를 숨긴다
- 계층형 아키텍처는 비대해지면서 도메인 로직이 여러 계층에 흩어지기 쉬운데, 이러면 하나의 서비스 레이어에서 여러 개의 유스케이스를 담당하게 될 수 있다는 얘기
- 이거 Application Layer라는 이름으로 Facade Pattern 쓰는 거랑 비슷한 듯
- 근데 이게 결국 Service of Service 아닌가 싶은 생각이 듦
동시 작업이 어려워진다
- 레이어드 아키텍처는 계층을 분리해서 작업을 할 수 없음(한 명은 도메인, 한 명은 영속성, 한 명은 웹 계층)
- 헥사고날은 도메인만 정의되면 독립적으로 개발이 되긴 하겠지만, 그게 맞는 걸까?
02 의존성 역전하기
단일 책임 원칙
- 컴포넌트를 변경하는 이유는 오직 하나뿐이어야 한다.
- Single Reason to Change Principle
- 어떤 다른 이유로 소프트웨어를 변경하더라도 이 컴포넌트에 대해 전혀 신경 쓸 필요가 없다.
- 컴포넌트 의존도 증가 == 변경비용 증가
부수효과
- 특정 컴포넌트를 변경했을 때 언제나 다른 무언가가 망가져 변경에 대한 부수효과가 생김 → 비용 증가
의존성 역전 원칙
- 코드상의 어떤 의존성이든 그 방향을 바꿀 수(역전시킬 수) 있다.
- ex) 도메인 계층와 영속성 계층 사이 의존성 역전
- 도메인 계층에 repository interface를 둬서 영속성 계층이 도메인 계층을 의존하게 함
클린 아키텍처
- 로버트 C. 마틴, ‘클린 아키텍처’
- 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있음
- 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 함
- 계층 간 모든 의존성이 안쪽으로 향해야 한다.
- 도메인 계층이 영속성이나 UI 같은 외부 계층과 철저히 분리되어야 하므로, 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수해야 한다.
- ex) JPA Entity와 Domain 분리

육각형 아키텍처(헥사고날 아키텍처)

- 모든 의존성이 코어를 향함
- port and adatpers
- adapter: 애플리케이션과 상호작용
- port: 코어와 adapter 사이 통신
- port와 use case 구현체를 결합해 애플리케이션 계층을 구성
- domain entity → 마지막 계층
장점
- 의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드를 의존하지 않게 함으로써 영속성과 UI에 특화된 모든 문제로부터 도메인 로직의 결합을 제거하고 코드 변경 이유의 수 줄임 → 유지보수성 증가
- 각 레이어에 맞게 자유롭게 모델링 가능
- 도메인 코드는 비즈니스 문제에 따라 모델링
- 영속성 코드도 영속성 문제/UI 문제에 따라 모델링
03 코드 구성하기
- screaming architecture: 애플리케이션의 기능을 코드를 통해 볼 수 있게 만들자(로버트 마틴)
- 기능을 기준으로 코드를 구성하면 기반 아키텍처가 명확하게 보이지 않는다.
(아키텍처의 가시성을 떨어뜨린다)
아키텍처적으로 표현력이 있는 패키지 구조
buckpal/
│
│── account
├── adapter/
├── in/
│ ├── web/
│ ├── AccountController.java
│
└── out/
│ ├── persistence/
│ ├── AccountPersistenceAdapter.java
│ └── SpringDataAccountRepository.java
│
├── domain/
│ ├── Account.java
│ ├── Activity.java
├── application/
│ └── SendMoneyService.java
├── port/
│ ├── in/
│ │ ├── SendMoneyUseCase.java
│ │
│ └── out/
│ ├── LoadAccountPort.java
│ └── UpdateAccountStatePort.java
└
- architecture-code gap or model-code gap
- 아키텍처가 코드에 직접적으로 매핑될 수 없는 추상적 개념
- 패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 목표하던 아키텍처로부터 멀어지게 된다.
- 이렇게 표현력 있는 패키지 구조는 아키텍처에 대한 적극적인 사고를 촉진시킴
- 많은 패키지가 생기고, 현재 작업 중인 코드를 어떤 패키지에 넣어야 할지 계속 생각해야 하기 때문
- 대부분은 package-private 수준으로 둬도 된다.
- 단, application 패키지와 domain 패키지 내의 일부 클래스는 public으로 지정해야 한다.
- 의도적으로 어댑터에서 접근 가능해야 하는 포트들은 public이어야 한다.
- 도메인 클래스들은 서비스, 그리고 잠재적으로 어댑터에서도 접근 가능하도록 public이어야 한다.
- 서비스는 incoming port interface 뒤에 숨을 수 있으므로 public일 필요는 없다.
의존성 주입의 역할
- 가장 본질적인 요건은 애플리케이션 계층이 incoming/outgoing 어댑터에 의존성을 갖지 않는 것이다.
- 모든 계층에 의존성을 가진 중립적인 컴포넌트를 하나 도입, 대부분의 클래스를 초기화하는 역할을 함
AccountController
|→ SendMoneyUseCase ← SendMoneyService
(interface) |→ LoadAccountPort ← AccountPersistenceAdapater
(interface)
- 이를 통해 의존성의 방향이 역전되고, 애플리케이션 계층이 어댑터에 의존하지 않게 된다.
- 결과적으로, 도메인 로직은 외부 의존성으로부터 완전히 독립적이 되며, 유지보수성과 테스트 용이성이 크게 향상된다.