본문 바로가기
Engineering/SW Architecture

클린 아키텍쳐 (4) - 아키텍처 [소리치는 아키텍처, 클린 아키텍처, 프레젠터와 험블 객체]

by 쿨쥰 2023. 3. 22.

이전 글 : 아키텍처 [독립성 / 선긋기 / 경계 해부학 / 정책과 수준/ 업무 규칙]

2023.03.19 - [Engineering/SW Architecture] - 클린 아키텍쳐 (3) - 아키텍처 [독립성 / 선긋기 / 경계 해부학 / 정책과 수준/ 업무 규칙]

 

클린 아키텍쳐 (3) - 아키텍처 [독립성 / 선긋기 / 경계 해부학 / 정책과 수준/ 업무 규칙]

이전 글 : 컴포넌트 설계 원칙 2023.03.16 - [Engineering/SW Architecture] - 클린 아키텍쳐 (2) - 컴포넌트 원칙 클린 아키텍쳐 (2) - 컴포넌트 원칙 이전 글 : SW개발 패러다임과 기본 설계 원칙 2023.03.12 - [Engine

skidrow6122.tistory.com

 

 


 

< 소리치는 아키텍처 >

주택의 설계도면을 보고있다고 가정한다면, 이 주택이 집인지 빌딩 내 사무실인지 용도를 알 수 있다.

즉, 이 아키텍처는 집이야 라고 소리치고 있을 것이다.

우리의 아키텍처는 상위 수준의 디렉터리 구조, 최상위 패키지의 소스 파일을 참조했을때 뭐라고 소리치는가? 나는 헬스케어 시스템이야!! 인가? 나는 스프링이야!! 또는 하이버네이트야!! 라고 소리치는가?

아키텍처의 테마

이바 야콥슨의 유스케이스 주도 접근법 이라는 개념을 주목해보자.

이 책에서 야콥슨은 SW아키텍처는 시스템의 유스케이스를 지원하는 구조라고 지적했다.

주택이나 빌딩의 설계도면이 해당 건축물의 유스케이스에 대해 소리치는 것처럼, SW 어플리케이션의 아키텍처도 어플리케이션의 유스케이스에 대해 소리쳐야 한다.

아키텍처는 프레임워크에 대한 것이 아니다.

아키텍처를 프레임워크 중심으로 만들어 버리면 가장 핵심적 요소인 유스케이스가 중심이 되는 아키텍처가 절대로 나올 수 없다.

아키텍처의 목적

좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크, 도구, 환경에 전혀 구애받지 않고 유스케이스를 지원하는 구조를 아무런 문제 없이 기술 할 수 있다.

설계 도면에서도 첫번째 관심사는 바로 이 주택이 거주하기에 적합한 공간임을 확실히 하는것이지, 이 건축물이 벽돌로 지어지는지 콘크리트로 지어지는지를 확인 하는 것이 아니다.

다시 한번 상기하지만, 좋은 아키텍처는 프레임워크, DB, 웹 서버, 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만든다.

좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합은 분리 시킨다.

웹은 어떠한가? 당연히 웹은 아키텍처가 아니라 웹은 전달 메커니즘이며, 어플리케이션 아키텍처에서도 그와 같이 다루어야 한다.

어플리케이션이 웹을 통해 전달 된다는 사실은 세부사항이며, 시스템 구조를 지배해서는 안된다.

즉, 시스템 아키텍처는 시스템이 어떻게 전달될지에 대해 가능하다면 아무것도 몰라야 하므로 미루어야 할 결정사항 중 하나이다.

프레임워크는 도구일 뿐, 삶의 방식이 아니다.

프레임워크는 매우 강력하고 상당히 유용할 수 있다는 점은 사실이다.

실제 프레임워크 제작자는 그들의 프레임워크가 모든것을 아우르는, 어디에나 스며드는, 이른바 “프레임워크가 모든 것을 같게 하자” 라는 태도를 취하게 하지만, 이는 분명히 우리가 취하고 싶은 태도가 아니다.

프레임워크가 아키텍처의 중심을 차지하는 일을 막을 수 있는 전략을 개발하라.

테스트하기 쉬운 아키텍처

아키텍처가 유스케이스를 최우선으로 한다면, 그리고 프레임워크와는 적당한 거리를 둔다면, 프레임워크를 전혀 준비하지 않더라도 필요한 유스케이스 전부에 대해 단위 테스트를 할 수 있어야한다.

즉, 테스트를 돌리는데 웹서버가 반드시 필요한 상황이 되어서도 안되며, DB가 연결되어 있어야만 테스트를 돌릴 수 있어서도 안된다.

엔티티 객체는 반드시 오래된 방식의 간단한 객체여야하며, 프레임워크나 DB 등 여타 복잡한 사항들에게 의존해서는 안된다.

즉, 유스케이스 객체가 엔터티 객체를 조작해야 한다는 뜻이다.

아키텍처는 시스템을 이야기해야 하며, 시스템에 적용한 프레임워크에 대해 이야기 해서는 안된다.

새로온 개발자가 아키텍처를 본 후 “모델처럼 보이는 것들은 확인했는데 뷰와 컨트롤러는 어딨나요?” 라고 물어본다면 아래와 같이 답하자.

“아, 그것은 세부사항이므로 당장은 고려할 필요 없습니다. 나중에 결정할 거에요.”

 

 

 

< 클린 아키텍처 >

수십년간 아키텍처 관점에서 나온 여러가지 아이디어를 보면, Hexagonal 아키텍쳐, Boundary-control-Entity 등 다양한 관점이 소개되었지만, 공통점은 바로 관심사의 분리이다.

소프트웨어를 계층으로 분리함으로써 관심사의 분리라는 목표를 달성할 수 있다. 각 아키텍처는 최소한 업무규칙을 위한 계층 하나와 사용자와 시스템 인터페이스를 위한 또 다른 계층 하나를 반드시 포함한다. 이들 아키텍처들은 모두 시스템이 다음과 같은 특징을 가지도록 한다.

  • 프레임워크 독립성 - 프레임워크가 지닌 제약사항 안으로 시스템을 욱여 넣도록 강제하지 않는다.
  • 테스트 용이성 - 업무 규칙은 UI, DB, 웹서버, 또는 여타 외부 요소가 없이도 테스트 할 수 있다.
  • UI 독립성 - 업무 규칙을 변경하지 않은채 웹 UI를 콘솔 UI로 대체 할 수 있을 만큼 UI를 쉽게 변경할 수 있다.
  • DB 독립성 - 업무 규칙은 DB에 결합 되지 않으며, RDB를 NoSql 등으로 교체할 수 있다.
  • 모든 외부 에이전시에 대한 독립성 - 업무 규칙은 외부 세계와 인터페이스에 대해 전혀 알지 못한다.

유명한 다이어그램

의존성 규칙

각각의 동심원은 소프트웨어에서 서로 다른 영역을 표현하며, 안으로 들어갈 수록 고수준의 소프트웨어가 된다.

바깥쪽 원은 메커니즘이고, 안쪽 원은 정책이며, 이러한 아키텍처가 동작하도록 하는 가장 중요한 규칙은 의존성 규칙이다.

“소스 코드의 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다.”

내부 원에 속한 요소는 외부 원에 속한 그 어떤 것도 알지 못한다. 특히 내부의 원에 속한 코드는 외부의 원에 선언된 그 어떤 것에 대해서도 그 이름을 언급 해서는 안된다.

다이어그램에서 원은 4개로 표시 되었지만, 이는 그저 개념을 설명하기위한 하나의 예시일 뿐 꼭 정해진 분리방식은 아니다.

하지만 어떤 경우에도 의존성 규칙은 적용되며, 안쪽으로 이동할 수록 추상화와 정책의 수준은 높아진다.

엔티티

엔티티는 전사적인 핵심 업무 규칙을 캡슐화 한다. 엔티티는 메서드를 가지는 객체이거나 일련의 데이터 구조와 함수의 집합일 수도 있다.

전사적이지 않은 단순한 어플리케이션 이라면 엔티티는 해당 어플리케이션의 업무 객체가 된다. 이 경우 엔티티는 가장 일반적이며 고수준인 규칙을 캡슐화 한다.

운영 관점에서 특정 어플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안된다는 뜻이다.

유스케이스 - 인터랙션

유스케이스 계층의 소프트웨어는 어플리케이션에 특화된 업무 규칙을 포함한다. 또한 유스케이스 계층의 소프트웨어는 시스템의 모든 유스케이스를 캡슐화 하고 구현한다.

엔티티로 in/out 되는 데이터 흐름을 조정하며, 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끌어 준다.

이 계층에서 발생한 변경이 엔티티에 영향을 줘서는 안되며, DB, UI, 또는 여타 공통 프레임워크와 같은 외부 요소에서 발생한 변경이 이 계층에 영향을 줘서도 안된다.

운영 관점에서 어플리케이션이 변경된다면 유스케이스가 영향을 받으며, 유스케이스의 세부사항이 변하면 이 계층의 코드 일부는 영향을 받을 것이다.

인터페이스 어댑터 - 컨트롤러

어댑터는 데이터를 유스케이스와 엔터티에게 가장 편리한 형식에서 DB나 웹 같은 외부 에이전시에게 가장 편리한 형식으로 변환한다.

이 계층은 GUI의 MVC 아키텍처를 모두 포괄한다. 프레젠터, 뷰, 컨트롤러가 모두 이 인터페이스 어댑터 계층에 속한다.

모델은 그저 데이터 구조 정도에 지나지 않으며, 컨트롤러에서 유스케이스로 전달되고, 다시 유스케이스에서 프레젠터와 뷰로 되돌아 간다.

마찬가지로 이 계층은 데이터를 엔터티 유스케이스에게 DB가 이용하기에 가장 편리한 형식으로 변환한다.

이 원 안에 속한 어떤 코드도 데이터베이스에 대해 조금도 알아서는 안된다.

프레임워크와 드라이버

가장 바깥쪽 계층은 일반적으로 DB나 웹 프레임워크 같은 프레임워크나 도구들로 구성된다.

일반적으로 이 계층에서는 안쪽 원과 통신하기 위한 접합 코드 외에는 특별히 더 작성해야할 코드가 그다지 많지 않다.

여기는 모든 세부사항이 위치하는 곳이며, 우리는 이러한 웹과 DB같은 것들을 모두 외부에 위치시켜서 피해를 최소화 한다.

경계 횡단하기

우측 하단 다이어그램은 원의 경계를 횡단하는 방법을 보여준다.

이 방법에서 컨트롤러와 프레젠터가 다음 계층에 속한 유스케이스와 통신하는 모습을 확인할 수 있다.

  • 제어흐름 : 컨트롤러에서 시작해서, 유스케이스를 지난 후 프레젠터에서 실행되면서 마무리 됨
  • 소스 코드 의존성 : 각 의존성은 유스케이스를 향해 안쪽을 가르킴

Java 같은 언어에서는 인터페이스와 상속 관계를 적절히 배치함으로써, 제어 흐름이 경계를 가로지르는 바로 그지점에서 소스 코드 의존성을 제어흐름과는 반대가 되게 만들 수 있다.

예를 들어 유스케이스에서 프레젠터를 호출해야 한다고 가정해 하면, 이때 절대 직접 호출해서는 안되는데, 직접 호출 해버리면 의존성 규칙 (내부의 원에서는 외부 원에 있는 어떤 이름도 언급해서는 안된다)을 위배하기 때문이다.

따라서 유스케이스가 내부 원의 인터페이스를 호출하도록 하고(입출력 포트) 외부 원의 프레젠터가 그 인터페이스를 구현하도록 만든다.

경계를 횡단하는 데이터의 모습

경계를 가로지르는 데이터는 흔히 간단한 데이터 구조로 이루어져 있다.

기본적인 구조체나 간단한 DTO등 원하는 대로 고를 수 있으며, 또는 함수를 호출할 때 간단한 인자를 사용해서 데이터로 전달할 수도 있다. 데이터를 해쉬맵으로 묶거나 객체로 구성할 수도 있지만 중요한 점은 격리되어 있는 데이터 구조가 경계를 가로질러 전달된 다는 사실이다.

꾀를 부려서 엔터티의 객체 그자체 또는 DB의 row 자체를 전달해버리면, 데이터 구조가 의존성을 가져버려 의존성 규칙을 위배해버린다.

따라서 경계를 가로질러 데이터를 전달할 때, 데이터는 항상 내부의 원에서 사용하기 가장 편리한 형태를 가져야만 한다.

전형적인 예시

DB를 사용하는 웹 기반 Java 시스템의 전형적인 시나리오

 

웹서버는 사용자로부터 입력 데이터를 모아 좌측 상단의 Controller로 전달한다.

Controller는 데이터를 평범한 Java 객체 (POJO) 로 묶은 후 InputBoundary 인터페이스를 통해 UseCaseInteractor로 전달한다.

UseCaseInteractor는 이 데이터를 해석해서 Entities가 어떻게 변화할지를 제어하는 데 사용한다.

UseCaseInteractor는 DataAccessInterface를 사용하여 Entities가 사용할 데이터를 DB에서 불러와서 메모리로 로드한다.

Entities가 완성되면, UseCaseInteractor는 Entities로 부터 데이터를 모아서 또 다른 평범한 자바 객체인 OutputData를 구성한다.

OutputData는 OutPutBoundary 인터페이스를 통해 Presenter로 전달된다.

Presenter가 맡은 역할은 OutPutData를 ViewModel과 같이 화면에서 출력 할 수 있는 형식으로 재구성하는 일이다.

ViewModel 또한 평범한 Java객체 이고 주로 문자열과 프래그로 구성되며, View에서는 이 데이터를 화면에 출력한다.

(OutPutData 에서는 DateTime 객체를 포함할 수 있는 반면, Presenter 에 이르러서는 ViewModel을 로드 할 때 이 DataTime 객체를 사용자가 보기에 적합한 형식의 String으로 변환)

Button 과 MenuItem 등 화면을 컨트롤 할 수 있는 요소는 ViewModel에 위치한다.

즉, ViewModel에서 HTML 페이지로 데이터를 옮기는 일을 빼면, View에서 해야 할 일은 거의 남아 있지 않다.

의존성의 방향에 주목해보자. 모든 의존성은 경계선을 안쪽으로 가로지르고 있다.

 

SW를 계층으로 분리하고 의존성 규칙을 준수하면 본질적으로 테스트하기 쉬운 시스템을 만들게 될 것이며, 그에 따른 이점을 누릴 수 있다.

DB나 웹 프레임워크 같은 시스템의 외부 요소가 outdated 되어도, 이들 요소를 야단스럽지 않게 교체할 수 있다.

 

 

 

< 프레젠터와 험블 객체 >

프레젠터는 험블 객체 패턴을 따른 형태로, 아키텍처 경계를 식별하고 보호하는데 도움이 된다.

실제로 이전 장에서 본 클린 아키텍처 BP는 험블 객체의 구현체들로 가득차 있다.

험블 객체 패턴

이것은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안 되었으며, 아이디어는 매우 단순하다. 행위들을 두개의 모듈 또는 클래스로 나누되 테스트 용이성에 주목해서 분리하는 것이다.

  • 험블 객체 : 가장 기본적인 본질을 남기고 테스트하기 어려운 행위들이 모인 모듈
    • 뷰 - 데이터를 GUI 로 이동시키지만 데이터를 직접 처리하지는 않는다.
  • 나머지 : 험블 객체에 속하지 않은, 테스트하기 쉬운 행위들이 모인 모듈
    • 프레젠터 - 어플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만든다

예를 들어, 어플리케이션에서 어떤 필드에 날짜를 표시하고자 한다면,

  • 어플리케이션은 프레젠터에 DateTime 객체를 전달한다.
  • 프레젠터는 해당 데이터를 적절한 String 따위의 문자열로 만들고, 이 문자열을 view model 이라고 부르는 간단한 데이터 구조에 담는다.
  • 뷰는 view model에서 이 데이터를 찾는다.
  • 화면에 보이는 버튼은 모두의 이름이 있을 것인데, 그 이름은 view model 내부에서 문자열로 존재하며, 프레젠터에 의해 view model에 역시 위치하게 된다.
  • 특정 버튼을 비활성화 해야 한다면, 프레젠터는 view model 에서 적절힌 boolean 타입의 플래그로 설정한다.

화면에 표시되고 어플리케이션에서 어느정도 제어할 수 있는 요소라면 무조건 뷰 모델 내부에 문자열, boolean, enum 형태로 표현한다.

뷰는 view model의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 전혀 없다. 따라서 뷰는 humble 하다.

테스트와 아키텍처

테스트 용이성은 좋은 아키텍처가 지녀야 할 속성이다.

험블 객체 패턴이 역시 좋은 예인데, 행위를 테스트하기 쉬운부분과 어려운 부분으로 분리하면 아키텍처 경계가 정의되기 때문이다.

바로 윗 단락의 프레젠터와 뷰 사이의 경계는 이러한 경계 중 하나이다.

데이터베이스 게이트웨이

유스케이스 인터랙터와 DB사이에는 DB 게이트웨이가 위치한다.

이 게이트웨이는 다형적 인터페이스로 어플리케이션이 DB에 수행하는 생성, 조회, 갱신, 삭제 작업과 관련된 모든 메서드를 포함한다.

유스케이스 계층은 당연히 SQL 을 허용하지 않으므로 필요한 메서드를 제공하는 이 게이트웨이 인터페이스를 호출한다.

그리고 인터페이스의 구현체는 DB계층에 위치시킨다.

  • 험블 객체 : 게이트웨이 인터페이스의 구현체
    • 이 구현체에서 직접 SQL을 쓰거나 DB에 대한 임의의 인터페이스를 통해 게이트웨이의 메서드에서 필요한 데이터에 접근한다.
  • 나머지 : 인터랙터
    • 어플리케이션에 특화된 업무 규칙을 캡슐화 하므로 험블 객체가 아니며 테스트가 용이함.
    • 테스트가 용이한 이유는 게이트웨이를 스텁이나 테스트 더블로 적당히 교체할 수 있기 때문이다.

ORM 은 어느 계층에 속하는가? 물론 DB계층이다.

실제로 ORM은 게이트웨이 인터페이스와 DB사이에서 일종의 또 다른 험블 객체 경계를 형성한다.

서비스 리스너

어플리케이션이 다른 서비스와 반드시 통신해야 한다면, 또는 어플리케이션에서 일련의 서비스를 제공해야 한다면, 여기에서도 서비스 경계를 생성하는 험블 객체 패턴을 확인 할 수 있다.

  • 어플리케이션은 데이터를 간단한 데이터 구조 형태로 로드한다.
  • 이 데이터 구조를 경계를 가로질러서 특정 모듈로 전달한다.
  • 해당 모듈은 데이터를 적절한 포맷으로 만들어서 외부 서비스로 전송한다.
  • 반대로, 외부로부터 데이터를 수신하는 서비스의 경우, 서비스 리스너가 서비스 인터페이스로 부터 데이터를 수신하고, 데이터를 애플리케이션에서 사용할 수 있게 간단한 데이터 구조로 포맷을 변경한다.
  • 그 후 이 데이터 구조는 서비스 경계를 가로질러서 내부로 전달 된다.

각 아키텍처 경계마다 경계 가까이에 숨어 있는 험블 객체 패턴을 발견할 수 있다.

경계를 넘나드는 통신은 거의 모두 간단한 데이터 구조를 수반할 때가 많고, 대개 그 경계는 테스트하기 어려운 무언가와 테스트하기 쉬운 무언가로 분리 될 것이다.

그리고 이러한 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템의 테스트 용이성을 크게 높일 수 있다.

댓글