Posts [클린아키텍처] 34장 빠져 있는 장
Post
Cancel

[클린아키텍처] 34장 빠져 있는 장

clean-architecture-book

‘클린 아키텍처’ 기술 서적에 대해 학습했던 내용을 정리하기 위한 목적의 TIL 포스팅입니다.🙆‍♂️

계층 기반 패키지

  • 기술적 관점에서 해당 코드가 하는 일에 기반하여 그 코드를 분할
  • 계층 사이의 의존성은 모두 아래를 향하도록 (단방향)

image 출처: https://skidrow6122.tistory.com/26

  • 마틴 파울러가 지적했듯이 커져가고 복잡해져가는 소프트웨어를 감당하기에는 부족하다.
  • 업무 도메인에 대해 아무것도 말해주지 않는다는 문제점

기능 기반 패키지

  • 서로 연관된 기능, 도메인 개념, 또는 (도메인 주도 설계 용어를 사용한다면) Aggregate Root 에 기반하여 수직의 얇은 조각으로 코드를 나누는 방식
  • 모든 타입이 하나의 자바 패키지에 속하며, 패키지 이름은 그 안에 담긴 개념을 반영해 짓는다
    • 이를 통해 코드의 상위 수준 구조가 업무 도메인에 대해 무언가를 알려주게 된다.

image 출처: https://skidrow6122.tistory.com/26

  • 또 다른 이점으로 유스케이스 변경에 대한 영향도를 파악하기 쉽다.
    • ‘주문 조회하기’ 유스케이스가 변경될 경우 영향가는 코드를 모두 찾는 작업이 더 쉬워질 수 있다.
    • 변경해야 하는 코드가 여러 군대 퍼져있는게 아니라 한 패키지에 몰아져있기 떄문에..
  • ‘계층 기반 패키지’와 ‘기능 기반 패키지’ 두 접근법은 모두 차선책이다.

포트와 어댑터

  • 엉클밥에 따르면, ‘포트와 어댑터’ 혹은 ‘육각형 아키텍처’, ‘경계, 컨트롤러, 엔티티’ 등의 방식으로 접근하는 이유는 업무/도메인에 초점을 둔 코드가 프레임워크나 DB와 같은 기술적인 세부 구현과 독립적이며 분리된 아키텍처를 만들기 위해서다.
  • 요약하자면 아래 이미지에서 제시하는것처럼, 그런 코드 베이스는 ‘내부(도메인)’와 ‘외부(인프라)’로 구성됨을 흔히 볼 수 있다.

image 출처: https://skidrow6122.tistory.com/26

  • ‘내부’ 영역은 도메인 개념을 모두 포함하는 반면, ‘외부’ 영역은 외부 세계(ex. UI, DB, 서드파티 통합)와의 상호작용을 포함한다.
  • 여기서 주요 규칙은 바로 ‘외부’가 ‘내부’에 의존하며, 절대 그 반대로는 안된다는 점이다.
  • 아래 이미지는 ‘주문 조회하기’ 유스케이스를 이 방식으로 구현한 모습이다.

image 출처: https://skidrow6122.tistory.com/26

  • 여기서 domain 패키지가 ‘내부’ 이며, 나머지 패키지는 모두 ‘외부’다. 의존성이 ‘내부’ 를 향해 흐르는 모습에 주목하라.

이전 다이어그램의 OrdersRepository가 Orders 라는 간단한 이름으로 바뀌었다. 이는 도메인 주도 설계라는 세계관에서 비롯된 명명법으로, 도메인 주도 설계에서는 ‘내부’에 존재하는 모든 것의 이름은 반드시 ‘유비쿼터스 도메인 언어’ 관점에서 기술하라고 조언한다. 바꿔 말하면, 도메인에 대해 논의할 떄 우리는 ‘주문’에 대해 말하는 것이지, ‘주문 레포지토리’에 대해 말하는것이 아니다.

컴포넌트 기반 패키지

  • 저자는 SOLID, REP, CCP, CRP 등 이 책에서 나온 여러 조언을 전적으로 동의하지만 ‘컴포넌트 기반 패키지’를 지양한다.

