본문 바로가기
이슈와해결

마이크로서비스 아키텍처에서 하나의 도메인 서비스에 다른 도메인이 필요하다면 ?

by Renechoi 2023. 12. 13.

이 글의 탄생 배경

 

회사에서 작은 회의를 하다가 패키지 구조에 대한 이야기를 한 적이 있다. 패키지 구조 설정에 있어서 "도메인 위주로 할 것이냐 레이어 위주로 할 것이냐"에 대한 논의였다. 그 중, 한 프로젝트 내에서 다른 도메인으로 구분되는 경우, 각 도메인들은 어떻게 통신을 해야하느냐 부분에서 열띤 토론을 한 경험이 있다.

 

회사의 아키텍처는 마이크로서비스 아키텍처 기반의 사가 패턴이 기본 기조이다. 따라서 서비스는 크게 도메인 서비스와 오케스트레이션 서비스로 나뉘어 움직인다. 이 말인 즉슨... 핵심이 되는 도메인 별로 서비스가 개별 프로젝트로 나뉘고 따라서 도메인 서비스는 일반적으로 패키지 구조가 하나의 응집력 있는 계층 구조를 형성한다. 그렇기 때문에 도메인 서비스에 다른 도메인이 있다? 조금 이례적인 질문이지만 그럼에도 또 나올 법한 질문이기도 했다.

 

도메인 주도 설계의 저자인 에릭 에반스는 책에서 "유연하고 풍부한 지식이 담긴 설계를 만들려면 다용도로 사용할 수 있는 팀의 공유 언어와 그 언어에 대한 활발한 실험이 필요하다."고 말하면서 유비쿼터스 언어가 얼마나 잘 공유되고 사용되는가가 성공적인 프로젝트의 핵심 요소라고 이야기했다. 작은 궁금증에서 시작된 물음에 꼬투리를 잡고 아래와 같은 글을 써서 팀내에 공유했었다. 글의 목적은 어느 것이 맞다 틀리다를 논하기 보다 지식의 회색지대를 좁히고 공통 언어의 영역을 넓혀서 공통과 차이를 잘 이해해보자는 것이였다. 그때 쓴 글을 조금 각색해서 다시 정리해보았다. 용어나 코드는 다른 것으로 각색하거나 추상화하였다.

 


 

commons 수립하기

 

아키텍처 논의에 앞서 이견이 없었던 부분은 다음과 같다고 생각합니다.

  1. 소프트웨어 프로젝트는 먼저 도메인과 도메인 로직에 집중해야 한다.
  2. 아키텍처를 고민하는 이유는 비즈니스 모델을 잘 표현하기 위함이다.

즉, 비즈니스 요구사항을 만족시킨다는 대전제 하에, 안정적이면서도 유연한 설계를 목표로 하는 것입니다.

 

어떤 질문과 어떤 답변

한 프로젝트 내에 서로 다른 도메인이 나뉘는 경우 통신을 어떻게 해야 하나요?

답변 1:

파사드에서 해당 서비스들을 의존하고 해당 서비스에 위임된 역할을 호출하여 조합합니다.

답변 2:

파사드에서 다른 파사드를 의존하고 해당 파사드에 필요한 내용을 요청합니다.

AService를 예로 들어 설명해보겠습니다. AService에는 ADomainBDomain이라는 하위 도메인이 존재합니다. 이때 ADomain에서 BDomain 엔티티의 정보가 필요한 경우 어떻게 해야 할까요?

 

첫 번째 답변의 경우, ADomainFacadeBService 의존을 추가합니다.

 

public class SimpleADomainFacade implements ADomainFacade {

    private final ADomainCrudService aDomainCrudService;
    private final BService bService; // BService 의존성 추가

    // ADomain에서 BDomain 정보를 가져와 조합합니다.
    public BDomainResponse somethingToDoWithBDomain(String aDomainId) {
        // ADomainId를 사용하여 BDomainId를 찾는 로직
        String bDomainId = findBDomainIdByADomainId(aDomainId);

       // 가져온 정보로 무언가를 조합...
    }
}

 

반면 두 번째 답변의 경우 service가 아니라 facade를 의존성으로 추가할 것입니다.

 

public class SimpleADomainFacade implements ADomainFacade {

    private final ADomainCrudService aDomainCrudService;
    private final BFacade bFacade; // BFacade 의존성 추가

