계층형 아키텍처

  • 목적
    • 선택의 폭을 넓히고 변화하는 요구사항과 외부 요인에  빠르게 적응할 수 있게 해준다.
    • 애플리케이션의 목적은 비즈니스 규칙이나 정책을 반영한 모델을 만들어서 사용자가 이러한 규칙과 정책을 편리하게 활용할 수 있게 한다. 행동은 상태를 바꾸는 주체이기때문에 상태가 아니라 행동을 중심으로 모델링 해야 한다.

 

  • 문제
    • 코드에 나쁜 습관들이 스며들기 쉽게 만들고 점점 더 변경하기 어렵게 만드는 허점들을 노출한다.
      • 데이터 베이스 주도 설계 유도
      • 영속성 계층을 토대로 만들어진다. 
      • ORM 프레임워크를 결합하면 비즈니스 규칙을 영속성 관점과 섞고 싶은 유혹을 쉽게 받는다
      • 영속성 계층과 도메인 계층 사이에 강한 결합이 생긴다. 
    • 지름길을 택하기 쉬워진다. 
      • 상위계층에 위치한 컴포넌트에 접근해야 한다면 간단하게 컴포넌트를 계층 아래로 내려 버리면 된다. 
      • 어떤 것을 할수 있는 선택지가 있다면 누군가는 반드시 그렇게 하기 마련이다.
    • 테스트하기 어려워진다.
      • 간단한 변경의 경우 도메인을 웹계층에 구현하게 된다. 이런경우 테스트를 구현하려면 웹 계층에서 영속성 계층을 모킹해야 한다. 
      • 테스트의 복잡도가 올라가게 된다.
    • 유스케이스를 숨긴다.
      • 도메인 로직이 여러계층에 흩어지기 쉽다.
      • 여러개의 유스케이스를 담당하는 아주 넓은 서비스가 만들어지기도 한다. 
    • 동시 작업이 어려워진다. 
      • 영속성 계층을 먼저 개발해야 하고 그 다음에 도메인 계층, 웹계층을 만들어야 한다.

클린 아키텍처

  • 설계가 비즈니스 규칙의 테스트를 용이하게 한다. 
  • 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있다.
  • 도메인 코드는 바깥으로 향하는 어떤 의존성도 없어야 한다. 
  • 의존성 역전원칙을 이용하여 모든 의존성이 도메인 코드를 향해야 한다.
  • 코어에는 유스케이스에서 접근하는 도메인 엔티티들이 있다. 
  • 유스케이스는 흔히 서비스라고 부르던 것들이고 단일 책임을 갖기 위해 세분화 되어야 한다. (넓은 서비스 문제 해결)
  • 도메인 계층이 영속성이나 UI 같은 외부 계층과 철저하게 분리 돼야 하므로 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수 해야 한다. 
  • 도매인 계층과 영속성 계층이 데이터를 주고 받을때 두 엔티티를 서로 변환해야 한다. 도메인 계층과 다른 계층들 사이에서도 마찬가지다.

헥사고날 아키텍처(포트와 어댑터 아키텍처)

  • 클린 아키텍처의 원칙을 조금 더 구체적으로 만들어 준다.
  • 웹어댑터, 외부 시스템 어댑터 -> 입력포트(인터페이스) <- 유스케이스 -> 엔티티 <- 유스케이스 -> 출력포트(인터페이스) <- 영속성 어댑터, 외부시스템 어댑터 
  • 도메인 중심 아키텍처에 적합

아키텍처적으로 표현력 있는 패키지 구조

business

L sub business

    L adapter

        L in

            L web - controller

        L out

            L persistent adapter

            L repository

     L domain - entity

     L application

         L port

            L in - usecase

            L out

  • 이 패키지 구조는 아키텍처-코드 갭 혹은 모델-코드 갭을 효과적으로 다룰 수 있는 강력한 요소다
  • 패키지 구조가 아키텍처를 반영할 수 없다면 시간이 지남에 따라 코드는 점점 목표하던 아키텍처로부터 멀어지게 될 것이다 
  • 패키지 내의 클래스들은 특성에 따라 접근제어자를 이용하여 계층간의 우발적인 의존성을 막을 수 있다. 
  • domain 패키지 내에서는 DDD가 제공하는 모든 도구를 이용해 우리가 원하는 어떤 도메인 모델이든 만들 수 있다. 
  • 아키텍처-코드갭을 넓히고 아키텍처를 반영하지 않는 패키지를 만들어야 하는 경우도 생길 수 있다. 