image 출처: https://skidrow6122.tistory.com/26

  • 위 이미지처럼 웹컨트롤러가 서비스계층을 건너뛰는 거처럼 인접한 계층을 건너뛰는 일은 지양해야한다.
    • 하지만 CQRS 패턴을 지키려고 시도하는등 경우에 따라 의도된 결과도 있긴하다.
  • 필요한것은 지침(아키텍처 원칙)으로, “웹 컨트롤러가 절대로 리포지토리에 접근해선 안된다”와 같은 원칙이 필요하다. 물론 문제는 강제성이다.
    • 자금이 바닥나거나 납기가 다가오면 이번 한번쯤은 괜찮겠지~ 하고 어기기 쉽기 마련이다.
    • 이럴때 빌드시 정적 분석 도구를 사용해서 아키텍처적인 위반 사항이 없는지를 검사하여 자동으로 강제(빌드 실패)하는 방법도 있다.
    • 하지만 베스트는 컴파일러 단에서 아키텍처를 강제하는 방식이다.
  • ‘컴포넌트 기반 패키지’를 도입해야 하는 이유는 바로 이 때문이다. 이 접근법은 지금까지 우리가 본 모든것들을 혼합한것으로, 큰 단위의 단일 컴포넌트와 관련된 모든 책임을 하나의 자바 패키지로 묶는데 주안점을 둔다.
  • 이 접근법은 서비스 중심적인 시각으로 소프트웨어 시스템을 바라보며, MSA 가 가진 시각과도 동일하다.
  • 컴포넌트 기반 패키지에서도 사용자 인터페이스를 큰 단위의 컴포넌트로부터 분리해서 유지한다. 그림 34.6에서 ‘주문 조회하기’ 유스케이스가 어떤 모습인지 보여준다.

image 출처: https://skidrow6122.tistory.com/26

  • 본질적으로 이 접근법에서 ‘업무 로직’과 영속성 관련 코드를 하나로 묶는데, 이 묶음을 나는 ‘컴포넌트’라 부른다.
    • 엉클밥은 컴포넌트를 배포 가능한 가장 작은 단위로 정의하지만(java의 jar 파일) 저자는 멋지고 깔끔한 인터페이스로 감싸진 연관된 기능들의 묶음으로 정의한다.
  • 컴포넌트 기반 패키지 접근법의 주된 이점은 주문과 관련된 무언가를 코딩해야 할 때 오직 한 곳, 즉 OrdersComponent 만 둘러보면 된다는 점이다.
    • 이 컴포넌트 내부에서 관심사의 분리는 여전히 유효하며, 따라서 업무 로직은 데이터 영속성과 잘 분리되어 있다.
    • 하지만 이는 컴포넌트 구현과 관련된 세부사항으로, 사용자는 알 필요가 없다.
    • 이는 마이크로 서비스 지향 아키텍처를 적용했을때 얻는 이점과도 유사하다.
    • 즉, 주문 처리와 관련된 모든 것들을 캡슐화하는 별도의 OrdersService 가 존재한다.
    • 큰 차이는 결합 분리 모드에 있다.
    • 모노리틱 애플리케이션에서 컴포넌트를 잘 정의하면 MSA로 전환하는데 손쉬울것이다.

구현 세부사항엔 항상 문제가 있다

  • 자바의 public 지시자를 무분별하게 사용한다는건 프로그래밍 언어가 제공하는 캡슐화 관련 이점을 활용하지 않겠따는 뜻이다.
  • 이로 인해 누군가가 구체적인 구현 클래스의 인스턴스를 직접 생성한 코드를 작성하는 일을 절대 막을 수 없으니, 결국 지향하는 아키텍처 스타일을 위반하게 될것이다.

조직화 vs 캡슐화

  • 모든 타입을 public 지시자로 지정한다면, 패키지는 단순 폴더와 같이 묶는 방식으로만 전락하게 되어 캡슐화를 위한 메커니즘이 사라지게 된다.
  • public 타입을 코드 베이스 어디에서도 사용할 수 있따면 패키지를 사용하는데 따른 이점이 거의 없다. 따라서 사실상 패키지를 사용하지 않는것과 같아지게되고 최종적으로 어떤 아키텍처 스타일로 만들려고 하는지는 아무런 의미가 없어진다. public 지시자를 과용하면 이 장의 앞에서 제시한 네 가지 아키텍처 접근법은 본질적으로 완전히 같아진다.

