새로운 Domain Layer의 캡슐화

김상현
7 min readOct 27, 2021

--

Image credit : https://unsplash.com/photos/OTDyDgPoJ_0

여기, Domain Layer를 캡슐화하기 위한 새로운 제안이 있습니다.

Plastic 프로젝트를 소개합니다.

현재 우리는 프로그래밍 역사에 의해 도메인을 중심으로 설계하고, 이것을 위한 세부 사항들을 외부 종속성으로 여기는 것이 매우 당연하다는 것을 깨달았습니다. DDD, EBI Architecture, Clean Architecture와 같은 많은 애플리케이션 설계 주제들이 모두 그것을 이야기합니다. 이것에 대해서는 이미 많은 자료들이 있기에 더 깊은 이야기를 하지 않겠습니다.

그리고 언급된 내용을 구현하려는 애플리케이션들은 대개 아래와 같은 모습으로 구현됩니다.

그림상 오른쪽에 배치된 B타입의 API Layer는 Domain Service Layer라 불리울 수 있습니다. Domain 개체들의 Ochestration을 위한 용도일 수 있습니다.

제가 소개하려는 Plastic은 이 API Layer를 지원하기 위한 프로젝트입니다.

Plastic은 Command Pattern, Source Generator, PipelineContext… 이 3가지를 내세워 효과적으로 Domain Layer를 캡슐화하고 다양한 Interface로의 노출을 지원합니다.

Source Generator를 통한 소스 코드 자동화는 매우 제한된 영역에서 사용되며 적용을 최소화하였습니다.

Command Pattern

Command Pattern은 개체 자체가 명령이라는 단순한 Design Pattern 입니다.

일반적으로 Application은 사용자에게 특정 명령을 노출하고 지정받기에 이 Command Pattern으로 표현하기에 매우 효과적입니다. 읽기, 쓰기, 지우기 등 이 모든것들은 하나의 Command가 될 수 있습니다.

또한 각 Command는 Single Transaction을 가지기에 Domain Layer를 캡슐화하기 위한 단위로 매우 적절합니다. 이러한 내용은 이미 Usecase 혹은 Interactor와 같은 이름으로 불리워 구현되어 있긴 합니다.

허나, Plastic은 몇가지 차이점을 가집니다.

우선 명시적 종속성을 위반하지 않습니다. CQRS의 구현에서 자주 사용되는 Request & Response 는 소스 코드의 유연함을 위해 Mediator Pattern을 통해 구현되곤 합니다. 그렇지만 이는 명시적 종속성을 위반합니다. 해당 개체가 운용되기 위해 필요로 하는 종속성을 숨기는 것을 이야기합니다. Microsoft는 이에 대해 꽤 요긴한 자료를 게시하였습니다. 이곳을 참고하세요.

Plastic이 생성하는 Command는 생성자를 통해 명시적으로 주입됩니다.

class AddController : ControllerBase
{
public AddController(AddCommand addCommand)
{

var result = addCommand.Execute( … );
}

Mediator Pattern의 또 다른 불편은 항상 명령에 대한 파라메터 개체를 새로 생성해야 한다는 것입니다. Plastic은 Command 작성시 Generic으로 파라메터 타입을 지정할 수 있습니다. 물론 Tuple의 사용도 가능합니다.

또한 Plastic에서의 Command는 실행과 유효성 검사를 분리하는 방법으로 `유효성 검사`를 지원합니다.

아래는 Command 기본 추상체입니다.

public interface ICommandSpecification<in TParam, TResult>
{
Task<TResult> ExecuteAsync(
TParam param, CancellationToken token = default);
Task<Response> CanExecuteAsync(
TParam param, CancellationToken token = default);
}

특이한 점으로는 구현체의 내부에 있습니다. 사용자 `CanExecuteAsync(…)`를 원하는 순간에 호출할 수 도 있지만… 해당 함수는 `ExecuteAsync(…)`를 호출할때 내부에서 가장 먼저 호출 되는 함수입니다.

이것은 실행 전 유효성 검사를 강제하기 위함입니다.

Source Generator

자동화가 필수인 시대에서 Source code 조차 자동화를 할 수 있는 Source Generator는 기존 전통 프로그래밍에서 불가능한 했던 많은 경로를 열어주었습니다. (물론 이러한 메타프로그래밍은 이전부터 존재했습니다.)

Plastic은 Source Generator를 활용하여 사용자를 지원합니다.

수많은 Command들을 일일이 IoC Container (ServiceCollection)에 등록하지 않아도 좋습니다. Command를 작성하는것만으로 이미 생성자를 통해 주입할 수 있는 준비가 되어 있을 것입니다.

물론 컴파일 시점에 처리됩니다.

그러나 Plastic에서 Source generator의 사용은 매우 제한적입니다. 메타프로그래밍은 패러다임마저 무시할 수 있는 매우 강력한 힘이기에 엄격한 관리 대상입니다. 좋지 않은 사례로 Spring framework의 ‘@Autowired’를 이야기할 수 있습니다. Plastic은 소비측의 패러다임, 질서, 규칙등을 무시하는 메타프로그래밍을 허락하지 않습니다. 또한 최소로 사용합니다.

Spring famework의 ‘@Autowired’는 private 멤버 필드로 주입을 허용합니다.

PipelineContext

특정 명령과 Binding된 경로 사이에 Pipeline을 구비하고 매회 직렬 실행시키는 아이디어는 Asp.net에서 비롯하여 MediatR까지 유지되며 매우 효과적인 결과를 보여주었습니다.

Plastic 또한 이러한 Pipeline을 가지고 있습니다. Plastic에서의 특징은 Source Generator를 통한 `PipelineContext`를 각 Pipe에 제공한다는 점입니다.

public class PipelineContext
{
public readonly object? Parameter;
public readonly Type CommandSpec;
public readonly IReadOnlyList<object> Services;
...

각 Pipe들은 이러한 context를 가지고 추가적인 로직을 수행할 수 있는데 재미난점은 Services 속성에 있습니다.

Services는 실행 목표가 된 Command의 생성자로 주입될 종속성 개체의 인스턴스들입니다.

각 Pipe는 이러한 인스턴스들을 커맨드 수행 전,후에 접근하여 보다 다양한 로직을 수행할 수 있습니다. 가장 대표적인 시나리오는 Single Business Transaction입니다. Command로 인한 종속성의 변경 사항을 한번에 처리할 수 있으며 실패할 경우 PipelineContext를 통해 Command 실행 결과를 실패로 처리할 수도 있습니다.

마치며

Plastic 프로젝트는 이제 막 시작되었습니다.

이번 포스트에서 소개된 내용은 Plastic에서 사용 가능한 최소 단위입니다. 향후 Plastic.Bindings를 통해 다양한 Interface로 (CLI, GUI, Web API, RPC) 손쉽게 노출하는 것을 지원할 예정입니다.

감사합니다.

--

--