계층형 아키텍처에 대해 불만만 늘어놓았으니 이번 장에서는 대안에 대해 이야기해 보자. 먼저 SOLID의 'S'와 'D'를 담당하는 단일 책임 원칙(SRP)과 의존성 역전 원칙(DIP)으로 시작한다.
단일 책임 원칙
'오로지 한 가지 일만 하는 것'은 단일 책임이라는 말을 가장 직관적으로 해석한 것이므로, 단일 책임 원칙을 자주 위와 같이 해석한다. 하지만 오해의 여지가 있다는 점에 주의해야 한다.
'책임'은 사실 '한 가지 일만 한다'는 것보다는 '변경할 이유'로 해석해야 한다. 컴포넌트를 변경할 이유가 오로지 한 가지밖에 없다면 컴포넌트는 딱 한 가지 일만 하게 된다. 그리고 여기서의 초점은 변경할 이유가 오직 한 가지라는 것이다.
아키텍처에서는 컴포넌트를 변경할 이유가 한 가지라면 우리가 어떤 다른 이유로 소프트웨어를 변경하더라도 이 컴포넌트에 대해서는 전혀 신경 쓸 필요가 없다. 변경이 있더라도 여전히 우리가 기대한 대로 동작할 것이기 때문이다. 안타깝게도 우리의 애플리케이션에서 변경은 컴포넌트 간의 의존성을 통해 너무도 쉽게 전파될 수 있다.
그림에서 컴포넌트 A는 다른 여러 컴포넌트에 의존하는 반면 컴포넌트 E는 의존하는 것이 전혀 없다. E를 변경할 유일한 이유는 새로운 요구사항에 의해 E의 기능을 바꿔야 할 때뿐이다. 반면 A의 경우에는 모든 컴포넌트에 의존하고 있기 때문에 다른 어떤 컴포넌트가 바뀌든지 같이 바뀌어야 한다.
많은 코드는 단일 책임 원칙을 위반하기 때문에 시간이 갈수록 변경하기가 더 어려워지고 그로 인해 변경 비용도 증가한다.
의존성 역전 원칙
계층형 아키텍처에서 계층 간 의존성은 항상 다음 계층인 아래 방향을 가리킨다. 단일 책임 원칙을 고수준에서 적용할 때 상위 계층들이 하위 계층들에 비해 변경할 이유가 더 많다는 것을 알 수 있다.
쉽게 말해서 서비스 로직이 바뀌는 것보단 엔티티가 변경되면 비교적 많은 코드들이 수정을 요구한다.
그러므로 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 변경할 때마다 잠재적으로 도메인 계층도 변경해야 한다. 그러나 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 영속성 코드가 바뀐다고 해서 도메인 코드까지 바꾸고 싶지는 않다.
이 의존성을 어떻게 없앨 수 있을까? ➜ 의존성 역전 원칙(DIP)이 답을 알려준다.
도메인 코드와 영속성 코드 간의 의존성을 역전시켜서 영속성 코드가 도메인 코드에 의존하고, 도메인 코드를 '변경할 이유'의 개수를 줄여보자.
- 도메인 계층에 영속성 계층의 엔티티와 리포지토리와 상호작용하는 서비스가 하나 있다.
- 엔티티는 도메인 객체를 표현하고 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 중심으로 하기 때문에 먼저 엔티티를 도메인 계층으로 올린다.
- 그러나 영속성 계층의 리포지토리가 도메인 계층에 있는 엔티티에 의존하기 때문에 두 계층 사이에 순환 의존성이 생긴다.
- 바로 이 부분에 DIP를 적용해 준다. 도메인 계층에 리포지토리에 대한 인터페이스를 만들고, 실제 리포지토리는 영속성 계층에서 구현하게 하는 것이다.
이 묘수로 영속성 코드에 있는 의존성으로부터 도메인 로직을 해방시켰다. 이것이 바로 이제 다뤄볼 두 가지 아키텍처 스타일의 핵심 기능이다.
클린 아키텍처
로버트 C. 마틴은 클린 아키텍처에서는 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, UI 기술, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있다고 이야기했다.
➜ 이는 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 함을 의미한다. 대신 의존성 역전 원칙의 도움으로 모든 의존성이 도메인 코드를 향하고 있다.
이 아키텍처에서 계층들은 동심원으로 둘러싸여 있다. 이 아키텍처에서 가장 중요한 규칙은 의존성 규칙으로, 계층 간의 모든 의존성이 안쪽으로 향해야 한다는 것이다. 코어에는 주변 유스케이스에서 접근하는 도메인 엔티티들이 있고, 유스케이스는 앞에서 서비스라고 불렸던 것들인데, 단일 책임을 갖기 위해 조금 더 세분화되어 있다.
➜ 이렇게 하면 넓은 서비스 문제를 피할 수 있다.
도메인 코드에서는 어떤 영속성 프레임워크나 UI 프레임 워크가 사용되는지 알 수 없기 때문에 특정 프레임워크에 특화된 코드를 가질 수 없고 비즈니스 규칙에 집중할 수 있다.
➜ 그래서 도메인 코드를 자유롭게 모델링할 수 있다.
➜ 예를 들어, 도메인 주도 설계(DDD)를 가장 순수한 형태로 적용해 볼 수도 있다.
➜ 영속성이나 UI에 특화된 문제를 신경 쓰지 않아도 된다면 이렇게 하기가 굉장히 수월해진다.
💦 이에 대한 대가도 있는데 도메인 계층이 영속성이나 UI 같은 외부 계층과 철저하게 분리돼야 하므로 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수해야 한다.
도메인 계층은 영속성 계층을 몰라야 하기 때문에 도메인 계층에서 사용한 엔티티 클래스를 영속성 계층에서 함께 사용할 수 없고 두 계층에서 각각 엔티티를 만들어야 한다.
즉, 도메인 계층과 영속성 계층이 데이터를 주고받을 때, 두 엔티티를 서로 변환해야 한다는 뜻이다.
육각형 아키텍처(헥사고날 아키텍처)
'육각형 아키텍처'라는 용어는 알리스테어 콕번이 만든 용어로, 꽤 오랫동안 사용돼 왔다.
육각형 안에는 도메인 엔티티와 이와 상호작용하는 유스케이스가 있다. 육각형에서 외부로 향하는 의존성이 없기 때문에 마틴이 클린 아키텍처에서 제시한 의존성 규칙이 그대로 적용된다는 점을 주목해야 한다. 대신 모든 의존성은 코어를 향한다.
왼쪽에 있는 어댑터들은 (애플리케이션 코어를 호출하기 때문에) 주도하는 어댑터들이다. 오른쪽에 있는 어댑터들은 (애플리케이션 코어에 의해 호출되기 때문에) 주도되는 어댑터들이다.
애플리케이션 코어와 어댑터들 간의 통신이 가능하려면 애플리케이션 코어가 각각의 포트를 제공해야 한다. 주도하는 어댑터에게는 그러한 포트가 코어에 있는 유스케이스 클래스 중 하나에 의해 구현되고 어댑터에 의해 호출되는 인터페이스가 될 것이고, 주도되는 어댑터에게는 그러한 포트가 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 될 것이다.
설명을 붙이면,
▪️ "포트"는 애플리케이션 코어의 기능을 외부와 연결시키는 연결점이다.
▪️ "어댑터"는 애플리케이션 코어와 외부 간의 "연결" 역할을 하는 요소다.
➜ 이는 애플리케이션 코어의 비즈니스 로직이 그대로 유지될 수 있도록, 필요에 따라 변화하는 외부 요인들을 적절하게 처리한다.
▪️ "주도하는 어댑터"는 외부(예: 사용자 인터페이스 또는 자동화된 테스트)로부터 입력을 받아 애플리케이션 코어의 비즈니스 로직을 호출하는 역할을 한다. 이러한 어댑터가 호출하는 것은 "포트"를 통해 제공되는 애플리케이션 코어의 '유스케이스'다.
▪️ "주도되는 어댑터"는 애플리케이션 코어가 외부에 데이터를 전달할 필요가 있을 때 사용된다. 예를 들어, 데이터베이스 저장이나 네트워크 통신 등을 처리할 수 있다.
: 애플리케이션 코어가 어댑터와 통신하기 위해서는 적절한"포트"를 제공해야 한다. 그리고 이러한 포트는 주도하는 어댑터에게는 애플리케이션 코어에서 호출될 수 있는 인터페이스가 되고, 주도되는 어댑터에게는 애플리케이션 코어가 호출할 수 있는 인터페이스가 된다. 이렇게 함으로써 애플리케이션의 핵심 로직은 외부의 변화에 영향을 받지 않으며, 외부와의 인터페이스만을 관리하는 어댑터를 통해 필요에 따라 쉽게 변경이나 확장이 가능해진다.
그래서 이 아키텍처 스타일은 '포트와 어댑터' 아키텍처로도 알려져 있다. 클린 아키텍처처럼 육각형 아키텍처도 계층으로 구성할 수 있다.
- 가장 바깥쪽에 있는 계층은 애플리케이션과 다른 시스템 간의 번역을 담당하는 어댑터로 구성돼 있다.
- 다음 포트와 유스케이스 구현체를 결합해서 애플리케이션 계층을 구성할 수 있다.
- 마지막 계층에는 도메인 엔티티가 위치한다.
유지보수 가능한 소프트웨어를 만드는 데 어떻게 도움이 될까?
지금까지 본 아키텍처를 활용하면 의존성을 역전시켜 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 함으로써 영속성 UI에 특화된 모든 문제로부터 도메인 로직의 결합을 제거하고 코드를 변경할 이유의 수를 줄일 수 있고, 변경의 이유가 적다는 건 곧 유지보수성이 좋다는 것이다.
'만들면서 배우는 클린 아키텍처'라는 책을 토대로 정리한 포스팅입니다.
자세한 내용은 책을 참고하시기 바랍니다. 문제시 바로 삭제하겠습니다.