JAVA/DDD

DDD Value Object와 Reference Object 3부

100win10 2020. 12. 23. 12:28

DDD 이터니티 조용호 님의 블로그를 보고 다시 한번 공부하기 위해서 정리하게 되었습니다!

 


 

 

생명 주기 제어


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

 

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

 

 

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

 

  • 따라서 시스템은 임의의 ENTRY POINT에 접근 가능해야 한다
  • 또한 ENTRY POINT는 REFERENCE 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에 대한 관리 인터페이스를 구성하는 방법에는 두 가지가 존재한다.
  1. 각각의 ENTRY POINT가 스스로 관리 인터페이스를 제공한다.
  2. 별도의 객체가 ENTRY POINT에 대한 관리 인터페이스를 제공한다.

 

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class EntryPoint { 
 
    private final String identity;
 
 
 
    public EntryPoint (String identity) {
 
    this.identity = identity;
    
    }
 
      
 
    public String getIdentity() {
 
    return identity;
 
    }
 
 
 
    public EntryPoint persist() {
 
    Registar.add(this.getClass(), this);
    
    return this;
 
    }
 
}
cs

 

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

 

  • 이제 메모리 내의 ENTRY POINT 컬렉션을 관리할 Registrar 클래스를 작성한다.
  • Registrar 클래스는 SINGLETON이며 EntryPoint들의 Class identity를 사용하여 각 ENTRY POINT들을 관리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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);
 
    }
 
}
cs

 

 

 

 

  • 이제 Customer 객체가 EntryPoint를 상속 받도록 수정한다.
  •  고객에 대한 검색 키로는 고객 번호인 customerNumber를 사용한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Customer extends EntryPoint {
 
 
 
    ...
 
 
 
    public Customer(String customerNumber, String name, String address) {
 
    super(customerNumber);
 
    this.customerNumber = customerNumber;
 
    this.name = name;
 
    this.address = address;
 
}
cs

 

 

 

  • 이제 Registrar 클래스를 사용하여 고객의 유일성을 유지할 수 있다
  • ENTRY POINT 관리를 위한 두 가지 방법 중에서 먼저 Customer 클래스 자체에 컬렉션 관리 인터페이스를 추가하는 방식을 살펴보자
  • 우선 Customer 클래스의 검색을 위한 테스트 케이스를 작성하자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void setUp() {
 
        Registrar.init();
 
        }
 
 
 
public void testCustomerIdentical() {
 
        Customer customer =
 
        new Customer("CUST-01""홍길동""경기도 안양시").persist();
 
        Customer anotherCustomer = Customer.find("CUST-01");
 
        assertSame(customer, anotherCustomer);
 
        }
}
cs

(눈이 아파서 색을 바꿨습니다...)

CustomerTest.java

 

 

 

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

 

 

 

 

  • 이제 실패하는 테스트를 가지게 되었다.
  • 코드를 작성하기 전에 테스트를 작성하는 것은 좋은 습관이다.
  • Test-First Approach 또는 Test-Driven Development라고 불리는 이 방법은 우선 실패하는 테스트를 작성한 후 테스트를 성공시키는 방법으로 코드를 작성한다
  • 이 테스트를 통과하도록 Customer 클래스를 수정하자.
1
2
3
4
5
6
7
8
9
10
11
12
public static Customer find(String customerName) {
        return (Customer)Registrar.get(Customer.class, customerName);
 
        }
 
 
 
public Customer persist() {
 
        return (Customer)super.persist();
 
        }
cs

Customer.java

 

 

 

 

 

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

 

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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;
 
    }
 
}
cs

 

 

 

 

참조


이터니티 - Domain-Driven Design의 적용