도메인 모델 구현하기

  • 도메인에 관련된 주요 기능을 포함하는 엔티티 구현
  • 비즈니스 규칙 유효성 검증
  • rich domain model
    • 엔티티에서 가능한 한 많은 도메인 로직 구현
    • 상태 변경 메소드 제공
    • 비즈니스 규칙에 맞는 유효한 변경만 허용
    • 유스케이스는 사용자의 의도만 표현
  • anemic domain model 
    • 도메인 로직을 가지지 않음
    • 엔티티 상태를 표현하는 필드와 getter, setter 메서드만 포함
    • 도메인 로직은 유스케이스에 구현

 

유스케이스 구현하기

  • 하는일
    • 입력을 받는다
    • 비즈니스 규칙을 검증한다.
    • 모델 상태를 조작한다
    • 출력을 반환한다
  • 입력 유효성 검증
    • 구문상(syntatical)의 유효성 검증
    • 이로인해 유스케이스가 오염 되지 않아야 함
    • 애플리케이션 계층의 책임
    • 입력모델이 다루기(anti corruption layer) 
    • Bean validation API 사용하기
    • 입력모델 생성자에서 validation 하기
  • 비즈니스 규칙 검증
    • 유스케이스의 맥락속에서 의미적인(semantical) 유효성 검증
    • 도메인 모델의 현재 상태에 접근
    • 도메인 엔티티 안에 구현
  • 출력 모델
    • 유스케이스에 맞게 구체적일 수록 좋음
    • 계속 질문하고 의심스럽다면 가능한 한 적게 반환하자
    • 단일 책임 원칙 적용
    • 도메인 엔티티를 출력모델로 사용하고 싶은 유혹을 견뎌내야 함

웹 어댑터 구현하기

  • 주도하는 / 인커밍 어댑터
  • 웹어댑터에서 애플리케이션 서비스로 제어흐름
  • HTTP와 관련된 것은 애플리케이션 계층으로 침투해서는 안됨
    • 다른 인커밍 어댑터의 요청에 대해 동일한 도메인 로직을 수행할 수 있는 선택지
    • 좋은 아키텍처에서는 선택의 여지를 남겨둔다
  • 의존성 역전원칙
    • 포트 인터페이스를 통해 서비스 호출
    • 포트 : 애플리케이션 코어가 외부세계와 통신할 수 있는 곳에 대한 명세
  • 책임
    • HTTP 요청을 자바 객체로 매핑
    • 권한검사
    • 입력유효성 검증
    • 입력을 유스케이스의 입력 모델로 매핑
    • 유스케이스 호출
    • 유스케이스의 출력을 HTTP로 매핑
    • HTTP 응답 반환
  • 콘트롤러 나누기
    • 가능한한 좁고 다른 컨트롤러와 적게 공유하는 웹 어댑터 조각 구현
    • 클래스마다 코드는 적을 수록 좋다
    • 모든 연산을 단일 콘트롤러에 넣는 것은 데이터 구조의 재활용을 촉진하게 됨
    • 메서드와 클래스명은 유스케이스를 최대한 반영해서 지어야 함
    • 콘트롤러자체의 모델을 갖거나 원시값을 받도록 함
    • 서로다른 연산에 대한 동시 작업이 쉬워짐
    • 병합충돌이 일어나지 않게됨
    • 더 파악하기 쉽고, 테스트하기 쉽다

영속성 어댑터 구현하기

  • 주도되는 / 아웃고잉 어댑터
  • 애플리케이션에 의해 호출되고 호출하지는 않음
  • 포트 인터페이스를 통해 통신
  • 책임
    • 입력을 받는다 (입력모델)
    • 입력을 데이터베이스 포맷으로 매핑한다 (JPA 엔티티)
    • 입력을 데이터베이스로 보낸다
    • 데이터베이스 출력을 애플리케이션 포맷으로 매핑한다
    • 출력을 반환한다
  • 포트 인터페이스 나누기
    • 인터페이스 분리 원칙
    • 클라이언트가 오로지 자신이 필요로 하는 메서드만 알면 되도록 넓은 인터페이스를 특화된 인터페이스로 분리
    • 매우 좁은 포트를 만드는 것은 프러그 앤드 플레이 경험으로 만든다
  • 연속성 어댑터 나누기
    • 애그리거트 하나당 하나의 영속성 어댑터 구현
      • 영속성 기능을 이용하는 도메인 경계를 따라 자동으로 나눠짐
      • 여러개의 바운디드 컨텍스트의 영속성 요구사항을 분리하기 위한 토대
    • 여러 종류의 어댑터 구현(JPA, native sql , ..)
  • 트랜잭션 
    • 영속성 어댑터를 호출하는 서비스에 위임

