6장. 영속성 어댑터 구현하기

Seungjae·2022년 6월 9일
0

우아한 스터디

목록 보기
7/10
  • 의존성을 역전시키기 위해 영속성 계층을 애플리케이션 계층의 플러그인으로 만드는 방법을 알아보자!

의존성 역전


  • 코어의 서비스가 영속성 어댑터에 접근하기 위해 포트를 사용, 해당 포트는 영속성 어댑터가 구현하는 형태이다.

  • 포트는 영속성 작업을 수행하고 DB와 통신할 책임을 가진 영속성 어댑터 클래스에 의해 구현된다.

  • 이러한 포트 계층은 영속성 계층에 대한 서비스의 직접적인 의존을 없애기 존재한다!

    • 이로 인해, 영속성 코드를 리팩토링하더라도 코어 코드를 변경하는 결과로 이어지지 않게 된다!
    • 부수효과를 막을 수 있다!

영속성 어댑터의 책임


  1. 입력을 받는다.
  2. 입력을 DB 포맷으로 매핑한다.
  3. 입력을 DB로 보낸다.
  4. DB 출력을 애플리케이션 포맷으로 매핑한다.
  5. 출력을 반환한다.
  • 영속성 어댑터는 포트 인터페이스를 통해 입력을 받는다.

    • 입력 모델 - 인터페이스가 지정한 도메인 엔티티나 특정 DB 연산 전용 객체가 된다.
  • 입출력 과정에서 매핑이 발생한다.

  • 영속성 어댑터의 입출력 모델이 영속성 어댑터 내부에 있는 것이 아니라 애플리케이션 코어에 있기 때문에, 영속성 어댑터 내부를 변경하는 것이 코어에 영향을 미치지 않게 된다!

지속적으로 강조되고 있는 핵심

⇒ 외부의 변경이 애플리케이션 코어에 영향을 미치지 못하게 한다!

그게 왜 중요...?

⇒ 애플리케이션 코어에는 실질적으로 비즈니스적 가치를 전달하는 도메인 로직이 존재하기 때문에!

포트 인터페이스 나누기


  • 일반적으로 특정 엔티티가 필요한 모든 DB 연산을 하나의 리포지토리 인터페이스에 넣어둔다.

  • 하지만 이러면 모든 서비스가 실제로는 필요하지 않은 메서드에도 의존하게 된다!

  • 10개의 메서드를 가지고 있는 인터페이스 중 A 서비스는 3번 메서드 1개만 사용하더라도 이 “넓은” 포트 인터페이스에 의존성을 갖게 되는 것이다. → 코드에 불필요한 의존성 발생

    • 코드를 이해하고 테스트하기 어렵게 만든다!
  • 이러한 상황은 ISP(인터페이스 분리 원칙)를 어긴 것이다.

    • ISP → 클라이언트가 오로지 자신이 필요로 하는 메서드만 알게 되도록 넓은 인터페이스를 특화된 인터페이스로 분리해야 한다.
    • 즉 클라이언트가 필요로하는 최소의 인터페이스만 제공해야한다!
  • 아래의 그림은 ISP를 적용하여 불필요한 의존성을 제거한 것이다.

  • 각 서비스는 필요한 메서드에만 의존하게 된다.

  • 포트의 이름을 좀 더 포트의 역할을 명확하게 표현하도록 네이밍할 수 있다.

  • 대부분의 경우 각 포트 인터페이스 당 메서드 1개만 존재할 것이기에 필요한 것만 모킹할 수 있다.

  • 매우 좁은 포트 → 플러그 앤드 플레이(plug-and-play) 경험

    • 서비스코드 작성시 다른 과정 필요없이 그저 필요한 포트에만 꽂아서 사용할 수 있는 경험!
  • 물론 응집성이 매우 높고 함께 사용될 때가 많은 경우, 하나로 묶을 수 있을 것이다.