    // ADomain에서 BDomain 정보를 가져와 조합합니다. 
    public BDomainResponse somethingToDoWithBDomain(String aDomainId) {
       public BDomainResponse somethingToDoWithBDomain(String aDomainId) {
        // ADomainId를 사용하여 BDomainId를 찾는 로직
        String bDomainId = findBDomainIdByADomainId(aDomainId);
       // 가져온 정보로 무언가를 조합... 
    }

 

어떻게 보면 코드 한 줄의 차이입니다.

 

여기서 잠깐...!

그런데 이런 의문이 들 수 있습니다.

마이크로서비스 아키텍처로 구분하는 이유가 도메인 별로 최대한 쪼개어서 최소한의 컴포넌트 수준의 도메인 서비스를 구성하기 위함인데, 그렇게 구성된 도메인 서비스 안에 또 다른 도메인이 존재한다니... 그러면 애초에 설계가 잘못된 거 아니냐?!

 

그럴 수도 있고 아닐 수도 있는 것 같습니다.

 

분명 Adomain은 MSA 서비스에서 구성할 수 있는 요금제 관련 최소 단위 도메인이 맞을 것입니다. 그런데 Adomain 내부에는 존재하는 하위 도메인인 기능1, 기능1 템플릿, 기능 1 연관 템플릿 등 분명 각각의 다른 비즈니스 요구사항을 만족시키는 명확한 책임과 역할을 갖고 있습니다.

 

비즈니스 요구사항의 변동성과 복잡도에 따라 이와 같은 상황은 충분히 존재할 수 있는 상황 같습니다. 그래서 이 논의에서는 '그럴 수 있다' 정도로 답변을 정리하면 좋겠습니다.

 

다양한 아키텍처와 우리의 아키텍처

좀 더 논의를 이어가기 전에 먼저 아키텍처 패턴에 대한 '일반적인 지식'을 좀 더 살펴보고 갔으면 합니다. 일반적으로 아키텍처는 다음과 같은 3가지로 나뉠 수 있을 것입니다.

 

1. 레이어드 아키텍처

  • 정의: 소프트웨어를 명확하게 분리된 여러 계층으로 구성하는 아키텍처 패턴. 각 계층은 특정한 역할과 책임을 가지며, 상위 계층은 하위 계층에 의존.
  • 구성: 일반적으로 프레젠테이션(또는 UI) 레이어, 비즈니스 로직 레이어, 퍼시스턴스(또는 데이터 액세스) 레이어, 데이터베이스 레이어 등으로 구성.
  • 특징:
    • 모듈성: 각 레이어는 독립적으로 개발 및 유지보수가 가능하며, 변경 사항이 다른 레이어에 미치는 영향을 최소화.
    • 재사용성: 하위 레이어의 컴포넌트는 다양한 상위 레이어에서 재사용될 수 있음.
    • 유연성: 각 레이어는 다른 레이어와의 명확한 인터페이스를 통해 상호작용하므로, 한 레이어를 변경하거나 교체하는 것이 비교적 용이.
    • 테스트 용이성: 각 레이어는 독립적으로 테스트할 수 있어, 유닛 테스트 및 통합 테스트가 용이.

 

https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html

 

2. 클린 아키텍처

  • 정의: 로버트 마틴(Robert Martin)이 제안한 아키텍처로, 원형의 계층 구조를 가짐.
  • 구성: 가장 바깥쪽에 UI 컴포넌트나 데이터베이스가 위치하고, 그 안쪽에 컨트롤러, 유즈케이스, 그리고 중심에 도메인 모델이 위치함.
  • 특징: 의존성이 내부로 향하며, 도메인 모델을 외부 변경으로부터 보호함.

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

3. 헥사고날 아키텍처

  • 구성: 외부에 어댑터(Adapter)가 있고, 그 다음으로 포트(Port), 중심에는 도메인 모델이 위치함.
  • 특징: 클린 아키텍처와 유사하게 내부 지향적 의존성을 가지며, 도메인 모델을 보호함.
  • 장점: 구조가 더 명확하며, 어댑터와 포트를 통해 구체적인 작업을 할 수 있음.

https://medium.com/@luishrsoares/whats-hexagonal-architecture-6da22d4ab600

 

 

최근에는 '컴포넌트 아키텍처'도 등장했는데, ‘만들면서 배우는 클린 아키텍처’의 저자 톰 홈버그(Tom Hombergs)가 Spring I/O 2022 세션에서 '레이어 기반이 아닌 컴포넌트로 만들자(직역 내용 - 원제: Let's build components, not layers by Tom Hombergs @ Spring I/O 2022)'라는 제목의 발표를 하면서 컴포넌트 기반 아키텍처에 대한 논의도 있는 듯 합니다.

 

우리의 아키텍처

우리의 아키텍처는 문서에 정의되어 있는데, Facade Layer Policy로서 사실상 레이어드 아키텍처인데, 조금 다른 점은 비즈니스 레이어 하위에 도메인 (혹은 서비스) 레이어와 인프라 레이어를 둔다는 점입니다. 레이어드 아키텍처에서 비즈니스 하위의 레이어를 퍼시스턴스와 데이터베이스 레이어로 나누는 표현에서 조금 다르지만 맥락은 비슷하게 사용할 듯 합니다.

 

공통의 목표

각 아키텍처에 대해 더 딥다이브 해볼 수 있지만 불필요하게 길어질 테니 생략하고... 중요한 포인트만 짚어보자면 이 부분이 아닐까 합니다. 즉 어떤 아키텍처든 목표로 하는 것은 도메인 레이어를 보존하고 쉽게 변화하는 부분에 대한 의존성을 최소화하는 것이라는 점입니다.

 

로버트 C. 마틴은 '클린 아키텍처'에서 이에 대해서 “소스 코드 의존성은 반드시 안쪽으로, 고수준의 정책을 향해야 한다."라는 언어로 표현합니다. 좋은 시스템 아키텍처란 의존성 규칙을 준수하면서 고수준의 정책을 저수준의 정책으로부터 잘 분리하는 것이 목표라고 이야기합니다. '고수준' '저수준'은 그럼 어떻게 정의하는가에 대한 명확한 정의는 없지만, 책의 내용에 따르면 대략적으로 다음과 같이 이해해볼 수 있을 것 같습니다.

