-
엔터프라이즈 애플리케이션 아키텍처 패턴 1 - 계층 구조책책책 책을 읽읍시다/프로그래밍 2023. 3. 12. 01:31
저자 : 마틴 파울러
옮긴이 : 최민석
책 표지 들어가며
흔히 접하는 디자인 패턴이 프로젝트의 수많은 코드 중 특정 문제를 다루기 위한 것이라면 이 책에 나오는 패턴들은 그보다 광범위하게 애플리케이션의 전반적인 문제를 다룬다. 전자의 해결 대상은 나무이고 후자의 것은 숲이다. 그리고 여기서 고민한 패턴들 대부분이 현재에는 프레임워크에 녹여져 있다. 초반부 내용은 JPA와 이를 구현한 Hibernate같은 ORM 기술에 적용되어 있고, 중반부는 MVC 프레임워크에서 찾아볼 수 있다. 지금으로서는 당연한 기술이지만 당시에 치열하게 반복 작업을 줄이려는 노력과 추상화에 힘쓴 덕분에 이렇게 편리하게 개발할 수 있지 않나 싶다. 내용을 하나하나 정리하기에는 비슷한 내용이 많고, 저수준의 프레임워크적인 내용이라 현재도 잘 알려져있거나 이름은 들어보았으나 자세하게 공부하지 못했던 패턴들을 위주로 정리해보았다.
발췌 내용 위주로 아무렇게나 정리
01장 계층화
시스템을 계층으로 분할하면 여러 중요한 이점이 있다.
- 다른 계층에 대한 정보 없이도 단일 계층을 하나의 일관된 계층으로 이해할 수 있다. 예를 들어, 이더넷이 작동하는 방법을 자세히 모르더라도 TCP 기반의 FTP 서비스를 구축할 수 있다.
- 동일한 기본 서비스를 가진 대안 구현으로 계층을 대체할 수 있다. FTP 서비스는 이더넷, PPP 또는 케이블 회사에서 제공하는 다른 프로토콜 기반에서 변경 없이 작동할 수 있다.
- 계층 간의 의존성을 최소화할 수 있다. 케이블 회사에서 물리 전송 시스템을 교체하더라도 IP만 작동하면 FTP 서비스를 변경할 필요가 없다.
- 계층은 표준화하기 좋은 위치다. TCP와 IP는 해당 계층이 작동하는 위치를 정의하기 때문에 표준이 됐다.
- 한 번 구축한 계층은 여러 다른 상위 서비스에서 사용할 수 있다. 예를 들어, TCP/IP는 FTP, 텔넷, SSH, HTTP에서 사용된다. TCP/IP가 없었다면 모든 상위 프로토콜에서 사실상 동일한 하위 프로토콜을 일일이 다시 구축해야 했을 것이다.
계층화는 중요한 기법이지만 단점도 있다.
- 계층은 전체가 효과적으로 캡슐화되지 않는다. 그 결과, 뭔가를 변경했을 때 다른 계층에 영향을 미치는 경우가 있다. 계층형 엔터프라이즈 애플리케이션에서 볼 수 있는 전형적인 예는 UI에 표시해야 하는 필드가 데이터베이스에도 있어야 하므로 그 사이에 있는 모든 계층에 해당 필드를 추가해야 한다는 것이다.
- 계층을 추가하면 성능이 저하된다. 일반적으로 각 계층에서는 정보를 한 표현에서 다른 표현으로 변환해야 한다. 다만, 기반 기능을 캡슐화하면 성능 저하가 보상될 만큼 효율이 향상되는 경우도 많다. 예를 들어, 트랜잭션을 제어하는 계층을 최적화하면 애플리케이션 전체가 빨라지는 효과가 있다.
그러나 계층형 아키텍처에서 가장 어려운 부분은 어떤 계층을 만들고 각 계층이 어떤 역할을 담당할지 결정하는 것이다.
세 가지 주요 계층
프레젠테이션(presentation) 논리는 사용자와 소프트웨어 간 상호작용을 처리한다. 이 계층은 명령줄이나 텍스트 기반 메뉴 시스템처럼 간단한 것일 수도 있지만 리치 클라이언트 그래픽 UI 또는 HTML 기반 브라우저 UI인 경우가 많다(이 책에서 리치 클라이언트는 HTML 브라우저와 구분하기 위한 개념으로서 윈도우/스윙/팻 클라이언트 UI를 의미한다). 프레젠테이션 계층의 주 역할은 사용자에게 정보를 표시하고 사용자가 내린 명령을 도메인과 데이터 원본에서 수행할 작업으로 해석하는 것이다.
계층 역할 프레젠테이션 서비스 제공, 정보 표시(창 또는 HTML), 사용자 요청(마우스 클릭, 키 누름), HTTP 요청, 명령줄 호출, 일괄 작업 API 처리 도메인 시스템의 핵심이 되는 논리 데이터 원본 데이터베이스, 메시징 시스템, 트랜잭션 관리자 및 다른 패키지와의 통신 데이터 원본(data source) 논리는 애플리케이션을 대신해 다른 시스템과 통신한다. 여기서 다른 시스템은 트랜잭션 모니터, 다른 애플리케이션, 메시징 시스템 등일 수 있다. 대부분의 엔터프라이즈 애플리케이션에서 가장 큰 데이터 원본 논리는 지속성 데이터를 저장하는 데이터베이스다.
나머지 논리는 비즈니스 논리라고도 하는 도메인 논리(domain logic)다. 이 논리는 애플리케이션이 수행해야 하는 도메인과 관련된 작업이다. 이러한 작업에는 입력과 저장된 데이터를 바탕으로 하는 계산, 프레젠테이션에서 받은 데이터의 유효성 검사, 프레젠테이션에서 받은 명령을 기준으로 작업 대상이 될 데이터 원본 논리를 결정하는 등의 작업이 포함된다.
도메인 논리 작업에서 가장 어려운 점 중 하나는 도메인 논리와 다른 형태의 논리를 구분하기 어려운 경우가 많다는 것이다. 그래서 필자는 웹 애플리케이션에 명령줄 인터페이스를 추가하는 것과 같이 근본적으로 다른 계층을 애플리케이션에 추가한다고 가정해보곤 한다. 계층을 추가하기 위해 복제해야 하는 기능이 있다면 도메인 논리가 프레젠테이션으로 유출됐다는 신호다. 비슷하게 관계형 데이터베이스를 XML 파일로 대체하기 위해 복제해야 하는 논리가 있는지 생각해볼 수 있다.
09장 도메인 논리 패턴
트랜잭션 스트립트
비즈니스 논리를 프로시저별로 구성해 각 프로시저가 프레젠테이션의 단일 요청을 처리하게 한다.
트랜잭션 스트립트 대부분의 비즈니스 애플리케이션은 일련의 트랜잭션으로 이루어진다. 트랜잭션으로 정보를 특정한 방법으로 정리해서 보여주거나 정보를 변경하는 등의 작업을 할 수 있다. 클라이언트 시스템과 서버 시스템 간의 각 상호작용에는 일정한 양의 논리가 포함된다. 이러한 논리는 데이터베이스에서 가져온 정보를 표시하는 간단한 것일 수도 있고 유효성 검사와 계산을 포함한 여러 단계의 작업일 수도 있다.
트랜잭션 스크립트(Transaction Script)는 이 모든 논리를 단일 프로시저로 구성하고 데이터베이스를 직접 또는 씬 데이터베이스 래퍼를 통해 호출한다. 각 트랜잭션은 자체 트랜잭션 스크립트로 실행되지만, 공통적인 하위 작업은 하위 프로시저로 분할할 수 있다.
트랜잭션 스크립트의 가장 큰 장점은 단순함이다. 작은 규모의 논리가 포함된 애플리케이션에서 자연스럽게 논리를 구성할 수 있는 방법이며, 코드를 실행할 때 발생하는 오버헤드가 적고 코드를 이해하기도 쉽다.
그러나 비즈니스 논리가 복잡해지면 좋은 설계 상태를 유지하기가 점차 어려워진다. 특히 문제가 되는 것은 트랜잭션 간의 코드 중복이다. 트랜잭션 스크립트의 주 목적이 트랜잭션 하나를 처리하는 것이므로 공통적인 코드가 중복되는 경향이 있다.
세심한 팩터링으로 이러한 여러 문제를 완화할 수 있지만 더 복잡한 비즈니스 도메인을 제대로 구현하려면 도메인 모델을 이용해야 한다. 도메인 모들은 코드를 구성하면서 가독성을 높이고 중복을 줄이기 위한 더 다양한 수단을 제공한다.
논리의 복잡도가 어느 수준 이상일 때 도메인 모델이 적합하다고 정확하게 말하기는 어렵다. 특히 둘 중 한 패턴에 익숙하다면 더 미묘하다. 트랜잭션 스크립트 설계를 도메인 모델 설계로 리팩터링하는 것도 가능하지만, 필요 이상으로 어렵다. 따라서 리팩터링보다는 처음부터 도메인 모델로 설계하는 것이 유리하다.
그러나 객체 신봉자라도 트랜잭션 스크립트를 처음부터 배제하는 것은 현명하지 않다. 우리가 해결해야 하는 문제 중에는 단순한 문제도 상당히 많으며, 단순한 문제는 단순한 해결책으로 훨씬 빨리 해결할 수 있다.
도메인 모델
동작과 데이터를 모두 포함하는 도메인의 객체 모델이다.
도메인 모델 비즈니스 논리는 경우에 따라 아주 복잡할 수 있다. 규칙과 논리는 매우 다양한 사례와 동작의 변형을 나타내며, 객체는 이러한 복잡성을 처리하기 위해 고안됐다. 도메인 모델(Domain Model)은 각 객체가 하나의 기업과 같이 복잡하거나 주문서의 내용 한 줄과 같이 간단한, 의미있는 하나의 대상을 나타내는 상호 연결된 객체의 연결망으로 이루어진다.
애플리케이션에서 도메인 모델을 구현하는 과정은 비즈니스 영역을 모델링하는 객체로 구성된 계층을 구성하는 과정이다. 이러한 객체 중에는 일상적인 업무에 사용되는 비즈니스 데이터를 나타내는 객체도 있고 비즈니스 규칙을 나타내는 객체도 있다. 이러한 데이터와 프로세스는 프로세스와 작업 대상 데이터를 가깝게 배치하기 위한 클러스터를 형성한다.
객체지향 도메인 모델은 종종 데이터베이스 모델과 비슷해 보이기도 하지만 실제로 둘 사이에는 차이점이 많다. 도메인 모델은 데이터와 프로세스가 혼합된 구조이고, 다중 값 속성과 복잡한 연결망을 가지며, 상속을 사용한다.
이 때문에 주로 두 가지 형식의 도메인 모델이 사용된다. 단순 도메인 모델은 대부분의 도메인 객체가 각 데이터베이스 테이블과 일치하므로 외형상 데이터베이스 설계와 거의 비슷해 보인다. 반면 리치 도메인 도멜은 상속, 전략, 다양한 [Gang of Four] 패턴, 그리고 복잡하게 상호 연결된 객체의 연결망을 포함하므로 데이터베이스 설계와는 상당히 다르게 보일 수 있다. 리치 도메인 모델은 복잡한 논리를 나타내는 데 적합하지만 데이터베이스에 매핑하기는 더 어렵다. 단순 도메인 모델에는 활성 레코드를 사용할 수 있지만 리치 도메인 모델에는 데이터 매퍼가 필요하다.
비즈니스의 동작은 자주 변경해야 하므로 이 계층을 손쉽게 수정, 구축, 테스트할 수 있게 만드는 일이 아주 중요하다. 이를 위해서는 도메인 모델과 시스템의 다른 계층 간의 결합을 최소화해야 한다. 다른 여러 계층화 패턴에서도 도메인 모델과 시스템의 다른 부분 간의 의존성을 최소화하기 위한 방법들이 많이 사용된다.
도메인 논리와 관련해서 흔히 하는 고민은 도메인 객체가 과하게 비대해지는 것이다. 예를 들어, 주문을 처리하는 화면을 만들 때 일부 주문의 동작은 특정 주문에만 필요할 수 있다. 이러한 동작을 모든 주문에 추가하면 주문 클래스가 특정 사례에 한 번만 사용되는 동작으로 가득 차서 지나치게 커질 수 있다. 이러한 문제를 예방하기 위해 사람들은 어떤 동작이 일반적인지 여부를 먼저 고려한 후, 일반적인 동작은 주문 클래스에 넣고, 특정한 동작은 일종의 사례별 클래스(트랜잭션 스크립트나 프레젠테이션 자체에 해당하는)에 넣는 방법을 생각했다.
그런데 이처럼 특정 사례의 동작을 분리하면 중복이 발생할 우려가 있다. 주문에서 분리된 동작은 찾기 어렵기 때문에 비슷한 동작이 필요할 때 이를 찾아보지 않고 간단하게 중복하는 경우가 많다. 중복은 곧바로 복잡성과 일관성 문제를 일으키지만 객체가 비대해지는 문제는 우려하는 것만큼 자주 발생하지 않는다. 그리고 비대한 객체는 눈에 쉽게 띄며 수정하기도 쉽다. 따라서 특정 사례의 동작을 분리하지 말고 가장 적당한 객체에 모두 넣는 것이 좋다. 그리고 객체가 비대해지고 이것이 문제가 되면 해결하면 된다.
서비스 계층
사용 가능한 작업의 집합을 설정하고 각 작업에 대한 애플리케이션의 반응을 조율하는 서비스의 계층으로 애플리케이션의 경계를 정의한다.
서비스 계층을 포함한 구조 일반적으로 엔터프라이즈 애플리케이션은 저장하는 데이터와 구현하는 논리에 대한 다양한 인터페이스를 필요로 한다. 이러한 인터페이스에는 데이터 로더, 사용자 인터페이스, 통합 게이트웨이 등이 있다. 이러한 인터페이스는 용도는 서로 다르지만 데이터에 접근 및 조작하고 비즈니스 논리를 호출하기 위해 애플리케이션과의 상호작용을 공통으로 필요로 한다. 상호작용은 여러 리소스에 걸친 트랜잭션과 수행할 여러 응답의 조율을 포함하는 복잡한 작업일 수 있다. 상호작용의 논리를 각 인터페이스에서 별도로 인코딩하면 중복이 많이 생긴다.
서비스 계층(Service Layer)은 클라이언트 계층을 인터페이스하는 관점에서 애플리케이션의 경계[Cockburn PloP]와 사용 가능한 작업의 집합을 정의한다. 서비스 계층은 작업을 구현할 때 트랜잭션을 제어하며 응답을 조율하면서 애플리케이션의 비즈니스 논리를 캡슐화한다.
일종의 "비즈니스 논리" : 서비스 계층은 앞서 언급한 특성을 위반하지 않으면서 몇 가지 다른 방법으로 구현할 수 있다. 차이점은 서비스 계층 인터페이스 배후의 역할 할당에서 나타난다. 구현의 다양한 가능성을 설명하기 전에 몇 가지 기본 사항을 정리해보자.
서비스계층은 트랜잭션 스크립트나 도메인 모델과 마찬가지로 비즈니스 논리를 구성하기 위한 패턴이다. 필자를 비롯한 다수의 설계자는 "비즈니스 논리"를 "도메인 논리"와 "애플리케이션 논리"로 나누기를 좋아한다. 도메인 논리는 순수하게 문제 도메인(예: 계약의 수익 인식을 계산하는 전략)을 집중적으로 처리하며, 애플리케이션 논리는 애플리케이션 역할[Cockburn UC](예: 계산된 수익 인식을 계약 관리자와 통합된 애플리케이션에 알림)을 처리한다. 애플리케이션 논리는 종종 "워크플로 논리"라고도 하는데, "워크플로"를 다른 의미로 해석하는 사람들도 있다.
도메인 모델은 전통적인 디자인 패턴을 사용해 복잡성을 관리하고 도메인 논리 중복을 예방한다는 면에서 트랜잭션 스크립트보다 나은 방법이다. 그런데 애플리케이션 논리를 순수 도메인 객체 클래스에 넣으면 두 가지 부작용이 있다. 첫째, 도메인 객체 클래스가 특정 애플리케이션 논리를 구현하고 특정 애플리케이션 패키지를 사용하면 도메인 객체 클래스를 다른 애플리케이션에서 재사용하기 어려워진다. 둘째, 두 가지 종류의 논리를 동일한 클래스에 넣으면, 예를 들어 나중에 애플리케이션 논리를 워크플로 툴로 분리할 필요가 있을 때 다시 구현하기 어렵다. 이러한 이유로 서비스 계층은 각 유형의 비즈니스 논리를 별도의 계층으로 분리함으로써 계층화의 일반적인 장범을 제공하고 순수 도메인 객체 클래스를 애플리케이션 간에 재사용하기 쉽게 만들어준다.
구현의 변형: 구현의 기본적인 두 가지 변형으로 도메인 파사드(domain facade) 방식과 작업 스크립트(operation script) 방식이 있다. 도메인 파사드 방식에서는 서비스 계층을 도메인 모델 위에서 씬 파사드의 집합으로 구현한다. 파사드를 구현하는 클래스는 비즈니스 논리를 전혀 구현하지 않으며, 도메인 모델이 모든 비즈니스 논리를 구현한다. 씬 파사드는 클라이언트 계층이 애플리케이션과 상호작용하기 위한 작업 집합과 경계를 형성하며 서비스 계층의 근본적 특성을 나타낸다.
작업 스크립트 방식에서는 서비스 계층을 리치 클래스 집합으로 구현한다. 이러한 클래스 집합은 애플리케이션 논리는 직접 구현하지만, 도메인 노리는 캡슐화된 도메인 객체 클래스로 위임한다. 서비스 계층의 클라이언트에 제공되는 작업은 여러 스크립트로 구현되며, 이러한 스크립트는 연관된 논리의 특정 주제 영역을 정의하는 한 클래스에 포함된다. 이러한 각 클래스는 애플리케이션 "서비스"를 형성하며 서비스 형식의 이름이 "서비스"로 끝나는 경우도 흔하다. 서비스 계층은 이러한 여러 애플리케이션 서비스 클래스로 이뤄지며, 이러한 서비스 클래스는 각자의 역할과 공통 동작을 추상화하고 계창 상위 형식을 확장해야 한다.
서비스 및 작업 식별 : 서비스 계층 경계에 필요한 작업을 식별하는 과정은 아주 간단하다. 이러한 작업은 서비스 계층 클라이언트의 필요성에 의해 결정되는데, 가장 중요하고 우선적인 서비스 계층 클라이언트는 일반적으로 사용자 인터페이스다. 사용자 인터페이스는 사용자가 애플리케이션으로 수행하려는 유스 케이스를 지원하도록 설계되므로 서비스 계층 작업을 식별하는 시작점은 애플리케이션의 유스 케이스 모델과 사용자 인터페이스 설계다.
엔터프라이즈 애플리케이션의 유스 케이스는 도메인 객체를 대상으로 하는 조금 지루한 "CURD"(생성, 읽기, 갱신, 삭제) 유스 케이스인 경우가 많다. 즉, 도메인 객체를 만들고 이러한 객체의 컬렉션을 읽거나 업데이트하는 작업이다. 필자의 경험에 비춰보면 CRUD 유스 케이스와 서비스 계층 작업 간에는 거의 항상 일대일 대응 관계가 있다.
반면 이러한 유스 케이스를 실행해야 하는 애플리케이션의 역할은 전혀 지루하지 않다. 유효성 검사는 물론이고, 애플리케이션에서 도메인 객체를 생성, 업데이트, 삭제할 때 다른 사람이나 다른 통합된 애플리케이션에 알려야 하는 경우가 증가했다. 이러한 응답은 서비스 계층 작업에 의해 조율되고 트랜잭션을 통해 원자성을 유지해야 한다.
예제: 수익 인식
이 예제는 작업 스크립트 방식으로 서비스 계층을 활용해 애플리케이션 논리를 스크립팅하는 방법과 서비스 계층 작업에서 도메인 논리를 위임하는 방법을 알아본다.
작업 스크립트 방식 서비스 계층 다이어그램 public class ApplicationService { protected EmailGateway getEmailGateway() { //EmailGateway의 인스턴스를 반환 } protected IntegrationGateway getIntegrationGateway() { //IntegrationGateway의 인스턴스를 반환 } } public interface EmailGateway { void sendEmailMessage(String toAddress, String subject, String body); } public interface IntegrationGateway { void publishRevenueRecognitionCalculation(Contract contract); } public class RecognitionService extends ApplicationService { public void calculateRevenueRecognitions(long contractNumber) { Contract contract = Contract.readForUpdate(contractNumber); contract.calculateRecognitions(); getEmailGateway().sendEmailMessage( contract.getAdministratorEmailAddress(), "RE: Contract #" + contractNumber, contract + " has had revenue recognitions calculated." ); getIntegrationGateway().publishRevenueRecognitionCalculation(contract); } public Money recognizedRevenue(long contractNumber, Date asOf) { return Contract.read(contractNumber).recognizedRevenue(asOf); } }
'책책책 책을 읽읍시다 > 프로그래밍' 카테고리의 다른 글
엔터프라이즈 애플리케이션 아키텍처 패턴 3 - 데이터 전송 객체, 기본 패턴 (0) 2023.03.14 엔터프라이즈 애플리케이션 아키텍처 패턴 2 - 객체&관계형 매핑, 리포지토리, 프런트 컨트롤러 (5) 2023.03.13 SQL AntiPatterns : 개발자가 알아야 할 25가지 SQL 함정과 해법QL (0) 2023.02.23 오브젝트 6 - 다형성, 서브타이핑, 일관된 협력 (0) 2023.02.14 오브젝트 5 - 상속 vs. 합성 (0) 2023.02.14