ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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에 대한 관리 인터페이스를 구성하는 방법에는 두 가지가 존재한다.
    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의 적용

    '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
Designed by Tistory.