  • 고수준 : 추상화의 영역, 비즈니스의 요구사항을 일반 언어로 표현하는 개념
  • 저수준 : 추상화 레벨의 명세를 어떻게 구현할 것인가에 대한 영역
  • 예시:
    • 제품을 출시한다 -> 특정 제품을 특정 가격과 매핑하여 어떤 플랫폼에 출시한다.
    • 데이터를 저장한다. -> 카우치베이스에 어떠한 key 정책을 갖고 저장한다.

대부분의 아키텍처는 비즈니스 로직(고수준)이 세부 구현(저수준)에 영향을 받지 않는 아키텍처를 구성하는 것을 지향하며, 이를 구현하는 아키텍트들도 같은 지향점을 갖고 있을 것이라 생각합니다.

 

차이의 시작점

그런데 그렇다면 왜 의견 차이가 발생하는 것일까요? 다시 문제 상황으로 돌아와서, 왜 우리는 같은 질문에 대해 다른 답변을 내놓는 것일까요?

 

그리고 다른 답변, 다른 의견, 다른 생각... 중에 정답과 아닌 것들이 존재할까요?

 

같은 언어를 쓰면서도 다른 이야기를 하고 있던 이유

저는 3가지 이유를 생각해보았습니다.

  • 이유 1: 개념에 대한 인식 차이
  • 이유 2: 아키텍처에 대한 다양한 해석
  • 이유3: 비즈니스 요구사항에 대한 이해도 차이

 

예를 들면...

예를 들어 '파사드'라는 용어를 사용하면서 서로 다른 의미를 생각하고 있었습니다. 제가 생각한 '파사드'는 건축에서의 '정면'이라는 의미에 초점을 맞추었고, 이는 비즈니스 요구사항을 명세하는 역할로 해석했습니다. 이러한 관점은 클린 아키텍처에서 언급하는 '유스케이스'와 유사합니다. 예를 들어, '파사드'가 특정 비즈니스 프로세스를 명세하고, 이를 통해 시스템의 다른 부분과의 상호작용을 정의한다고 볼 수 있습니다.

 

public class BillingFacade {
    // 비즈니스 프로세스를 명세하는 파사드
    private final PaymentService paymentService;
    private final InvoiceService invoiceService;

