ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 3부
    [옛날 글들] 도메인 주도 설계 2024. 4. 17. 14:21
    728x90

    이전글 : 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 2부

     

    이 글은 제가 2008년 6월부터 10월까지 5개월간 마이크로소프트웨어에 연재했던 "도메인 주도 설계의 적용"이라는 원고의 원글입니다.  잡지에 맞추어 편집을 하는 과정에서 지면 상의 제약으로 인해 수정되거나 삭제된 부분이 있어 제 블로그에 원글을 올립니다. 도메인 주도 설계(Domain-Driven Design)에 관심 있는 분들에게 도움이 되었으면 합니다. 

    영속성(Persistence) 리포지토리

    리포지토리는 도메인 객체 생성 이후의 생명주기를 책임진다. 도메인 객체가 생성되고 상태가 초기화된 후에는 리포지토리에게 넘겨진다. 리포지토리는 객체를 넘겨받아 내부 저장소에 보관하고 요청이 있을 경우 객체를 조회하여 반환하거나 삭제한다. 클라이언트 입장에서 리포지토리는 커다란 메모리에 도메인 객체들을 담고 있는 객체 풀(object pool)과 같다. 클라이언트는 생성된 객체를 리포지토리에게 전달하고 객체가 필요한 경우 리포지토리에게 객체 풀 안의 어딘가에 잠자고 있는 도메인 객체를 찾아 달라고 요청한다.

     

    리포지토리의 기능을 메모리 컬렉션에 대한 오퍼레이션으로 바라보는 것은 도메인 모델을 단순화하기 위한 중요한 추상화 기법이다. 도메인 모델을 설계하고 필요한 오퍼레이션을 식별하는 동안 우리는 하부의 어떤 메커니즘이 도메인 객체들의 생명 주기를 관리하는지에 관한 세부사항을 무시할 수 있다. 리포지토리가 제공하는 인터페이스의 의미론을 메모리 컬렉션에 대한 관리 개념으로 추상화함으로써 자연스럽게 하부의 데이터 소스와 관련된 영속성 메커니즘을 도메인 모델로부터 분리할 수 있다.

     

    복잡성을 관리하는 중요한 방법은 서로 다른 관심사를 고립시켜 한 번에 하나의 이슈만을 해결하도록 하는 것이다. 도메인을 모델링할 때는 리포지토리를 통해 모든 객체가 메모리에 있다는 착각을 줌으로써 하부 인프라 스트럭처에 대한 부담 없이 도메인 로직에 집중할 수 있다. 또한 하부의 데이터 접근 로직을 리포지토리에 집중시킴으로써 도메인 로직과 데이터 접근 로직을 자연스럽게 분리시킬 수 있다. 영속성 메커니즘이 리포지토리 내부로 제한되어 있기 때문에 도메인 모델에 영향을 미치지 않고서도 영속성 메커니즘을 교체하는 것이 가능하다.

     

    따라서 리포지토리를 모델링 할 때는 하부의 영속성 메커니즘에 관한 세부사항을 배제하고 메모리 컬렉션을 관리하는 객체로 모델링한다. 리포지토리의 인터페이스는 메모리 내의 객체 풀을 관리한다는 의도를 나타내도록 명명한다. 리포지토리를 사용하는 클라이언트 역시 데이터베이스에 대한 고려는 하지 않는다. 리포지토리의 클라이언트는 객체 정보가 일반 파일에 저장되어 있는지, XML에 저장되어 있는지, 데이터베이스에 저장되어 있는지 관심조차 없다.

     

    리포지토리를 구현할 때는 잠시 관점을 바꾸어 현재 사용 중인 하부의 데이터 소스를 고려해야 한다. 프로그래머라는 직업은 단기 기억 상실증에다가 다중 인격까지 갖추어야 하는 어려운 직업이다. 지금까지 객체 지향 신봉자로 살아왔다면 지금부터는 데이터베이스 신봉자로 살아야 한다. 리포지토리를 사용하는 클라이언트를 개발할 때는 하부의 데이터 소스를 몰라도 되지만 내부 구현을 할 때는 그렇지 않다.

     

    Eric Evans는 그의 저서 Domain-Driven Design에서 리포지토리를 다음과 같이 설명하고 있다.

    전역적으로 접근 될 필요가 있는 각 객체 타입에 대해 해당 타입의 모든 객체들을 메모리 컬렉션으로 저장하고 있는 듯한 착각을 일으키는 객체를 생성한다. 잘 알려진 전역 인터페이스를 통해 이 객체들에 접근할 수 있도록 한다. 실제 데이터 저장소에 데이터를 추가하고 삭제하는 실제적인 작업을 캡슐화하는 추가/삭제 메서드를 작성한다. 특정 쿼리 조건을 만족하는 객체 또는 객체들의 컬렉션을 반환하는 조회 메서드를 추가함으로써 실제 저장소와 쿼리 기술을 캡슐화한다. 모든 객체 저장소와 접근을 리포지토리로 위임함으로써 클라이언트가 모델에만 초점을 맞추도록 한다.


    결론적으로 리포지토리는 영속성 메커니즘을 캡슐화하기 위한 훌륭한 지점이다. 따라서 우리가 개발한 주문 애플리케이션에 영속성 메커니즘을 추가하기 위해서는 리포지토리의 내부 구현을 바꾸어야 한다. 그러나 이를 위해서는 한 가지 풀어야 할 숙제가 남아 있다. 바로 도메인 객체와 리포지토리의 결합도를 낮추는 것이다.
     

    public class OrderLineItem { 
      private ProductRepository productRepository = new ProductRepository();
    
      public OrderLineItem(String productName, int quantity) {
        this.product = productRepository.find(productName);
        this.quantity = quantity;
      }
    }

     

    OrderLineItem은 인스턴스 변수로 Product 리포지토리를 포함하며 클래스 로딩 시 new 연산자를 사용하여 Product 리포지토리의 인스턴스를 직접 생성한다. OrderLineItem은 생성자에 전달된 상품명을 가진 Product 객체를 찾기 위해 Product 리포지토리를 사용한다. 이제 참조 객체들을 메모리가 아닌 관계형 데이터베이스 내에 관리하기로 정책을 수정했다고 하자. 기존의 Product 리포지토리는 메모리 컬렉션을 관리하는 Registrar를 사용하고 있으므로 어쩔 수 없이 Product 리포지토리의 내부 코드를 수정할 수밖에 없다. 이것은 개방-폐쇄 원칙(OCP, OPEN-CLOSE PRINCIPLE) 위반한다.

     

    하지만 이제부터는 참조 객체들을 데이터베이스에서만 관리할 것 아닌가? 참조 객체를 메모리에서 관리할 필요가 없다면 Product 리포지토리 내부를 변경한다고 해서 문제 될 것이 무엇인가? 우리는 이미 참조 객체에 대한 처리 로직을 리포지토리 내부로 고립시켰다. 현재의 설계가 변경에 따른 파급 효과를 최소화할 수 있는 구조를 가지고 있기 때문에 리포지토리 내부를 수정하는 것이 그렇게 큰 문제가 될 것 같지는 않다. 어차피 시간도 없는데 OCP까지 고려할 필요가 있을까? 눈 딱 감고 Product리포지토리를 수정해서 데이터베이스 접근 코드를 추가해도 되지 않을까?

     

    그러나 문제는 엉뚱한 곳에서 발생한다. OrderLineItem은 Product 리포지토리에 의존한다. Product 리포지토리는 데이터베이스에 의존한다. 따라서 OrderLineItem 역시 데이터베이스에 의존하고 OrderLineItem을 사용하는 모든 Customer, Order 역시 데이터베이스에 의존하기 때문에 OrderLineItem을 사용하는 모든 도메인 클래스들이 데이터베이스에 직간접적으로 의존하는 결과를 낳는다. 즉, 거의 대부분의 도메인 클래스가 데이터베이스와 결합된게 된다.

     

    따라서 단위 테스트를 수행하기 위해서는 DBMS가 실행 중이어야 하고 필요한 데이터들이 미리 입력되어 있어야 하며 각 단위 테스트가 종료된 후에는 다른 테스트에 영향을 미치지 않도록 모든 데이터베이스의 상태를 초기화해야 한다. 데이터베이스는 속도가 느리고 따라서 결과에 대한 피드백 또한 느리다. 만사가 그렇겠지만 불편하다면 피하게 될 확률이 높다. 단위 테스트가 개발의 리듬을 방해해서는 안 된다. 따라서 단위 테스트를 데이터베이스로부터 독립시켜야 한다.

     

    한마디로말하자면 문제는 결합도(coupling)다. OrderLineItem은 Product 리포지토리에 강하게 결합되어 있다. 두 클래스가 강하게 결합되어 있으므로 Product 리포지토리 없이 OrderLineItem이 존재할 수 없다. 따라서 OrderLineItem을 사용하려면 Produc t리포지토리가 존재해야 하고, Product 리포지토리가 존재하기 위해서는 데이터베이스가 구동 중이어야 한다.

     

    결합도는 양날의 검이다. 객체 간의 결합은 자연스러운 것이다. 각 클래스가 높은 응집도를 유지하기 위해 다른 클래스와 협력하는 것은 객체 지향의 기본 원리이다. OrderLineItem은 Product 참조 객체를 얻기 위해 Product 리포지토리와 협력해야 한다. 문제는 OrderLineItem과 Product 리포지토리 간의 결합도가 필요 이상으로 높다는 것이다. OrderLineItem이 직접 Product 리포지토리의 인스턴스를 생성하기 때문에 OrderLineItem과 Product리포지토리를 서로 분리시킬 수 있는 방법이 없다. 즉, 구체적인 클래스인 OrderLineItem이 또 다른 구체적인 클래스인 Product리포지토리에 의존한다는 것이 문제가 되는 것이다. 대부분의 구체적인 클래스 간의 의존성은 전체적인 애플리케이션의 유연성을 저해한다.

     

    결합도를 낮추는 일반적인 방법은 OrderLineItem과 Product 리포지토리 사이의 직접적인 의존 관계를 제거하고 두 클래스가 추상에 의존하도록 설계를 수정하는 것이다. 즉, 구체적인 클래스가 추상적인 클래스에 의존하게 함으로써 전체적인 결합도를 낮추는 것이다. 구체적인 클래스들 간의 의존 관계를 추상 계층을 통해 분리함으로써 OCP를 위반하는 설계를 제거할 수 있다.

     

    따라서 OrderLineItem과 Product리포지토리가 모두 인터페이스에 의존하도록 설계를 수정한다면 유연하고 낮은 결합도를 유지하면서도 OCP를 위반하지 않는 설계를 만들 수 있다. 두 클래스가 인터페이스에 의존하도록 수정하는 가장 간단한 방법은 무엇일까? Product 리포지토리를 인터페이스와 구현 클래스로 분리하고 OrderLineItem과 Product 리포지토리의 구현 클래스가 Product 리포지토리의 인터페이스에 의존하도록 하면 된다.

     

    우선 ProductRepository 리팩토링하자. 구체적인 클래스에서 인터페이스를 추출하는 EXTRACT INTERFACE 적용하자. 인터페이스 명은 ProductRepository 하고 구현 클래스 명은 CollectionProductRepository 하자.

    package org.eternity.customer;
    
    public interface ProductRepository {
      public void save(Product product); 
      public Product find(String productName);
    }
    package org.eternity.customer.memory;
    
    import org.eternity.common.Registrar;
    import org.eternity.customer.Product;
    import org.eternity.customer.ProductRepository;
    
    public class CollectionProductRepository implements ProductRepository {
      public ProductRepository() {
      }
     
      public void save(Product product) {
        Registrar.add(Product.class, product);
      }
    
      public Product find(String productName) {
        return (Product)Registrar.get(Product.class, productName);
      }
    }


    EXTRACT INTERFACE 리팩토링을 통해 구체적인 클래스인 CollectionProductRepository 인터페이스인 ProductRepository 의존하도록 수정했다. 이제 OrderLineItem ProductRepository 인터페이스에 의존하도록 코드를 수정하자.

    public class OrderLineItem {
    
    private ProductRepository productRepository = new CollectionProductRepository();      
      public OrderLineItem() {       
      }    
    
      public OrderLineItem(String productName, int quantity) {
        this.product = productRepository.find(productName);
        this.quantity = quantity;
      }
    }

     

    OrderLineItem productRepository 속성의 타입을 ProductRepository 인터페이스로 변경함으로써 OrderLineItem 인터페이스에 의존하도록 수정했다. OrderLineItem 생성자 내부에서는 ProductRepository 인터페이스 타입의 productRepository만을 사용하기 때문에 인터페이스에만 의존하고 구체적인 클래스에는 의존하고 있지 않다. 그러나 여전히 OrderLineItem 자체는 구체적인 클래스인 CollectionProductRepository 강하게 결합되어 있다. 원인이 무엇일까?

     

    다음글 : 도메인 주도 설계의 적용 - 3. 의존성 주입과 관점 지향 프로그래밍 4부

    728x90
Designed by Tistory.