티스토리 뷰
개요
# 참고
IMPLEMENTING DOMAIN DRIVEN DESIGN (도메인 주도 설계 구현) - 반 버논 지음
# 개요
DDD에 대한 기술서적을 읽고 DDD가 무엇인지 이해하고 어떻게 코드에 적용시킬 수 있는지 고민해 보려고 한다.
# 6장의 로드맵
- 값으로 모델링하기 위해 도메인 개념의 특징을 이해하는 방법을 배우자
- 통합의 복잡성을 최소화하기 위해 값 객체를 활용하는 방법을 살펴보자
- 값으로 표현된 도메인 표준 타입의 사용을 확인하자
6장. 값 객체
값 객체란 DDD의 필수적인 구성 요소다. 6장에서는 유비쿼터스 언어를 사용해 도메인 개념을 모델링하는 값에 관해 논의하고, 도메인 주도 설계의 목표에 관해 다른다.
# 값 객체의 이점
측정하고 수량화하거나 설명해주는 값 타입은 생성, 테스트, 사용, 최적화, 유지 관리가 쉽다.
가능하다면 엔티티 대신 값 객체를 사용해 모델링하도록 노력해야 한다. 도메인 개념이 엔티티로 모델링돼야 할 때도 엔티티의 설계는 자식 엔티티의 컨테이너보다는 값의 컨테이너로 동작하는 쪽으로 기울어야 한다. 값 타입은 생성과 테스트, 사용, 최적화, 유지 관리 등이 더 쉽다.
# 잘못된 방식
엔티티에 너무 열광한 나머지, 프로젝트의 시작부터 도메인 모델의 모든 요소가 데이터베이스 테이블로 매핑 하고, 모든 특성은 퍼블릭 접근자 메소드를 통해 세팅되고 검새돼야만 한다는 일반적인 사고방식을 따랐다. 모든 객체가 데이터베이스 기본키를 갖고 있기 때문에, 모델은 크고 복잡한 그래프로 촘촘하게 꿰매졌다. 이런 생각은 주로 모든 대상이 정규화돼야 하고 외래키를 통해 참조돼야 한다는 관계적 데이터베이스의 관점에 지나치게 영향을 받은 데서 비롯된다.
# 올바른 방식
값 인스턴스는 생성하고 전달한 후 잊어버릴 수 있다. 우리는 소비자가 이를 어떻게든 잘못된 방향으로 수정하진 않는지, 어쨌든 수정을 하고 있긴 한지 걱정할 필요가 없다. 값이란 단순히 필요에 따라 오가며, 손상을 일으키지 않고 해롭지도 않은 대상일 뿐이다.
Q. 도메인 개념을 값으로 모델링해야 할지 알 수 있는 방법은 무엇일까?
A. 모델 요소의 특성에만 신경을 쓰고 있다면, 이를 값 객체로 분리하면 좋다. 값 객체를 변경이 불가능한 것으로 취급하자. 식별자는 부여하지 말고, 엔티티를 유지할 때 필요한 설계 복잡성을 피하도록 하자.
6-1. 값의 특징
도메인 개념을 값 객체로 모델링할 땐 유비쿼터스 언어를 확실히 활용해야 한다. 개념을 값으로 나타낼지 결정할 땐, 바드시 다음과 같은 특징의 대부분을 포함하고 있어야 한다.
- 도메인 내의 어떤 대상을 측정하고, 수량화 하고, 설명한다.
- 불변성이 유지될 수 있따.
- 관련 특성을 모은 필수 단위로 개념적 전체를 모델링한다.
- 측정이나 설명이 변결될 땐 완벽히 대체 가능하다.
- 다른 값과 등가성을 사용해 비교할 수 있다.
모델 내의 설계 오소를 분석하기 위해 이 접근법을 사용한다면, 훨씬 더 많은 값 객체를 사용할 수 있다.
6-2. 측정, 수량화, 설명
값 객체는 도메인 내에 있는 어떤 대상을 측정하고 수량화하고 설명하는 개념이다. 사람에게는 나이가 있따. 나이는 실재하는 어떤 대상은 아니지만, 사람이 살아온 햇수를 수량화한다. 또한 사람은 이름을 가진다. 이름 자체는 실재하는 사물은 아니지만, 이는 사람을 어떻게 부를지 설명해 준다. 이러한 개념들은 개념적 전체와 깊은 관련이 있따.
6-3. 불변성
값인 객체는 일단 생성되면 변경할 수 없다. 값 클래스의 생성자를 사용해 인스턴스를 생성하고, 해당 인스턴스 상태의 기반이 될 객체를 매개변수로 전달한다. 이 매개변수는 바로 값의 특성으로 사용되거나, 아니면 새롭게 구성되는 하나 이상의 특성을 뽑아내는 바탕이 된다.
인스턴스화 자체가 객체의 불변성을 보장하지는 않는다. 일단 객체가 인스턴스화되고 생성으 ㅣ방법에 따라 초기화된 후부턴 어떤 메소드도 상태의 변경을 초래할 수 없다. setter는 프라이빗하며 숨겨진 메소드이며, 인스턴스 외부에서 호출할 수 없다. 게다가 생성자를 제외한 어떤 메소드도 센터를 호출하지 못하도록 구현해야 한다.
기호에 따라 엔티티의 참조를 갖고 있는 값 객체를 설계할 수도 있따. 그러나 이 방식은 주의해야 하는데, 참조된 엔티티가 상태를 변경할 땐 값도 함께 변경되는데, 이는 불변성이란 속성을 위반한다. 따라서 값 타입이 참조하는 엔티티는 컴포지션의 불변성, 표현성, 편리함 등을 위해 사용된다고 생각하는 것이 좋다. 객체가 자신의 행동으로 인해 변경되어야 한다고 생각한다면, 대신할 대상을 활용하는 편이 어떤지 생각해 보는 것이 좋다.
6-4. 개념적 전체
값 객체는 하나 이상의 개별적 특성을 가질 수 있으며, 각 특성은 서로 연관돼 있다. 특성을 개별적으로 사용한다면 응집력 있는 의미를 제공하지 못한다. 모든 특성이 함께 있어야만 완벽히 의도에 맞는 측정이나 설명이 만들어진다. 도메인 내부 개념은 전체성이 매우 중요하기 때문에, 값 객체를 향한 부모의 참조는 단순한 특성이 아니다. 이는 모델 내에서 값 객체를 포함하고 있는 부모 객체/대상의 속성으로 봐야 한다. 그렇다면 값 객체의 타입은 하나 이상의 특성을 갖는다. (예시) 값 객체가 갖고 있는 두 특성은 서로의 힘을 합쳐 값 객체의 의미를 알려주게 된다. 즉, 값 객체는 여러 특성을 가질수 있으며, 이 여러 특성을 모두 사용해야지만 값 객체의 의미를 나타낼 수 있다.
6-5. 대체성
불변값의 변하지 않는 상태가 현재의 전체 값을 올바르게 나타내고 있는 이상, 엔티티는 반드시 새당 값의 참조를 갖고 있어야 한다. 만약 상태가 올바르지 않은 상황이 왔다면, 현재의 전체를 올바르게 나타내는 새로운 값으로 전체 값을 완전히 대체해야 한다.
대체성의 개념은 숫자의 컨텍스트를 통해 쉽게 이해할 수 있다. total의 값을 수정해야 하는 경우, total을 3에서 4로 바로 수정하지 않고, 단순히 total을 정수 4로 다시 설정한다. 즉, 단순히 전체 값을 대체해 버리는 것이다.
6-6. 값 등가성
값 객체 인슽턴스를 또 다른 인스턴스와 비교할 땐 객체 등가성 테스트가 사용된다. 등가성은 두 객체의 타입과 특성을 비교해서 결정된다. 만약 타입과 특성이 같다면, 해당 값은 등가로 간주한다. 더 나아가 만약 등가 관계의 값 인스턴스가 둘 이상 있다면 등가 값 인스턴스 중 무엇이든 해당타입의 엔티티의 속성에 할당할 수 있고, 해당 할당은 속성 값을 변경시키지 않는다.
두 인스턴스의 각 특성을 서로 비교한다. 이때 equals() 비교에서 각 특성이 null인 상황을 대비할 필요가 없다. equlas() 메소드는 다섯 가지 값 특징 중 하나인 값 등가성을 확인하는 값 객체의 요구사항을 충족시킨다. 여기서 등가성 의 판단에서 null 매개변수를 배제한다. 각 속성/특성은 두 값 모두의 측면에서 비교된다. 각 속성/특성 사이의 등가성이 모두 확인되면, 전체 값이 서로 같다고 간주하게 된다. 특성의 쿼리 메소드를 통해 해당 특성에 접근한다. 이는 각 특성이 명시적 상태로 존재할 필요 없이, 파생된 특성의 도입을 가능케 해준다.
특정 애그리게잇 인스턴스를 식별자로 쿼리할 땐 값 등가성이 필요하며 불변성도 매우 중요하다. 고유 실별자는 절대 변경되어선 안되며, 이는 불변성을 통해 보장할 수 있다. 설계하고 있는 개념이 다른 객체로부터 고유하게 식별된 엔티티여야 하는지, 아니면 값 등가 만으로도 충분하지는 스스로 질문해보자,
6-7. 부작용이 없는 행동
객체의 메소드는 부작용이 없는 함수로 설계할 수 있다. 함수란 고유의 상태를 변경하지 않고 출력을 만들어내느 객체의 오퍼레이션을 말한다. 특정 오퍼레이션을 수행할 때 어떤 수정도 발생하지 않는다면, 해당 오퍼레이션은 부작용이 없다고 말할 수 있다. 부변성 값 객체의 메소드는 반드시 부작용이 없는 함수여야 하는데, 불변성의 특성을 침해해선 안 되기 때문이다.
# 함수형 프로그래밍
함수형 프로그래밍 언어는 일반적으로 이런 특성을 강화시킨다. 사실 수순한 함수형 언어는 오직 부작용이 없는 행동만을 허용하며, 모든 클로저는 오직 불변성 값 객체만을 받아들이거나 만들어내도록 요구한다.
# CQRS
버트랜드 마이어는 그의 커맨드-쿼리 분리 원칙에서 부작용이 없는 함수를 쿼리 메소드로 나타냈다. 쿼리 메소드는 객체에게 질문을 한다. 정의에 따라 객체에게 질문했기 때문에 대답이 바뀌어서는 안 되며, 부작용을 없앨 수 있다.
# 값이 엔티티를 참조할 때
Q. 값 객체 메소드가 매개변수로 전달된 엔티티를 수정할 수 있도록 혀용해야 할까?
A. 해당 메소드가 엔티티의 수정을 초래한다면 정말 부작요이 없다고 할 수 있을까? 이런 메소드는 테스트하기 쉬울까? 내상각엔 쉽지 않거나 더 어려울 것이다. 따라서 엔티티르 매개변수로 받는 값의 메소드가 있다면, 엔티티가 자신의 규칙에 맞춰 스스로 변경하는데 사용할 수 있도록 결과를 응답하는 편이 최선이다.
# 코드리뷰
float priority = businessPriority.priotityOf(product);
위 코드에서 잘못된 점에 대해 설명하겠다.
1. 값이 Product에 의존토록 할 뿐만 아니라 해당 엔티티의 형태를 이해하도록 강제하고 있따는 점에 주의하자. 가능하면 의존하는 값을 제한하고 스스로의 타입과 그 특성의 타입을 이해하자.
2. 코드를 읽는 사람은 Product의 어떤 부분이 사용될지 모른다. 표현이 명시적이지 않으며, 이는 모델의 명확성을 약화시킨다. Product의 실제 속성 일부나 파생된 속성을 전달했다면 훨씬 좋다.
3. 엔티티를 매개변수로 갖는 모든 값 메소드가 엔티티의 수정을 유발하지 않는다는 점을 쉽게 증명할 수 없고, 그러힉 때문에 테스트하기가 더욱 어려워진다. 그러므로 값이 수정을 일으키지 않음을 약속하지만, 실제로 수정이 없다고 증명하긴 쉽지 않다.
값 객체를 설계하는 대신 기본 언어 타입(primitive/wrapper)을 사용하기로 결정한다면, 여러분의 모델을 속이는 결과가 된다. 기본 언어 값 타입에는 도메인에 맞춘 부작용이 없는 함수를 할당할 수 없다. 모든 도메인 특정 행동은 값에서 분리된다. 프로그래밍 언어가 기본 타입을 새로운 행동으로 패치할 수 있도록 허용한다 해도, 정말로 도메인에 관한 깊은 통찰을 얻도록 하는지는 의문이다.
6-8. 미니멀리즘으로 통합하기
모든 DDD 프로젝트에는 항상 다수의 바운디드 컨텍스트가 있으며, 이는 컨텍스트를 통합하는 올바른 방법을 찾아야 한다. 가능하다면, 값 객체를 사용해 유딥되는 업스트림 컨텍스트로부터 다운스트림 컨텍스트의 개념을 모델링 하면 좋다. 이를 통해 우선순위를 미니멀리즘에 따라 통합할 수 있으며, 이는 다운스트림 모델을 관리하는 책임이라 볼 수 있는 속성의 수를 최소화해준다. 불변 값을 결과로서 사용한다면 책임을 덜 수 있다. 조금 더 보충
6-9. 값으로 표현되는 표준 타입
여러 시스템과 어플리케이션에선 표준 타입이 필요하다. 엔티티(대상)이나 값 객체(설명)은 그 자체로 존재하기도 하지만, 타입에 따라 해당 대상을 구분해주는 표준 타입이 존재할 수도 있다. 이를 타입 코드나 룩업이라고 종종 부른다. 이런 타입 코드란 예를 들어 휴대폰 번호에도 집 전화번호, 회사 전화번호, 여러 종류의 전화번호 타입이 있다.
Q. 그렇다면 다양한 타입의 전화번호는 클래스 계층구조로 모델링돼야 할까?
A. 각 타입별로 별도의 클래스를 사용하면 클라이언트가 이를 구분하기 어려워진다.
여기선 Home, Mobile, Work, Other등을 사용해 표준 타입을 사용해 전화의 타입을 나타내는 것이 좋다. 표준타입의 사용은 통화를 허위로 지정하느 상황을 피할 수 있도록 도와준다. 잘못된 타입을 할당할 순 있지만, 없는 타입을 할당하지는 못한다.
# 표준 타입 수준
표준화 타입은 수준에 따라 애플리케이션 수준에서만 유지 될 수도 있고, 사내 공유 데이터베이스 수준으로 중요성이 올라 갈 수 있으며, 국가적, 국제적 표준으로 사용할 수도 있따. 때론 표준화의 수준이 표준 타입을 가져와 모델에서 사용하는 방식에 영향을 미친다. 표준 타입은 스스로를 전담하는 자신만의 네이트브 바운디드 컨텍스트에 따라 자신의 수명이 결정되기 때문에, 어떤 유형의 표준화 단체가 유지 관리하는지에 상관없이, 가능하면 사용하는 컨텍스트의 값으로 이를 처리하기 위해 노력해야 한다.
# 열거형
아주 간단한 예로 두 타입이 존재하는 그룹의 회원을 모델링하는 표준 타입을 생각해 보자. 일반회원이 있을 수 있고, 기업회원이 있을 수 있다. 이런경우에는 자바 열거형이 표준 타입을 지원하는 한 방법을 보여준다. 자바 열거형을 사용하면 아주 간단히 표준 타입을 지원할 수 있다. 열거형은 유한한 수의 값을 제공하고, 이는 매우 경량이며 컨벤션에 따라 부작용이 없는 행동을 갖게 된다. 결론적으로, 표준 타입에는 열거형을 사용하는 편이 최선이라고 생각한다. 단일 카테고리 내에 표준 타입이 될 수 있는 다양한 인스턴스를 갖고 있다면, 코드 생성을 통해 열거형을 만드는 방법을 고려해 보면 좋다. 코대 생성 접근법은 해당하는 영속성 저장소에서 현존하는 모든 표준 타입을 읽어와ㅡ 각 행마다 고유한 타입/상태를 만들어준다.
6-10. 값 객체의 저장
객체 관계형 매핑(ORM)은 인기가 많고 널리 사용이 된다. 그러나 ORM을 사용해 모든 클래스를 테이블로 매핑하고 모든 특성을 열로 매핑하면 복잡도를 가중시키며, 이런 매핑이 항상 가능하다고 보장되지도 않는다.
# 데이터 모델 누수의 부정적 영향을 거부하라
값 객체를 ORM을 통해 데이터 저장소로 저장하는 대부분의 경우는 비정규화된 방식으로 저장된다. 이는 저장소와 값을 가져오는 과정을 깔끔하게 최적화하도록 해주고, 영속성 저장소의 누수를 막아준다. 값이 이런 방식으로 저장된다는 점은은 다행스러운 일이다.
그러나 모델 내의 값 객체가 반드시 관계형 영속성 저장소의 엔티티로 저장돼야 할 때가 있다. 즉 저장 시에 특정 값 객체 타입의 인스턴스가 해당 타입을 위한 관계형 데이터베이스 테이블에서 자신만의 행을 차지하고, 자신만의 데이터베이스 기본키 열을 갖게 된다. 예를 들어 이는 ORM을 통해 값 객체 인스턴스의 컬렉션을 지원할 때 발생하며 이런 상황에선 값 타입의 영속성 데이터가 데이터베이스의 엔티티로 모델링 된다.
Q. 도메인 모델 객체가 데이터 모델의 설계를 반영해야 하고, 값보다는 엔티티가 돼야 할까?
A. 그렇지 않다. 값 객체를 반영해야 한다.
객체 저장소를 다루기 위해 필요한 방법에 따라 영속성 저장소를 모델링하되, 팀이 도메인 모델 내의 값 속성을 개념화하는 방향에 영향을 미치지 않도록 해야 한다. 또한 도메인 모델을 위해 데이터 모델을 설계해야하지, 데이터 모델을 위해 도메인 모델을 설계하면 안된다.
데이터 모델이 어떤 기술적 측면을 사용하든, 그에 해당하는 엔티티, 기본 키, 참조 무결성, 인덱스가 단순히 도메인 객체의 모델링 방법을 주도하도록 해선 안 된다. DDD는 비정규화된 방식으로 데이터를 구조화하는 문제에 관한 것이 아니다. DDD는 일관된 바운디드 컨텍스트 내에 유비쿼터스 언어를 모델링하는 데 관한 문제다. 나는 DDD측면을 유지하며 데이터 구조를 따르지 않도록 권장한다.
6-11. ORM과 한 열로 직렬화되는 여러 값
여러 값 객체의 컬렉션을 ORM을 사용해 관계형 데이터베이스로 매핑하는 데는 고유한 문제가 있다. 컬렉션이란 엔티티가 갖고 있는 List나 Set을 일컬으며, 이 컬렉션엔 인스턴스가 담겨 있지 않을 수도 있고 하나 이상의 인스턴스가 담겨 있을 수도 있다. 극복할 수 없는 문제는 아니지만 객체 관계형 임피던스 불일치가 확연하게 드러난다.
하이버네이트 객체 관계형 매핑의 한 가지 선택지로, 컬렉션의 모든 객체를 텍스트적 표현으로 직렬화한 후에 그 표현을 하나의 열로 저장하는 방법이 있다. 이 접근법에는 몇 가지 단점이 있지만 긍정적이 부분도 있고 무시하지 못하는 단점도 있다.
# 열 넓이
컬렉션에 포함된 값 오소으 최대 수나 직렬화된 각 값의 최대 크기를 단정할 수 없다. 일부 객체 컬렉션은 최대 크기의 제한 없어 어떤 수의 요소든 담을 수 있다. 또한 컬렉션에 담긴 각 값 요소를 직렬화된 문자로 표현할 때의 너비를 가늠할 수 없을 떄도 있따. 이는 오버플로우 발생을 초래할 수 있기 때문에 최대 너비를 미리 결정할 수 없거나 열 너비의 최대 값을 초과 할 수 있다면, 이런 방식은 피하는 편이 좋다.
# 쿼리해야 하는 경우
값 컬렉션이 평면적인 텍스트 표현으로 직렬화되기 때문에, SQL 쿼리 표현식에선 개별 값의 특성을 사용할 수없다. 각 특성으로의 쿼리가 가능해야 한다면, 이 옵션은 사용할 수 없다. 하지만 값 특성으로 쿼리해야 하는 경우는 드물다.
'DDD' 카테고리의 다른 글
8장. 도메인 이벤트 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.15 |
---|---|
7장. 서비스 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.15 |
5장. 엔티티 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.14 |
4장. 아키텍처 / IDDD (도메인 주도 설계 구현) (0) | 2022.03.02 |
3장. 컨텍스트 맵 / IDDD (도메인 주도 설계 구현) (0) | 2022.02.25 |