    public BillingResponse processBilling(BillingRequest request) {
        // 결제 및 청구서 처리 로직
        PaymentResult paymentResult = paymentService.processPayment(request.getPaymentDetails());
        Invoice invoice = invoiceService.createInvoice(request.getInvoiceDetails(), paymentResult);

        return new BillingResponse(invoice, paymentResult);
    }
}

 

이 경우, '파사드'는 특정 비즈니스 프로세스를 명세하고, 시스템의 다른 부분과의 상호작용을 정의하는 역할을 합니다. 이는 비즈니스 로직의 추상화와 명세화에 중점을 둔 접근 방식입니다.

 

반면, 다른 관점에서 '파사드'는 서비스 레이어에서 이미 정의된 '비즈니스 규칙'을 조합하는 역할에 더 집중할 수 있습니다. 이 경우, '파사드'는 서비스 레이어의 기능을 조합하여 더 복잡한 비즈니스 프로세스를 구현하는 데 사용됩니다. 이러한 접근 방식은 레이어드 아키텍처에서 의도하는 '각 레이어는 계층상 아래에 위치한 레이어에만 의존한다는 원칙을 잘 준수하는 구현이 될 것입니다.

 

public class OrderFacade {
    // 서비스 레이어의 기능을 조합하는 파사드
    private final ProductService productService;
    private final OrderService orderService;

    public OrderResponse placeOrder(OrderRequest request) {
        // 제품 확인 및 주문 처리 로직
        Product product = productService.getProductById(request.getProductId());
        Order order = orderService.createOrder(product, request.getQuantity());

        return new OrderResponse(order);
    }
}

 

이 경우, '파사드'는 서비스 레이어의 기능을 조합하여 복잡한 비즈니스 프로세스를 구현하는 데 사용됩니다. 이는 레이어 간의 명확한 분리와 각 레이어의 책임에 중점을 둔 접근 방식입니다.

 

두 번째 접근 방식에 따르면 어떤 파사드의 요구에 따라 생겨난 비즈니스 규칙이 본 도메인의 파사드에는 존재하지 않는 경우가 생겨날 수 있을 것입니다. 만약 첫 번째 의미 즉, 파사드가 유스케이스를 명세하는 역할이라면 이러한 상황은 파사드와 서비스 간의 비대칭으로 해석됩니다. 그러나 두 번째 의미라면 이 역시도 특별한 상황은 아닐 것입니다. 또한 자신이 사용하지 않는 인터페이스에는 의존하지 않아야 한다는 ISP 원칙도 지킬수도 있겠지요.

 

아키텍처에 대한 이해 및 적용 방식에 대한 차이도 생각해볼 수 있습니다. 각자가 선호하거나 익숙한 아키텍처 패턴에 따라 동일한 문제에 대한 접근 방식이 달라질 수 있습니다. 예를 들어, 레이어드 아키텍처를 선호하는 개발자는 시스템을 계층적으로 보고 각 레이어의 역할과 책임에 중점을 둘 수 있는 반면, 클린 아키텍처나 헥사고날 아키텍처에 익숙한 개발자는 도메인 중심의 설계와 의존성 규칙에 더 많은 중점을 둘 수 있습니다.

 

무엇보다도 각 아키텍처에서 각자가 중요하게 생각하는 부분들만을 따와서 이른바 '커스텀 아키텍처'를 생각하고 있는 점도 큰 이유가 될 것입니다. 어찌되었든, 아키텍처가 무엇이다 라는 정의는 있지만 그것이 반드시 그래야 한다라고 생각하는 경우는 드물고 또 그것이 맞지도 않을 테니까요.

 

소프트웨어 시스템이라는 것이 마치 셰프가 손님에 맞게 조금씩 다른 맛의 요리를 만드는 것처럼 비즈니스 특성과 요구사항에 맞게 유연하게 설계하고 구현할 수 있는 점도 중요한 포인트일 것이라 생각합니다.

 

다시 출발지점으로

다시 돌아와서 그렇다면 이러한 논의가 의미를 가지려면 어떻게 할 수 있을까를 고민해보았습니다. 저는 논의에 있어서도 추상과 구현의 원칙을 적용해보면 어떨까 합니다. 즉, 추상적으로 우리가 이러해야 한다는 전제를 계속 상기하면서 구체적으로 바뀔 수 있는 구현에 대해서는 여러 가지 가능성을 검토해보는 것입니다.

 

그런 의미에서 이번 고민을 하면서 생각해본 주제들과 최근에 나누었던 논의들과의 접점이 있는 부분을 다음과 같이 정리해보았습니다.

 

1. 우발적 중복

글의 주요 내용을 유지하면서 사내의 대외비 수준의 코드를 일반론적으로 변경한 내용은 다음과 같습니다.

 


 

중복 코드는 유해하다는 사실에는 이견이 없지만, 때로는 중복이 유용하기도 합니다. 예를 들어, 요청에 대한 Dto를 하나의 RequestDto가 아니라 생성과 업데이트 요청을 각각 전달하는 별도의 Dto로 만드는 경우를 생각해보겠습니다.

 

public class DomainACreateRequest {

