ASP.NET Core로 구현하는 클린 아키텍쳐 (1)

아키텍쳐를 두고 고민하는 것이 비단 아키텍트만의 고민은 아닐 것이다. 모든 개발자들이 자신의 커리어를 쌓으면서 계속 마주하게 되는 문제중의 하나가 좋은 아키텍쳐에 대한 목마름이다. 운이 좋게도 나는 3년 전, 런던에 소재한 핀테크 스타트업에서 일하던 중, 개인적으로 가장 이상적이라고 할 수 있는 아키텍쳐를 만났다. 금융 거래를 위한 새로운 플랫폼을 구축하면서 모든 구성원들이 아키텍쳐를 고민했고 최종적으로 선택한 아키텍쳐는 엉클 밥의 클린 아키텍쳐에 기반한 제이슨 타일러의 해석이었다.

클린 아키텍쳐

클린 아키텍쳐를 포함한 근래의 아키텍쳐(헥사고날 아키텍쳐, 오니온 아키텍쳐등)들이 지향해온 핵심은 관심사의 분리(seperation of concerns)라고 할 수 있다. 아키텍쳐 관점에서 그 것은 전체 소프트웨어를 다수의 레이어로 분리해 구현하는 것이다. 이들 레이어중 최소한 하나는 비지니스 로직을 담아야 하고 또 하나는 인터페이스를 정의한다. 이 아키텍쳐들을 적용하여 제작한 시스템은 다음의 특성을 갖는다.

  • 프레임워크에 독립적이다 - 특정 개발 라이브러리에 종속성이지 않다는 의미다.
  • 테스트 가능하다 - UI, DB, 웹서버등을 사용하지 않고 테스트할 수 있다.
  • UI에 독립적이다 - 웹 UI가 콘솔 앱으로 대치될 수 있다, UI 변경이 다른 레이어의 변경을 초래하지 않는다.
  • DB에 독립적이다 - 비지니스 로직이 데이터베이스에 종속적이지 않다. 최소한의 노력으로 Oracle에서 SQL Server로, RDB에서 NoSQL로 변경할 수 있다.
  • 외부 시스템에 독립적이다 - 외부 시스템의 동작은 인터페이스로 정의한다. 외부 시스템의 변경은 구현 상세에 대한 변경일뿐, 메인 시스템에 영향을 미치지 않는다.

위 사항들을 고려하면서 아래의 다이어그램을 보자. 클린 아키텍쳐를 표방하는 계층(레이어) 구조로써 양파같은 구조를 하고 있다. 동심원간에 안쪽으로 향하는 검정색 화살표는 계층간의 의존성을 표현하는데 외부 레이어는 그와 인접한 내부 레이어만 인식하고 종속성을 갖는다.

엉클밥 블로그

종속성의 방향성

다이어그램에서 각 동심원은 소프트웨어의 영역을 표현하는데 바깥쪽은 일종의 동작방식(mechanism)을, 안쪽은 정책(policy)이다. 바깥쪽 레이어는 안쪽 레이어에 종속성을 갖는다. 즉, 안쪽에서 정의된 클래스, 인터페이스는 바깥쪽에서 사용할 수 있지만, 안쪽에서 바깥쪽을 볼 수는 없다.

메일 서비스를 예로 들면, 안쪽(Use Cases)에서는 IMailService 인터페이스에 SendMail() 라는 메서드를 정의한 후, 메일 전송이 필요한 곳에서 호출한다 (code to interface, not implementation). 소프트웨어가 동작할 때는, 바깥쪽(External Interface)에서 IMailService 인터페이스를 구현한 서비스가 종속성 주입방식으로 안쪽으로 전달되고 해당 인스턴스는 실제로 메일을 보낸다.

소스의 종속성이 바깥에서 안쪽으로 흐른다는 것이 핵심 개념이다. 정책은 안쪽에서 결정하고 바깥쪽에서는 이를 구현한다.

엔티티

엔티티는 엔터프라이즈 레벨에서 데이터 구조와 펑션을 캡슐화한다. 그 대상이 무엇이든 상관없이 다른 애플리케이션에 공유되는 것이 엔티티다. 기업내에서 비즈니스 도메인을 엔티티라고 할 수 있고, 주문 시스템, 판매 시스템등을 애플이케이션이라고 할 수 있다.

만약, 엔터프라이즈 규모가 아닌 단일 애플리케이션이라면 엔티티는 애플리케이션의 비즈니스 객체다. 일반적이고 고수준(상세구현의 반대)의 비즈니스 로직을 담으며 외부 변화에 가장 영향을 덜 받는다. UI에서 리스트를 페이징하거나 보안을 강화한다고 해서 엔티티를 변경하지는 않는다.

유스케이스

