쉽게 말하는, 의존성 역전하기

dante Yoon·2022년 5월 3일
14
post-thumbnail

서론

이전 시간에 단테는 만들면서 배우는 클린 아키텍처를 읽고 계층형 아키텍쳐 문제점에 대해 정리해보았다.
오늘은 이전 시간에 늘어놓은 불만들의 대안에 대해 생각해보기로 했다.

의존성 역전 (DIP)

의존성 역전은 SOLID 법칙의 마지막 D에 해당하는 개념입니다.

DISCLAIMER

이번 포스팅에서는 SOLID의 D와 첫글자에 해당하는 S인 단일 책임 원칙에 대해서도 이야기해볼 것입니다.

단일 책임 원칙

하나의 객체는 하나의 책임만 갖는다는 뜻을 의미합니다.

하지만 이것은 일반적인 해석이고, 자칫 원래의 깊이 숨어있는 의미를 발견하지 못하게할 수 있어 조심해야 합니다.

단일 책임 원칙의 본래 뜻은 아래와 같습니다.

컴포넌트를 변경하는 이유는 오직 하나 뿐이어야 한다.

그래서 영어로 쓰면 Single Responsibility principle인 이 원칙은 사실 단일 변경 이유 원칙(Single Reason to Change Principle)이라고 읽어야 합니다.

단일 변경 이유 원칙

컴포넌트를 변경할 이유가 한 가지라면 컴포넌트는 딱 한가지 일만 하게 된다는 뜻으로, 변경할 이유는 오직 한 가지의 경우에만 발생한다는 뜻입니다.

아키텍쳐에서 이것은 어떤 의미를 가지고 있을까요?

모종의 이유로 소프트웨어가 변경되었을 때 이 원칙이 적용된 컴포넌트에 대해서는 신경을 쓸 필요가 없습니다. 우리가 기대한 대로 동작할 것이기 때문입니다.

올, 좀 잘 그렸는데?

위 그림에서 컴포넌트 A와 E의 차이점은 무엇일까요?
A는 자기 자신을 제외한 다른 컴포넌트들에게 의존하고 있지만 E는 의존하는 컴포넌트가 아무것도 없습니다.

컴포넌트 E를 변경할 이유는 E에 대한 요구사항이 바뀌는 일 말고는 없습니다. B,D가 바뀌든 A,C가 바뀌든 E는 바뀌지 않습니다.

A는 B,C,D,E가 변경된다면 불가피하게 수정해야 합니다.

레거시 코드가 가져다 주는 편견

새로운 팀에 소속된 여러분은 기존 사람들이 퇴사하기 전에 남긴 레거시 코드를 살펴보기 시작했습니다.

먼저 이해하기 어렵고 가독성이 낮아 코드를 읽는데 애를 먹은 여러분은 어찌저찌 한 코드 뭉치를 수정했더니 예상치 못한 사이드이펙트가 우후죽순으로 생기는 모습을 바라볼 수 밖에 없었습니다.

몇일의 밤을 지새운 후 성공적으로 유지보수한 여러분에게 프로젝트 매니저는 새로운 기능을 이상한 방식으로 동작하게 해달라고 요청을 했습니다. 이에 여러분은 더욱 변경이 적은 방식을 제안하기로 했습니다. 아주 핵심적인 특정 컴포넌트를 변경하는 방법을 제시한 것입니다.
하지만 프로젝트 매니저는 이를 거절하고 비용이 더 많이드는 방식으로 구현하기를 요청했습니다.

알고보니, 당신이 제안한 핵심 컴포넌트는 이전 팀이 수정하기만 하면 버그를 양산해내는 골칫거리 컴포넌트로 프로젝트 매니저에게 각인되었던 것입니다.

좋은 소프트웨어를 만들어야 하는 이유

위의 이야기는 현실상황에서 일어날 법한 이야기입니다. 올바른 아키텍처로 변경에 따른 사이드 이펙트를 최소화 하는 것은 개발자 경험뿐만 아니라 클라이언트의 경험에도 큰 영향을 미친다는 것을 보여줍니다.

의존성 역전 원칙

계층형 아키텍처에서 계층간 의존성은 상위 계층들이 하위 계층들에 비해 변경할 이유가 더 많아지게 합니다.

이전 포스팅에서 보여드렸던 계층형 아키텍처의 형태를 다시 보여드리겠습니다.

아 이게 아니지

우리는 영속성 코드가 변경된다고 도메인 코드까지 바꾸고 싶지 않습니다.
이러한 이슈를 의존성 역전 원칙을 통해 해결할 수 있습니다.

의존성 역전 원칙은 다음과 같은 의미를 포함합니다.

코드 상의 어떤 의존성이든 그 방향을 바꿀 수(역전시킬 수)있다.