    private String domainAId;
    private String domainAName;
    private BigDecimal amount;
    private Boolean isActive;
    private Boolean isArchived;
    private String createdBy;
    private String creationIp;

 

 

public class DomainAUpdateRequest {

    private String domainAId;
    private String domainAName;
    private BigDecimal amount;
    private Boolean isActive;
    private Boolean isArchived;
    private String modifiedBy;
    private String modificationIp;

 

Dto는 처음에는 같은 속성을 갖고 있지만, 시간이 지나면서 두 Dto가 서로 다른 검증 로직이나 추가적인 속성을 필요로 하는 경우가 발생할 수 있습니다.

 

public class DomainAUpdateRequest {

    private String domainAId;
    private String domainAName;
    private BigDecimal amount;
    private Boolean isActive;
    private Boolean isArchived;
    private String modifiedBy;
    private String modificationIp;
    private String updateReason; // 새로운 속성: 업데이트 이유
    // 업데이트 시 특정 로직에 필요한 추가적인 검증 메소드
    public boolean isValidForUpdate() {
        // 업데이트에 필요한 특정 조건을 검증하는 로직
    }
}

 

이처럼 두 코드는 같은 목적을 가지고 시작했지만, 서로 다른 방향으로 발전할 수 있습니다. 이를 '우발적 중복'이라고 할 수 있으며, 특정 상황에서는 이러한 중복이 유용할 수 있습니다.

 

'클린 아키텍처'에서는 이와 같은 중복 쓰임에 대해 '우발적 중복'이라는 표현을 사용합니다.

 

여기서 한단계 더 나아가서 그렇다면 표현 계층에서 받은 Request를 그대로 도메인 계층으로 내리는 것은 어떨까요 ?

 

최근에 나눈 Template Project 논의에서 이러한 내용이 있었는데요.

 

계층별로 반드시 해당하는 Dto 혹은 Vo가 규정될 필요는 없다.


이 내용에 따르면 응용 계층의 RequestDto가 불변객체임을 가정할 때 다음과 같이 Service 레이어까지 데이터를 전송하는 데 무리가 없을 겁니다.

 

void save(SomethingCreateRequest createRequest);

 

 

    @Override
    @Transactional
    public void save(SomethingCreateRequest createRequest) {
        GlobalTemplate entity = createRequest.toEntity();
        persistenceFactory.save(entity);
    }

 

그런데 이런 생각도 해볼 수 있지 않을까요? 즉, 엔티티는 request의 변화에 의존하게 된다는 것입니다. 위의 구현에서는 request에서 dto로 변환을 하지만 만약, 정적 팩토리 메서드가 entity 내부에 위치한 경우라면 다음과 같이 entity는 dto를 직접적으로 참조하게 됩니다.

 

public class Entity(Dto dto){
    /// 
    public static Entity from(Dto dto){
        ///
        return Entity;
    }
}

 

엔티티가 고수준의 개념이고 Dto가 저수준의 개념이라면, 이러한 구현은 우리가 원하는 방향과 다를 수도 있는 것이죠.

 

인터페이스를 통해 제어를 역전하여 의존성을 역전하듯이, 이 문제 역시 하나의 다른 Vo를 중간에 둠으로써 해결할 수 있습니다. 예를 들어 표현 계층에서 응용 계층으로 Dto를 그대로 내린 뒤, 응용 계층에서 적절한 필요 로직을 수행한 뒤 Command객체로 변환하여 도메인 레이어로 내리는 것입니다.

 

void save(SomethingCreateCommand createCommand);

 

도메인 레이어는 응용 계층과 Command로만 통신하며 모든 필요한 요청은 Command에서 모두 생성된 뒤 도메인으로 내려집니다. 이제 엔티티는 Dto의 변화에 상관 없는 구현을 유지할 수 있게 됩니다. 어떻게 보면 굉장히 비효율적인 프로세스가 될 수도 있긴 하지만 여기서 의도하는 바가 도메인 레이어의 변화사항을 극도로 제한하는 것이라면 그 의도는 확실히 성취합니다.

 

2. 관심사의 분리

또 하나의 방법은 객체 변환 로직을 자동화하는 것입니다. '객체 변환'이라는 로직이 하나의 관심사라고 할 때, 이를 완전히 메인 로직에서 분리해버리는 것입니다.

 

예를 들면 다음과 같은 mapper를 구현합니다.

 

public class VoMapper {