엔티티가 애플리케이션간에 공유할 수 있는 내용을 정의한다면, 유스케이스 레이어는 해당 애플리케이션이 제공해야 할 유스케이스를 구현한다. 주문 또는 판매 애플리케이션을 예로 들었듯이 그 역할에 맞는 기능을 구현하기 때문에 애플리케이션 레이어라고도 부른다. 이 레이어의 변경 내용으로 엔티티 레이어에 영향을 미치지 않고, 또한 외부 레이어의 변경에 영향을 받지 않는다. 관심사의 분리가 제대로 되어야 하는 것이다.

인터페이스 어댑터

인터페이스 어댑터를 객체에 비유하면 DTO(Data Transfer Object)와 같다. 바깥쪽에 있는 데이터베이스 또는 웹에 특화된 데이터 구조(모델)를 유스케이스와 엔티티 레이어에서 사용하는 데이터 구조와 매핑하는 역할을 한다. GUI에서 MVC(Model, View, Controller) 패턴과 유사한 일이 이 레이어에서 일어난다.

데이터 관리에 SQL 데이터베이스를 사용한다면 인터페이스 어댑터 레이어와 그 바깥쪽에는 SQL 데이터베이스가 인식하는 모델을 사용하지만 안쪽 레이어서는 엔티티 모델을 사용하기 때문에 외부 SQL 데이터베이스와 무관하게 동작할 수 있는 해 주는것이 이 레이어의 역할이다.

프레임워크 & 드라이버

가장 외곽에 있는 레이어이며 일반적으로 프레임워크, 툴링과 관련있다. 웹 프레임워크, 데이터베이스등을 구현상세(implemention detail)라고 한다. 구현상세에 대해 부연설명을 한다면,

  • UI를 제공한다는 비즈니스 요구사항이 있는데, 웹 또는 모바일로 구현할 수 있다. 이때, 웹 또는 모바일은 구현상세다.
  • 데이터를 영구적으로 저장하고 관리한다는 요구사항이 있는데, RDB 또는 NoSQL을 사용할 수 있다. 그래서, 데이터베이스 자체는 구현 상세다.

구현 상세는 하나가 다른 것을 대치할 수 있다는 것을 의미하면서 각각의 구현이 다를 수도 있다는 것을 암시한다. 전체 소프트웨어의 관점에서 보면 상세한 구현 내용은 비즈니스 요구사항을 만족시키기 위한 요소지만 그 상세함이 애플리케이션을 결정하지는 않는다.

레이어의 경계를 넘어서

다이어그램의 우하단에 인터페이스 어댑터 레이어와 유스케이스 레이어가 어떻게 커뮤니케이션 하는지 정리되어 있다. 분홍색의 곡선 화살표는 컨트롤의 흐름을 표현하고, 직선 화살표는 소스의 종속성을 표현한다. 컨트롤러가 유스케이스 레이어에 정의되어 있는 Use Case Input Port를 사용하거나, Use Case Output Port를 구현하는 것으로 소스에 대한 종속성은 안쪽 방향으로 흐른다.

반면에, 컨트롤 흐름을 보면 유스케이스에서 프레젠터를 호출하는 구간이 있고, 이때 종속성의 방향과 컨트롤의 방향이 반대가 된다. 이 문제를 해결하기 위해 Dependency Inversion Principal을 사용한다. 즉, 유스케이스에서는 Use Case Output Port 인터페이스에 정의한 어떤 메서드를 호출하겠지만, 결과적으로는 그 인터페이스를 구현하는 Prensenter의 메서드가 수행되는 것이다.

어떤 것들이 레이어의 경계를 넘는가?

엔티티, 데이터베이스 테이블의 구조같은 것들이 경계를 넘나드는 것은 이 아키텍쳐를 속이는 일이다. 경계를 넘는 것들은 DTO처럼 아주 가벼운 객체들이어야 한다. 간단하고 분리되어 있는 데이터 컨테이너 말이다. 또 다른 예로는, 함수 호출에 사용하는 파라미터들이 많다면 그 것을 묶어서 해시맵에 넣거나, 클래스로 정의하여 사용할 수도 있겠다.

데이터베이스 쿼리가 반환하는 구조를 그대로 안쪽으로 넘기면 유스케이스를 구현하는게 한결 수월할 것이다. 하지만, 이는 클린 아키텍쳐의 종속성 원칙을 위배하는 일이다. 안쪽에서 바깥쪽의 구조를 참조해야 하기 때문이다. 경계를 넘는 객체는 그 방향도 지켜야 한다.

중간정리

소프트웨어를 레이어로 분리하면서 종속성 원칙을 지킨다면 독립적이고 테스트 가능한 시스템을 구축할 수 있다. 이 간단한 규칙을 지키는 것은 결코 어려운 일이 아니며 앞으로 있을 많은 고민거리를 덜어준다. 데이터베이스, 웹 프레임워크등 시스템의 외부환경이 변경되거나 또는 그 생명을 다하면 최소한의 노력으로 그것들을 교체할 수 있다.

다음 꼭지에서는 이 아키텍쳐에 기반하여 ASP.NET Core 솔루션을 구성하고 도메인을 시작으로 구현해 볼 것이다.

참고자료