-
DDD Value Object와 Reference Object 3부JAVA/DDD 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() 메소드를 추가한다.
12345678910111213141516171819202122232425262728293031public 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들을 관리한다.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113import 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를 사용한다
12345678910111213141516171819public 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 클래스의 검색을 위한 테스트 케이스를 작성하자
1234567891011121314151617181920public 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 클래스를 수정하자.
123456789101112public 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 클래스의 코드를 나타낸 것이다.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import 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 참조
'JAVA > DDD' 카테고리의 다른 글
AGGREGATE와 REPOSITORY 2부 (0) 2020.12.29 AGGREGATE와 REPOSITORY 1부 (0) 2020.12.28 DDD - Value Object와 Reference Object 4부 (0) 2020.12.24 DDD - Value Object와 Reference Object 2부 (0) 2020.12.22 DDD - Value Object와 Reference Object (0) 2020.12.21