Eternity's Chit-Chat

aeternum.egloos.com



Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 3부 Domain-Driven Design

생명 주기 제어

객체 지향 시스템은 거대한 객체들의 네트워크로 구성되어 있다. 객체는 상호 연결된 객체들간의 협력을 통해 할당된 책임을 완수한다. 일반적으로 한 객체에서 다른 객체로 이동하기 위해 객체 간의 연관 관계를 이용한다. 따라서 특정한 작업을 수행하기 위해서는 얽히고 설킨 수 많은 객체들 중 어떤 객체에서 항해를 시작할 것인지를 결정해야 한다.

 

SQL 쿼리를 통해 어떤 결과 목록에라도 접근이 가능한 관계형 데이터베이스와 달리 객체 지향 시스템은 임의의 결과 목록에 자동으로 접근할 수 있는 메커니즘을 제공하지 않는다. 모든 객체가 메모리 상에 존재한다고 가정하고 객체와 객체 간의 관계를 항해함으로써 목적 객체로 이동한다. 따라서 어떤 객체 그룹을 사용할 필요가 있다면 해당 객체 그룹 간의 관계를 항해하기 위한 시작 지점을 선정해야 한다. 이와 같이 객체 그래프 상에서 항해를 시작하기 위한 시작 객체를 ENTRY POINT라고 한다. 객체 그룹의 ENTRY POINT는 항상 REFERENCE OBJECT여야 한다. VALUE OBJECTENTRY POINT가 될 수 없다.

 

사용자 요청이 시스템 내에 도착하면 시스템은 요청을 처리할 객체 그룹을 찾는다. 이 객체 그룹 중 ENTRY POINT에 해당하는 REFERENCE OBJECT가 그룹을 대표하여 요청을 전달받고 작업을 수행하기 위해 필요한 객체들과의 협력을 통해 요청을 완수한다.

 

따라서 시스템은 임의의 ENTRY POINT에 접근 가능해야 한다. 또한 ENTRY POINTREFERENCE OBJECT이므로 ENTRY POINT에 접근할 때마다 동일한 객체 인스턴스를 반환 받아야 한다. 이것은 동일한 ENTRY POINT의 요청에 대해 항상 동일한 식별자를 지닌 객체가 반환된다는 것을 의미한다. 따라서 동일한 ENTRY POINT에 대한 요청 결과로 반환 받은 객체들은 “==” 테스트를 통과해야만 할 것이다.

 

이처럼 ENTRY POINT의 유일성과 추적성을 유지하기 위해서는 ENTRY POINT를 관리하는 특별한 객체가 필요하다. 이 특별한 객체는 특정한 ENTRY POINT의 목록을 유지하고 클라이언트에 ENTRY POINT에 대한 관리 인터페이스를 제공한다. , ENTRY POINT와 관련된 추가, 수정, 삭제, 조회 등의 컬렉션 처리를 수행한다. ENTRY POINT가 필요한 경우 관리 객체에게 해당 ENTRY POINT를 찾아 줄 것을 요청한다. 모든 ENTRY POINT에 대한 검색이 해당 관리 객체를 통해 이루어지기 때문에 시스템의 모든 부분은 항상 동일하고 유일한 ENTRY POINT를 대상으로 작업을 수행할 수 있다.

 

ENTRY POINT에 대한 관리 인터페이스를 구성하는 방법에는 두 가지가 존재한다.

  • 각각의 ENTRY POINT가 스스로 관리 인터페이스를 제공한다.
  • 별도의 객체가 ENTRY POINT에 대한 관리 인터페이스를 제공한다.

 

두 방법 모두 생성된 ENTRY POINT를 메모리 내에서 검색하기 위한 메커니즘을 필요로 한다.   이를 처리하기 위해 ENTRY POINT는 메모리 내에서 자신을 손쉽게 검색할 수 있도록 검색 키를 제공해야 한다. 우선 모든 ENTRY POINT 대한 LAYER SUPERTYPEEntryPoint 클래스를 만들고 검색 키를 반환하는 getIdentity() 메소드를 추가한다.



EntryPoint.java

package org.eternity.common;

 

public class EntryPoint { 

private final String identity;

 

public EntryPoint (String identity) {

this.identity = identity;

}

      

public String getIdentity() {

return identity;

}

 

public EntryPoint persist() {

Registrar.add(this.getClass(), this);

return this;

}

}



EntryPointENTRY POINT 검색에 사용될 검색 키인 identity를 생성자의 인자로 전달받는다. 따라서 EntryPoint를 상속 받게 될 모든 ENTRY POINT는 객체 생성 시 자신의 identity를 제공하도록 강제된다. 객체가 생성된 후에는 persist() 메소드를 통해 ENTRY POINT 관리 객체를 사용하여 자기 자신을 등록한다. 등록된 ENTRY POINT는 검색 키를 사용하여 다시 조회할 수 있다.

 

이제 메모리 내의 ENTRY POINT 컬렉션을 관리할 Registrar 클래스를 작성한다. Registrar 클래스는 SINGLETON이며 EntryPoint들의 Class identity를 사용하여 각 ENTRY POINT들을 관리한다.



Registar.java

package org.eternity.common;

 

import java.util.Collection;

import java.util.Collections;

import java.util.HashMap;

import java.util.Map;

 

