의존성의 끝이 도메인을 향하게 하자

바이너리·2022년 1월 26일
3
post-thumbnail

의존성 역전하기

단일 책임 원칙

육각형 아키텍처를 이해하고 잘 활용하기 위해 중요한 개념은 객체지향 원칙 SOLID 중 S(Single Responsibility Principle), D(Dependency Inversion Principle)입니다.

단일 책임 원칙: 컴포넌트를 변경하는 이유는 단 하나여야 한다.
의존성 역전 원칙: 고수준 모듈은 저수준 모듈에서 아무것도 가져오지 않아야 하며, 추상화에 의존해야 한다. 추상화는 세부 사항에 의존하면 안 된다.

이 원칙들이 왜 중요할까요?

컴포넌트를 변경하는 이유가 단 하나라는 건, 다른 컴포넌트의 변경에도 영향을 받지 않는다는 걸 의미합니다.

유지보수와 확장을 생각했을 때 굉장히 이상적인 말 처럼 들리지만, 실제 코드 세계에서는 많은 컴포넌트가 상호 간 의존성을 가지고 있기 때문에 쉽게 실현되지 않습니다.

계층형 아키텍처에서 Repository 클래스의 코드가 변경되면, 해당 클래스를 참조하는 Service, 연쇄적으로는 Controller까지 모두 변경되어야 합니다.


부수 효과(Side Effect)

어떠한 프로시저/함수가 실행 회수, 시기와 상관없이 같은 입력에 대해 같은 출력을 반환할 것 이라고 기대하는 것을 멱등성이라고 합니다. 부수 효과를 가진 함수는 실행 도중에 컴포넌트의 상태를 변경할 수 있는 함수를 말합니다.

코드가 부수 효과를 많이 포함하고 있다면, 교체했을 때 어떠한 부작용을 일으킬 지 예측하기 어렵습니다. 확장과 유지보수에 대한 두려움은 변화를 포기하거나 선택할 수 없게끔 만듭니다.


의존성 역전

저번 글에서 살펴봤듯, 웹 -> 도메인 -> 영속성으로 향하는 의존성을 가진 애플리케이션 구조는 결국 영속성이 모든 것을 지배합니다. 도메인 계층은 모든 설계와 코드에서 최우선 순위를 지켜야 하고, 프레임워크나 다른 계층에 종속되지 않아야 합니다.
영속성 계층의 코드가 수정될 때 도메인 로직의 코드도 함께 수정해야 하는 상황은 바람직하지 않습니다.

의존성 역전 원칙을 적용하면 이 고민을 멋있게 풀어나갈 수 있습니다.

기존의 계층형 아키텍처에서 서비스(도메인)이 레포지토리(영속성) 계층에 의존성을 가진 모습입니다.

도메인 계층이 영속성 계층에 의존하기 때문에 레포지토리 클래스가 바뀌면 서비스 클래스까지 영향이 전파됩니다.

이 때, DIP를 적용해서 기존의 Repository를 추상화하면 의존 관계를 역전할 수 있습니다 ❗️❗️

Repository의 책임을 추상화한 인터페이스(Port)를 만들고, 클래스(Adaptor)가 그것을 구현하도록 만들었습니다.
(실제 DIP를 엄격하게 따르기 위해서는 Service 또한 의존성을 역전시켜줘야 합니다)

그리고, Service와 Port를 Domain 계층으로 묶는다면, 이전과 달리 영속성 계층의 의존성 도메인 계층을 향하도록 구성할 수 있습니다.

모든 계층이 추상화된 인터페이스인 포트에 의존해서 구현체가 어떻게 이루어져 있는지 알지 않아도 되고, 구현체인 어댑터를 갈아끼우면 클래스의 변경이 전파되지 않을 것 같네요 😮

실제로 그럴까요?


클린 아키텍처

로버트 C. 마틴은 책 '클린 아키텍처'를 통해 용어를 정립했고, 아래와 같이 말했습니다.

  • 비즈니스 규칙의 테스트를 용이하게 하는 설계를 지니고
  • 비즈니스 규칙이 프레임워크/데이터베이스/UI/외부 애플리케이션/인터페이스로부터 완전히 독립적이어야 한다.

즉, 도메인 코드가 바깥 계층으로 향하는 어떤 의존성도 가지지 않는다는 것을 의미합니다. 위에서 확인한 것 처럼 DIP를 사용하면 모든 의존성이 바깥에서 도메인 계층을 향하게 만들 수 있습니다.

(출처: Clean Architecture — A Little Introduction)