이전 포스팅에서 계층형 아키텍처에서 보여드렸던 구조에서 부터 다시 이야기를 이어가겠습니다.

도메인 계층에 영속성 계층의 엔티티와 리포지토리와 상호작용하는 서비스가 하나 있습니다.

엔티티는 도메인 객체를 표현하고 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 중심으로 합니다. 먼저 엔티티를 도메인 계층으로 올리겠습니다.

도메인 계층과 영속성 계층간에 순환 의존성이 생겼습니다.

의존성 역전 원칙이 필요한 부분이 바로 이 순환 의존성이 생기는 부분입니다!

순환 의존성이 왜 나쁜가

앞서 보여드렸던 순환 의존성은 영어로 circular dependency / cyclic dependency라고 하는데요, 도메인, 영속성 사이에 서로 의존하는 관계가 생겨버리면
사실상 계층형 아키텍처가 아니라 단일 아키텍처가 되어버립니다.

계층을 나누어 서로 맡은 역할과 책임을 분리하길 원하는 계층형 아키텍처 구조를 와해시켜버리기 때문입니다.

서로 얽히고 섥혀 있으면 동시작업하는 것도 더 어렵고 테스트를 진행하는 것도 더 어려워지겠죠?

순환 의존성을 DIP로 해결하자

어떻게 했는지 그림을 살펴보면 두 가지 과정을 거쳤습니다.
1. 도메인 계층에 리포지토리에 대한 인터페이스를 만들었습니다.
2. 실제 리포지토리는 영속성 계층에서 구현했습니다.

도메인 계층에 대한 인터페이스를 도입함으로써 의존성을 역전시킬 수 있고, 그 덕분에 영속성 계층이 도메인 계층에 의존하게 됩니다.

원래는 도메인 계층의 서비스가 리포지토리를 의존했다면, 리포지토리 인터페이스를 도메인 계층에 위치시킴으로 영속성 계층이 도메인 계층을 의존하게 바꾼 것입니다.

어렵죠? 저도 익숙해지려면 시간이 좀 걸릴 것 같습니다.

클린 아키텍처

로버트 C 마틴이라는 클린 아키텍처라는 책의 저자가 있습니다. 그는 저서 클린 아키텍처에서 설계가 비즈니스 규칙의 테스트를 용이하게 하고, 비즈니스 규칙은 아래 항목으로 부터 독립적으로 할 수 있게 한다고 했습니다.

  • 프레임워크
  • 데이터베이스
  • UI
  • 외부 어플리케이션
  • 인터페이스

위와 같은 설계를 위해서는 도메인코드가 외부 계층으로 향하는 어떤 의존성도 없어야 하며 의존성이 필요하다면 의존성 역전 원칙의 도움으로 모든 의존성을 받는 방향이 되어야 한다고 했습니다.

클린 아키텍처

이 아키텍처에서 각 계층들은 동심원으로 둘러싸여 있습니다. 이 동심원들이 말하고자 하는 것은 모든 의존성은 내부를 가르켜야 한다는 것입니다.

여기서 가장 가운데 있는 노른자는 비즈니스 규칙에 해당하는 도메인 엔티티가 있습니다. 용어를 잘 구분해보도록 합시다.

그리고 유스케이스는 서비스라고 불리는 것으로 단일 책임을 갖고 있습니다. (앞에서 알아보았던 단일 책임의 원칙 기억하시죠?)

그리고 그 외부에는 영속성을 제공하거나 UI를 지원하는 등 비즈니스 규칙을 지원하는 애플리케이션의 다른 컴포넌트들이 있습니다.

각 동심원의 계층을 중심으로 구분해봅시다.

도메인 코드에서는 영속성 프레임워크나 UI프레임워크에 대한 정보를 가지고 있지 않습니다.

따라서 프레임워크에 대해 자유로워 도메인 코드를 자유롭게 모델링 할 수 있습니다.
적절한 비유일지 모르겠지만, 인간이 수천년간 땅 위에서 도시를 건설하고 상하수도를 만들고 도로를 포장해도 지구 내핵의 모습은 변화하지 않았습니다.

트레이드 오프

모든 설계에는 이점과 단점이 짝궁처럼 따라 붙습니다.

오타인줄 알았지? 어림없지.

도메인 계층이 영속성이나 UI같은 외부 계층과 분리되므로 인해 애플리케이션 엔티티에 대한 유지보수는 각 계층에서 알아서 해야 합니다.

영속성 계층에서 ORM 프레임워크를 사용한다고 할 때, ORM 프레임워크는 객체 필드와 데이터베이스 칼럼의 매핑을 서술한 메타데이터를 담고 있는 엔티티 클래스를 필요로 합니다.

