-
도메인 주도 개발4 - Entity와 Value1FrameWork/Spring&Spring-boot 2023. 11. 12. 15:12
Entity와 Value1
도출한 모델은 크게 엔티티(Entity)와 밸류(Value)로 구분할 수 있다. 게시글<도메인 주도 개발3 - 도메링 모델 도출>요구사항 분석 과정에서 만든 모델은[그림 1.6]과 같은데 이 그림에는 엔티티도 존재하고 밸류도 존재한다. 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 이 둘의 차이를 명확하게 이해하는 것은 도메인을 구현하는데 있어 중요하다.
엔티티
엔티티의 가장 큰 특징은 식별자를 가진다는 것이다. 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다. 예를 들어 주문 도메인에서 각 주문은 주문번호를 가지고 있는데 이 주문번호는 각 주문마다 서로 다르다. 따라서 주문번호가 주문의 식별자가 된다. 앞서 주문 도메인 모델에서 주문에 해당하는 클래스가 Order이므로 Order가 엔티티가 되며 주문번호를 속성으로 갖게 된다.
주문에서 배송지 주소가 바뀌거나 상태가 바뀌더라도 주문번호가 바뀌지 않는 것처럼 엔티티의 식별자는 바뀌지 않는다. 엔티티를 생성하고 속성을 바꾸고 삭제할 때까지 식별자는 유지된다.
public class Order{ private String orderNumver; @Override public boolean equals(Object obj){ if(this == obj) return true; if(obj == null) return false; // obj가 Orderclass인지 확인 if(obj.getClass() != Order.class) return false; // obj를 Order 형으로 형변환해서 other 변수에 대입 //obj를 Object 형으로 바꿨기 때문에 비교를 위해 형변환 Order other = (Order)obj; if(this.orderNumber == null)return false; return this.orderNumber.equals(other.orderNumber); } @Override public int hashCode(){ final int prime = 31; int result =1; result = prime*1 + (orderNumber == null) ? 0 : orderNumber.hashCode()); return reuslt; } }
엔티티의 식별자 생성
특정 규칙을 따르는 식별자
엔티티 식별자를 생성하는 시점은 도메인 특성과 사용하는 기술에 따라 달라진다. 흔히 식별자는 다음주 한가지 방식으로 생성한다.
- 특정 규칙에 따라 생성
- UUID나 Nano ID와 같은 고유 식별자 생성기 사용
- 값을 직접 입력
- 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)
주문번호, 운송장번호, 카드번호와 같은 식별자는 특정 규칙에 따라 생성한다. 이 규칙은 도메인에 따라 다르고, 같은 주문번호라도 회사마다 다르다. 예를 들어 최근 두 온라인 서점에서 구매한 책의 주문번호는 각각 '29348908431902890'와 '002-B123980000'인데 두 번호의 구조가 완전히 다른 것을 알 수 있다.
흔히 사용하는 규칙은 현재 시간과 다른 값을 조합하는 것이다. 한 온라인 쇼핑 사이트에서 구매한 책 주문번호는 '202311110000'인데, 이 주문번호의 앞 번호인 '20231111'은 2023년 11월 11일을 의미한다. 날짜와 시간을 이용해서 식별자를 생성할 때 주의할 점은 같은 시간에 동시에 식별자를 생성해도 같은 식별자가 만들어지면 안 된다는 것이다.
직접값을 입력하는 식별자
회원의 아이디나 이메일과 같은 식별자는 값을 직접 입력한다. 사용자가 직접 입력하는 값이기 때문에 식별자를 중복으로 입력하지 않도록 사전에 방지하는 것이 중요하다.
.../sports/score/users/talk/#read?articleId = 893253&bbsId=F01
일련번호를 식별자로 사용하기도 한다. 예를 들어 위는 포털 사이트에서 게시글 URL의 일부를 발췌한 것이다. 위 URL에서 articleId 파라미터 값인 893253이 일련번호 방식의 식별자에 해당한다.
일련번호 사용 식별자
일련 번호 방식은 주로 데이터베이스가 제공하는 자동 증가 기능을 사용한다. 자동 증가 컬럼은 DB 테이블에 데이터를 삽입해야 비로소 값을 알 수 있기 때문에 테이블에 데이털르 추가하기 전에는 식별자를 알 수 없다. 이 것은 엔팉티 객체를 생설 할 때 식별자를 전달 할 수 없음을 의미 한다.
밸류 타입
private class ShippingInfo{ //받는 사람 private String receiverName private String receiverPhoneNumber; //주소 private String ShippingAddress1; pricate String ShippingAddress2; private String shippingZipcode; }
shippingInfo 클래스는 그림 위와 같이 주문물품을 받는 사람과 주소에 대한 데이터를 가지고 있다. 받는 사람과 주소는 개념적으로 하나다.
ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다. 즉 두 필드는 실제로 하나의 개념을 표현한고 있다. 비슷하게 shippingAdress1 필드, shippng Address2 필드, shippingZipcode 필드는 주소라는 하나의 개념을 표현한다.
Receiver.java
public class Receiver{ private String name; private String phoneNumber; public Recevier(String name, String phoneNumber){ this.name = name; this.phoneNumber = phoneNumber; } public String getName(){ return name; } public String getPhoneNumber(){ return phoneNumber; } }
밸류 타입은 개념적을 완전한 하나를 표현할 때 사용한다. 예를 들어 받는 사람을 위한 밸류 타입인 Receiver를 위와 같이 작성할 수 있다.
Receiver는 '받는 사람'이라는 도메인 개념을 표현한다. 앞서 ShippingInfo의 receiverName 필드와 receiverPhoneNumber 필드가 이름을 갖고 받는 사람과 관련된 데이터라는 것을 유추한다면 Receiver는 그 자체로 받는 사람을 뜻한다. 밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것이다.
Address.java
public class Address{ private String address1; private String address2; private String zipcode; public Address(String address1, String address2, String zipcode){ this.address1 = address1; this.address2 = address2; this.zipcode = zipcode; } ... }
ShippingInfo의 주소 관련 데이터도 위처럼 Address 밸류 타입을 사용해서 보다 명확하게 표현할 수 있다.
OrderLine.java
public class OrderLine{ private Product product; private int price; private int quantity; private int amounts; ... }
밸류 타입이 꼭 두개 이상의 데이터를 가져야 하는 것은 아니다. 의미를 명확하게 표현하기 위해 밸류 타입을 사용하는 경우도 있다. 이를 위한 좋은 예가 OrderLine이다.
Money.java
public class Money{ private int value; public Money(int value){ this.value = value; } public int getValue(){ return this.value } }
OrderLine의 price와 amounts int 타입의 숫자를 사용하고 있지만 이들은 '돈'을 의미하는 값이다. 따라서 '돈'을 의미하는 Money 타입을 만들어 사용하면 코드를 이해하는데 데 도움이 된다.
OrderLine.java
public class OrderLine{ private Product product; private Money price; private int quantity; private Money amounts; .... }
위 코드는 Money를 사용하도록 OrderLine을 변경한 코드이다. Money 타입 덕에 price나 amount가 금액을 이미한다는 것을 쉽게 알 수 있다.
Money.java
public class Money{ private int value; ...생성자,getValue() public Money add(Money money){ return new Money(this.vlaue + money.value); } public Money multiply(int multiplier){ return new Money(value * multiplier) } }
밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다는 것이다. 예를 들어 Money 타입은 위와 같이 돈 계산을 위한 기능을 추가 할 수 있다.
밸류 타입은 코드의 의미를 더 잘 이해할 수 있도록 해준다.
밸류를 변경하기 보다 새로 생성
package com.example.domain.purchase; public class Money { private int value; public Money add(Money money){ return new Money(this.value + money.value); } //value를 변경할 수 있는 메서드 없음 }
밸류 객체 데이어를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호하는 것이 권장된다. 예를 들어 위의 Money클래스의 add()메서드를 보면 Money 인스턴스를 새로 생성한고 있다.
불변(immutable) 타입 과 참조 투명성과 관련된 문제
Money처럼 데이터 변경 기능을 제공하지 않은 타입을 불변immutable이라고 표현하다. 밸류 타입을 불변으로 구현하는 이유는 여러이유가 있는데 가장 중요한 이유는 안전한 코드를 작성할 수 있다는 데 있다.
Money price = ....; OrderLine line = new OrderLine(product,price,quantity); //만약 price.setValue(0)로 값을 변경할 수 있다면?
Money price = new Money(1000) OrderLine line = new OrderLine(product,price,2); // ->[price=1000, quantity=2, amounts=2000] price.setValue(2000); // ->[price=2000, quantity=2, amounts=2000]
그런데 만약 Money가 setValue()와 같은 메서드를 제공해서 값을 변경할 수 있다면 어떻게 될까? 이 이경우 위 코드 처럼 OrderLine의 price값이 잘못 반영되는 상황이 발생하게 된다.
public class OrderLine{ ... private Money price; public OrderLine(Product product, Money price, int quantity){ this.product = product; //Money가 불변 객체가 아니라면, //price 파라미터가 변경될 때 발생하는 문제를 방지하기위해 //데이버를 복사한 새로운 객체를 생성해야한다. this.price = new Money(price.getValue()); this.quantity = quantity; this.amounts = calculateAmounts(); } }
이런 문제가 발생하는 것을 방지하려면 OrderLine 생성자는 위 코드와 같이 새로운 Money객체를 생성하도록 코드를 작성해야 한다.
Money가 분변이면 위와 같은 코드를 작성할 필요가 없다. Money의 데이터를 바꿀수 없기 때문에 파라미터로 전달밭은 price를 안전하게 사용할 수 있다.
💡 tip) 불변객체
불변 객체는 참조 투명성과 스레드에 안전한 특징을 갖고 있다. 불변객체에 대한 자세한 내용은 다음 링크 참조https://ko.wikipedia.org/wiki/%EB%B6%88%EB%B3%80%EA%B0%9D%EC%B2%B4
두 밸류 객체를 비교할 때는 모든 속성이 같은지 비교
public class Receiver{ privte String name; private String phoneNumber; public boolean equals(Object other){ if (other == null) return false; if (this == other) return true; if(!(other instanceof Receiver)) return false; Receiver that = (Receiver)other; return this.name.equals(that.name)&& this.phoneNumber.equals(that.phoneNumber) } ... }
[출처 - 도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지, 최범균 저]
https://www.hanbit.co.kr/store/books/look.php?p_code=B4309942517
'FrameWork > Spring&Spring-boot' 카테고리의 다른 글
Spring 기본 15 - 싱글톤 방식의 주의점 (0) 2023.11.13 Spring 기본 14 - 싱글톤 컨테이너 (0) 2023.11.12 Spring 기본 13 - 스프링 빈 설정 메타 정보 - BeanDefinition (0) 2023.11.10 Spring 기본 12 - BeanFactory와 ApplicationContext (0) 2023.11.10 Spring 기본 11 - 스프링 빈 조회 (0) 2023.11.10