7. 헥사고날 아키텍쳐 이해해보자
- 비즈니스 로직이 외부(DB, API, 프레임웍)에 의존하지 않도록 인터페이스로 격리하는 것
- Controller (http 진입점)이 UseCase(비즈니스 로직) 호출하고,
- UseCase 는, Port(인터페이스)를 통해서 Adapter (Db, Redise, Kafka)
- UseCase는 DB 가 MySQL 인지 뭔지 몰라도되고,
- UseCasesms 캐시가 Redise 인지 뭔지 몰라도 됨.
Port 와 Adapter
- port in : UseCase를 어떻게 호출하는가 (인터페이스)
- port out : UseCase가 외부에 뭘 요청하는가 (인터페이스)
- Adapter : Port 를 실제로 구현하는 클래스
흐름 보기
1. Controller 진입점
- Controller 에서는 UseCase를 주입받고, 구현체를 직접 참조하진 않음
- Controller 에서 UseCase 호출
2. Port In (입력 인터페이스)
- 그냥 뭘 할수있느가만 정의함.
- 여기에서 UseCase에 대한 인터페이스가 정의된다.
3. UseCase 구현 (핵심 비즈니스로직)
- Port In 을 구현하고,
- PortOut 인터페이스를 주입받는다.
- PortOut 인터페이스를 통해서 외부 의존성과 소통가능함 (DB 참조 등.)
4. Port Out (출력 인터페이스)
- 여기도 뭘 할수있는가만 정의함.
- 예를들면, save (저장) / findByUserId (DB 에서 찾아라)
5. Adapter (실제 DB구현)
- 레파지토리 주입받는다 (JPA)
- JPA 통해서 구현
6. Entity (DB 전용 모델)
- 디비 엔티티 정의
- domain model 과 enitity 가 어떻게 다르나?
- Domain Model 은 순수한 코틀린 data class이고, 비즈니스로직을 가질수 있다.
- Entity 는 DB 스키마에 맞춤
의존 방향 (아래 -> 위)
- 요 의존성은 gradle build 설정에서 강제 됨
┌─────────────────────────────────┐
│ │
│ ┌───────────────────────┐ │
│ │ │ │
│ │ Domain Model │ │ ← 아무것도 의존하지 않음
│ │ (DailyStep) │ │ 가장 순수한 코드
│ │ │ │
│ └───────────────────────┘ │
│ │
│ UseCase + Port │ ← Domain만 의존
│ │
│ application/ │
├─────────────────────────────────┤
│ │
│ Adapter │ ← application에 의존
│ Entity │
│ │
│ adapter/ │
├─────────────────────────────────┤
│ │
│ Controller │ ← application에 의존
│ │
│ bootstrap/ │
└─────────────────────────────────┘
호출방향 vs 의존방향
-
의존한다 ?
- A가 B 에 의존한다 = A 코드 안에서 B를 import 해서 쓴다.
-
호출 흐름 (런타임에 실제로 실행되는 순서)
- Controller -> UseCase -> Adapter
-
의존 방향 (코드에서 import 하는 방향)
- Controller -> import -> UseCase (Port In)
- UseCase -> import -> PortOut
- Adapter -> import -> PortOut (Adapter가 PortOut 인터페이스를 import 해서 구현한다.)
즉, 호출방향과 의존방향은 다를 수 있다. DIP
-
만약 DIP(의존성 역전 원칙) 가 없다면?
- UseCase 가 DB 에 저장하고싶다. 근데, DB 저장 코드는 Adapter에 있음.
- UseCase 가 Adapter를 직접 호출해야함.
- 이럴 경우, UseCase보다 하위 모듈인 Adapter를 직접호출 할수밖에 없음.
- 즉, 상위 모듈이 하위 모듈에 의존하게 됨.
- 근데, 이게 왜 문제?
- Adapter가 바뀌면, UseCase도 바뀌어야함.
- 테스트 시, 실제 DB 찔러야함.
-
DIP 가 있다면
- UseCase 가 Port 인터페이스에 의존함.
- Adapter도 Port 인터페이스에 의존함.
- 즉, 하위 Adapter가 상위 Port에 의존하여, 방향이 뒤집혀짐.
- 즉, Adapter는 원래 '의존 당하는 쪽'이었는데, 반대로 Adapter가 '의존하는 쪽'이 된거임.
기능 추가한다고 가정해보자
- DB 추가 필요하다면
- 테이블 설계하고, 엔티티, SQL 마이그레이션
- Port 부터 작성
- in : UseCase 인터페이스 정의
- out : Adapter 인터페이스 정의
- UseCase 구현
- Adapter 구현
- Controller 추가
- 이 때, API 스펙에 맞춰서 리스폰 매핑해서 반환
- Test 추가