그림은 클린 아키텍처라고 부르는 구조를 추상화해서 나타낸 것 입니다. 이 아키텍처에서 가장 강조하는 것은 의존성에 대한 규칙으로, 계층 간의 모든 의존성이 바깥쪽에서 안쪽으로 향해야 한다는 것 입니다.

아키텍처의 가장 중심에는 Entity가 위치합니다. 그 바깥쪽에는 Use Case가 있는데, 이전에 다루었던 Service를 단일 책임을 가지도록 세분화한 개념을 의미합니다.

그 바깥쪽에는 영속성을 제공하는 Persistence, Web 컨트롤러, 서드파티 컴포넌트와 같이 애플리케이션을 지원하는 다양한 계층들이 위치할 수 있습니다.

이 구조의 장점은 무엇일까요?

우선, 도메인 코드는 아무 의존성을 가지지 않기 때문에 어떤 UI, 영속성 프레임워크를 사용하는 지 몰라도 되고 모를 수 밖에 없습니다. 따라서 비즈니스 로직 그 자체에 집중된 구조를 가질 수 있습니다.


하지만 클린 아키텍처는 공짜가 아니고, 실제로 구현해보면 다양한 대가를 치러야만 한다는 걸 느낄 수 있습니다.

의존성에 대한 엄격한 규칙 때문에, 각 계층에서 사용해야 할 모델을 만들어주고 그것을 계층마다 변환해주는 매핑 작업이 필요합니다.

추상화된 인터페이스와 다양한 구현체를 만들어 주는 것, 서비스를 여러 개의 작은 유스케이스로 분리해보는 경험에서 감당할 수 있는 범위 이상의 클래스 개수를 만날 수 있습니다.

이 아키텍처에서는 JPA에서 사용하던 엔티티 클래스를 도메인 계층에 포함할 수 없습니다. 영속성에 대한 여러 제약사항이 클래스에 포함되니까요. 이러한 경우에서는 도메인 계층에서 사용하는 엔티티와 영속성 계층에서 사용하는 엔티티를 분리해줘야 합니다.


여러 계층에서 사용하는 모델을 철저하게 분리할 것 인지, 공통된 모델을 공유할 것 인지, 등등의 전략은 다양한 경우가 존재할 수 있습니다.


육각형 아키텍처(Hexagonal Architecture)

(출처: What is “Hexagonal Architecture”?)

클린 아키텍처를 조금 더 일반적으로 풀어낸 것이 육각형 아키텍처(헥사고날 아키텍처)입니다. 위에서 언급했던 포트(추상화된 인터페이스)와 어댑터(포트의 구현체)를 중심으로 설계할 수 있습니다.

육각형이란 이름은 다른 시스템 혹은 어댑터와 연결될 수 있는 면이 여러개다!라는 의미로 지어졌고, 실제로는 더 많은 면을 가져도 무방합니다.

아키텍처의 내부에는 도메인 엔티티, 엔티티와 상호작용하는 유스케이스가 위치합니다. 클린 아키텍처에서 거듭 강조한 것 처럼 모든 의존성은 코어를 향합니다. 외부에는 애플리케이션과 상호작용하는 다양한 어댑터들(웹 인터페이스, 데이터베이스 등)이 존재합니다.

코어와 어댑터를 연결시켜주는 창구 역할을 하는 것이 포트입니다.
Input Port는 내부의 Use Case 클래스에 의해 구현되고, Driving Adaptor가 이를 호출해서 사용합니다. Output Port는 외부의 Driven Adaptor에 의해 구현되고, 내부의 코어가 이를 호출해서 사용합니다.

이 아키텍처는 그래서 육각형 아키텍처(헥사고날 아키텍처)이지만 포트와 어댑터 아키텍처로도 불립니다.


정리

육각형 아키텍처를 적용하면 의존성을 역전시켜서 도메인 코드가 외부의 코드에 의존하지 않도록 구성할 수 있습니다. 이전에 존재했던 문제점인 영속성으로의 의존, 도메인 책임의 전파, 계층 간의 결합이라는 문제점을 해소합니다.

추상화된 포트에만 의존해서 구현체가 바뀌어도 도메인 영역은 영향을 받지 않고, 각 계층이 완벽하게 분리되어서 독립적으로 모델링 할 수 있게끔 도와줍니다.


사실 이 글에서는 계층형 아키텍처, 육각형 아키텍처에 대한 개념만 언급했습니다. DDD가 무엇인지, 더 정교하고 효율적으로 아키텍처를 적용하는 방법은 무엇인지, 실제로 적용할 때 어떤 장벽(네이밍, 매핑 전략 등)이 있을지는 많은 고민과 공부가 필요한 문제인 것 같습니다.

🙇‍♀️

profile
01101001011010100110100101101110

0개의 댓글