우아한테크코스 테코톡

만들면서 배우는 클린 아키텍처

만들면서 배우는 클린 아키텍처

계층형 아키텍처의 문제는 무엇일까?

  • 웹 -> 도메인 -> 영속성
  • 웹 계층에서는 요처을 받아 도메인 혹은 비즈니스 계층에 있는 서비스로 요청을 보낸다
  • 서비스에서는 필요한 비즈니스 로직을 수행하고, 도메인 엔티티의 현재 상태를 조회하거나 변경하기 위해 영속성 계층 컴포넌트를 호출한다
  • 사실 계형 아키텍처는 견고 아키텍처 패턴이다
  • 계층을 이해하고 구성한다면 웹 계층이나 영속성 계층에 독립적으로 도메인 로직을 작성할 수 있다
  • 원한다면 도메인 로직에 향을 주지 않고 웹 계층과 영속성 계층에 사용된 기술을 변경할 수 있다. 기존 기능에 영향을 주지 않고 새로운 기능을 추가할 수도 있다
  • 잘 만들어진 계층형 아키텍처는 선택폭을 넓히고, 변화하는 요구사항과 외부 요인에 빠르게 적응할 수 있게 해준다.
  • 계층 아키텍처는 코드에 나쁜 습관들이 스며들기 쉽게 만들고 시간이 지날수록 소프웨어를 점점 더 변경하기 어렵게 만는 수많은 허점들을 노출한다.

계층형 아키텍처는 데이터베이스 주도 설계를 유도한다

  • 정의에 따르면 전통적인 계층형 아키텍처의 토대는 데이터베이스다.
  • 웹 계층은 도메인 계층에 의존하고, 도메인 계층은 영속성 계층에 의존하기 때문에 자연 스레 데이터베이스에 의존하게 된다.
  • 모든 것이 영속성 계층을 토대로 만들어진다. 이런 방식은 다양한 이유로 문제를 초래 한다
  • 우리는 보통 비즈니스를 관장는 규칙이나 정책을 반영 모델을 만들어서 사용자가 이러한 규칙과 정책을 편리하게 활용할 수 있게한다
  • 우리는 상태가 아니라 행동을 중심으로 모델링한다 어떤 애플리케이션든 상태가 중요한 요소이긴 하지만 행동이 상태를 바꾸는 주체이기 때문에 행동이 비즈니스를 이끌어간다
  • 데이터베이스를 토대로 아키텍처를 만드는 이유는 아마 데이터베이스의 구조를 먼저 생하고 이를 토대로 도메인 로직을 구현해서 이다
    • 전적인 계층형 아키텍에서는 합리적인 방법
    • 비즈니스 관점에서는 전혀 맞지 않다
  • 다른 무엇보다도 도메인 로직을 먼저 만들어야지 우리가 로직을 제대로 이해했는지 확인할 수 있다 그리고 도메인 로직이 맞다는 것을 확인한 후에 이를 기반으로 영속성 계층과 웹 계층을 만들어야한다

ORM

  • 데이터베이스 중심적인 아키텍처가 만들어지는 가장 큰 원인은 ORM 프레임워크를 사용하기 때문이다 ORM 프레임워크를 계층형 아키텍처와 결합하면 비즈니스 규칙을 영속성 관점과 섞고 싶은 유혹을 쉽게 받는다
  • ORM에 관리되는 엔티티들은 일반적으로 영속성 계층을 둔다. 계층은 아래 방향으로만 접근이 가능하기 때문에 도메인 계층에서는 엔티티에 접근할 수 있다 그리고 엔티티에 접근할 수 있다면 분명 사용되기 마련이다
  • 이렇게 되면 영속성 계층과 도메인 계층 사이에 강한 결합이 생긴다 서비스는 영속성 모델을 비즈니스 모델처럼 사용하게 되고 이로 인해 도메인 로직 뿐만 아니라 즉시로딩 지연로딩 데이터베이스 트랜잭셔, 캐시 플러시 등등 영속성 계층과 관련 작업들을 해야한다
  • 영속성 코드가 사실상 도메인 코드에 녹아들어가서 둘 중 하나만 바꾸는 것이 어려워 진다
    • 이는 유연하고 선택의 폭을 넓혀준다던 계층형 아키텍처의 목표와 정확히 반대되는 상황이다.

지름길을 택하기 쉬워진다

  • 전통적인 계층형 아키텍처에서 전체적으로 적용된는 유일한 규칙은, 특정한 계층에서는 같은 계층에 있는 컴포넌트나 아래에 있는 계층에만 접근 가능하다는 것이다
    • 해당 규칙 외 다른 규칙을 강제하지 않는다
  • 만약 상위 계층에 위치한 컴포넌트에 접근해야 한다면 간단하게 컴포넌트를 계층 아래로 내려버리면 된다
    • 딱 한번 이렇게 하는 것은 괜을 수 있다.
    • 처음이 힘들지 그다음 부터는 죄책감이 훨씬 덜하다
  • 영속성 계층은 컴포넌트를 아래 계층으로 내릴수록 비대해진다. 어떤 계층에 속하지 않는 것처럼 보이는 헬퍼 컴포넌트나 유틸리티 컴포넌트들이 아래 계층으로 내릴 가능성이 큰 후보이다
  • 아키텍처 지름길 모드를 끄고싶다면 해당 규칙이 깨졋을 때 빌드가 실패하도록 강제 해야 한다

