만들면서 배우는 클린 아키텍처 08-09

inseo24·2024년 7월 10일

clean-architecture

목록 보기
4/5

08 경계 간 매핑하기

  • 각 계층의 모델을 매핑을 어떻게 할 것인가?

‘매핑하지 않기’ 전략

  • 포트 인터페이스가 도메인 모델을 입출력 모델로 사용하면 두 계층 간의 매핑을 할 필요가 없다.
  • 문제점
    • 웹 계층과 영속성 계층은 특별한 요구사항이 있을 수 있다.
      • 웹은 JSON 직렬화를 위한 애너테이션 / 영속성은 ORM을 위한 애너테이션
      • 도메인과 애플리케이션 계층은 웹/영속성과 관련된 특수한 요구사항에 관심이 없음에도 도메인 모델에서 이런 모든 요구사항을 다뤄야 한다.
      • 즉, 단일 책임 원칙을 위반하게 된다.
  • 적합할 때
    • 모든 계층이 정확히 같은 구조의, 정확히 같은 정보를 필요로 한다면 ‘매핑하지 않기 전략’이 유효하다.
  • 그러나 애플리케이션 계층이나 도메인 계층에서 웹과 영속성 계층을 다루게 되면 다른 전략을 취해야 함

‘양방향’ 매핑 전략

  • 각 어댑터가 전용 모델을 가지고 있어 해당 모델을 도메인 모델로, 도메인 모델을 해당 모델로 매핑할 책임을 가짐
    • 웹 계층에선 웹 모델을 인커밍 포트에서 필요한 도메인 모델로 매핑, 인커밍 포트에 의해 반환된 도메인 객체를 다시 웹 모델로 매핑(영속성은 아웃고잉 포트)
  • 장점
    • 단일 책임 원칙 만족
    • 매핑하지 않기 전략 다음으로 간단함, 매핑 책임이 명확
  • 단점
    • 너무 많은 보일러플레이트 코드 발생 ← 개발 속도 더뎌짐
    • 도메인 모델이 계층 경계를 넘어서 통신하는 데 사용
      • 인커핑 포트와 아웃고잉 포트는 도메인 객체를 입력 파라미터/반환값으로 사용함
      • 즉, 도메인 모델이 바깥쪽 계층의 요구에 따른 변경에 취약해짐

‘완전’ 매핑 전략

  • 각 연산이 전용 모델을 필요로 하여 웹 어댑터와 애플리케이션 계층 각각이 자신의 전용 모델을 각 연산을 실행하는 데 필요한 모델로 매핑한다.
    • Command, Request or else
  • 웹 계층(or 인커밍 어댑터 종류 중 아무거나)과 애플리케이션 계층 사이 상태 변경 유스케이스 경계를 명확하게 할 때 가장 적합함
  • 장점
    • 여러 유스케이스의 요구사항을 함께 다뤄야 하는 매핑에 비해 구현하고 유지보수하기 훨씬 쉬움
  • 단점
    • 더 많은 보일러플레이트 코드 발생

‘단방향’ 매핑 전략

  • 동일한 ‘상태’ 인터페이스를 구현하는 도메인 모델과 어댑터 모델을 이용하면 각 계층은 다른 계층으로부터 온 객체를 단방향으로 매핑하기만 하면 된다.
    • ex) Account → interface AccountState ← 웹/영속성 모델
    • 즉, 모든 계층의 모델들이 같은 인터페이스를 구현함
    • 이 인터페이스는 관련 있는 특성(attribute)에 대한 getter를 제공해 도메인 모델의 상태를 캡슐화함
    • 도메인 모델은 풍부한 행동을 구현할 수 있고, 애플리케이션 계층 내의 서비스에서 이러한 행동에 접근할 수 있다.
      • 도메인 객체를 바깥 계층으로 전달하고 싶으면 매핑 없이 할 수 있다.
      • 바깥 계층에선 상태 인터페이스를 이용할지, 전용 모델로 매핑할지 결정할 수 있다.
        • 행동을 변경하는 것이 상태 인터페이스에 의해 노출돼 있지 않아 실수로 도메인 객체의 상태를 변경하는 일은 발생하지 않는다.
  • 이 매핑은 factory라는 DDD 개념과 잘 어울린다.
    • factory는 어떤 특정 상태로부터 도메인 객체를 재구성할 책임을 가지고 있다.
  • 이 전략에서 매핑 책임은 명확하다. 한 계층이 다른 계층으로부터 객체를 받으면 해당 계층에서 이용할 수 있도록 다른 무언가로 매핑하는 것이다. 그러므로 각 계층은 한 방향으로만 매핑한다.
  • 단점은 개념적으로 어렵다는 것 ← 나도 모르겠어
  • 이 전략은 계층 간의 모델이 비슷할 때 가장 효과적이다.
    • ex) 읽기 전용 연산의 경우, 상태 인터페이스가 필요한 모든 정보를 제공하기 때문에 웹 계층에서 전용 모델로 매핑할 필요가 전혀 없다. ← 이러면 매핑하지 않기 전략이랑 뭐가 다른데?…

