Clean Architecture - 의존성 역전 (Dependency Inversion Principle)
in DEV on Clean Architecture, Dip, Dependency-inversion-principle, Srp, Hexagonal
이 포스트에서는 단일 책임 원칙과 의존성 역전 원칙에 대해 알아본다.
단일 책임 원칙과 의존성 역전 원칙은 SOLID 에서 각각 S 와 D 를 담당하고 있다.
SOLID
- SRP (Single Responsibility Principle, 단일 책임 원칙)
- 한 클래스는 하나의 책임만 가져야 함
- OCP (Open/Closed Principle, 개방/폐쇄 원칙)
- 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 함
- LSP (Liskov Substitution Principle, 리스코프 치환 원칙)
- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 함
- ISP (Interface Segregation Principle, 인터페이스 분리 원칙)
- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 나음
- DIP (Dependency Inversion Principle)
- 추상화에 의존해야지 구체화에 의존하면 안됨
- 의존성 주입 (Dependency Injection) 은 위 원칙을 따르는 방법 중 하나임
목차
- 1. 단일 책임 원칙 (SRP, Single Responsibility Principle)
- 2. 의존성 역전 원칙 (DIP, Dependency Inversion Principle)
- 3. 클린 아키텍처
- 4. 육각형 아키텍처 (헥사고날 아키텍처, Hexagonal architecture)
- 정리하며…
- 참고 사이트 & 함께 보면 좋은 사이트
1. 단일 책임 원칙 (SRP, Single Responsibility Principle)
단일 책임 원칙의 일반적인 해석은 아래와 같다.
“하나의 컴포넌트는 오로지 한 가지 일만 해야 한다.”
하지만 단일 책임 원칙의 실제 정의는 아래와 같다.
“컴포넌트를 변경하는 이유는 오직 하나뿐이어야 한다.”
책임은 ‘오로지 한 가지 일을 하는 것’ 보다 ‘변경할 이유’ 로 해석되어야 한다.
컴포넌트를 변경할 이유가 오로지 한 가지라면 컴포넌트는 딱 한 가지 일만 하게 된다.
이는 아키텍처 관점으로 보면 컴포넌트를 변경할 이유가 한 가지라면 어떤 다른 이유로 소프트웨어를 변경하더라도 이 컴포넌트에 대해 전혀 신경쓸 필요가 없다는 것을 의미한다.
하지만 컴포넌트를 변경할 이유는 컴포넌트 간의 의존성을 통해 쉽게 전파된다.
많은 코드가 단일 책임 원칙을 위반하기 때문에 점점 더 변경 비용이 증가하게 된다.
2. 의존성 역전 원칙 (DIP, Dependency Inversion Principle)
계층형 아키텍처에서 계층 간 의존성은 항상 다음 계층인 아래 방향을 가리킨다.
단일 책임 원칙을 적용하면 상위 계층들이 하위 계층들에 비해 변경할 이유가 더 많다는 것을 알 수 있다.
따라서 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 변경할 때마다 도메인 계층도 변경해야 한다.
영속성 코드 변경에 따른 도메인 코드 영향도를 줄이려면 의존성을 제거해야 하는데 그 답은 바로 의존성 역전 원칙 (Dependency Inversion Principle) 을 적용하는 것이다.
의존성 역전 원칙의 의미는 아래와 같다.
“코드상의 어떤 의존성이든 그 방향을 역전시킬 수 있다.”
사실 의존성은 양쪽 코드를 모두 제어할 수 있을 때만 역전시킬 수 있다.
예) 서드파티 라이브러리에 의존 시 해당 라이브러리를 제어할 수 없기 때문에 이 의존성은 역전 불가
도메인 코드와 영속성 코드 간의 의존성을 역전시켜서 영속성 코드가 도메인 코드에 의존하게 하여 도메인 코드의 ‘변경할 이유’ 의 개수를 줄일 수 있다.
아래는 Clean Architecture - 계층형 아키텍처 문제점 에 나왔던 예시 그림이다.
도메인 계층에 서비스가 영속성 계층의 엔티티, 레파지토리와 상호 작용하고 있다.
엔티티는 도메인 객체를 표현하고 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 주임으로 하기 때문에 엔티티를 먼저 도메인 계층으로 옮긴다.
하지만 그러고나면 영속성 계층의 레파지토리가 도메인 계층에 있는 엔티티에 의존하기 때문에 두 계층 사이에 순환 의존성이 생긴다.
바로 이 부분에 DIP 를 적용한다.
도메인 계층에 리포지터리에 대한 인터페이스를 만들고, 실제 리포지터리는 영속성 계층에서 구현하는 것이다.
도메인 계층에 인터페이스를 도입하여 의존성을 역전시킴으로서 영속성 계층이 도메인 계층에 의존하게 되었다.
3. 클린 아키텍처
클린 아키텍처에서는 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 프레임워크, 데이터베이스, 그 밖의 외부 애플리케이션이나 인터페이스로부터 독립적일 수 있다.
이는 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 함을 의미한다.
대신 의존성 역전 원칙을 적용하여 모든 의존성이 도메인 코드를 향해야 한다.
위 그림을 보면 모든 의존성은 도메인 로직을 향해 안쪽으로 향한다.
위 아키텍처의 코어에는 주변 유스케이스에서 접근하는 도메인 엔티티들이 있다.
클린 아키텍처에서는 도메인 계층과 애플리케이션 계층을 합쳐 ‘애플리케이션 코어’ 라고 부르기 때문에 위에서 코어라고 지칭함
유스케이스는 Clean Architecture - 계층형 아키텍처 문제점 에서 서비스라고 불렀던 것들인데 단일 책임 (= 변경할 단 한가지의 이유)을 갖기 위해 세분화되어 있다.
코어 주변으로 비즈니스 규칙을 ‘지원’ 하는 애플리케이션의 다른 컴포넌트들이 있다.
‘지원’ 은 영속성을 제공하거나 UI 를 제공하는 것을 의미한다.
바깥쪽의 계층들은 다른 서드파티 컴포넌트에 어댑터를 제공할 수 있다.
도메인 코드에서는 어떤 영속성 프레임워크나 UI 프레임워크가 사용되는지 알 수 없기 때문에 특정 프레임워크에 특화된 코드를 가질 수 없고 비즈니스 규칙에 집중할 수 있다.
그래서 도메인 코드를 자유롭게 모델링할 수 있다.
예) 도메인 주도 설계(DDD) 를 가장 순수한 형태로 적용 가능
클린 아키텍처 적용 시 도메인 계층이 영속성이나 UI 와 같은 외부 계층과 철저하게 분리되어야 하므로 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수해야 한다.
예를 들어 영속성 계층에서 ORM 프레임워크 사용 시 ORM 프레임워크는 DB 구조 및 객체 필드, DB 컬럼의 매핑을 서술한 메타데이터를 담고 있는 엔티티 클래스를 필요로 한다.
도메인 계층은 영속성 계층을 모르기 때문에 도메인 계층에서 사용한 엔티티 클래스를 영속성 계층에서 함께 사용할 수 없고 두 계층에서 각각 엔티티를 만들어야 한다.
즉, 도메인 계층과 영속성 계층이 데이터를 주고 받을 때 두 엔티티를 서로 변환해야 한다. 이는 도메인 계층과 다른 계층들 사이에서도 동일하다.
위의 현상은 바람직한 일이다.
이를 통해 도메인 코드를 프레임워크에 특화된 문제로부터 해방시키고 결합도 제거할 수 있다.
도메인 계층과 영속성 계층의 결합을 그대로 수용하는 ‘매핑하지 않기’ 전략을 비롯한 여러 매핑 전략에 대해서는 Clean Architecture - 경계 간 매핑 전략 을 참고하세요.
4. 육각형 아키텍처 (헥사고날 아키텍처, Hexagonal architecture)
아래는 다소 추상적인 클린 아키텍처 좀 더 구체적으로 만들어주는 육각형 아키텍처이다.
육각형 아키텍처는 헥사고날 아키텍처, 혹은 포트와 어댑터 아키텍처라고도 불린다.
육각형인 이유
애플리케이션이 다른 시스템이나 어댑터와 연결되는 4개 이상의 면을 가질 수 있음을 보여주기 위해 일반적인 사각형대신 육각형을 사용했다고 함
육각형 안에는 도메인 엔티티, 그리고 엔티티와 상호 작용하는 유스케이스가 있다.
육각형 외부로 향하는 의존성이 없으므로 클린 아키텍처에서 제시한 의존성 규칙이 그대로 적용된다.
모든 의존성은 코어를 향한다.
육각형 바깥쪽에는 애플리케이션과 상호작용하는 다양한 어댑터들이 있다.
왼쪽의 어댑터들은 애플리케이션 코어를 호출하기 때문에 애플리케이션을 주도하는 어댑터들(= Driving adapter) 이다.
반면 오른쪽의 어댑터들은 애플리케이션 코어에 의해 호출되기 때문에 애플리케이션에 의해 주도되는 어댑터들(= Driven adapter) 이다.
애플리케이션 코어와 어댑터들 간의 통신이 가능하려면 애플리케이션 코어가 각각의 포트를 제공해야 한다.
- Driving adapter
- 포트가 코어에 있는 유스케이스 클래스 중 하나에 의해 구현됨
- 어댑터에 의해 호출되는 인터페이스가 됨
- Driven adapter
- 포트가 어댑터에 의해 구현됨
- 코어에 의해 호출되는 인터페이스가 됨
클린 아키텍처처럼 육각형 아키텍처도 계층으로 구성할 수 있다.
- 가장 바깥쪽 계층
- 애플리케이션과 다른 시스템 간의 번역을 담당하는 어댑터로 구성
- 포트와 유스케이스 구현체를 결합하여 애플리케이션 계층 구성
- 이 두가지가 애플리케이션의 인터페이스를 정의
- 도메인 엔티티 계층
정리하며…
의존성을 역전시켜서 도메인 코드가 다른 바깥쪽 코드에 의존하지 않게 함으로써 영속성과 UI 에 특화된 모든 문제로부터 도메인 로직의 결합을 제거하고 코드를 변경할 이유의 수를 줄일 수 있다.
변경할 이유가 적을수록 유지보수성은 더 좋아진다.
도메인 코드는 비즈니스 문제에 딱 맞도록 자유롭게 모델링될 수 있고, 영속성 코드와 UI 코드도 영속성과 UI 문제에 맞게 자유롭게 모델링될 수 있다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 톰 홈버그 저자의 만들면서 배우는 클린 아키텍처을 기반으로 스터디하며 정리한 내용들입니다.