영속성 어댑터 나누기


  • 영속성 어댑터 또한 포트처럼 나눌 수 있다.
    • 영속성 연산이 필요한 도메인 클래스 하나당 하나의 영속성 어댑터를 구현하는 방식으로 나눌 수 있다.

  • 이로 인해, 각 영속성 기능을 이용하는 도메인 경계를 확인할 수 있다.

  • 더 많이 나눌 수도 있다. ex) JPA 어댑터와 SQL 어댑터 하나를 만들고 각각 특정한 좁은 영속성 포트를 구현하도록!

  • 도메인 코드는 영속성 포트에 의해 정의된 명세를 어떤 클래스가 충족 시키는지에 관심 없다!

  • 도메인 클래스당 하나의 영속성 어댑터로 나눠 도메인 경계를 확인하는 방식은 나중에 여러 개의 바운디드 컨텍스트(DDD에서 하나의 경계화된 단위)의 영속성 요구사항을 분리하기 위한 좋은 토대가 될 수 있다!

    • 각 바운디드 컨텍스트 간의 경계를 명확하게 구분하고 싶다면, 바운디드 컨텍스트 각각이 영속성 어댑터를 하나씩 가지고 있어야 한다.
  • 이렇게 맥락을 나눴을 때, A 맥락이 다른 맥락인 B의 무엇인가 필요하다면 → 전용 인커밍 포트를 통해 접근한다.

    • 즉, A맥락은 B맥락을 또 다른 인커밍 어댑터로 보고 있다.
    • 저번에 스터디에서 이야기 나온 하나의 프로젝트 내에서 여러개의 도메인이 나왔을 때!
      • 이 책에서는 각각의 도메인을 헥사고날 아키텍처로 나누고 서로를 인커밍 어댑터로 취급하며 소통하는 방식을 추구하는 것 같다!

스프링 데이터 JPA 예제


  • 스프링 데이터 JPA를 사용하여 앞에서 설명한 것들을 예제 코드로 구현하고 있다.

영속성 Layer(아웃고잉)에서의 입출력...

  • 예제를 보면 도메인 엔티티 ↔  영속성 엔티티 를 서로 매핑해가며 사용하고 있다.
    • 물론 Cost가 있지만, 영속성 측면과의 타협 없이 풍부한 도메인 모델을 생성하고 싶다면 도메인 모델과 영속성 모델 구분하고 양방향매핑하는 것이 좋다!
  • 여기까지는 OK!
  • 근데 왜! Usecase, Web Layer(인커밍)에서는 입출력 포맷을 각각 나눠서 다시 정의할 것을 추천해놓고 이 예제에서는 도메인 엔티티 그대로 사용할까?
    • 저의 생각은... → 클라이언트는 누구인가? [Usecase]
      • Usecase는 주로 도메인 로직을 통한 도메인 엔티티의 상태 변경을 주로 수행한다.
      • 즉 어차피 영속성 Layer로 다루는 것, 이 Layer에 요구하는 것 모두 도메인 엔티티이다.
      • 그렇기에 영속성 Layer에서는 굳이 입출력을 따로 정의할 것을 주장하지 않는다고 생각한다.

DB 트랜잭션은 어떻게 해야 할까?


  • 트랜잭션은 하나의 특정한 유스케이스에 대해서 일어나는 모든 쓰기 작업에 걸쳐 있어야 한다.

    • 그래야 한번에 커밋되고, 중간에 실패할 경우 한번에 롤백할 수 있으니까!
  • 헥사고날 아키텍처 구조에서 영속성 어댑터는 어떤 데이터베이스 연산이 유스케이스에 포함되는지 알 수 없다!

    • 즉, 이 책임은 영속성 어댑터 호출을 관장하는 서비스에 위임해야 한다!

근데 이것도 이 @Transaction도 외부에 대한 의존성 아닌가?

⇒ 그래서 책에서는 오염되지 않고 깔끔하게 처리하고 싶다면 AspectJ 같은 도구를 이용해 AOP로 트랜잭션 경계를 코드에 위빙할 것을 추천하고 있다.

AspectJ를 사용한 위빙이란?

컴파일 타임이나 또는 로드 타임때, 타깃 클래스 파일의 바이트 코드를 조작하여 부가기능을 직접 넣어주는 방법

유지보수 가능한 SW를 만드는 데 어떻게 도움이 될까?


  • 도메인 코드에 플러그인처럼 동작하는 영속성 어댑터를 만들면 → 도메인 코드가 영속성과 관련된 것들로부터 분리 → 풍부한 도메인 모델을 만들 수 있다!

  • 좁은 포트 인터페이스 사용(ISP 적용) → 포트마다 다른 방식으로 구현 가능 → 유연함

    • 또한 테스트 용이성, 불필요한 의존성 제거!
profile
코드 품질의 중요성을 아는 개발자 👋🏻

0개의 댓글

관련 채용 정보