ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 도메인 주도 설계의 적용-4.ORM과 투명한 영속성 5부[끝]
    [옛날 글들] 도메인 주도 설계 2024. 5. 8. 08:55
    728x90

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

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

     

    Hibernate에서 사용할 맵핑 파일의 작성이 끝났으므로 Hibernate Spring 통합을 위해 Spring 빈 컨텍스트에 Hibernate 관련 빈을 추가하자. 우선 데이터소스를 추가하자.

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <beans xmlns="http://www.springframework.org/schema/beans" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> 
     
      <bean id="dataSource"
              class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
        <property name="url" value="jdbc:hsqldb:hsql://localhost" />
        <property name="username" value="sa" />
        <property name="password" value="" />
      </bean>
    </beans>

     

    이제 데이터 소스를 사용하여 Hibernate Session을 생성할 sessionFactory 빈을 등록한다. “dataSource” 프로퍼티에는 앞에서 선언한 dataSource 빈을 설정한다. “mappingResources” 프로퍼티에는 Hibernate 맵핑 파일 목록을 설정한다. “hibernateProperties” 프로퍼티에는 Hibernate를 설정하기 위해 필요한 각종 설정 정보를 설정한다. 여기에서 가장 중요한 것은 “hibernate.dialect” 프로퍼티로 Hibernate가 현재 사용중인 데이터베이스에 최적화된 SQL문을 생성할 수 있도록 정확한 값을 설정해주어야 한다. 우리는 HSQLDB를 사용하기 때문에 “org.hibernate.dialect.HSQLDialect”를 값으로 설정했다. “hibernate.show_sql”은 콘솔에 SQL을 출력할 지 여부를 지정하며 “hibernate.format_sql”은 출력 포맷을 정렬할 것인지를 지정한다. “hibernate.hbmddl.auto” 속성으로 “create-drop”을 주면 Session이 시작될 때 Hibernate 맵핑 정보를 바탕으로 데이터베이스 스키마가를 자동으로 생성하고, Session이 종료될 때 생성된 모든 테이블들을 제거한다. 따라서 매번 일일이 데이터베이스 스키마를 조정할 필요가 없이 Hibernate 맵핑 파일만을 사용해서 코드와 테이블을 동기화할 수 있다.

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
      <property name="dataSource" ref="dataSource" />
      <property name="mappingResources">
        <list>
          <value>org/eternity/customer/Customer.hbm.xml</value>
          <value>org/eternity/customer/Product.hbm.xml</value>
          <value>org/eternity/customer/Order.hbm.xml</value>
        </list>
      </property>
      <property name="hibernateProperties">
        <props>
          <prop key="hibernate.dialect">
            org.hibernate.dialect.HSQLDialect
          </prop>
          <prop key="hibernate.show_sql">true</prop>
          <prop key="hibernate.format_sql">true</prop>
          <prop key="hibernate.hbm2ddl.auto">create-drop</prop>
        </props>
      </property>
     </bean>

     

    이제 리포지토리를 구현하자. 우리는 이전 아티클에서 리포지토리의 인터페이스와 구현 클래스를 분리했으므로 도메인 레이어의 다른 부분에는 영향을 미치지 않고 Hibernate 전용 리포지토리를 구현할 수 있다. Spring Hibernate를 쉽게 연동할 수 있는 기반 클래스인 org.springframework.orm.hibernate3.support.HibernateDaoSupport를 제공한다. 이 클래스는 앞에서 선언한 SessionFactory를 사용하여 내부적으로 HibernateTemplate을 생성한다. 하위 클래스에서는 getHibernateTemplate()를 호출함으로써 Hibernate Session 관련 기능을 사용할 수 있다. OrderRepository 인터페이스의 구현 클래스인 HibernateOrderRepository의 코드를 살펴보자.

    package org.eternity.customer.hibernate;
    
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    import org.eternity.customer.Customer;
    import org.eternity.customer.Order;
    import org.eternity.customer.OrderRepository;
    import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
    
    public class HibernateOrderRepository extends HibernateDaoSupport 
            implements OrderRepository {
      public Order delete(Order order) {
        getHibernateTemplate().delete(order);
        return order;
      }
    
      @SuppressWarnings("unchecked")
      public Order find(String orderId) {
        List<Order> result = (List<Order>)getHibernateTemplate()
                                     .find("from Order where orderId=?", orderId);
        
        if (result != null && result.size() > 0) {
          return result.get(0);
        }        
    
        return null;
      }
    
      @SuppressWarnings("unchecked")
      public Set<Order> findAll() {
        return new HashSet<Order>(
            getHibernateTemplate().loadAll(Order.class));
      }
    
      @SuppressWarnings("unchecked")
      public Set<Order> findByCustomer(Customer customer) {
        return new HashSet<Order>(
            getHibernateTemplate().find("from Order where customer=?", customer));
      }
    
      public void save(Order order) {
        getHibernateTemplate().save(order);       
      }
    }

     

    HibernateOrderRepository HibernateDaoSupport를 상속 받고 OrderRepository를 실체화한다. 각 메소들은 getHibernateTemplate()를 사용하여 데이터베이스 관련 작업을 수행한다. find() findByCustomer() 메소드에서 HQL(Hibernate Query Language)의 사용을 눈여겨 보기 바란다. HQL Hibernate 전용 쿼리 언어로 SQL과 유사한 문법을 제공하지만 테이블 기반이 아닌 객체 기반의 쿼리를 작성할 수 있도록 한다. , 위 쿼리에서 사용된 Order orderId는 테이블명과 컬럼명이 아니라 클래스명과 속성 명이다.

     

    이제 리포지토리 Hibernate와 연동하도록 수정했으므로 Spring 빈 컨텍스트에 추가하자. Hibernate 기반 리포지토리를 설정하기 위해 “sessionFactory” 프로퍼티에 앞에서 설정한 “sessionFactory” 빈을 의존 주입하도록 설정한다.

      <bean id="productRepository" 
            class="org.eternity.customer.hibernate.HibernateProductRepository">
        <property name="sessionFactory" ref="sessionFactory"/>
      </bean>
     
      <bean id="customerRepository" 
            class="org.eternity.customer.hibernate.HibernateCustomerRepository">
        <property name="sessionFactory" ref="sessionFactory"/>
      </bean>
    
      <bean id="orderRepository" 
            class="org.eternity.customer.hibernate.HibernateOrderRepository">
        <property name="sessionFactory" ref="sessionFactory"/>
      </bean>

     

    , 이것 저것 고칠게 너무 많다. 애초에 아키텍처를 잡을 때 영속성을 고려했어야 했는데 아키텍처의 중요성을 뼈저리게 느끼게 되는 순간이다. 순간 불안한 느낌이 든다. 뭔가 망가지지 않았을까? 이럴 때 의지할 수 있는 건 단 하나밖에 없다. 회귀 테스트를 믿으라.

     

    테스트를 실행하기 전에 한가지 더 알려 둘 것이 있다. 가능하면 단위 테스트는 데이터베이스에 의존하지 않은 채로 실행할 수 있어야 한다. , 데이터베이스가 가동되지 않아도 각 클래스만을 고립시켜 테스트할 수 있도록 테스트 케이스를 작성해야 한다. 정확하게 말하면 데이터베이스를 포함시키는 테스트는 단위 테스트가 아니라 통합 테스트이다.

     

    데이터베이스와 연계해야 하는 통합 테스트에서 가장 어려운 문제는 테스트 데이터를 고립시키는 것이다. 우선 개발자들이 사용하는 테스트 데이터들이 충돌하지 않아야 한다. 만약 테스트 데이터가 서로 충돌하여 테스트가 실패한 경우 문제의 원인을 발견하기가 쉽지 않게 된다. 두 번째 문제는 테스트 데이터들의 초기화와 정리 문제이다. 테스트를 실행하기 위해서는 테스트에 필요한 테스트 데이터들이 데이터베이스에 입력되어 있어야 한다. 테스트가 종료된 후에는 사용된 테스트 데이터들을 제거해야 한다. 테스트가 종료된 후에도 테스트 데이터들이 그대로 존재할 경우 다음 테스트 결과에 영향을 미칠 수 있기 때문에 테스트를 고립시키라는 기본 원칙을 위배하게 된다.

     

    앞의 문제를 해결할 수 있는 가장 좋은 방법은 개발자 별로 개발용 데이터베이스를 가지는 것이다. , 자신만의 고립된 개발환경에 샌드 박스를 구성하고 개발을 진행하는 것이다. Hibernate와 같은 ORM을 사용하면 데이터베이스 간의 호환성 문제를 해결할 수 있기 때문에 본 아티클에서 사용하는 HSQLDB와 같은 가벼운 데이터베이스를 사용하여 개발자 환경에서 어플리케이션을 개발한 후 Oracle이나 Informix를 사용하는 개발 서버에서도 별다른 수정 없이 어플리케이션을 실행시키는 것이 가능하다. 그러나 이것은 DBA에 의해 엄격하게 데이터베이스가 관리되는 프로젝트의 경우 적용하기가 쉽지 않다.

     

    개발자들 간의 충돌을 막으면서도 테스트 데이터의 정리도 자동으로 수행하는 방법은 각 테스트수행이 종료된 경우 트랜잭션을 롤백시키는 것이다. , 테스트 실행이 시작되었을 때 자동으로 트랜잭션을 시작해서 데이터베이스 작업을 수행하고 결과를 검증한 후에 트랜잭션을 롤백시킴으로써 테스트 데이터들이 자동적으로 제거되도록 하는 것이다. 각 개발자들의 테스트 로직은 트랜잭션에 의해 격리되기 때문에 상호 간에 영향을 미치지 않게 된다.

     

    테스트가 시작될 때 트랜잭션을 자동으로 시작하고 종료될 때 자동으로 롤백시키기 위해서는 어떻게 해야 할까? 간단하다. 테스트 클래스가 Spring org.springframework.test.AbstractTransactionalSpringContextTests 클래스를 상속받기만 하면 된다. 이 클래스는 이 전 아티클에서 살펴본 AbstractDependencyInjectionSpringContextTests 클래스의 기능에 트랜잭션 자동 롤백 기능이 추가된 것이다. AbstractTransactionalSpringContextTests 클래스는 Spring 빈 컨텍스트가 한 번만 로딩되도록 하여 테스트가 빠르게 실행되도록 하며, 빈 컨텍스트에 선언된 빈들이 자동으로 테스트 클래스에 의존 주입되도록 하고, 매 테스트 메소드 실행 시 자동으로 트랜잭션을 시작하고 종료 시 롤백 시킴으로써 데이터베이스를 항상 동일한 상태로 유지될 수 있도록 한다.

     

    OrderRepository 테스트 클래스의 코드를 살펴보자.

    package org.eternity.customer;
    
    import org.springframework.test.AbstractTransactionalSpringContextTests;
    
    public class OrderRepositoryTest extends AbstractTransactionalSpringContextTests {
      private Customer customer;     
      private OrderRepository orderRepository;
      private ProductRepository productRepository;
      private CustomerRepository customerRepository;
         
      public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
      }
    
      public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
      }
      
      public void setCustomerRepository(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
      }
        
      @Override
      protected String[] getConfigLocations() {
        return new String[] {
          "org/eternity/persistence-beanContext.xml",
           "org/eternity/order-beanContext.xml"
        };
      }
    
      public void onSetUpInTransaction() throws Exception {
        productRepository.save(new Product("상품1", 1000));
        productRepository.save(new Product("상품2", 5000));           
    
        customer = new Customer("CUST-01", "홍길동", "경기도 안양시", 200000);
        
        customerRepository.save(customer);
      }
        
      public void testOrdreCount() throws Exception {
        orderRepository.save(customer.newOrder("CUST-01-ORDER-01")
                                     .with("상품1", 5)
                                     .with("상품2", 20)
                                     .with("상품1", 5));
        orderRepository.save(customer.newOrder("CUST-01-ORDER-02")
                                     .with("상품1", 20)
                                     .with("상품2", 5));            
    
        assertEquals(2, orderRepository.findByCustomer(customer).size());
      }
         
      public void testDeleteOrder() throws Exception {
        orderRepository.save(customer.newOrder("CUST-01-ORDER-01")
                                     .with("상품1", 5)
                                     .with("상품2", 20));
                                     
        Order order = orderRepository.find("CUST-01-ORDER-01");     
        
        orderRepository.delete(order);
                              
        assertNull(orderRepository.find("CUST-01-ORDER-01"));
        assertNotNull(order);        
      }   
    }

     

    이전의 테스트 클래스에서 딱 3가지만 수정함으로써 자동 트랜잰션 롤백이 가능한 테스트 클래스로 변경했다. 수정된 부분을 찾을 수 있겠는가? 첫 번째로 테스트 클래스가 AbstractTransactionalSpringContextTests를 상속 받도록 수정했다. 두 번째로 getConfigLocations() 메소드가 새로 추가된 빈 컨텍스트 파일인 persistence-beanContext.xml 파일도 함께 반환하도록 수정됐다. 마지막으로 onSetUp() 메소드의 명칭을 onSetUpInTransaction()으로 수정했다. onSetUpInTransaction() 메소드는 트랜잭션이 시작되고 각 테스트 메소드가 실행되기 전에 실행되는 메소드로 트랜잭션 컨텍스트 내에서 테스트 픽스처를 셋업하기 위해 사용된다. 트랜잭션이 실행되기 전에 수행될 셋업 작업이 필요한 경우 onSetUpBeforeTransaction() 메소드를 오버라이딩하면 된다.

     

    AbstractTransactionalSpringContextTests는 트랜잭션을 관리하기 위해 빈 컨텍스트에 설정된 트랜잭션 매니저를 사용한다. Hibernate Session을 사용하여 트랜잭션을 관리하도록 트랜잭션 관리자 설정 정보를 추가하자.

     

      <bean id="transactionManager" 
            class="org.springframework.orm.hibernate3.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
      </bean>
    </beans>

     

    모든 준비가 끝났다. 테스트를 실행해 보자. 녹색 막대다. 마침내 우리의 주문 시스템이 영속성을 가지게 되었다. , 한숨 돌렸다. 정말 힘든 하루였던 것 같다. 연말 인센티브는 살아 있는 걸까?

    그림 4 리포지토리 구현 클래스만 변경되었을 뿐 도메인 모델은 변합이 없다. 이것이 바로 도메인 주도 설계, 낮은 결합도, 비 침투적인 인프라 스트럭처의 힘이다.

     

    도메인을 닮은 어플리케이션

    부부는 함께 살면 얼굴이 닮는다고 한다. 어플리케이션 역시 자신이 투영하고 있는 도메인과 오래도록 동고동락할 수 밖에 없다. 따라서 가장 좋은 어플리케이션은 시간이 지남에 따라 도메인과 닮아가는 어플리케이션이 아닌가 생각된다. 백년해로하는 노부부처럼 말이다.

     

    그러나 시간이 지나면서 자연스럽게 얼굴이 닮아 가는 부부와 달리 어플리케이션의 경우 도메인을 닮기 위해서는 시작부터 도메인을 염두에 두고 출발해야 한다. 실제 도메인의 용어와 개념을 차용하여 어플리케이션을 구성하고, 도메인을 추상화한 단일 모델을 통해 개발을 이끌어 나가며, 비침투적인 인프라 스트럭처를 사용하여 도메인의 독립성을 보장하는 것이 그 출발점이 될 것이다.

     

    도메인 모델 패턴은 도메인과 어플리케이션 간의 표현적 차이를 최소화하기 위해 도메인 레이어를 구성하는 방법이다. 그러나 도메인 모델을 구성하기 위한 원칙이나 비침투적인 인프라 스트럭처 없이 DOMAIN MODEL 패턴을 적용할 경우 프로젝트의 실패로 이어질 확률이 높다. 물론 도메인 모델 패턴을 적용하는데 있어 가장 큰 어려움은 객체 지향 분석/설계 기술을 가진 팀을 구성하는 일일 것이다. 본 아티클이 도메인 모델 패턴을 사용하여 어플리케이션을 구축하려고 하는 팀에게 작지만 알찬 정보를 제공했기를 바라면서 아티클을 마치고자 한다.

     

    4부 끝


     

    728x90
Designed by Tistory.