테스트하기 어려워진다

  • 계층형 아키텍처를 사용할 때 일반적으로 나타나는 변화의 형태는 계층을 건너 뛰는 것이다
    • 바로 웹 계층에서 영속성 계층으로 접근 하는것을 의미
  • 유즈케이스가 확장된다면 더 많은 도메인 로 웹 계층에 추가해서 애플리케이션 전반에 걸쳐 책임이 섞이고 핵심 도메인 로직들이 퍼저나갈 확률이 높다
  • 웹 계층 테스트에서 도메인 계층뿐만 아니라 영속성 계층도 목킹해야 한다는 것이다 이렇게 되면 단위 테스트의 복잡도가 올라간다. 테스트 설정이 복잡해지는 것은 테스트를 전혀 작성하지 않는 방향으로 가는 첫걸이다. 왜냐하면 복잡할 설정을 할 시간이 없기 때문이다
  • 웹 컴포넌트의 규모가 커지면 다양한 영속성 컴포넌트에 의존성이 많이 쌓이면서 테스트 복잡도를 높인다. 어느 순간에는 실제로 테스트 코드를 작성하는 것보다 종속성을 이해하고 목을 만드는데 더많은 시간이 걸리게 된다

유스케이스를 숨긴다

  • 개발자들은 새로운 유스케이스를 구현하는 새로운 코드를 짜는 것을 선호한다 실제로는 새로운 코드를 짜는 데 시간을 쓰기보다는 기존 코드를 바꾸는 데 더 많은 시간을 쓴다
    • 신규 프로젝트에서도 마찬가지다
  • 계층형 아키텍처에서는 도메인 로직이 여러 계층에 걸쳐 흩어지기 쉽다
    • 유스케이스가 간단해서 도메인 계층을 생략한다면 웹 계층에 존재할 수도 있고 메인 계층과 영속성 계층 모두에서 접근할 수도 있도록 특정 컴포넌트를 아래로 내렸다면 영속성 계층에 존재할 수도 있다
    • 이럴 경우 새로운 기능을 추가할 적당한 위치를 찾는일은 이미 어렵다
  • 계층형 아키텍처는 너비에 관한 규칙을 강제하지 않는다. 그렇게 대문에 시간이 지나면 여러개의 유스케이스를 담당하는 아주 넓은 서비스가 만들어지기도 한다
    • 넓은 서비스는 영속성 계층에 많은 의존성을 갖게 되고, 다시 웹 레이어의 많은 컴포넌트가 이 서비스에 의존하게 된다.
    • 그럼 서비스를 테스트하기도 어려워지고 작업해야 할 유스케이스를 책임지는 서비스를 찾기도 어려워진다.

동시 작업이 어려워진다

  • 지연되는 소프트웨어 프로젝트에 인력을 더하는 것은 개발을 늦출 뿐이다.
  • 지연되지 않은 소프트웨어 프로젝트에 대해서도 어느 정도 적용되는 사실이다. 모든 상황에서 50명 정도 되는 큰 규모의 개발팀이 10명 정도 되는 작은 규모의 개발 팀보다 5배 빠를 거라고 기대할 수는 없다.
  • 여러 하위 팀으로 쪼개서 각기 분리된 파트를 개발할 수 있는 아주 규모가 큰 애플리케이션을 만들고 있다면 그럴 수도 있겠지만 대부 분의 경우에는 서로 도움을 주고받으며 개발해야 한다.
  • 하지만 적절한 규모에서는 프로젝트에 인원이 더 투입될 경우 확실히 더 빨라진다고 기대할 수 있다.
    • 이러한 기대를 충족시키려면 아키텍처가 동시 작업을 지원해야 하지만 이렇게 하기란 쉽지는 않다. 그리고 계층형 아키텍처는 이런 측면에서는 그다지 도움이 되지 않는다.
  • 계층형 아키텍처에서는 모든 것이 영속성 계층 위에 만들어지기 때문에 영속성 계층을 먼저 개발해야 하고, 그다음에 도메인 계층을, 그리고 마지막으로 웹 계층을 만들어야 한다. 그렇기 때문에 특정 기능은 동시에 한 명의 개발자만 작업할 수 있다.
  • 코드에 넓은 서비스가 있다면 서로 다른 기능을 동시에 작업하기가 더욱 어렵다. 서로 다른 유스케이스에 대한 작업을 하게 되면 같은 서비스를 동시에 편집하는 상황이 발생하고, 이는 병합 충돌(merge conflict)과 잠재적으로 이전 코드로 되돌려야 하는 문제를 야기하기 때문이다.