언제 어떤 매핑 전략을 사용할 것인가?

  • ‘그때그때 다르다’
    • 팀 내에서 합의할 수 있는 가이드라인을 정해둬야 한다.
      • 어떤 상황에서 어떤 매핑 전략을 가장 먼저 택해야 하는가에 답할 수 있어야 한다.
      • 이 전략을 최우선으로 택해야 하는지도 설명할 수 있어야 한다.
  • ex) 가이드라인 예시
    • 변경 유스케이스에서
      • 웹 계층과 애플리케이션 계층 사이에는 ‘완전 매핑’ 전략을 사용한다. → 유효성 검증 규칙이 명확해지고, 특정 유스케이스에서 필요하지 않은 필드를 다루지 않아도 된다.
      • 애플리케이션과 영속성 계층 사이에서는 ‘매핑하지 않기’ 전략을 첫 번째 선택지로 둔다. → 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해서
        • 하지만 애플리케이션 계층에서 영속성 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꿔서 영속성 문제를 영속성 계층에 가둘 수 있다.
    • 쿼리 작업을 한다면,
      • 웹 계층 ↔ 애플리케이션, 애플리케이션 ↔ 영속성 계층 사이에서 ‘매핑하지 않기’ 전략이 첫 번째 선택지가 된다. → 매핑 오버헤드를 줄이고 빠르게 코드를 짜기 위해서
        • 하지만 애플리케이션 계층에서 영속성 혹은 웹 문제를 다뤄야 하게 되면 ‘양방향’ 매핑 전략으로 바꾼다.

09 애플리케이션 조립하기

왜 조립까지 신경써야 할까?

  • 코드 의존성은 올바른 방향을 가리켜야 한다. 모든 의존성은 안쪽으로, 애플리케이션의 도메인 코드 방향으로 향해야 도메인 코드가 바깥 계층의 변경으로부터 안전하다.
  • 모든 클래스에 대한 의존성을 가지는 설정 컴포넌트(configuration component)가 있어야 한다.
  • 설정 컴포넌트의 책임
    • 웹 어댑터/유스케이스/영속성 어댑터 인스턴스 생성
    • HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장
    • 웹 어댑터에 유스케이스 인스턴스 제공 or 유스케이스에 영속성 어댑터 인스턴스 제공
    • 영속성 어댑터가 실제 데이터베이스에 접근할 수 있도록 보장
    • 설정 파일이나 커맨드라인 파라미터 등과 같은 설정 파라미터 소스에 접근

스프링의 클래스패스 스캐닝으로 조립하기

  • @Component 가 붙은 클래스를 classpath scanning을 통해 객체 생성 + Lombok의 @RequiredArgsConstructor 를 이용해 모든 final 필드를 인자로 받는 생성자 자동 생성해서 애플리케이션 컨텍스트에 추가
  • 아래와 같이 애너테이션을 만들어서 적용도 가능
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface PersistenceAdapter {
    
        @AliasFor(annotation = Component.class)
        String value() default "";
    }
  • 단점
    • 프레임워크에 종속적
    • 스프링 전문가가 아니면 원인을 찾기 어려운 일이 발생할 수 있음
      • 애플리케이션 컨텍스트에 실제로 올라가지 않길 원하는 클래스가 있을 수 있다.
      • 추적하기 어려운 에러를 일으킬 수 있음

스프링의 자바 컨피그로 조립하기

  • ex) PersistenceAdapterConfiguration
    • @Configuration + @Bean + @EnabledJpaRepositories
  • 비슷한 방법으로 웹 어댑터, 혹은 애플리케이션 계층의 특정 모듈을 위한 설정 클래스를 만들 수 있다.
    • 테스트 유연성 증가
    • @Component 애너테이션 코드를 여기저기 붙이도록 강제하지 않음
  • 단점
    • 설정 클래스가 생성하는 빈이 설정 클래스와 같은 패키지에 존재하지 않으면 이 빈들을 public으로 만들어야 한다.
      • 가시성을 제한하기 위해 패키리를 모듈 경계로 사용하고 각 패키지 안에 전용 설정 클래스를 만들 수 있다. 하지만 이렇게 하면 하위 패키지를 사용할 수 없다.
profile
나 개발자

0개의 댓글