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에 대한 관리 인터페이스를 구성하는 방법에는 두 가지가 존재한다.
- 각각의 ENTRY POINT가 스스로 관리 인터페이스를 제공한다.
- 별도의 객체가 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 |
참조