[클린 아키텍처] Ch 06. 영속성 어댑터 구현하기

.·2022년 5월 11일
0

전통적인 계층형 아키텍처 → 데이터베이스 주도 설계

육각형 아키텍처 → 의존성을 역전시켜 영속성 계층을 애플리케이션 계층의 플러그인으로 작동

6-1. 의존성 역전

application.service.Service →(호출) application.port.out.OutgoingPort ←(구현) adapter.out.persistence.OutgoingAdapter

  • 영속성 어댑터 = 주도되는/아웃고잉 어댑터
  • 포트: 애플리케이션 서비스가 영속성 문제(의존성) 없이 도메인 코드를 개발하기 위한 간접 계층
    • 코어에 영향을 미치지 않으면서 영속성 계층 수정 가능

6-2. 영속성 어댑터의 책임

  • 입출력 모델이 영속성 어댑터가 아닌 애플리케이션 코어에 있다.
  1. 포트 인터페이스를 통해 입력을 받는다
  2. 입력을 데이터베이스 포맷으로 매핑한다
    • 입력 모델: 인터페이스가 지정한 도메인 엔티티/특정 데이터베이스 연산 전용 객체
      • 애플리케이션 코어에 존재. 영속성 어댑터 변경이 코어에 영향을 미치지 않음.
    • 데이터베이스 포맷: JPA 엔티티 객체
  3. 입력을 데이터베이스로 보낸다
  4. 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
    • 포트에 정의된 출력 모델이 애플리케이션 코어에 위치한다.
  5. 출력을 반환한다

6-3. 포트 인터페이스 나누기

필요없는 화물을 운반하는 무언가에 의존하고 있으면 예상하지 못했던 문제가 생길 수 있다.

  • 로버트 C. 마틴

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
    • 상태 변경 메서드 가짐
    • 불변성 유지
    • 입력 유효성 검증을 통해 유효하지 않은 도메인 모델 생성이 불가능
  • AccountJpaEntityActivityJpaEntity
  • AccountRepository extends JpaRepository<AccountJpaEntity, Long>ActivityRepository extends JpaRepository<ActivityJpaEntity, Long>
  • AccountPersistenceAdapter implements LoadAccountPort, updateAccountStatePort

도메인 모델 ↔ 데이터베이스 모델 양방향 매핑

풍부한 도메인 모델을 위해

  • 매핑하지 않으면? JPA로 인해 도메인 모델을 타협해야 한다.
    • JPA 엔티티는 기본 생성자를 필요로 한다.
    • 영속성 계층: @ManyToOne 관계 설정
    • 도메인 모델: 데이터의 일부만 가져오고 싶어서 반대의 관계를 원할 수도 있음

6-6. 데이터베이스 트랜잭션은 어떻게 해야 할까?

영속성 어댑터는 어느 연산이 무슨 유스케이스에서 사용되는지 알 수 없다.

  1. @Transactional 애노테이션을 Service 클래스에 붙여서 모든 public 메서드를 트랜잭션으로 감싼다.
  2. AspectJ 도구를 사용해서 서비스를 Transactional로 오염시키지 않고 AOP로 트랜잭션 경계를 코드에 위빙한다.

6-7. 유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?

  • 도메인 코드에 플러그인처럼 동작하는 영속성 어댑터
    • 풍부한 도메인 모델
  • 좁은 포트 인터페이스
    • 포트마다 다른 바식으로 구현할 수 있는 유연함
    • 포트 뒤에서 영속성 계층 수정, 교환 가능

느낀 점

  • 포트 인터페이스를 나누지 않으면 단위 테스트이 어려움이 생긴다.
    • 서비스를 단위 테스트하기 위해 리포지토리를 모킹해야 했다. 여러 가지 방식이 있었는데 그 중에서 리포지토리 인터페이스를 테스트 클래스 안에서 직접 필요한 것들만 구현해서 사용하는 방식을 한 적이 있다. JpaRepository를 상속한 리포지토리였기 때문에 JpaRepository의 모든 메서드들을 override해야했고, 코드가 굉장히 지저분해졌었다. 해당 방식이 아닌 모킹하는 다른 방법들도 있을테지만 서비스가 불필요한 넓은 인터페이스를 사용할 때 테스트하기 어려움을 겪었었던 경험이었다.

질문

  1. p69 영속성 어댑터를 훨씬 더 많은 클래스로 나눌 수도 있다. JPA 어댑터 하나와 평이한 SQL 어댑터 하나를 만들고 각각이 영속성 포트의 일부분을 구현하면 된다.
    • 하나의 포트를 두 개의 어댑터가 따로 구현한다? 그렇다면 각 어댑터에서 구현하지 않는 메서드가 생길 것 같은데 이것도 불필요한 의존성을 가지지 않나. 포트를 또 나누어서 각 어댑터에서 구현하면 되는 것 아닌가

0개의 댓글