유지보수 가능한 소프트웨어를 만드는데 어떻게 도움이 될까?

  • 계층형 아키텍처로 만들든 다른 아키텍처 스타일로 만들든, 계층형 아키텍처의 함정을 염두에 두면 지름길을 택하지 않고 유지보수하기에 더 쉬운 솔루션을 만드는 데 도움이 될 것이다.

의존성 역전하기

  • 먼저 단일 책임 원칙(Single Responsibility Principle, SRP)과 의존성 역전 원칙(Dependency Inversion Principle, DIP)에 대해 이야기하는 것으로 시작. 두 원칙은 SOLID 원칙에서 각각 S’와 D’를 담당하고 있다.

단일 책임의 원칙

  • 한가지 컴포넌트는 오로지 한가지 일을 해야하고 그것을 올바르게 수행해야 한다 좋은 조언이지만 단일 책임 원칙의 실제 의도는 아니다
  • 단일 책임 원칙의 실제 정의는, 컴포넌트를 변경하는 이유는 오직 하나뿐이어야 한다

의존성 역전 원칙

  • 계층형 아키텍처에서 계층 간 의존성은 항상 다음 계층인 아래 방향을 가리킨다.
  • 단일 책임 원칙을 고수준에서 적용할 때 상위 계층들이 하위 계층들에 비해 변경할 이유가 더 많다는 것을 알 수 있다.
    • 그러므로 영속성 계층에 대한 도메인 계층의 의존성 때문에 영속성 계층을 변경할 때마 다 잠재적으로 도메인 계층도 변경해야 한다.
    • 그러나 도메인 코드는 애플리케이션에서 가장 중요한 코드다. 영속성 코드가 바뀐다고 해서 도메인 코드까지 바꾸고 싶지는 않다.
  • 단일 책임 원칙과 달리 의존성 역전 윈칙은 이름 그대로를 의미한다 코드상의 어떤 의존성이든 그 방향을 바꿀 수 (역선시킬 수)있다
  • 사실 의존성의 양쪽 코드를 모두 제어할 수 있을 때만 의존성을 역전시킬 수 있다. 만약 서드파티 라이브러리에 의존성이 있다면 해당 라이브러리를 제어할 수 없기 때문에 이 의존성은 역전시킬 수 없다.
  • 엔티티는 도메인 객체를 표현하고 도메인 코드는 이 엔티티들의 상태를 변경하는 일을 중심으로 하기 때문에 먼저 엔티티를 도메인 계층으로 올린다.
  • 이제는 영속성 계층의 리포지토리가 도메인 계층에 있는 엔티티에 의존하기 때문 에 두 계층 사이에 순환 의존성(circular dependency)이 생긴다. 이 부분이 바로 DIP를 적용하는 부분이다. 도메인 계층에 리포지토리에 대한 인터페이스를 만들고, 실제 리포지토리는 영속성 계층에서 구현하게 하는 것이다.

클린 아키텍처

  • 이 아기텍처에서 가장 주요한 규칙은 의존성 규칙으로, 계층 간의 모든 의존성이 안쪽으로 향해야 한다는 것이다.
  • 이 아키텍처의 코어(core)’에는 주변 유스케이스에서 접근하는 도메인 엔티티들이 있다. 유스케이스는 앞에서 서비스라고 불렀던 것들인데, 단일 책임(즉, 변경할 단 한 가지의 이유)을 갖기 위해 조금 더 세분화돼 있다. 이를 통해 이전에 이야기했던 넓은 서비스 문제를 피할 수 있다.
  • 코어 주변으로 비즈니스 규칙을 지원하는 애플리케이션의 다른 모든 컴포넌트들을 확인할 수 있다. 여기서’지원’은 영속성을 제공하거나 UI를 제공하는 것 등을 의미한다. 또 한 바깥쪽 계층들은 다른 서드파티 컴포넌트에 어댑터를 제공할 수 있다.
  • 클린 아키텍처에는 대가가 따른다. 도메인 계층이 영속성이나 UI같은 외부 계층과 철저하게 분리돼야 하므로 애플리케이션의 엔티티에 대한 모델을 각 계층에서 유지보수해야 한다.
  • 가령 영속성 계층에서 ORM(object-relational mapping, 객체-관계 매핑) 프레임워크를 사용한다고 해보자. 일반적으로 ORM 프레임워크는 데이터베이스 구조 및 객체 필드와 데이터베이스 칼럼의 매핑을 서술한 메타데이터를 담고 있는 엔티티 클래스를 필요로 한다. 도메인 계층은 영속성 계층을 모르기 때문에 도메인 계층에서 사용한 엔티티 클래스를 영속성 계층에서 함께 사용할 수 없고 두 계층에서 각각 엔티티를 만들어야 한다.

© 2020. All rights reserved.

SIKSIK