public class Registrar {

private static Registrar soleInstance = new Registrar();

private Map<Class<?>,Map<String,EntryPoint>> entryPoints =

new HashMap<Class<?>, Map<String, EntryPoint>>();

 

public static void init() {

soleInstance.entryPoints =

    new HashMap<Class<?>, Map<String, EntryPoint>>();

}

 

public static void add(Class<?> entryPointClass, EntryPoint newObject){

soleInstance.addObj(entryPointClass, newObject);

}

 

public static EntryPoint get(Class<?> entryPointClass, String objectName) {

return soleInstance.getObj(entryPointClass, objectName);

}

 

public static Collection<? extends EntryPoint> getAll(

Class<?> entryPointClass) {

return soleInstance.getAllObjects(entryPointClass);

}

 

private void addObj(Class<?> entryPointClass, EntryPoint newObject) {

Map<String,EntryPoint> theEntryPoint =

      entryPoints.get(entryPointClass);

if (theEntryPoint == null) {

      theEntryPoint = new HashMap<String,EntryPoint>();

      entryPoints.put(entryPointClass, theEntryPoint);

}

 

theEntryPoint.put(newObject.getIdentity(), newObject);

}

 

private EntryPoint getObj(Class<?> entryPointClass, String objectName) {

Map<String,EntryPoint> theEntryPoint =

      entryPoints.get(entryPointClass);

return theEntryPoint.get(objectName);

}

 

@SuppressWarnings("unchecked")

private Collection<? extends EntryPoint> getAllObjects(

Class<?> entryPointClass) {

Map<String,EntryPoint> foundEntryPoints =

entryPoints.get(entryPointClass);

 

return (Collection<? extends EntryPoint>)

Collections.unmodifiableCollection(foundEntryPoints != null ?

entryPoints.get(entryPointClass).values() :

Collections.EMPTY_SET);

}

}



이제 Customer 객체가 EntryPoint를 상속 받도록 수정한다. 고객에 대한 검색 키로는 고객 번호인 customerNumber를 사용한다.



Customer.java

public Customer extends EntryPoint {

 

...

 

public Customer(String customerNumber, String name, String address) {

super(customerNumber);

this.customerNumber = customerNumber;

this.name = name;

this.address = address;

}


 

이제 Registrar 클래스를 사용하여 고객의 유일성을 유지할 수 있다. ENTRY POINT 관리를 위한 두 가지 방법 중에서 먼저 Customer 클래스 자체에 컬렉션 관리 인터페이스를 추가하는 방식을 살펴보자. 우선 Customer 클래스의 검색을 위한 테스트 케이스를 작성하자.



CustomerTest.java

public void setUp() {

Registrar.init();

}

      

public void testCustomerIdentical() {

Customer customer =

new Customer("CUST-01", "홍길동", "경기도 안양시").persist();

Customer anotherCustomer = Customer.find("CUST-01");

assertSame(customer, anotherCustomer);           

}

 

모든 테스트 케이스가 독립적이어야 한다는 테스트의 기본 원칙을 지키기 위해 setUp() 메소드 안에서 Registrar를 초기화시켰다. 테스트 메소드인 testCustomerIdentical() 안에서는 Customer 클래스의 인스턴스를 생성한 후 persist() 메소드를 사용하여 Registrar에 등록한다. Customer 클래스의 검색 키인 고객 번호를 find 메소드의 인자로 전달하여 Customer 객체를 조회한 후 반환된 anotherCustomer가 이미 등록된 Customer 클래스와 동일한 식별자를 가지고 있는 지 검사한다. 동일성 식별에 “==” 연산자가 사용되었음에 주목하자.

 

이제 실패하는 테스트를 가지게 되었다. , 내가 이 이야기를 했던가? 코드를 작성하기 전에 테스트를 작성하는 것은 좋은 습관이다. Test-First Approach 또는 Test-Driven Development라고 불리는 이 방법은 우선 실패하는 테스트를 작성한 후 테스트를 성공시키는 방법으로 코드를 작성한다. 이 테스트를 통과하도록 Customer 클래스를 수정하자.



Customer.java

public static Customer find(String customerName) {

  return (Customer)Registrar.get(Customer.class, customerName);

}


public Customer persist() {

  return (Customer)super.persist();

}


 

EntryPointpersist()를 오버라이딩 한 이유는 persist 메소드의 반환형을 Customer로 수정하고 싶기 때문이다. EntryPoint 클래스의 persist() 메소드의 경우 EntryPoint 타입을 반환하기 때문에 persist() 메소드를 호출하는 클라이언트 측에서 매번 형변환을 해야 한다. 따라서 EntryPoint 클래스의 persist() 메소드를 오버라이드하여 각 ENTRY POINT가 자신의 타입을 반환하도록 하여 형변환을 할 필요가 없도록 만드는 것이 더 사용하기 편리한 인터페이스를 만드는 방법이다.

 

다음은 수정된 Customer 클래스의 코드를 나타낸 것이다.



Customer.java

package org.eternity.customer;

 

import org.eternity.common.EntryPoint;

import org.eternity.common.Registrar;

 

public class Customer extends EntryPoint {

private String customerNumber;

private String name;

private String address;

private long mileage;

      

public Customer(String customerNumber, String name, String address) {

super(customerNumber);

this.customerNumber = customerNumber;

this.name = name;

this.address = address;

}   

 

public static Customer find(String customerName) {

return (Customer)Registrar.get(Customer.class, customerName);

}

      

public Customer persist() {

return (Customer)super.persist();

}

      

public void purchase(long price) {

mileage += price * 0.01;

}

      

public boolean isPossibleToPayWithMileage(long price) {

return mileage > price;

}

 

public boolean payWithMileage(long price) {

if (!isPossibleToPayWithMileage(price)) {

return false;

}

 

mileage -= price;

 

return true;

}

 

public long getMileage() {

return mileage;

}

}


핑백

  • Domain-Driven Design | Jongmin Kim&#039;s Blog 2014-09-02 01:18:38 #

    ... 의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 4부 [4] 2008/11/17 Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 3부 2008/11/16 Domain-Driven Design의 적용-1.VALUE OBJECT와 ... more