경계간 매핑하기

  • 매핑하지 않기(No mapping) 전략
    • 도메인과 애플리케이션 계층은 웹이나 영속성과 관련된 특수한 요구사항에 관심이 없지만 다루게 된다. 
    • 단일 책임 원칙 위반
    • 도메인 모델에 특정 커스텀 필드를 두도록 요구할 수 있다. 파편화된 도메인 모델
    • 모든 계층이 정확히 같은 구조의, 같은 정보를 필요하다면 완벽한 선택지 
  • 양방향(Two way) 매핑 전략
    • 각 계층이 전용모델를 가지고 있어 다른 계층에는 영향이 없다
    • 깨끗한 도메인 모델
    • 단일 책임 원칙 만족
    • 매핑 책임이 명확
    • 너무 많은 보일러 플레이트 코드 발생
    • 도메인 모델이 경계를 넘어서 통신하는데 사용. 바깥쪽 계층의 요구에 따른 변경에 취약
  • 완전(Full) 매핑 전략
    • 각 연산마다 별도의 입출력 모델 사용
    • 전역 패턴으로 추천하지 않음
    • 웹계층과 애플리케이션 계층사이에서 상태 변경 유스케이스의 경계를 명확하게 할때 가장 빛을 발한다. 
  • 단방향(One way) 매핑 전략
    • 모든 계층의 모델들이 같은 인터페이스를 구현
    • 특성에 대한 getter 메소드를 제공해서 도메인 모델의 상태를 캡슐화
    • 한 계층이 다른 계층으로부터 객체를 받으면 해당 계층에서 이용할 수 있도록 무언가로 매핑
    • 계층간의 모델이 비슷할때 가장 효과적
  • 언제 어떤 전략을 사용할 것인가?
    • 그때그때 다르다
    • 특정 작업에 최선의 패턴이 아님에도 깔끔하게 느껴진다는 이유로 선택해버리는것은 무책임한 처사다
    • 어제는 최선이었던 전략이 오늘은 아닐수도 있다
    • 빠르게 코드를 짤 수 있는 간단한 전략으로 시작해서 계층 간 결합을 떼어내는 데 도움이 되는 복잡한 전략으로 갈아타기
    • 어떤 상황에서 어떤 전략을 택해야 할지 가이드라인을 정해둬야 한다

아키텍처 경계 강제하기

  • 계층 경계를 넘는 의존성은 항상 안쪽 방향으로 향해야 한다.
  • 접근 제한자
    • package-private(default)
    • 자바 패키지를 통해 응집적인 모듈로 만들어 줌
    • 모듈의 진입점으로 활용될 클래스들만 public으로 만듬
    • 몇 개 정도의 클래스로만 이뤄진 작은 모듈에서 가장 효과적
  • 컴파일 후 체크
    • ArchUnit
    • 의존성 규칙 위반을 발견하면 예외
    • 패키지 사이의 의존성 방향이 올바른지 자동으로 체크
    • 실패에 안전하지는 않다
      • 오타의 경우 
      • 패키지명 리팩토링
      • 클래스를 하나도 찾지 못했을때 실패하는 테스트 추가 필요
    • 언제나 코드와 함께 유지 보수
  • 빌드 아티팩트
    • 모듈과 아키텍처의 계층 간의 의존성을 강제
    • 순환의존성 체크
    • 다른 모듈을 고려하지 않고 특정 모듈의 코드를 격리한채로 변경할 수 있음
    • 새로 의존성을 추가하는 일이 우연이 아닌 의식적인 행동이 됨
    • 빌드 스크립트 유지보수 비용

의식적으로 지름길 사용하기

  • 유스케이스간 모델 공유하기
  • 도메인 엔티티를 입출력 모델로 사용하기
  • 인커밍 포트 건너뛰기
  • 애플리케이션 서비스 건너뛰기

 

 

                                  

 

+ Recent posts