전통적인 계층형 아키텍처 → 데이터베이스 주도 설계
육각형 아키텍처 → 의존성을 역전시켜 영속성 계층을 애플리케이션 계층의 플러그인으로 작동
6-1. 의존성 역전
application.service.Service →(호출) application.port.out.OutgoingPort ←(구현) adapter.out.persistence.OutgoingAdapter
- 영속성 어댑터 = 주도되는/아웃고잉 어댑터
- 포트: 애플리케이션 서비스가 영속성 문제(의존성) 없이 도메인 코드를 개발하기 위한 간접 계층
- 코어에 영향을 미치지 않으면서 영속성 계층 수정 가능
6-2. 영속성 어댑터의 책임
- 입출력 모델이 영속성 어댑터가 아닌 애플리케이션 코어에 있다.
- 포트 인터페이스를 통해 입력을 받는다
- 입력을 데이터베이스 포맷으로 매핑한다
- 입력 모델: 인터페이스가 지정한 도메인 엔티티/특정 데이터베이스 연산 전용 객체
- 애플리케이션 코어에 존재. 영속성 어댑터 변경이 코어에 영향을 미치지 않음.
- 데이터베이스 포맷: JPA 엔티티 객체
- 입력을 데이터베이스로 보낸다
- 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
- 포트에 정의된 출력 모델이 애플리케이션 코어에 위치한다.
- 출력을 반환한다
6-3. 포트 인터페이스 나누기
필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 생길 수 있다.
Interface Segregation Principle, ISP (인터페이스 분리 법칙)
클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야 한다.
인터페이스 분리 전 vs 후
- 일반적으로
Account
엔티티에 관한 모든 연산을 가지는 AccountRepository extends JpaRepository<Account, Long>
같은 하나의 리포지토리 인터페이스를 관련된 모든 서비스(SendMoneyService
, RegisterAccountService
등)에서 사용한다.
- 각 서비스는 필요 없는 리포지토리의 메서드들에까지도 불필요한 의존성을 가지게 된다.
- 특정 서비스를 단위 테스트하기 위해 리포지토리를 모킹할 때도 문제가 된다.
- 포트에 필요한 메서드만 존재하며 포트의 이름이 역할을 잘 나타낸다.
- plug-and-play: 재설정하거나 조정하는 과정 없이 연결하는 즉시 완벽하게 작동하는 방식
- 서비스 코드를 필요한 포트에 그저 꽂기만 하면 된다.
AccountRepository
는 영속성 어댑터에서 포트를 구현하면서 사용하게 된다.
6-4. 영속성 어댑터 나누기
- 영속성 어댑터를 각 영속성 기능을 이용하는 도메인 경계를 따라 나눈다.
- 하나의 애그리거트(도메인 클래스)당 하나의 영속성 어댑터 혹은 JPA 어댑터 하나&SQL 어댑터 하나
- 여러 개의 bounded context(도메인간의 경계)의 영속성 요구사항을 분리하기 좋은 토대가 된다.
6-5. 스프링 데이터 JPA 예제
Account
- 상태 변경 메서드 가짐
- 불변성 유지
- 입력 유효성 검증을 통해 유효하지 않은 도메인 모델 생성이 불가능
AccountJpaEntity
와 ActivityJpaEntity
AccountRepository extends JpaRepository<AccountJpaEntity, Long>
와 ActivityRepository extends JpaRepository<ActivityJpaEntity, Long>
AccountPersistenceAdapter implements LoadAccountPort, updateAccountStatePort
도메인 모델 ↔ 데이터베이스 모델 양방향 매핑
풍부한 도메인 모델을 위해
- 매핑하지 않으면? JPA로 인해 도메인 모델을 타협해야 한다.
- JPA 엔티티는 기본 생성자를 필요로 한다.
- 영속성 계층:
@ManyToOne
관계 설정
- 도메인 모델: 데이터의 일부만 가져오고 싶어서 반대의 관계를 원할 수도 있음
6-6. 데이터베이스 트랜잭션은 어떻게 해야 할까?
영속성 어댑터는 어느 연산이 무슨 유스케이스에서 사용되는지 알 수 없다.
@Transactional
애노테이션을 Service 클래스에 붙여서 모든 public 메서드를 트랜잭션으로 감싼다.
AspectJ
도구를 사용해서 서비스를 Transactional로 오염시키지 않고 AOP로 트랜잭션 경계를 코드에 위빙한다.
6-7. 유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
- 도메인 코드에 플러그인처럼 동작하는 영속성 어댑터
- 좁은 포트 인터페이스
- 포트마다 다른 바식으로 구현할 수 있는 유연함
- 포트 뒤에서 영속성 계층 수정, 교환 가능
느낀 점
- 포트 인터페이스를 나누지 않으면 단위 테스트이 어려움이 생긴다.
- 서비스를 단위 테스트하기 위해 리포지토리를 모킹해야 했다. 여러 가지 방식이 있었는데 그 중에서 리포지토리 인터페이스를 테스트 클래스 안에서 직접 필요한 것들만 구현해서 사용하는 방식을 한 적이 있다.
JpaRepository
를 상속한 리포지토리였기 때문에 JpaRepository의 모든 메서드들을 override해야했고, 코드가 굉장히 지저분해졌었다. 해당 방식이 아닌 모킹하는 다른 방법들도 있을테지만 서비스가 불필요한 넓은 인터페이스를 사용할 때 테스트하기 어려움을 겪었었던 경험이었다.
질문
- p69 영속성 어댑터를 훨씬 더 많은 클래스로 나눌 수도 있다. JPA 어댑터 하나와 평이한 SQL 어댑터 하나를 만들고 각각이 영속성 포트의 일부분을 구현하면 된다.
- 하나의 포트를 두 개의 어댑터가 따로 구현한다? 그렇다면 각 어댑터에서 구현하지 않는 메서드가 생길 것 같은데 이것도 불필요한 의존성을 가지지 않나. 포트를 또 나누어서 각 어댑터에서 구현하면 되는 것 아닌가