ABOUT ME

-

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

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

     

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

     

    너무  보폭으로 진행한  같다잠시 숨을 고르고 ProductRepository 대한 테스트를 추가하자지금 까지는 JUnit에서 제공하는 TestCase 클래스를 상속 받아 테스트 클래스를 작성했지만 Spring 사용할 경우 빈컨텍스트에 대한 세밀한 제어가 가능한 AbstractDependencyInjectionSpringContextTests 상속받는 것이 좋다테스트를 실행하고 녹색 막대인지를 확인하자.

    package org.eternity.customer;
    
    import org.springframework.test.AbstractDependencyInjectionSpringContextTests;
     
    public class ProductRepositoryTest extends AbstractDependencyInjectionSpringContextTests {
      private Registrar registrar;
      private ProductRepository productRepository;           
    
      public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
      }
    
      @Override
      protected String[] getConfigLocations() {
        return new String[] { "org/eternity/order-beanContext.xml" };
      }
    
      public void onSetUp() throws Exception {
        registrar.init();
      }
    
      public void testProductSave() throws Exception {
        Product saveProduct = new Product("상품1", 1000);
        productRepository.save(saveProduct);
    
        assertSame(saveProduct, productRepository.find("상품1"));
      } 
    }

     

    getConfigLocations() 메소드는 AbstractDependencyInjectionSpringContextTests 클래스가 테스트를 수행하기 위해 사용할 Spring  컨텍스트 파일의 경로를 반환한다. onSetUp() 메소드는 모든 테스트 메소드가 실행되기 전에 실행되는 메소드이다. AbstractDependencyInjectionSpringContextTests 클래스의 setUp() 메소드는 getConfigLocations() 메소드가 반환하는  켄텍스트를 로드한  서브 클래스의 onSetUp() 메소드를 호출한다. AbstractDependencyInjectionSpringContextTests 클래스는 매번  컨텍스트를 로딩하지 않고 최초에 로딩한  켄텍스트를 재사용한다따라서 테스트 메소드 호출 시마다 매번 불필요한  컨텍스트 로딩을 막을  있으므로 테스트 수행 속도를 향상시킬  있다.

     

    AbstractDependencyInjectionSpringContextTests 클래스를 사용함으로써 얻을  있는  다른 장점은 Spring getConfigLocations() 메소드에 설정된 빈들을 조사하여 테스트 케이스의 프로퍼티와 동일한 타입의빈이 존재할 경우 자동으로 의존성을 주입한다는 점이다 예에서 ProductRepositoryTest 클래스는ProductRepository를 설정할  있는 setter 메소드를 제공하기 때문에  컨텍스트에 선언된 ProductRepository 타입의 빈을 자동으로 프로퍼티에 설정해 준다따라서 별도로 빈을 룩업할 필요가 없다.

     

    Registrar ProductRepository 대한 테스트가 성공했으니 용기를 내서 주문 시스템 전체의 결합도를 낮추어보자

    관점을 바꾸자

    ProductRepository 경우와 마찬가지로 CustomerRepository OrderRepository EXTRACT INTERFACE리팩토링을 수행해서 CustomerRepository, OrderRepository 인터페이스와 CollectionCustomerRepository, CollectionOrderRepository 클래스로 분리하자. Registrar 대한 setter 메소드를 추가한  이들을 Spring 빈컨텍스트에 정의한다.

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <beans xmlns="http://www.springframework.org/schema/beans" 
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
           xmlns:context="http://www.springframework.org/schema/context" 
           xsi:schemaLocation="http://www.springframework.org/schema/beans 
              http://www.springframework.org/schema/beans/spring-beans-2.5.xsd  
              http://www.springframework.org/schema/context
              http://www.springframework.org/schema/context/spring-context-2.5.xsd"> 
    
      <bean id="registrar" class="org.eternity.common.EntryPointRegistrar"/>
    
      <bean id="productRepository" class="org.eternity.customer.memory.CollectionProductRepository">
        <property name="registrar" ref="registrar"/>
      </bean>
     
      <bean id="customerRepository" class="org.eternity.customer.memory.CollectionCustomerRepository">
        <property name="registrar" ref="registrar"/>
      </bean>
    
      <bean id="orderRepository" class="org.eternity.customer.memory.CollectionOrderRepository">
        <property name="registrar" ref="registrar"/>
      </bean>
    </beans>

     

    리포지토리들을  컨텍스트에 추가했으니 망가진 것이 없는 지 확인하기 위해 회귀 테스트를 수행하도록하자. 기존의 테스트 케이스들이 Spring 경량 컨테이너의 지원을 받을  있도록 테스트 케이스를 수정하자우선 OrderTest 클래스를 AbstractDependencyInjectionSpringContextTests 클래스의 서브 클래스로 변경한 후ProductRepositoryTest와 동일한 방식으로 getConfigLocations() onSetUp() 메소드를 오버라이딩한다.

     

    package org.eternity.customer;
     
    import org.eternity.common.Registrar;
    importorg.springframework.test.AbstractDependencyInjectionSpringContextTests;
    
    public class OrderTest extends AbstractDependencyInjectionSpringContextTests {
      private Customer customer;   
      private OrderRepository orderRepository;
      private ProductRepository productRepository;      
    
      @Override
      protected String[] getConfigLocations() {
        return new String[] { "org/eternity/order-beanContext.xml" };
      }
    
      public void onSetUp() throws Exception {
        ((Registrar)applicationContext.getBean("registrar")).init();
    
        orderRepository = (OrderRepository)applicationContext.getBean("orderRepository");
        productRepository = (ProductRepository)applicationContext.getBean("productRepository");
    
        productRepository.save(new Product("상품1", 1000));
        productRepository.save(new Product("상품2", 5000));
               
        customer = new Customer("CUST-01", "홍길동", "경기도 안양시", 200000);
      }
    
      public void testOrderPrice() throws Exception {
        Order order = customer.newOrder("CUST-01-ORDER-01")
                              .with("상품1", 10)
                              .with("상품2", 20);
    
        orderRepository.save(order);
    
        assertEquals(new Money(110000), order.getPrice());
      }
    }

     

    테스트를 실행해 보자이런 빨간 막대다호출 스택을 뒤져보니 OrderLineItem 생성자에서 productRepository 참조할  NullPointerException 발생했다

     

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

     

    그러고 보니 OrderLineItem Order에서 직접 new 연산자를 사용하여 인스턴스를 생성했었다, OrderLineItem Spring  컨텍스트에 의해 관리되지 않는 객체이므로  컨텍스트로부터 획득되는 다른 리포지토리들처럼 Spring 의존성 주입 서비스를 받을  없다따라서 Spring 컨테이너 외부에서 생성되는 OrderLineItem ProductRepositoty null수 밖에 없다

    public Order with(String productName, int quantity) throws OrderLimitExceededException {
      return with(new OrderLineItem(productName, quantity));
    }

     

    Spring 컨테이너 외부에서 생성되는 객체에 대해 의존성 주입을 제공하는 가장 효과적인 방법은 AOP(Aspect-Oriented Programming)를 적용하는 것이다. AOP란 시스템 내의 관심사를 분리하는 프로그래밍 기법이다. AOP를 사용하면 시스템의 핵심 관심사(Core Concerns)와 횡단 관심사(Cross-Cutting Concerns)의 분리를 통해 결합도가 낮고 재사용이 가능한 시스템을 개발할 수 있다.

    Spring 2.x부터 기존의 프록시 기반 메커니즘에 AspectJ를 통합하여 더 강력하고 유연한 AOP 기능을 지원하게 되었다. 그 대표적인 것이 Spring 컨테이너 외부에서 생성되는 도메인 객체에 Spring 컨테이너에서 관리하는 빈을 의존 삽입할 수 있도록 해주는 기능이다. 이것은 AspectJ 5부터 지원되는 LTW(Load-Time Weaving) 기능을 사용하는 것으로 클래스 로더가 클래스를 로드 할 때 바이트 코드를 수정하여 Spring 빈을 삽입하는 것이다. 따라서 Order에서 new 연산자를 사용하여 OrderLineItem을 생성하더라도 CollectionProductRepository를 의존 삽입하는 것이 가능해 진다.

    AspectJ
     LTW를 사용하기 위해서는 “META-INF/aop.xml” 파일을 설정해야 한다.

    <!DOCTYPE aspectj PUBLIC
      "-//AspectJ//DTD//EN"    "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
    
    <!--
      CGLIB으로 생성된 오브젝트에 대해서는 weaving을 제거한다.
       이 옵션은 어플리케이션 스타트 시 속도를 향상시킨다.
    -->
    <aspectj>
      <weaver>
        <include within="org.eternity..*"/>       
        <exclude within="org.eternity..*CGLIB*"/>
      </weaver>
      <aspects>
        <include within="org.springframework.beans.factory.aspectj
                         .AnnotationBeanConfigurerAspect"/>
      </aspects>
    </aspectj>


    <include>
     엘리먼트를 사용하여 org/eternity 클래스 패스 하위에 포함된 클래스들에 대해서만 애스펙트를 적용하도록 설정한다. CGLIB에 의해 생성된 프록시 객체들은 애스펙트를 적용할 필요가 없으므로 <exclude> 엘리먼트를 사용하여 제외시킨다.

    이제 Spring에게 OrderLineItem의 클래스를 로드할 때 어떤 방법으로 ProductRepository 인스턴스를 의존 삽입해야 하는지를 알려줘야 한다. Spring 빈 컨텍스트 설정 파일에 OrderLineItem ProductRepository의 연결 정보를 추가하자

    <context:load-time-weaver/>
    <context:spring-configured/>
    <bean id="orderLineItem" 
        class="org.eternity.customer.OrderLineItem" abstract="true">
        <property name="productRepository" ref="productRepository"/>
    </bean>

     

    Spring에게 OrderLineItem의 클래스가 로딩될 때 productRepository 프로퍼티에 id “productRepository”로 선언된 빈을 의존 삽입하도록 설정했다. 실제로 빈을 생성할 필요는 없으므로 abstract 속성을 true 설정했다.

    <context:spring-configured>
     
    는 뒤에서 살펴 볼 @Configurable 어노테이션이 선언된 클래스들을 Spring 컨테이너가 관리하도록 설정한다
    . <context:load-time-weaver/> LTW를 사용하도록 선언한다.

    package org.eternity.customer;
    
    import org.springframework.beans.factory.annotation.Configurable;
    
    @Configurable(value="orderLineItem",preConstruction=true)
    public class OrderLineItem {
      private ProductRepository productRepository;
    
      public OrderLineItem() {
      }
    
      public OrderLineItem(String productName, int quantity) {
        this.product = productRepository.find(productName);
        this.quantity = quantity;
      }
    
      public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
      }

     

    @Configurable 어노테이션의 value에는 Spring 빈 컨텍스트에 정의한 OrderLineItem 빈의 id를 정의한다. preConstruction의 값을 true로 한 이유는 이 값을 true로 설정하지 않으면 기본적으로 생성자 호출이 끝난 후 의존성이 주입되기 때문이다. 따라서 위와 같이 생성자 내부에서 주입될 대상 객체를 호출하는 경우 NullPointerException이 발생하게 된다. 이를 방지하기 위해서는 생성자가 호출되기 전에 의존성이 주입되도록 preConstruction의 값을 true로 설정해 주어야 한다.

    이제 테스트를 실행하기 위해 JVM –javaagent 옵션의 값으로 spring-agent.jar의 전체 경로를 주도록 하자. 아래의 fullpath 부분은 각자의 개발 환경에 맞게 경로를 설정해 주면 된다

    -javaagent:/{fullpath}/spring-agent.jar


    이제 테스트를 실행해 보자. 녹색 막대다! 드디어 Spring 컨테이너를 사용하여 주문 시스템의 전체적인 결합도를 낮추는데 성공했다. 도메인 클래스들이 REPOSITORY의 인터페이스에만 의존할 뿐 실제적인 구현 클래스에 의존하지 않게 되었다. 힘든 리팩토링이었다. 이제는 메모리 컬렉션을 처리하는 REPOSITORY의 내부 구현을 데이터베이스에 접근하는 REPOSITORY로 대체하더라도 다른 클래스에 영향을 미치지 않을 것이다. 도메인 클래스가 REPOSITORY의 인터페이스에만 의존하기 때문에 Mock 객체를 사용하여 데이터베이스 없이도 테스트하는 것이 가능해졌다. 회귀 테스트도 성공했고 모든 것이 만족스럽다. 아차, 다운된 주문 시스템 때문에 고객들이 난리였었지. 실실 웃고 있을 때가 아니다. 실업자가 되기 전에 어서 주문 시스템에 영속성 메커니즘을 추가하도록 하자.

     

    3부 끝


    다음글 : 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 1부

    728x90
Designed by Tistory.