ABOUT ME

-

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

    이전글 : 도메인 주도 설계의 적용 - 2. 애그리게이트와 리포지토리 5부

     

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

     

     

    여기  사나이가 있다. 불안한 시선으로 방을 둘러보던 사나이는 자신의 몸에 새겨진 문신과 폴라로이드 사진에 적힌 문장  줄에 의지해 이전의 상황을 재구성하려고 노력한다. 그는 전직 보험 수사관으로, 아내가 죽던   범인에 의해 뒤통수를 강타당하고 쓰러진다.    사나이는 사건 이전의 일에 대해서는 완벽하게 기억하지만 사건 이후의 일에 대해서는 아주 짧은 순간만을 기억하는 정신적 혼란을 겪게 된다.  10분이 안 되는 짧은 시간 밖에 기억하지 못하는 그는 잊지 말아야  기억의 조각들을 자신의 몸과 폴라로이드 사진 위에 기록하며 과거와 현실 사이의 기억을 재편한다.

     

    크리스토퍼 놀런감독의 2000 작인 <메멘토> 단기 기억 상실증(Short-Term Memory Loss) 걸린 주인공이 현실을 기록하고, 기록을 통해 기억을 재생하고, 재생된 기억 위에 현실을 다시 재구성하는 반복적인 과정을 통해 아내를 죽인 범인을 쫓아 가는 과정을 그리고 있다. 주인공은 소멸되는 기억의 끈을 놓치지 않기 위해 끊임없이 자신의 몸과 폴라로이드 사진에 기록을 남기고 기록을 바탕으로 과거를 복구한다.

     

    애플리케이션은 <메멘토> 주인공과 같다. 종료 되는 순간 단기 기억 상실증(Short-Term Memory Loss) 걸린 애플리케이션의 모든 기억들은 깨끗하게 증발돼 버리고 만다. 사용자들이 공을 들여 입력한 데이터는 공중으로 사라져 버리고, 분노한 사용자들의 욕설과 비방이    들이닥치던 어느 , 실직자가  당신은 차가운 거리로 내몰리게 될지도 모른다. 따라서 애플리케이션은 상태를 끊임없이 기록하고, 기억을 재생하고, 현실을 재구성해야 한다. 유효 시간이 지나고 단기 기억 상실증으로 인해 모든 기억이 소멸되더라도 기록을 통해 애플리케이션의 기억을 되돌릴  있어야 한다.

     

    애플리케이션이 기억을 재생할  있다고 해서 모든 기억을 동시에 복구하는 것은 소모적인 일이다. 살아오면서 겪은 모든 일들을 일일이  기억하고 있을 수는 없다. 필요할  사진첩을 들춰 보고 일기장을 펼쳐 보고 다이어리를 열어 보고  순간을 회상할  있으면 족하다. 눈앞에 닥친 일들을 기억하기에도 벅찬 것이 현실이다. 애플리케이션 역시 마찬가지다. 지금 처리하기 위해 필요한 최소한의 정보만 기억하고 있으면 된다. 시스템의 메모리는 한정된 자원이다. 지금 당장 필요하지도 않은 정보들을 유지하기 위해 값비싼 자원을 낭비할 필요는 없다. 잠시 잊어버렸다가 필요할  기록을 들춰 보면 된다.

     

    신이 인간에게  최고의 선물은 망각이지만 인간이 컴퓨터에게  최고의 선물은 영구적인 기억력이다. 그러나 영구적인 기억력이 하늘에서 뚝하고 떨어지는 것은 아니다. 애플리케이션을 망각의 늪에서 구원하기 위해서는 지금까지 우리가 망각하고 있던  가지 요소가 필요하다. 바로 영속성(persistence) 그것이다.

    도메인객체의 생명주기

    다시 주문 도메인으로 돌아가 보자. 이전 아티클에서 엔트리 포인트 대한 저장, 조회 등의 컬렉션 연산을 수행하기 위해 리포지토리 사용했다. 기억이  나지 않는가? 머리를 쥐어뜯을 필요는 없다. 정도의 차이는 있지만 단기 기억 상실증은 모든 사람들에게 나타나는 일반적인 증상이다. 다행히 이전 아티클에서 리포지토리 관해 기록을  두었으니 아티클을 다시 읽어 보며 기억을 복구하기 바란다. 우리는 주문 도메인을 분석한  Customer, Product, Order 엔트리 포인트 식별하고  엔트리 포인트 생명 주기를 관리하기 위해 CustomerRepository, ProductRepository, OrderRepository 도메인 모델에 추가했다.

    그림 1 주문 도메인 모델의 리파지토리

     

    이제 Order OrderLineItem 생명 주기를   자세히  살펴보자. Order OrderLineItem Customer newOrder() 메서드를 통해 생성된다. newOrder() 메서드는 Order 생성 메서드(CREATION METHOD) order() 호출하며,  메서드는 내부적으로 생성자를 호출하여 Order 클래스를 생성한  반환한다. OrderLineItem Order with() 메서드를를 사용하여 생성되며, with() 메소드 역시 OrderLineItem 생성자를 호출하여 인스턴스를 생성한다.

    public class Order extends EntryPoint {
      public static Order order(String orderId, Customer customer) {          
        return new Order(orderId, customer);
      }
    
      Order(String orderId, Customer customer) {
        super(orderId);
        this.customer = customer;           
      }    
    
      public Order with(String productName, int quantity) throws OrderLimitExceededException {
        return with(new OrderLineItem(productName, quantity));
      }
    }

     

    Order OrderLineItem 생성자가 호출되는 순간 사용자가 입력한 주문 정보를 저장하고 있는 주문 애그리게이트 생성된다. 그러나 객체를 생성하는 것으로 끝난다면 지금까지 이야기해 왔던 참조 객체 추적성과 유일성을 만족시킬  없다. 생성된 주문 객체가 애플리케이션의 생명주기 동안 동일한 객체로 참조되기 위해서는 리포지토리 필요하다. 생성된 객체는 리포지토리 의해 관리되며 객체가 필요한 경우 리포지토리 통해 해당 참조 객체 얻을  있게 된다. 물론 리포지토리 통해 등록된 객체와 조회된 객체의 식별자(identity) 동일해야 한다. 테스트를 추가하자.

    public class OrderTest extends TestCase {
      public void testOrderIdentical() throws Exception {
        Order order = customer.newOrder("CUST-01-ORDER-01")
                              .with("상품1", 10)
                              .with("상품2", 20);
    
        orderRepository.save(order);            
    
        Order anotherOrder = orderRepository.find("CUST-01-ORDER-01");
        
        assertEquals(order, anotherOrder);
      }   
    }

     

    녹색 막대다. 리포지토리 통해 등록된 주문 객체들은 추적성과 유일성이라는 참조 객체 본연의 특성을 만족시키고 있다. 녹색 막대를 보고 났더니 기분이 상쾌해졌다. 이제  색다른 것을 시도해 볼까?

     

    이번에는 생성된 주문 내역을 삭제하는 시나리오를 구현해 보자. 주문의 생명 주기를 관리하는 클래스는 OrderRepository이므로 여기에 삭제 관련 메서드를 추가하자. , 잠깐. 이게 아니지. 가끔 깜빡할 때가 있다. 메서드가 아니라 테스트를 먼저 추가해야 한다. OrderRepositoryTest 클래스에 주문을 삭제하는 테스트를 추가하자.

    public class OrderTest extends TestCase {
      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("CUST-01-ORDER-01"); 
    
        assertNull(orderRepository.find("CUST-01-ORDER-01"));
        assertNotNull(order);
      }
    }

     

    주문 건을 생성한  리포지토리 등록한다. 등록이 성공하면 해당 주문을 리포지토리로부터 삭제한다. 삭제 역시 조회의 경우와 마찬가지로 삭제할 주문 객체를 명시하기 위해 주문 ID 인자로 전달한다. 리포지토리에서 삭제되었다는 것을 확인하기 위해 주문 객체를 조회한  결괏값이 null인지 확인한다

     

    여기에서 마지막 단정문을 눈여겨보자. 테스트 메서드는 리포지토리에서 delete() 메서드를 호출하여 주문 객체를 삭제하기 전에 find() 메소드를 호출하여 리포지토리로부터 생성된 객체에 대한 참조를 보관한다. delete() 메소드를 호출하여 리포지토리로부터 해당 객체를 삭제한  앞에서 조회한 객체가 null아닌지를 검증한다.

     

    리포지토리 관점에서의 삭제는  이상 해당 객체를 참조 객체 취급하지 않겠다는 것을 의미한다. , 시스템이 해당 주문 객체의 추적성을 보장하지 않겠다는 의미이다. 따라서 일단 삭제가 완료되면  이상 리포지토리 통해 해당 객체를 얻을  없게 된다. 이것을 주문 객체 자체의 소멸과 혼동해서는  된다. 단지 주문 객체가 리포지토리 제어에서 벗어나 추적성과 유일성을 잃을 뿐이지 객체 자체가 소멸되는 것은 아니다. 객체의 소멸은 가비지 컬렉터에 의해서만 가능하다.

     

    리포지토리 삭제 메서드를 추가하기 위해 Registrar 삭제와 관련된 기본 기능을 추가하자.  엔트리 포인트 클래스와 연관된 Map으로부터 검색 키의 엔트리를 삭제한  반환하도록 구현한다.

    public class Registrar {
      public static EntryPoint delete(Class<?> entryPointClass, String objectName) {
        return soleInstance.deleteObj(entryPointClass, objectName);
      }
    
      @SuppressWarnings("unused")
      private EntryPoint deleteObj(Class<?> entryPointClass, String objectName) {
        Map<String,EntryPoint> theEntryPoint = entryPoints.get(entryPointClass);
        return theEntryPoint.remove(objectName);  
      }
    }

     

    Registrar 사용하여 OrderRepository 주문 삭제 메서드를 추가한다.

    public class OrderRepository {
      public Order delete(String identity) {
        return (Order)Registrar.delete(Order.class, identity);
      }
    }

     

    테스트를 실행해 보자. 역시 예상대로 통과한다. 테스트 중독은 행복한 일이다.

     

    모든 테스트가 성공했고, 시스템은 참조 객체 정상적으로 추적하고 있으며, 리포지토리 객체의 생명 주기를 관리하기 위해 필요한 모든 기능을 제공하고 있다. 사용자들은 증가하고 주문은 나날이 늘어만 간다. 이런 추세라면 연말 인센티브는  거하게 기대해도 좋을  같다. 참으로 아름다운 것이 인생인  같다.

     

    그런데 갑자기 분위기가 이상하다. 사람들이 얼굴이 사색이 돼서 우왕좌왕하는 것을 보니 뭔가  문제가 생긴  같다. 이런, 시스템이 다운됐다니! 그럼 지금까지 처리된 데이터는 어떻게 되는 것인가. 맙소사! 인센티브는 고사하고 고객들한테 몰매를 맞기 전에 어서 짐을 싸서 줄행랑을 치는  좋을  같다. 죽지 못해 사는 것이 인생인  같다.

     

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

    728x90
Designed by Tistory.