  private static final ObjectMapper objectMapper = ObjectMapperConfig.getObjectMapper();

  public static <T, U> U convert(T from, Class<U> to) {
      try {    
          String json = objectMapper.writeValueAsString(from);
          return objectMapper.readValue(json, to);
      } catch (JsonProcessingException e) {
          throw new RuntimeException("Object mapping failed", e);
      }
  }

  public static <T, U> List<U> convert(List<T> from, Class<U> to) {
      return from.stream()
          .map(each -> convert(each, to))
          .collect(Collectors.toList());
      }
  }

 

mapper의 구현 방식은 다양할 수 있습니다. 핵심은 자동화에 있으며, 이는 '코드를 변환한다'는 고수준의 행위로 'builder를 사용하여 코드를 변환한다'는 저수준의 행위를 추상화하는 것입니다.

 

예를 들어 다음과 같은 구현을 생각해볼 수 있습니다:

 

public class DomainARequestDTOV1 {
    // 필드 생략 

    public DomainA toEntity() {
        return DomainA.builder()
                .domainAId(domainAId)
                .domainBId(domainBId)
                // ... 중간 생략 
                .build();
    }

    public static DomainARequestDTOV1 fromEntity(DomainA entity) {
        return DomainARequestDTOV1.builder()
                .domainAId(entity.getDomainAId())
                .domainBId(entity.getDomainBId())
                // ... 중간 생략 
                .build();
    }

    public static DomainARequestDTOV1 fromRequestDTO(DomainAResponseDTOV1 response) {
        return DomainARequestDTOV1.builder()
                .domainAId(response.getDomainAId())
                .domainBId(response.getDomainBId())
                // ... 중간 생략 
                .build();
    }
}

 

이러한 변환 작업을 다음과 같이 mapper에 위임합니다:

 

public class DomainARequestDTOV1 {
    // 필드 생략 

    public DomainA toEntity() {
        return VoMapper.convert(this, DomainA.class);
    }

    public static DomainARequestDTOV1 fromEntity(DomainA entity) {
        return VoMapper.convert(entity, DomainARequestDTOV1.class);
    }

    public static DomainARequestDTOV1 fromRequestDTO(DomainAResponseDTOV1 response) {
        DomainARequestDTOV1 dto = VoMapper.convert(response, DomainARequestDTOV1.class);
        dto.setModDate(now());
        return dto;
    }
}

 

이렇게 변경하면, 코드의 추상화 수준을 높이고, 변환 로직을 자동화하는 것을 목표로 삼을 수 있습니다. 이러한 접근은 코드의 유지보수를 용이하게 하고, 변화에 쉽게 대응할 수 있는 구조를 만들어줍니다.

 

이렇게 했을 때 장점은 개발 생산성 향상, 미적 퀄리티 증가와 같은 것들도 있겠지만... 실제로 좋아지는 점은 변화에 쉽게 대응할 수 있게 된다는 점이라고 생각합니다.

 

기존의 코드를 사용하면 필드 하나가 추가 되면(메인 행위) builder의 추가를 3번 더 해야 (부가 행위) 합니다. 만약 테스트 코드도 있었다면 코드마다 찾아서 해야하니 실제로는 10번 이상이 될 수도 있습니다. 그런데 mapper를 사용하면 필드를 추가 하는 작업 (메인 행위) 외에 다른 수정이 필요가 없게 되죠. 당연히 휴먼 에러도 줄어들게 될 것이고, 확장성도 증가합니다.

 

따지고 보면, mapper를 사용하면 위에서 언급한 계층 의존성 관점에서 제기한 엔티티가 dto를 의존하는 것의 문제도 사실은 큰 문제도 아니긴 합니다.

 

3. 제어의 역전

마지막으로 제어의 역전을 활용하는 정도에 대해서 이야기해보겠습니다. 일반적인 이야기 보다는 특수 케이스 하나를 다뤄보려고 합니다.

일반적으로 SpringDataJpa를 사용하면서 영속성 계층의 의존성을 역전시키죠.

 

https://techblog.woowahan.com/2647/

 

 

Facade는 Controller와 1:1로 대응되므로 어쩌면 사실상 제어를 역전할 필요가 없습니다. 그런데 이러한 관점은 어떨까요?

 

소프트웨어의 확장성을 고려해 V1이라는 표현을 쓰는데, 인터페이스를 통해 계층 간의 의존성을 일반화한다면, 실제 구현체에만 버전을 명시해주어도 됩니다.

 

예를 들어, 다음과 같이 DomainServiceControllerV2 DomainServiceFacadeV2 의존하고 있는데 해당 파사드는 일반 클래스입니다.

 

@RestController
@RequestMapping("/api/v2/domain-services")
@Tag(name = "도메인 서비스 템플릿")
public class DomainServiceControllerV2 {
    private final DomainServiceFacadeV2 domainServiceFacadeV2;

