표현
, 응용
, 도메인
, 인프라스트럭쳐
는 아키텍쳐를 설계할 때, 등장하는 전형적인 4가지 영역이다.표현
계층은 컨트롤러
가 위치하며, 사용자(웹 브라우저, 다른 서비스 등)의 요청을 받아 응용
계층에 전달하고, 전달받은 반환 값을 통해 사용자에 응답하는 역할을한다. 응용
계층의 메서드의 호출 인자로 전달하는 역할을 수행하고, 메서드의 반환 값을 사용자가 원하는 형태(ex JSON)로 변환하여 전달하는 역할을 수행한다.응용
계층은 서비스
가 사용자에게 제공해야할 기능을 구현한다. 이러한 기능을 구현하기 위하여, 도메인
계층의 도메인 모델
을 사용한다.응용
계층의 서비스
는 직접 도메인 모델
의 로직을 수행하는 것이 아닌 도메인 모델
에 정의된 로직을 호출하여, 도메인
로직의 수행을 도메인 모델
에 위임한다.도메인
계층은 도메인 모델
을 구현하며, 해당 도메인 모델
이 제공해야하는 핵심 도메인
로직을 메서드로 제공한다. 응용
계층은 이러한 핵심 도메인
로직을 호출한다.인프라스트럭쳐
계층은 구현 기술에 대한 것을 다룬다. 논리적인 개념을 표현하기보다는 기존에 제공되는 실제 구현(ex 라이브러리, 모듈 등)을 통해 외부 시스템과의 연동을 지원한다.레이어드 아키텍쳐
에서 이 4가지 영역(계층)의 역할과 관심사를 분리하는 것이 중요하며, 이를 적절히 설계하는 것은 확장하는 서비스에 대한 유지보수성을 매우 높인다.레이어드 아키텍쳐
에서는 상위 계층에서 하위 계층으로의 의존만이 존재한다.표현
계층은 응용
계층에 의존하고, 응용
계층이 도메인
계층에 의존한다. 그리고, 도메인
계층은 인프라스트럭쳐
계층에 의존한다.인프라스트럭쳐
계층이 도메인
에 의존하거나, 도메인
이 응용 계층에 의존하지는 않는다.응용
계층에서는 도메인
계층만을 의존해야하지만, 외부 연동을 위해 인프라스트럭쳐
계층을 의존하기도 한다.인프라스트럭쳐 계층
에 응용
계층이 종속될 수 있다는 JPA
→ ElasticSearch
)을 사용하기 위해서 특정 계층의 코드를 대거 수정해야한다.고수준 모듈
은 의미가 있는 단일 기능을 제공하는 모듈을 의미한다.
CalculateDiscountService
가격 할인 계산이라는 도메인 기능을 구현 및 제공하는 고수준 모듈이다.저수준 모듈, 즉, 고수준 모듈
에서 필요로하는 여러 하위 기능을 제공하는 모듈을 의미한다.
JPARepository
는 RDBMS
에서 특정 데이터를 읽고 쓰는 기능을 구현 및 제공하는 저수준 모듈
이다.저수준 모듈
이 인프라스트럭쳐
계층의 구성요소만을 의미하지는 않는다. 저수준
, 고수준
은 계층 간의 상대적인 관점에서 발생하는 차이일 뿐이다.고수준 모듈
의 기능을 구현하기 위해서는 저수준 모듈
의 여러 하위 기능을 적절히 조합해야한다.
고수준 모듈
이 직접적으로 저수준 모듈
을 직접 의존한다면, 위에서 언급한 단위 테스트의 어려움과 유연성의 악화를 야기한다.이런 상황에서는 DIP
(Dependancy Inversion Principle, 의존성 역전 원칙)를 활용하여, 저수준 모듈
이 고수준 모듈
에 의존하도록 바꾸면된다.
어떻게 할 수 있을까라는 질문에는 인터페이스
라는 답이 있다.
아래의 그림과 같이, 저수준 모듈
이 제공해야하는 하위 기능을 정의한 인터페이스
를 고수준 모듈
과 같은 계층에 선언하고, 저수준 모듈
이 해당 인터페이스
를 상속받아 기능을 구현하는 것이다.
즉, 고수준 모듈
은 저수준 모듈
이 어떻게 구현되어있는지 알 필요가 없다. 그냥 원하는 기능만 수행해주면 된다.
또한, 응용
계층에서 인프라스트럭쳐
계층을 의존하고 싶을 때에도, 인터페이스
만 응용
계층에 선언하고, 해당 인터페이스
를 인프라스트럭쳐 계층
에서 구현하면 된다.
하지만, 위의 의존-상속의 그림을 위해 철저하게 의존 관계를 지키는 것 또한, 오히려 우리의 최종 목표인 확장 가능한 유연한 개발과 멀어지는 경우도 있다.
유연한을 너무 뽐내버린 통 밖에서도 고통받는 통아저씨를 상상하자.
현재 상황을 보고,DIP
를 지켰을 때, 얻는 이점을 고려하여 적당히 잘 검토해야한다.
통을 나와도 유연함을 뽐내고 있다.
이를 통해, 프로그래머는 해당 인터페이스
를 구현한 대역 객체를 생성하여 단위테스트를 구현 객체 없이 수행할 수 있으며, 구현 객체를 변경하고 싶을 때, 새로운 구현 객체를 변경하고, 코드의 변경을 최소화한 상태로 의존성만 새로이 주입해주면 된다.
엔티티
: 고유의 식별자를 갖는 객체로 스스로의 라이프사이클을 가진다. 도메인 고유의 개념을 표현하며, 이와 관련된 기능을 제공한다.밸류
: 식별자가 없는 객체로, 개념적으로 하나의 값을 표현할 때 사용한다(주소, 금액). 엔티티
의 속성뿐만 아니라, 다른 밸류의 속성으로 사용될 수 있다.애그리거트
: 연관된 엔티티
와 밸류
객체를 하나의 개념적인 단위로 묶은 것이다.레포지터리
: 도메인 모델의 영속성을 처리하며, 도메인 모델의 읽기/쓰기와 같은 기능을 제공한다.도메인 서비스
: 특정 엔티티
에 속하지 않는 도메인 로직을 제공하며, 여러 엔티티
와 밸류
를 조합하여 하나의 기능으로 구현한다.엔티티
를 같은 것으로 취급하지 말아야한다. 연관성을 수 있더라도 100% 같은 개념이 아니다.엔티티
는 데이터와 함께 도메인 기능을 함께 제공한다. 또한, 복합적인 데이터를 밸류
로 처리한다.@Entity
를 사용하여, JPA에 의존하는 도메인 엔티티
로 사용하는 경우가 많긴하며, 이러한 밸류
는 @Convert
를 사용하여 처리하기도 한다.엔티티
는 데이터와 기능을 함께 제공하는 객체다.엔티티
는 기능을 구현하고 캡슐화하여 외부로부터 데이터가 임의로 변경되는 것을 막는다.밸류
는 불변한 객체로 구현할 것을 권장하는 편이다. 변경을 원할 때는 밸류
객체 자체를 교체하는 것을 의미한다.엔티티
와 밸류
가 관여된다. 이는 도메인 모델의 복잡성을 높인다. 애그리거트
다. 애그리거트
: 관련 객체를 하나로 묶은 논리적 집합으로 예를 들어 주문이라는 상위 개념(애그리거트
)에는 주문, 배송지 정보, 주문자, 주문 목록 등이 하위 모델로 포함된다.애그리거트
는 군집 내에 속한 객체들을 관리하는 루트 엔티티
를 가지고, 루트 엔티티
는 애그리거트
에 속한 엔티티
와 밸류
에 접근하거나 활용하여 애그리거트
가 구현해야할 기능을 제공한다.애그리거트
내의 객체들은 루트 엔티티
를 통해 캡슐화된다.애그리거트
를 단위로 이들의 관계를 파악하고, 이를 통해 도메인 모델을 이해하고 구현하게 된다면, 도메인 모델의 큰 그림을 파악하고 관리할 수 있다.애그리거트
를 어떻게 구현하는지에 따라, 구현의 복잡도, 트랜잭션
의 범위, 기술에 대한 제약 등이 변경될 수 있기 때문에, 충분히 많은 고려가 이루어져야한다.레포지터리
는 도메인 객체의 영속성을 물리적 저장소를 통해 관리하는 도메인 모델이다. 엔티티
와 밸류
가 도메인의 요구사항을 통해 도출된 도메인 모델이라면, 레포지터리
는 해당 요구사항의 구현을 위한 도메인 모델이다.레포지터리
는 도메인 객체에 대한 읽기/쓰기에 대한 기능을 정의한다. 일반적으로 사용하는 findById
, save
, delete
등과 같은 메서드를 선언한다.엔티티
, 루트 엔티티
) 단위로 동작한다.레포지터리
를 통해 얻은 도메인 객체를 통해 기능을 실행한다.인프라스트럭쳐
계층에 속하는 저수준 모듈
이다.응용
계층의 서비스
는 의존성 주입을 통해 레포지터리
구현 객체에 접근한다.응용
계층의 서비스
는 트랜잭션
을 활용하여 도메인 객체를 읽거나 쓰는데, 트랜잭션
은 보통 구현 기술에서의 처리에 영향을 받는다.* 위에서 언급했지만, 다시 한 번 간단하게 짚고 넘어간다. 아래에서 나타낸 그림과 같이 계층 구조의 웹 어플리케이션은 동작한다.
1. 표현
계층의 컨트롤러
는 사용자가 요청(ex. HTTP request
)할 때 전달한 데이터 형식을 검증하고, 이를 응용
계층의 서비스
메서드의 전달인자로 변환하여 서비스
로직을 수행하는 메서드를 호출한다.
2. 응용
계층의 서비스
메서드는 로직을 수행하면서, 레퍼지토리
를 활용하여 도메인 모델을 읽어 도메인 로직을 호출하거나, 도메인 객체를 영속적인 물리 저장소에 쓰는 역할을 한다.
* 올바르게 영속적인 물리 저장소에 반영되게 하기 위해 트랜잭션
을 응용
계층에서 관리한다.
3. 응용
계층의 서비스
메서드의 반환 값을 표현 계층의 컨트롤러
로 전달하여 사용자의 요청에 응답한다(HTTP response
).
인프라스트럭쳐
는 상위의 모든 계층들을 지원한다. 현재의 시스템 외부의 다른 시스템 구성요소와의 연결을 담당하며 이를 위한 구현 기술, 프레임워크, 보조기능 등을 구현한다.HTTP 통신
, Kafka
와 같은 메세징 큐
와의 연동, RDBMS
혹은 NoSQL
과 같은 데이터베이스
와의 연결, Redis
와 같은 캐시
와의 연결 등을 지원한다.인프라스트럭쳐
의 저수준 모듈
은 도메인
계층에서 정의된 고수준 인터페이스
를 구현하는 것이 유연
하고 테스트-용이
한 개발에 도움이 된다.
- 그러나, 무조건적으로
인프라스트럭쳐
계층에 대한 의존을 없애는 것이 마냥 좋은 것은 아니다.살을 내어주고 뼈를 때린다는 생각으로@Entity
,@Table
과 같은 JPA 전용 애노테이션을 직접 도메인 모델 클래스에 작성해주는 것이 오히려 빠르고 쉬운 개발에 더욱 도움이 될 경우도 있다.
- 즉, DIP의 장점인 유연함과 테스트-용이함 만큼 구현의 편리함도 중요하기 때문에, 서로의 장점에 있어서 손해보지 않는 선으로 의존성을 가져가는 것이 좋다.
패키지
에 위치하는데, 이에 대한 뚜렷한 정답은 없다(적당히 잘
하면 된다) .애그리거트
가 크면, 여러 하위 도메인 별로 모듈을 나누어 각 모듈 별로 계층 구조로 관리할 수 있다.도메인
계층을 또 여러 도메인 패키지
로 나누어서 관리할 수 있다.