image 출처: https://skidrow6122.tistory.com/26

  • 위 이미지에서 채택하려는 아키텍처 접근법과 아무런 관계 없이 화살표들이 모두 동일한 방향을 가리킨다.
    • 개념적으로 이 접근법들은 매우 다르지만, 구문적으로는 완전히 똑같다.
    • 이처럼 모든 타입을 public 으로 선언시 우리가 실제로 갖게되는것은 수평적 계층형 아키텍처를 표현하는 네 가지 방식에 지나지 않는다.
  • 자바의 접근 지시자는 완벽하진 않지만 무시하면 사서 고생하게 된다. 적절하게 사용시 타입을 패키지로 배치하는 방식에 따라서 각 타입에 접근할 수 있는 정도가 실제로 크게 달라질 수 있다.
  • 만약 다이어그램에서 패키지 구조를 다시 살려서 더 제한적인 접근 지시자를 사용할 수 있는 타입을 (흐리게) 표시하면, 다이그램은 상당히 인상적으로 변한다. 진한게 표시된 요소만 외부에서 접근 가능하도록 열어둠으로써 캡슐화와 은닉화의 장점을 더 잘 살릴수 있게 되는것이다.

image

  • 아키텍처 원칙을 강제할때 이와 같은 접근 지시자처럼 컴파일 단에서 의지하도록 할것을 권장한다. 출처: https://skidrow6122.tistory.com/26

다른 결합 분리 모드

  • 자바엔 OSGi 같은 모듈 프레임워크나 자바9에서 제공하는 새로운 모듈 시스템을 이용하면 모든 타입을 public 으로 지정하도록 일부만 외부에서 사용가능하도록 공표할 수 있다.
  • 다른 선택지로는 소스 코드 수준에서 의존성 분리하는 방법도 있다. 정확하게는 서로 다른 소스 코드 트리로 분리하는 방법이다. 포트와 어댑터를 예로 들면 아래와 같다.
    • 업무와 도메인용 소스 코드: OrderSerivce, OrdersServiceImpl, Orders
    • 웹용 소스 코드: OrdersController
    • 데이터 영속성용 소스 코드: JdbcOrderRepository
  • maven, gradle 같은 빌드 도구로 서로 분리되도록 구성해야하는데, 웹용 데이터 영속성용 소스코드는 컴파일 시점에서 도메인용 소스코드에 의존성을 가지며 도메인용 소스코드는 웹, 데이터 영속성 코드에 대해서 알지 못하도록 해야한다.
  • 애플리케이션을 구성하는 모든 컴포넌트 각각을 개별적인 소스 코드 트리로 구성해야 하는데 이는 너무 이상적인 해결책이고 현실에선 나누다보면 성능, 복잡성, 유지보수 문제가 생기게 된다.
  • 포트와 어댑터 ㅈ버근법을 적용할때는 이보다 간단한 방법을 사용하기도 하는데, 단순히 소스 코드 트리를 두 개 만드는것이다.
    • 도메인 코드(‘내부’)
    • 인프라 코드(‘외부’)
  • 인프라 코드를 단일 소스 코드에 모두 모아둔다는 말은 애플리케이션에서 특정 영역(ex. 웹 컨트롤러)에 잇는 인프라 코드가 애플리케이션의 다른 영역(ex. 데이터베이스 리포지토리)에 있는 코드를 도메인을 통하지 않고 직접 호출 할 수 있다는 뜻이다. 특히 해당 코드에 적절한 접근 지시자를 적용하는걸 잊어버린 경우라면 이러한 호출을 막기는 더욱 힘들다.

결론: 빠져 있는 조언

  • 최적의 설계를 꾀했더라도, 구현 전략에 얽힌 복잡함을 고려하지 않으면 설계가 순식간에 망가질 수도 있다는 사실을 강조하는데 그 목적이 있다.
  • 설계를 어떻게 해야만 원하는 코드 구조로 매핑할 수 있을지, 그 코드를 어떻게 조직화할지, 런타임과 컴파일타임에 어떤 결합 분리 모드를 적용할지를 고민하라.
  • 가능하다면 선택사항을 열어두되, 실용주의적으로 행하라.
  • 그리고 팀의 규모, 기술 수준, 해결책의 복잡성을 일정과 예산이라는 제약과 동시에 고려하라.
  • 또한 선택된 아키텍처 스타일을 강제하는데 컴파일러의 도움을 받을 수 있을지를 고민하며, 데이터 모델과 같은 다른 영역에 결합되지 않도록 주의하라. 구현 세부사항에는 항상 문제가 있는 법이다.

Reference

This post is licensed under CC BY 4.0 by the author.

[클린아키텍처] 33장 사례 연구: 비디오 판매

[클린코드] Chapter10-클래스