요즘은 구글링을 통해서 아주 작은 노력(?)과 컴퓨터만 있으면 웹 사이트를 뚝딱 만들 수 있다. PHP 나 Ruby 또는 Nodejs를 이용하며 순식간에 웹 서버를 개발할 수 있다.
시스템을 "동작"하도록 만드는 것은 그리 어려운 일은 아니다. 하지만 작성된 코드를 본다면 그다지 깔끔하지 않을 것이다. 이러한 상태에서는 유지보수나 신규 기능 추가는 엄청나게 어려운 일이 될 것이다. 그리고 밤낮 상관 없이 코드 앞에 앉아 코드를 수정하는 사람이 있어야 할 것이다.
하지만 제대로된 소프트웨어를 만들어 낸다면 위에 같은 일은 없을 것이다. 아무리 큰 소프트웨어라도 소수의 프로그래머만으로 프로그램이 지속적으로 동작하도록 만들 수 있을 것이다.
나는 그런 개발자(or 프로그래머)가 되고 싶다. 제대로된 소프트웨어를 만들 수 있는 개발자. 그런 개발자가 되기 위해서 나는 CLEAN 시리즈 책을 선택했고, 이 책을 사서 읽는 것이 나에게는 하나의 노력이자 미래의 유진호라는 개발자에게 투자다.
이 책을 완독하고 완벽하게 이해하는데까지 얼마의 시간이 소요될지는 모르겠지만... 틈틈히 책을 읽으면서 Clean Architecture의 내용을 내 것으로 정리할 예정이다. 많이 부족하지만 즐겁게 읽어주기를 바란다.
도메인 주도 설계
그렇다면 도메인은 무엇일까? 우리가 사는 세상에서 사건(이벤트)이 발생하는 집합이라고 생각하면 된다. 쇼핑몰을 한번 예를 들어보자. 쇼핑몰에서는 고객들이 주문하는 도메인(Order Domain)이 있고, 재고를 관리하는 도메인(Inventory Domain) 등이 있을 수 있다. 이러한 여러가지 도메인들이 서로 상호작용하며 설계하는 것이 바로 도메인 주도 설계 아키텍처이다.
이 도메인 주도 설계에서의 특징은 같은 객체(Object or Class)가 여러 개가 존재할 수 있다. 주문 도메인에서의 Product Entity은 고객에게 팔기 위한 정보(product_name, prduct_price, etc)들을 담고 있다. 하지만 재고관리 도메인에서는 Product Entity가 다르게 정의되어 있을 것이다.
한 줄로 요약하면 Context(문맥 또는 도메인에 따라서(ex. Custom과 Inventory 처럼))에 객체가 가진 데이터는 달라질 수 있다.
아키텍처를 설계할 때 전형적인 영역이 Presentation(표현) / Application(응용) / Domain(도메인) / Infrastructure(인프라스트럭처) 4가지 영역으로 나뉜다.
Presentation(= Controller or Router and Handler, Web Layer)
사용자의 요청(Request)을 받고 해석해서 Application 영역에 전달하고 Application 영역의 처리 결과를 다시 사용자가 이해할 수 있는 형식으로 변환해서 응답하는 역할을 수행한다.
Application(= Service, Service Layer)
사용자에게 제공해야할 기능(Service)을 구현한다. 이 기능을 구현하기 위해서 Domain 영역의 도메인 모델(Entity)을 사용하거나 Repository를 DI 받아서 호출한다. Application 서비스에서 로직을 직접 수행하기 보다는 도메인 모델에 비즈니스 로직(Repository) 수행을 위임한다.
일반적으로 Controller와 Dao(Repository)의 중간 영역에서 사용된다.
@Transactional
이 사용되어야 하는 영역이다.
Domain-Model 계층에 의존적이면 Domain-Model에 정의된 것들을 사용할 수 있다. 예를 들면, entity나 Repository 인터페이스
Domain(= Business Logic)
핵심로직(비즈니스 로직)을 구현한다.
우리가 흔히 알고 있는 Entity, DAO(Data Access Object), DTO(Data Transfer Object)가 도메인 별로(member, custom, pay, chat, ... etc) 정의된다.
실제 Database와 같이 데이터 저장소에 접근하는 영역이며, Repository 인터페이스를 호출하여 DB의 접근 후 데이터를 조회/수정/삭제 하며 결과를 Entity 클래스에 담아서 Repository에 전달해주는 계층이다.
Entity와 DTO의 분리이유
Entity는 실제 DB의 테이블과 매칭되는 클래스이다. Entity 클래스가 수정되면 여러 클래스에 영향을 끼치게 된다.(커플링이 심한 친구) 반면에, View와 통신하는 DTO클래스는 자주 변경되므로 분리해서 사용
Domain Entity, Aggregate-Roots and Value-Objects
VO vs DTO
VO는 DTO와 동일한 개념이지만 read only 속성을 갖는다.
VO는 특정한 비즈니스 값을 담는 객체(비즈니스 값이라는게 최종 Response 값을 지칭하는 것 같다.)이고, DTO는 Layer 간의 통신 용도로 오고가는 객체을 말한다.
Repository Interfaces(ORM)
Infrastructure
구현기술에 대해 다루는 영역
-DB연동, MQ의 메시지 송수신, Restful API호출 등
육각형 형태로 디자인 된 아키텍처로 유연함을 보여주며, 포트와 어댑터를 사용하는 것이 큰 특징이다.
Layer 간 원치 않은 종속성과 비즈니스 로직으로 인한 사용자 인터페이스 코드의 오염과 같은 객체 지향 소프트웨어 설계의 구조적 함정을 피할 수 있는 아키텍처이다.
Ports
DI를 위한 추상화 인터페이스
Adapters
포트를 통해 인프라와 실제로 연결하는 부분만 담당하는 구현체
Domain Model
실제 핵심 비즈니스 로직을 처리하는 부분
Domain Service(Use case)
도메인 모델과 어댑터를 이용해서 비즈니스 로직과 인프라를 오케스트레이션하는 Dumb한 레이어
기본적인 데이터의 흐름
Client → Adapter → Port를 통해 적절한 Application Service에 전달 → Domain Model → if(DB connection || External API) 오른쪽에 있는 Adapter를 통해 처리 → if(isFeedback) 외부 처리 완료 후, 다시 Application Service는 Domian Model의 처리 결과를 받아 다시 Client에게 응답한다.
Adapters
Adapter는 두 가지 종류가 있다. 사용자의 요청을 받아들일 때 사용하는 Primary adapter와 도메인 모델의 처리에 사용되는 Secondary adapter가 있다.
in/web/Primary adapter
웹 서비스로 사용될 경우에는 Controller가 Primary adapter 역할을 한다. 사용자의 요청을 받는 컴포넌트 역할을 한다.
사용자로부터 입력 받은 값의 유효성을 체크하고 전처리를 할 수 있다.
out/persistence/Secondary adapter
외부(External; Application을 하나의 독립적인 공간으로 인식해야 한다. 그래서 Application를 제외한 모든 것들은 External이라고 보면 좋을 것 같다.)와의 통신이 필요한 도메인 모델을 연결 할 수 있는 Adapter 역할을 한다.
Primary adapter와 동일하게 외부와의 통신에 대한 구현만 담당한다.
DB에 저장된 값을 조회하기 위해서 사용되는 PersistenceAdapter에게 요청을 보내야 한다.
Application
Port
In(w/ UseCase)
단순한 인터페이스이며, 비즈니스 로직에 필요한 UseCase 인터페이스를 정의한다.
구현은 Service 레이어(?)에서 된다. 이렇게 나누는 이유는 Service에 대한 기능적인 확장이 발생했을 경우, 기존 코드의 수정하지 않고 런타임 시, Adapater가 의존하는 Service 인스턴스만 변경하도록 하기 위함이다.[OCP]
Out
단순한 인터페이스이며, 외부에 데이터를 요청하는데 사용되는 인터페이스를 정의한다. 기존 Layerd 아키텍처에서는 Service 계층이 Repository 구현체에 의존하고 있었다면, Hexagonal 아키텍처에서는 Service 계층이 데이터를 요청하는 인터페이스에 의존한다. 구현체에 의존하지 인터페이스에 의존하기 때문에 변경에 더 유연하다.[OCP]
Service
UseCase를 통해 비즈니스 로직을 구현한다. 그리고 처리하는 비즈니스 로직에서 DB 조회 또는 External API 호출이 필요하면 외부와의 통신을 위해서 Port/Out에 정의한 인터페이스를 사용하여 로직을 수행한다.
Domain
Domain Model에서 사용되는 DTO(Data Transfer Object)를 정의하는 패키지