도메인 계층은 영속성 계층에 대한 정보가 없기 때문에 도메인 계층에서 사용한 엔티티 클래스를 영속성 계층에서 함께 사용할 수 없고 두 계층에서 각각 엔티티를 만들어야 합니다.

우리가 서버와 클라이언트 간의 데이터를 전달할 때 언어가 다르더라도 json이라는 형식으로 통일하여 전달하듯이, 두 계층 간에 데이터를 주고 받을 때 두 엔티티를 서로 변환해야 합니다.

육각형 아키텍처

영어로 읽으면 hexagonal-architecture라고 하는 어마무시한 단어가 됩니다. 걱정하지 마세요! 저도 잘 모르니깐요! 우리 같이 한번 찬찬히 들여다 봅시다.

이 용어는 Alistair Cockburn 이라는 분이 만드셨는데요, 해당 용어에 대해 블로그에 정리해두신 글이 있다고 하여 방문해보았습니다.

괜찮은데..? 나도 나중에..?

https://reflectoring.io/spring-hexagonal/

그림을 보면 애플리케이션 코어(도메인 계층 + 애플리케이션 계층)가 육각형으로 되어있습니다. 육각형인 이유는 다른 시스템이나 어댑터와 연결되는 4개 이상의 면을 가질 수 있다는 부분을 보여주기 위함이었다고 합니다.

가장 내부에 있는 엔티티가 도메인 엔티티인데요, 육각형에서 외부로 향하는 의존성이 없기 때문에 클린 아키텍처에서 말하는 의존성 규칙이 그대로 적용된 것을 볼 수 있습니다.

어댑터

육각형 바깥에는 애플리케이션과 상호작용하는 어댑터들이 있습니다. 웹 브라우저와 상호작용하는 웹 어댑터도 보이는군요. 외부 시스템과 상호작용하는 어댑터도 있고 데이터베이스와 상호작용하는 어댑터도 있습니다.

주도하는 어댑터 driving adapter

왼쪽의 검은색 화살표로 input port를 가르키는 어댑터들은 주도하는 어댑터라고 합니다. 애플리케이션 코어를 호출하기 때문에 주도하는 어댑터라고 부릅니다.

주도되는 어댑터 driven adapter

반대로 오른쪽에서 하얀색 화살표로 output port를 가르키는 어댑터들은 애플리케이션 코어에 의해 호출되기 때문에 주도되는 어댑터라고 부릅니다.

포트

애플리케이션 코어와 어댑터들 간에 통신이 가능하기 위해서는 애플리케이션 코어가 포트를 제공해주어야 합니다.

주도하는 어댑터에게는 그러한 포트가 코어에 있는 유스케이스 클래스에 의해 구현되고 호출되는 인터페이스가 될 것이며,
주도되는 어댑터에게는 그러한 포트가 어댑터에 의해 구현되고 코어에 의해 호출되는 인터페이스가 됩니다.

결국 여기서 하얀색 화살표는 구현하고 제공해주는 관계를 의미합니다.
검은색 화살표는 가져다가 사용해서 호출하는 것이라고 이해하면 편할 것 같습니다.

이러한 아키텍처 스타일은 포트와 어댑터(ports-and-adapters) 아키텍처로도 알려져 있습니다.

육각형 아키텍처의 장점

앞서 보았던 클린 아키텍처나 육각형 아키텍처 모두 의존성을 역전시켜 도메인 코드가 바깥쪽 코드를 의존하지 않게 함으로써 영속성과 UI에 특화된 모든 문제로부터 도메인 로직의 결합을 제거하고 코드를 변경할 이유의 수를 줄일 수 있게 했습니다.

글의 앞 부분에서 다뤘던 단일 책임의 원칙 기억하시죠?

이 글에서 계속 이야기했던 아키텍처는 결국 변경할 이유를 줄이고 유지보수성을 높이기 위한 소프트웨어를 만들기 위함이었으며 이 모든 것은 단일 책임의 원칙을 구현하기 위한 방법이었습니다.

또한 도메인 코드는 외부 요인으로 부터 자유롭게 모델링 될 수 있기에 비즈니스 문제에 딱 맞게 모델링 될 수 있고 영속성 코드, UI 코드도 각 문제에 맞게 자유롭게 모델링 될 수 있습니다.

이번 시간에는..

  • 단일 책임의 원칙
  • 이를 충족시키기 위한 의존성 역전 원칙
  • 의존성 역전 원칙을 이용한 클린 아키텍처와
  • 육각형 아키텍처의 장점 및 단점
    에 대해 알아보았습니다.

도움이 되셨다면 좋아요 및 댓글을 남겨주시면 글 작성에 큰 동기부여가 될 것 같습니다.

긴 글 읽어주셔 감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글