클린 아키텍처 (Chapter 18~26) - Robert C. Martin
By Bys on February 6, 2022
18. 경계 해부학
19. 정책과 수준
소프트웨어 시스템이란 정책을 기술한 것이다. 실제로 컴퓨터 프로그램의 핵심부는 이게 전부다.
컴퓨터 프로그램은 각 입력을 출력으로 변환하는 정책을 상세하기 기술한 설명서다.
소프트웨어 아키텍처를 개발하는 기술에는 이러한 정책을 신중하게 분리하고, 정책이 변경되는 양상에 따라 정책을 재편성하는 일도 포함된다.
동일한 이유로 동일한 시점에 변경되는 정책은 동일한 수준에 위치하며, 동일한 컴포넌트에 속해야 한다.
서로 다른 이유로, 혹은 다른 시점에 변경되는 정책은 다른 수준에 위치하며, 반드시 다른 컴포넌트로 분리해야 한다.
좋은 아키텍처라면 각 컴포넌트를 연결할 때 의존성의 방향이 컴포넌트의 수준을 기반으로 연결되도록 만들어야 한다. 즉, 저수준 컴포넌트가 고수준 컴포넌트에 의존하도록 설계되어야 한다.
수준
‘수준(level)’을 엄밀하게 정의하자면 ‘입력과 출력까지의 거리’다. 시스템의 입력과 출력 모두로부터 멀리 위치할수록 정책의 수준은 높아진다.
입력과 출력을 다루는 정책이라면 시스템에서 최하위 수준에 위치한다.
번역 컴포넌트는 이 시스템에서 최고 수준의 컴포넌트인데, 입력과 출력에서부터 가장 멀리 떨어져 있기 때문이다.
주목할 점은 데이터 흐름과 소스 코드 의존성이 항상 같은 방향을 가리키지는 않는다는 사실이다. 다시 한 번 말하지만 이것이 바로 소프트웨어 아키텍처가 가진 예술 중 하나다.
소스 코드 의존성은 그 수준에 따라 결합되어야하며, 데이터 흐름을 기준으로 결합되어서는 안 된다.
자칫하면 잘못된 아키텍처가 만들어지는데, 예를 들어 암호화 프로그램을 다음처럼 작성한다면 그렇게 된다.
function encrypt() {
while(true){
writeChar(translate(readChar()));
}
}
이는 잘못된 아키텍처다. 고수준인 encrypt 함수가 저수준인 readChar와 writeChar 함수에 의존하기 때문이다.
위 그림의 클래스 다이어그램은 이 시스템의 아키텍처를 개선해본 모습이다.
주목할 점은 Encrypt 클래스, CharWriter와 CharReader 인터페이스를 둘러싸고 있는 점선으로 된 경계다. 이 경계를 횡단하는 의존성은 모두 경계 안쪽으로 향한다.
이 경계로 묶인 영역이 이 시스템에서 최고 수준의 구성요소다.
이 구조에서 고수준의 암호화 정책을 저수준의 입력/출력 정책으로부터 분리시킨 방식에 주목하자. 이 방식 덕분에 이 함호화 정책을 더 넓은 맥락에서 사용할 수 있다.
입력과 출력에 변화가 생기더라도 암호화 정책은 거의 영향을 받지 않기 때문이다.
20. 업무 규칙
애플리케이션을 업무 규칙과 플러그인으로 구분하려면 업무 규칙이 실제로 무엇인지를 잘 이해해야만 한다. 업무 규칙에는 여러가지가 있다.
엄밀하게 말하면 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있는 규칙 또는 절차다.
더 엄밀하게 말하면 컴퓨터상으로 구현했는지와 상관없이, 업무 규칙은 사업적으로 수익을 얻거나 비용을 줄일 수 있어야 한다. 심지어 사람이 수동으로 직접 수행하더라도 마찬가지다.
대출에 N%의 이자를 부과한다는 사실은 은행이 돈을 버는 업무 규칙이다. 이러한 사실은 컴퓨터 프로그램으로 이자를 계산하든, 또는 직원이 주판을 튕겨 계산하든 하등의 관계가 없다.
이러한 규칙을 핵심 업무 규칙(Critical Business Rule)이라고 부를 것이다.
왜냐하면 이들 규칙은 사업 자체의 핵심적이며, 규칙을 자동화하는 시스템이 없다라도 업무 규칙은 그대로 존재하기 때문이다.
핵심 업무 규칙은 보통 데이터를 요구한다. 예를 들어 대출에는 대출 잔액, 이자율, 지급 일정이 필요하다.
우리는 이러한 데이터를 핵심 업무 데이터(Critical Business Data)라고 부르겠다.
심지어 이러한 데이터는 시스템으로 자동화되지 않은 경우에도 존재하는 그런 데이터다. 핵심 규칙과 핵심 데이터는 본질적으로 결합되어 있기 때문에 객체로 만들 좋은 후보가 된다.
우리는 이러한 유형의 객체를 엔티티(Entity)라고 하겠다.
엔티티(Entity)
엔티티는 컴퓨터 시스템 내부의 객체로서, 핵심 업무 데이터를 기반으로 동작하는 일련의 조그만 핵심 업무 규칙을 구체화한다.
엔티티 객체는 핵심 업무 데이터를 직접 포함하거나 핵심 업무 데이터에 매우 쉽게 접근할 수 있다.
엔티티의 인터페이스는 핵심 업무 데이터를 기반으로 동작하는 핵심 업무 규칙을 구현한 함수들로 구성된다.
대출을 뜻하는 Loan 엔티티가 UML 클래스로 어떻게 표현되는지를 보여준다.
Loan 엔티티는 세 가지의 핵심 업무 데이터를 포함하며, 데이터와 관련된 세 가지 핵심 업무 규칙을 인터페이스로 제공한다.
우리는 이러한 종류의 클래스를 생성할 때, 업무에서 핵심적인 개념을 구현하는 소프트웨어는 한데 모으고, 구축 중인 자동화 시스템의 나머지 모든 고려사항과 분리시킨다.
이 클래스는 업무의 대표자로서 독립적으로 존재한다. 이 클래스는 데이터베이스, 사용자 인터페이스, 서드파티 프레임워크에 대한 고려사항들로 인해 오염되어서는 절대 안 된다.
엔티티는 순전히 업무에 대한 것이며, 이외의 것은 없다.
유스케이스(Use Case)
모든 업무 규칙이 엔티티처럼 순수한 것은 아니다. 자동화된 시스템이 동작하는 방법을 정의하고 제약함으로써 수익을 얻거나 비용을 줄이는 업무 규칙도 존재한다.
이러한 규칙은 자동화된 시스템의 요소로 존재해야만 의미가 있으므로 수동 환경에서는 사용될 수 없다.
예를 들어 은행 직원이 신규 대출을 생성할 때 사용하는 애플리케이션을 상상해 보자. 은행에서 대출 담당자가 신청자의 신상정보를 수집하여 검증한 후, 신청자의 신용도가 500보다 낮다면 대출 견적을 제공하지 않기로 결정했다고 해 보자. 따라서 시스템에서 신상정보 화면을 모두 채우고 검증한 후, 신용도가 하한선보다 높은지가 확인된 이후에 대출 견적 화면으로 진행되어야 한다는 식으로 은행에서 업무 요건을 기술했다고 해 보자.
바로 이것이 유스케이스(Use Case)다. 유스케이스는 자동화된 시스템이 사용되는 방법을 설명한다.
유스케이스는 사용자가 제공해야 하는 입력, 사용자에게 보여줄 출력, 그리고 해당 출력을 생성하기 위한 처리 단계를 기술한다.
엔티티 내의 핵심 업무 규칙과는 반대로, 유스케이스는 애플리케이션에 특화된(application-specific)업무 규칙을 설명한다.
마지막 줄에서 Customer를 언급한다는 점에 주목하자. 이는 Customer 엔티티에 대한 참조이며, 은행과 고객의 관계를 결정짓는 핵심 업무 규칙은 바로 이 Customer 엔티티에 포함된다.
유스케이스는 엔티티 내부의 핵심 업무 규칙을 어떻게, 그리고 언제 호출 할지를 명시하는 규칙을 담는다. 엔티티가 어떻게 춤을 출지를 유스케이스가 제어하는 것이다.
주목할 또 다른 사실은 유스케이스는 사용자 인터페이스를 기술하지 않는다는 점이다.
유스케이스만 봐서는 이 애플리케이션이 우베을 통해 전달되는지, 리치 클라이언트인지, 콘솔 기반인지, 아니면 순수한 서비스인지를 구분하기란 불가능하다.
이 점은 매우 중요하다. 유스케이스는 시스템이 사용자에게 어떻게 보이는지를 설명하지 않는다. 이보다는 애플리케이션에 특화된 규칙을 설명하며, 이를 통해 사용자와 엔티티 사이의 상호작용을 규정한다.
시스템에서 데이터가 들어오고 나가는 방식은 유스케이스와는 무관하다.
유스케이스는 객체다. 유스케이스는 애플리케이션에 특화된 업무 규칙을 구현하는 하나 이상의 함수를 제공한다. 또한 유스케이스는 입력 데이터, 출력 데이터, 유스케이스가 상호작용하는 엔티티에 대한 참조 데이터 등의 데이터 요소를 포함한다.
엔티티는 자신을 제어하는 유스케이스에 대해 아무것도 알지 못한다. 이는 의존성 역전 원칙을 준수하는 의존성 방향에 대한 또 다른 예다.
엔티티와 같은 고수준 개념은 유스케이스와 같은 저수준 개념에 대해 아무것도 알지 못한다. 유스케이스는 엔티티에 의존한다. 반면 엔티티는 유스케이스에 의존하지 않는다.
결론
업무 규칙은 소프트웨어 시스템이 존재하는 이유다. 업무 규칙은 핵심적인 기능이다. 업무 규칙은 수익을 내고 비용을 줄이는 코드를 수반한다. 업무 규칙은 집안의 가보다.
업무 규칙은 사용자 인터페이스나 데이터베이스와 같은 저수준의 관심사로 인해 오염되어서는 안 되며, 원래 그대로의 모습으로 남아 있어야 한다.
이상적으로는 업무 규칙을 표현하는 코드는 반드시 시스템의 심장부에 위치해야 하며, 덜 중요한 코드는 이 심장부에 플러그인되어야 한다.
업무 규칙은 시스템에서 가장 독립적이며 가장 많이 재사용할 수 있는 코드여야 한다.
21. 소리치는 아키텍처
야콥슨(Ivar Jacobson)은 소프트웨어 아키텍처는 시스템의 유스케이스를 지원하는 구조라고 지적했다. 주택이나 도서관의 계획서가 해당 건축물의 유스케이스에 대해 소리치는 것처럼, 소프트웨어 애플리케이션의 아케틱처도 애플리케이션의 유스케이스에 대해 소리쳐야 한다. 아키텍처는 프레임워크에 대한 것이 아니다. 아키텍처를 프레임워크로부터 제공받아서는 절대 안 된다. 프레임워크는 사용하는 도구일 뿐, 아미텍처가 준수해야 할 대상이 아니다. 아키텍처를 프레임워크 중심으로 만들어버리면 유스케이스가 중심이 되는 아키텍처는 절대 나올 수 없다.
아키텍처의 목적
좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크나 도구, 환경에 전혀 구애받지 않고 유스케이스를 지원하는 구조를 아무런 문제 없이 기술할 수 있다.
주택에 대한 계획서를 다시 한번 생각해 보자. 아키텍트가 주목하는 첫 번째 관심사는 주택이 거주하기에 적합한 공간임을 확실히 하는 것이지, 벽돌로 지어지는지를 확인하는 것이 안다.
좋은 소프트웨어 아키텍처는 프레임워크, 데이터베이스, 웹 서버, 그리고 여타 개발 환경 문제나 도구에 대해서는 결정을 미룰 수 있도록 만든다.
프레임워크는 열어 둬야 할 선택사항이다. 좋은 아키텍처는 프로젝트의 훨씬 후반까지 레일스, 스프링, 하이버네이트, 톰캣, MySQL에 대한 결정을 하지 않아도 되도록 해준다.
좋은 아키텍처는 유스케이스에 중점을 두며, 지엽적인 관심사에 대한 결합은 분리시킨다.
결론
아키텍처는 시스템을 이야기해야 하며, 시스템에 적용한 프레임워크에 대해 이야기해서는 안 된다.
22. 클린 아키텍처
육각형 아키텍처(Hexagonal Architecture), DCI(Data, Context and Interaction), BCE(Boundary-Control-Entity)
이들 아키텍처는 모두 세부적인 면에서는 다소 차이가 있더라도 그 내용은 상당히 비슷하다. 이들의 목표는 모두 같은데, 바로 관심사의 분리(Separation of concerns)다.
이들은 모두 소프트웨어를 계층으로 분리함으로써 관심사의 분리라는 목표를 달성할 수 있었다.
이들 아키텍처는 모두 시스템이 다음과 같은 특징을 지니도록 만든다.
-
프레임워크 독립성
아키텍처는 다양한 기능의 라이브러리를 제공하는 소프트웨어, 즉 프레임워크의 존재 여부에 의존하지 않는다. 이를 통해 이러한 프레임워크를 도구로 사용할 수 있으며, 프레임워크가 지닌 제약사항안으로 시스템을 욱여 넣도록 강제하지 않는다. -
테스트 용이성
업무 규칙은 UI, 데이터베이스, 우베 서버, 또는 여타 외부 요소가 없이도 테스트할 수 있다. -
UI 독립성
시스템의 나머지 부분을 변경하지 않고도 UI를 쉽게 변경할 수 있다. 예를 들어 업무 규칙을 변경하지 않은 채 웹 UI를 콘솔 UI로 대체할 수 있다. -
데이터베이스 독립성
오라클이나 MS SQL 서버를 MongoDB, BigTable, CauchDB 등으로 교체할 수 있다. 업무 규칙은 데이터베이스에 결합되지 않는다. -
모든 외부 에이전시에 대한 독립성
실제로 업무 규칙은 외부 세계와의 인터페이스에 대해 전혀 알지 못한다.
위 그림의 다이어그램은 이들 아키텍처 전부를 실행 가능한 하나의 아이디어로 통합하려는 시도다.
의존성 규칙
그림에서 각각의 동심원은 소프트웨어에서 서로 다른 영역을 표현한다.
보통 안으로 들어가수록 고수준의 소프트웨어가 된다. 바깥쪽 원은 메커니즘이고, 안쪽 원은 정책이다.
이러한 아키텍처가 동작하도록 하는 가장 중요한 규칙은 의존성 규칙(Dependency Rule)이다.
소스 코드 의존성은 반드시 안쪽으로, 고소준의 정책을 향해야 한다.
내부의 원에 속한 요소는 외부의 원에 속한 어떤 것도 알지 못한다. 특히 내부의 원에 속한 코드는 외부의 원에 선언된 어떤 것에 대해서도 그 이름을 언급해서는 절대 안 된다. 여기에는 함수, 클래스, 변수, 그리고 소프트웨어 엔티티로 명명되는 모든 것이 포함된다.
같은 이유로, 외부의 원에 선언된 데이터 형식도 내부의 원에서 절대로 사용해서는 안 된다. 특히 그 데이터 형식이 외부의 원에 있는 프레임워크가 생성한 것이라면 더더욱 사용해서는 안 된다. 우리는 외부 원에 위치한 어떤 것도 내부의 원에 영향을 주지 않기를 바란다.
엔티티
엔티티는 전사적인 핵심 업무 규칙을 캡슐화한다. 엔티티는 메서드를 가지는 객체이거나 일련의 데이터 구조와 함수의 집하일 수도 있다.
기업의 다양한 애플리케이션에서 엔티티를 재사용할 수만 있다면, 그 형태는 그다지 중요하지 않다.
운영 관점에서 특정 애플리케이션에 무언가 변경이 필요하더라도 엔티티 계층에는 절대로 영향을 주어서는 안 된다.
유스케이스
유스케이스 계층의 소프트웨어는 애플리케이션에 특화된 업무 규칙을 포함한다. 또한 유스케이스 계층의 소프트웨어는 시스템의 모든 유스케이스를 캡슐화하고 구현한다.
유스케이스는 엔티티로 들어오고 나가는 데이터 흐름을 조정하며, 엔티티가 자신의 핵심 업무 규칙을 사용해서 유스케이스의 목적을 달성하도록 이끈다.
인터페이스 어댑터
인터페이스 어댑터(Interface Adaptor)계층은 일련의 어댑터들로 구성된다.
어댑터는 데이터를 유스케이스와 엔티티에게 가장 편리한 형식에서 데이터베이스나 웹 같은 외부 에이전시에게 가장 편리한 형식으로 변환한다.
이 계층은, 예를 들어 GUI의 MVC 아키텍처를 모두 포괄한다. 프레젠터, 뷰, 컨트롤러는 모두 인터페이스 어댑터 계층에 속한다.
프레임워크와 드라이버
가장 바깥쪽 계층은 일반적으로 데이터베이스나 웹 프레임워크 같은 프레임워크나 도구들로 구성된다.
일반적으로 이 계층에서는 안쪽 원과 통신하기 위한 접합 코드 외에는 특별히 더 작성해야 할 코드가 그다지 많지 않다.
웹은 세부사항이다. 데이터베이스는 세부사항이다. 우리는 이러한 것들을 모두 외부에 위치시켜서 피해를 최소화한다.
전형적인 시나리오
위 그림의 다이어그램은 데이터베이스를 사용하는 웹 기반 자바 시스템의 전형적인 시나리오를 보여준다.
- 웹 서버는 사용자로부터 입력 데이터를 모아 좌측 상단의 Controller로 전달한다.
- Controller는 데이터를 평범한 자바 객체(POJO)로 묶은 후, InputBoundary 인터페이스를 통해 UseCaseInteractor로 전달한다.
- UseCaseInteractor는 이 데이터를 해석해서 Entities가 어떻게 춤 출지를 제어하는데 사용한다.
- 또한 UseCaseInteractor는 DataAccessInterface를 사용하여 Entities가 사용할 데이터를 데이터베이스에서 불러와서 메모리로 로드한다.
- Entities가 완성되면, UseCaseInteractor는 Entities로부터 데이터를 모아서 또 다른 평범한 자바 객체인 OutputData를 구성한다.
- 그러고 나서 OutputData는 OutputBoundary 인터페이스를 통해 Presenter로 전달된다.
Presenter가 맡은 역할은 OutputData를 ViewModel과 같이 화면에 출력 할 수 있는 형식으로 재구성하는 일이다. ViewModel 또한 평범한 자바 객체다. ViewModel은 주로 문자열과 플래그로 구성되며, View에서는 이 데이터를 화면에 출력한다.
OutputData에서는 Date 객체를 포함할 수 있는 반면, Presenter는 ViewModel을 로드할 때 Date 객체를 사용자가 보기에 적절한 형식의 문자열로 변환한다. 이 변환은 Currency 객체나 다른 업무 관련 데이터 모두에 똑같이 적용된다. Button과 MenuItem의 이름은 ViewModel에 위치하며, 해당 Button과 MenuItem을 비활성화할지를 알려주는 플래그 또한 ViewModel에 위치한다.
따라서 ViewModel에서 HTML 페이지로 데이터를 옮기는 일을 빼면, View에서 해야 할 일은 거의 남아 있지 않다.
의존성의 방향에 주목하라. 모든 의존성은 경계선을 안쪽으로 가로지르며, 따라서 의존성 규칙을 준수한다.
결론
소포트웨어를 계층으로 분리하고 의존성 규칙을 준수한다면 본질적으로 테스트하기 쉬운 시스템을 만들게 될 것이며, 그에 따른 이점을 누릴 수 있다.
데이터베이스나 웹 프레임워크와 같은 시스템의 외부 요소가 구식이 되더라도, 이들 요소를 야단스럽지 않게 교체할 수 있다.
23. 프레젠터와 험블 객체
22장에서 프레젠터는 험블 객체(Humble Object) 패턴을 따른 형태로, 아키텍처 경계를 식별화 하고 보호하는 데 도움이 된다.
실제로 이전 장 ‘클린 아키텍처’는 험블 객체 구현체들로 가득 차 있었다.
험블 객체 패턴(Humble Object Pattern)
험블 객체 패턴은 디자인 패턴으로, 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안되었다.
아이디어는 매우 단순하다. 행위들을 두 개의 모듈 또는 클래스로 나눈다. 이들 모듈 중 하나가 험블이다.
가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 모듈에는 험블 객체에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮긴다.
예를 들어 GUI의 경우 단위 테스트가 어려운데, 화면을 보면서 각 요소가 필요한 위치에 적절히 표시되었는지 검사하는 테스트는 작성하기 매우 어렵기 때문이다. 하지만 GUI에서 수행하는 행위의 대다수는 쉽게 테스트할 수 있다. 험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 프레젠터와 뷰라는 서로 다른 클래스로 만들 수 있다.
프레젠터와 뷰
뷰는 험블 객체이고 테스트하기 어렵다.
이 객체에 포함된 코드는 가능한 한 간단하게 유지한다. 뷰는 데이터를 GUI로 이동시키지만, 데이터를 직접 처리하지는 않는다.
프레젠터는 테스트하기 쉬운 객체다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것이다. 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만든다.
예를 들어 애플리케이셔넹서 어떤 필드에 날짜를 표시하고자 한다면, 애플리케이션은 프레젠터에 Date 객체를 전달한다. 그러면 프레젠터는 해당 데이터를 적절한 포맷의 문자열로 만들고, 이 문자열을 뷰 모델(View Model)이라고 부르는 간단한 데이터 구조에 담는다. 그러면 뷰는 뷰 모델에서 이 데이터를 찾는다.
만약 애플리케이션에서 화면에 금액을 표시하고자 한다면, 애플리케이션은 페레젠터에 Currency 객체를 전달한다. 프레젠터는 해당 객체를 소수점과 통화 표시가 된 포맷으로 적절하게 변환하여 문자열을 생성한 후 뷰 모델에 저장한다. 만약 금액이 음수일 때 빨간색으로 변해야 한다면, 간단히 boolean 타입 플래그를 뷰 모델에 두고 적잘한 값으로 설정한다.
뷰는 뷰 모델의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 전혀 없다. 따라서 뷰는 보잘것없다(humble).
테스트와 아키텍처
테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 오랫동안 알려져 왔다.
험블 객체 패턴이 좋은 예인데, 행위를 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경계가 정의되기 때문이다.
프레젠터와 뷰 사이의 경계는 이러한 경계 중 하나이며, 이 밖에도 수많은 경계가 존재한다.
데이터베이스 게이트웨이
유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이(Database Gateway)가 위치한다.
이 게이트웨이는 다형적 인터페이스로, 애플리케이션이 데이터베이스에 수행하는 생성, 조회, 갱신, 삭제 작업과 관련된 모든 메서드를 포함한다.
유스케이스 계층은 SQL을 허용하지 않는다. 따라서 유스케이스 계층은 필요한 메서드를 제공하는 게이트웨이 인터페이스를 호출한다. 그리고 인터페이스의 구현체는 데이터베이스 계층에 위치한다. 이 구현체는 험블 객체다. 구현체에서 직접 SQL을 사용하거나 데이터베이스에 대한 임의의 인터페이스를 통해 게이트웨이의 메서드에서 필요한 데이터에 접근한다.
데이터 매퍼
하이버네이트 같은 ORM은 어느 계층에 속한다고 보는가?
먼저 분명히 해야 할 점이 있다. 객체 관계 매퍼(Object Relational Mapper, ORM)같은 건 사실 존재하지 않는다. 이유는 간단하다.
객체는 데이터 구조가 아니기 때문이다. 최소한 객체를 사용하는 사람 관점에서 객체는 데이터 구조가 아니다. 데이터는 모두 private으로 선언되므로 객체의 사용자는 데이터를 볼 수 없다.
사용자는 객체에서 public 메서드만 볼 수 있다. 따라서 사용자 관점에서 볼 때 객체는 단순히 오퍼레이션의 집합이다.
객체와 달리 데이터 구조는 함축된 행위를 가지지 않는 public 데이터 변수의 집합이다. ORM보다는 차라리 ‘데이터 매퍼(Data Mapper)’라고 부르는 편이나아 보이는데, 관계형 데이터베이스 테이블로부터 가져온 데이터를 데이터 구조에 맞게 담아주기 때문이다.
이러한 ORM 시스템은 어디에 위치해야 하는가? 물론 데이터베이스 계층이다. 실제로 ORM은 게이트웨이 인터페이스와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성한다.
서비스 리스너
애플리케이션이 다른 서비스와 반드시 통신해야 한다면, 또는 애플리케이션에서 일련의 서비스를 제공해야 한다면, 우리는 여기에서 서비스 경계를 생성하는 험블 객체 패턴을 발견할 수 있을까? 당연하다!
애플리케이션은 데이터를 간단한 데이터 구조 형태로 로드한 후, 이 데이터 구조를 경계를 가로질러서 특정 모듈로 전달한다.
반대로 외부로부터 데이터를 수신하는 서비스의 경우, 서비스 리스너(Service Listener)가 서비스 인터페이스로부터 데이터를 수신하고, 데이터를 애플리케이션에서 사용할 수 있게 간단한 데이터 구조로 포맷을 변경한다. 그런 후 이 데이터 구조는 서비스 경계를 가조릴러서 내부로 전달된다.
결론
각 아키텍처 경계마다 경계 가까이 숨어 있는 험블 객체 패턴을 발견할 수 있을 것이다.
경계를 넘나드는 통신은 거의 모두 간단한 데이터 구조를 수반할 때가 많고, 대개 그 경계는 테스트하기 어려운 무언가와 테스트하기 쉬운 무언가로 분리될 것이다.
그리고 이러한 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템의 테스트 용이성을 크게 높일 수 있다.
24. 부분적 경계
아키텍처 경계를 완벽하게 만드는 데는 비용이 많이 든다. 쌍방향의 다형적 Boundary인터페이스, Input과 Output을 위한 데이터 구조를 만들어야 할 뿐만 아니라,
두 영역을 독립적으로 컴파일하고 배포할 수 있는 컴포넌트로 격리하는 데 필요한 모든 의존성을 관리해야 한다. 이렇게 만들려면 엄청난 노력을 기울여야 하고, 유지하는 데도 또 엄청난 노력이 든다.
많은 경우에, 뛰어난 아키텍트라면 이러한 경계를 만드는 비용이 너무 크다고 판단하면서도, 한편으로는 나중에 필요할 수도 있으므로 이러한 경계에 필요한 공간을 확보하기 원할 수도 있다.
만약 그렇다면 부분적 경계(Partial Boundary)를 구현해 볼 수 있다.
마지막 단계를 건너뛰기
부분적 경계를 생성하는 방법 하나는 독립적으로 컴파일하고 배포할 수 있는 컴포넌트를 만들기 위한 작업은 모두 수행한 후, 단일 컴포넌트에 그대로 모아만 두는 것이다.
쌍방향 인터페이스도 그 컴포넌트에 있고, 입력-출력 데이터 구조도 거기에 있으며, 모든 것이 완전히 준비되어 있다. 하지만 이 모두를 단일 컴포넌트로 컴파일해서 배포한다.
일차원 경계
완벽한 형태의 아키텍처 경계는 양방향으로 격리된 상태를 유지해야 하므로 쌍방향 Boundary 인터페이스를 사용한다.
양방향으로 격리된 상태를 유지하려면 초기 설정할 때나 지속적으로 유지할 때도 비용이 많이 든다.
추후 완벽한 형태의 경계로 확장할 수 있는 공간을 확보하고자 할 때 활용할 수 있는 더 간단한 구조가 위의 그림에 나와 있다. 이는 Strategy 패턴을 사용한 전형적인 사례다.
퍼사드(Facade)
이보다 훨씬 더 단순한 경계는 퍼사드(Facade) 패턴으로, 그림과 같다.
이 경우에는 심지어 의존성 역전까지도 희생한다 경계는 Facade 클래스로만 간단히 정의된다. Facade 클래스에는 모든 서비스 클래스를 메서드 형태로 정의하고, 서비스 호출이 발생하면 해당 서비스 클래스로 호출을 전달한다. 클라이언트는 이들 서비스 클래스에 직접 접근할 수 없다.
하지만 Client가 이 모든 서비스 클래스에 대해 추이 종속성을 가지게 된 것을 주목하자. 정적 언어였다면 서비스 클래스 중 하나에서 소스 코드가 변경되면 Client도 무조건 재컴파일해야 할 것이다. 이러한 구조라면 비밀 통로 또한 정말 쉽게 만들 수 있다는 사실도 충분히 파악할 수 있을 것이다.
결론
아키텍처 경계를 부분적으로 구현하는 간단한 방법 세 가지를 살펴봤다. 이러한 접근법 각각은 나름의 비용과 장점을 지닌다.
아키텍처 경계가 언제, 어디에 존재해야 할지, 그리고 그 경계를 완벽하게 구현할지 아니면 부분적으로 구현할지를 결정하는 일 또한 아키텍트의 역할이다.
25. 계층과 경계
시스템이 세 가지 컴포넌트(UI, 업무 규칙, 데이터베이스)로만 구성된다고 생각하기 쉽다. 하지만 대다수의 시스템에서 컴포넌트의 개수는 이보다 훨씬 많다.
움퍼스 사냥 게임
텍스트 기반 UI는 그대로 유지하되, 게임 규칙과 UI를 분리해서 우리 제품을 여러 시장에서 다양한 언어로 발매할 수 있게 만든다고 가정해 보자.
게임 규칙은 언어 독립적인 API를 사용해서 UI 컴포넌트와 통신할 것이고, UI는 API를 사람이 이해할 수 있는 언어로 변환할 것이다.
그림처럼 소스 코드 의존성을 적절히 관리하면, UI 컴포넌트가 어떤 언어를 사용하더라도 게임 규칙을 재사용할 수 있다.
또한 게임의 상태를 영속적인 저장소에 유지한다고 가정해 보자. 그게 플래시 메모리나 클라우드, 혹은 단순히 RAM일 수도 있다. 어떤 경우라도 우리는 게임 규칙이 이러한 세부사항을 알지 않기를 바란다. 따라서 이번에도 역시 API를 생성하여, 게임 규칙이 데이터 저장소 컴포넌트와 통신할 때 사용하도록 만든다.
따라서 그림에서 보듯이 의존성 규칙을 준수할 수 있도록 의존성이 적절한 방향을 가리키게 만들어야 한다.
클린 아키텍처?
아키텍처 경계가 잠재되어 있을 수도 있다.
점선으로 된 테두리는 API를 정의하는 추상 컴포넌트를 가리키며, 해당 API는 추상 컴포넌트 위나 아래의 컴포넌트가 구현한다.
예를 들어 Language API는 English와 Spanish가 구현한다.
GameRules는 GameRules가 정의하고 Language가 구현하는 API를 이용해 Language와 통신한다.
마찬가지로 Language는 Language가 정의하고 Text Delivery가 구현하는 API를 이용해 TextDelivery와 통신한다. API는 (구현하는 쪽이 아닌) 사용하는 쪽에 정의되고 소속된다.
이러한 변형들을 모두 제거하고 순전히 API 컴포넌트만 집중하면 다이어그램을 단순화할 수 있다.
그림의 다이어그램은 모든 화살표가 위를 향하도록 맞춰졌다는 점에 주목하자. 그 결과 GameRules는 최상위에 놓인다.
이 구성은 데이터 흐름을 두 개의 흐름으로 효과적으로 분리한다. 왼쪽의 흐름은 사용자와의 통신에 관여하며, 오른쪽의 흐름은 데이터 영속성에 관여한다. 두 흐름은 상단의 GameRules에서 서로 만나며, GameRules는 두 흐름이 모두 거치게 되는 데이터에 대한 최종적인 처리기가 된다.
흐름 횡단하기
게임을 네트워크상에서 여러 사람이 함께 플레이할 수 있게 만든다고 해보자.
그림에서 보듯이 네트워크(Network) 컴포넌트를 추가해야 한다.
이 구성은 데이터 흐름을 세 개의 흐름으로 분리하며, 이들 흐름은 모두 GameRules가 제어한다.
시스템이 복잡해질수록 컴포넌트 구조는 더 많은 흐름으로 분리될 것이다.
결론
위 예제를 가져온 이유는 아키텍처 경계가 어디에나 존재한다는 사실을 보여주기 위함이다.
아키텍트로서 우리는 아키텍처 경계가 언제 필요한지를 신중하게 파악해내야 한다. 또한 우리는 이러한 경계를 제대로 구현하려면 비용이 많이 든다는 사실도 인지하고 있어야 한다.
이와 동시에 이러한 경계가 무시되었다면 나중에 다시 추가하는 비용이 크다는 사실도 알아야 한다.
아키텍트인 우리는 어떻게 해야 할까? 정답은 만족스럽지 못하다.
한편으로는 매우 똑똑한 일부 사람들이 우리에게 수년 동안 말해왔듯이, 추상화가 필요하리라고 미리 예측해서는 안 된다. 이것이 바로 YAGNI(You Aren’t Going to Need it)가 말하는 철학이다.
이 문구에는 지혜가 담겨 있는데, 오버 엔지니어링(Over Engineering)이 언더 엔지니어링(Under Engineering)보다 나쁠 때가 훨씬 많기 때문이다.
프로젝트 초반에는 구현할 경계가 무엇인지와 무시할 경계가 무엇인지를 쉽게 결정할 수 없다. 대신 지켜봐야 한다. 시스템이 발전함에 따라 주의를 기울여야 한다. 경계가 필요할 수도 있는 부분에 주목하고, 경계가 존재하지 않아 생기는 마찰의 어렴풋한 첫 조짐을 신중하게 관찰해야 한다.
우리의 목표는 경계의 구현 비용이 그걸 무시해서 생기는 비용보다 적어지는 바로 그 변곡점에서 경계를 구현하는 것이다. 목표를 달성하려면 빈틈없이 지켜봐야 한다.
26. 메인(Main) 컴포넌트
모든 시스템에는 최소한 하나의 컴포넌트가 존재하고, 이 컴포넌트가 나머지 컴포넌트를 생성하고, 조정하며, 관리한다. 나는 이 컴포넌트를 메인(Main)이라고 부른다.
메인 컴포넌트는 가장 낮은 수준의 정책이다. 메인은 시스템의 초기 진입점이다. 운영체제를 제외하면 어떤 것도 메인에 의존하지 않는다. 메인은 모든 팩토리(Factory)와 전략(Strategy), 그리고 시스템 전반을 담당하는 나머지 기반 설비를 생성한 후, 시스템에서 더 높은 수준을 담당하는 부분으로 제어권을 넘기는 역할을 맡는다.
메인은 클린 아키텍처에서 가장 바깥 원에 위치하는, 지저분한 저수준 모듈이라는 점이다. 메인은 고수준의 시스템을 위한 모든 것을 로드한 후, 제어권을 고수준의 시스템에게 넘긴다.
결론
메인을 애플리케이션의 플러그인이라고 생각하자. 메인은 플러그인이므로 메인 컴포넌트를 애플리케이션의 설정별로 하나씩 두도록 하여 둘 이상의 메인 컴포넌트를 만들 수도 있다.
예를 들어 개발용 메인 플러그인, 별도의 테스트용 메인 플러그인, 그리고 또 다른 상용 메인 플러그인을 만들 수 있다.
또한 배포하려는 국가별로, 관할 영역별로, 고객별로 메인 플러그인을 만들 수도 있다.
Reference
- 클린 아키텍처 (로버트 C. 마틴)
book
clean-architecture
component
]