    @GetMapping("/domain-service-templates/search")
    @Operation(summary = "특정 조건을 기반으로 도메인 서비스 템플릿을 검색하는 용도")
    public CommonResponseEntity<DomainServiceResponseDTOV1> search(DomainServiceRequestDTOV1 domainServiceRequestDTOV1){
        return domainServiceFacadeV2.search(domainServiceRequestDTOV1);
    }
}

 

파사드를 인터페이스로 만들면 다음과 같이 되겠죠.

 

@RestController
@RequestMapping("/api/v2/domain-services")
@Tag(name = "도메인 서비스 템플릿")
public class DomainServiceControllerV2 {
    private final DomainServiceFacade facade;

    public DomainServiceControllerV2(DomainServiceFacadeV2 domainServiceFacadeV2) {
        this.facade = domainServiceFacadeV2;
    }

    @GetMapping("/domain-service-templates/search")
    @Operation(summary = "특정 조건을 기반으로 도메인 서비스 템플릿을 검색하는 용도")
    public CommonResponseEntity<DomainServiceResponseDTOV1> search(DomainServiceRequestDTOV1 domainServiceRequestDTOV1){
        return facade.search(domainServiceRequestDTOV1);
    }
}

 

그리고 Facade는...

 

public interface DomainServiceFacade {
    //... 로직
}

public class DomainServiceFacadeV1 implements DomainServiceFacade {
    // V1 버전의 구현
}

@Facade
public class DomainServiceFacadeV2 implements DomainServiceFacade {
    // V2 버전의 구현
}

 

여기서 Facade 애노테이션은 의미의 명확성을 위해 Component로 만들어주되 이름만 Facade인 애노테이션입니다. 순전히 인지적 공감대 형성을 위한 애노테이션입니다.

 

어쨌든 중요한 부분은... 실제 사용하는 Facade 구현체를 Componenet로 만들어주면 해당 클래스가 주입될 테니 개발자의 의도에 따라 버전 2를 사용하게 될 것입니다. 표현 계층에서의 의존성 변화는 당연히 없구요.

 

표현 계층과 응용 계층의 의존성을 굳이 역전할 필요가 있냐 싶지만 굳이 마다할 이유는 없지 않나 싶기도 합니다. 일단 코드가 깔끔해진다는 장점이 있으니까요.

 

마무리

어찌 보면 별 것 아닌 차이에서 시작한 논점인데 그간 생각하고 있던 포인트들과의 접점도 있어서 여러모로 생각 거리들과 공부 거리들이 있었던 것 같습니다.

 

한 블로그에서 이런 댓글이 있었는데 인상 깊어서 공유해보겠습니다.

 

https://techblog.woowahan.com/2647/

 

정말 그런 것 같습니다. 확장에는 열려 있고 변경에는 닫혀있기... 어찌 보면 이론과 실전의 괴리에서 어떻게 타협할 것인가의 문제인 것 같기도 하고, 은탄환은 없다는 사실에 어떻게 받아들이고 대응할 것인가의 문제인 것 같기도 합니다. 어떤 지향점을 갖고 발전하는 게 저는 의미 있는 일 같습니다.

 

마지막으로, 글을 쓰면서 제가 잘못 이해한 부분이 있을 수도 있습니다. 잘못된 부분이 있거나 달리 생각하시는 부분이 있다면 함께 나눠주시면 또 다른 논의로 이어갈 수도 있고 좋을 것 같습니다.

 

 


 

 

레퍼런스

반응형