도메인 주도 설계

도메인 주도 설계 책 구입은 2012년도 였다. 운 좋게 책 읽기 모임을 알게 되었고, 그 곳에서 다른 industry에서 근무 하시는 분들과 함께 읽었다. 그렇게 책을 읽은 후 한 동안 잊혀졌고, 현재 다시 읽고 있다.

개인적으로 이 책을 다시 읽으 면서 느끼는 감정이란, 놀라움과 답답함이다. 당시 읽고 느낀 생각이 지금과 전혀 다르다. 당연한 이야기 일 수도 있다.

내가 어떤 책을 읽기 위해서는 그 책을 읽기 위한 준비? 혹은 기본 지식이 있어야 함을 새삼 느낀다. 현재 책을 읽으면서, 당시 몰랐던 오탈자가 보이기도 하고, 표현의 오류 및 원리를 드러내기 위한 표현이 어색해 보이기도 하다. 이는 내 개인적인 의견일 뿐이다. 그럼에도 나름 기존 지식을 동원해서 다시 읽고 있으며, 처음 읽었을때와는 비교 할 수 없을 정도로 읽는 속도는 더디다.

앞으로 책을 사기 전에는 우선적으로,

  • 서점에 가서 책을 읽은 후에 사야겠다.
  • 번역서는 되도록 사지 말아야 겠다.
  • 번역서 보다는 지은이의 지식, 노하우, 철학이 들어간 책을 구입해야겠다.
  • 무엇보다도 내가 읽기 쉬운 책을 선택해야겠다.

만약 이 책을 구입하려는 계획이 있으시거나, 저처럼 읽는데 어려움이 있으신 분은 아래 링크 책을 먼저 읽어 보시거나 블로그를 먼저 읽어 보셨으면 합니다. 참고로 아래 링크는 저와 어떠한 이해 관계가 없음을 밝힙니다.

DDD 관점에서 Lombok

lombok을 처음 접했을 때가 2000년도 후반이었다. 거의 처음 나왔을때, 우연히 알게 되었다. 처음 접했을 때, 우와!!! 했던 기억이 있었다. APT기술을 이용한 기술이라고 알게 된 건 그 후로 몇 년 뒤였다.

최근 DDD에 대해, 아니 OOP에 대한 공부를 다시 하면서, 많은 생각이 든다. 지금까지 왜? 내가 작성한 코드에 만족을 못했는지… 개인적으로, 가장 큰 이유는 이해력 부족과, 읽기 능력 혹은 어휘 능력 부족이었다.

이야기의 주제로 돌아와서, lombok은 잘 못 사용 할 경우 필요악이다. 내가 작성하는 코드, 프로젝트에서 사라져야 할 dependency는 lombok이다. 만약 구지 사용해야 한다면, @Getter 정도만 사용해야 겠다.

Type level에서 @Data @Setter 는 사용 하지 않는 편이 좋을 것 같다. @Getter는 field에 따로 지정 하는 것이 좋을 것 같다.

이 글은 조영호님 블로그 DDD 편의 글 을 읽고, 정리한 글임을 밝힌다.

Reference Object와 Value Object

객체 세계에서 Object는 Reference Object와 Value Object로 구성된다.

Reference Object의 특징

  • application 내에서 유일해야 한다. 즉 == 테스트를 통과되어야 한다.
  • 추적이 가능해야 하므로, 식별성(identity)이 주어져야 한다. 식별성은 아주 중요 하다!
  • 어떠한 행동에 의해 객체 내부 상태가 변한다. (불필요한 setter method를 만들지 말아라!!! 상태는 어떤 이벤트에 의한 행동의 결과로서 변화하는 것임을 잊지 말자!!!)
  • 다른 Reference Object나 Value Object를 집합의 개념으로 포함시킬 수 있다. 포함하고 있는 객체들의 생명 주기를 관리 해야 한다. 객체망 탐색의 시작위치가 되고, 비즈니스 로직을 수행하는 시작점 역시 된다.

Reference Object 설명 중 개인적인 생각

아래는 본문이다. (Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 3부)

사용자 요청이 시스템내에 도착하면 시스템은 요청을 처리할 객체 그룹을 찾는다.
이 객체 그룹중 entry point에 해당하는 reference object가 그룹을 대표하여 요청을 전달받고
작업을 수행하기 위해 필요한 객체들과의 협력을 통해 요청을 완수한다.

사용자 요청은 무엇일까? Service객체라고 생각된다. application logic을 구현하거나 infra structure layer에서 특정 service를 제공하거나. 이 문맥에서는 application logic 즉, business flow를 정의한 것이 합당해 보인다. application logic은 비즈니스 문제를 해결해야 하는 책임을 수행하기 위해, domain object, 동일 layer에 있는 application logic, 혹은 하위 layer에 있는 infra layer와도 협력해야 한다. 다시 돌아와서, 비즈니스 문제는 우리가 작성한 domain object가 해결해야 하는 책임이 있다. 그 중 후보가 되는 object는 entry point object이다. entry point object는 그 자체 내에서 위임등을 사용하여 비즈니스 문제를 해결한다. 또한 application layer에서 비즈니스 문제를 해결하기 위해, entry point object는 모두 초기화 되어야 한다. 이 초기화는 Repository가 책임을 진다. 나중에 학습할 jpa에서 lazy or eager fetch는 중요하지 않다. 객체가 초기화가 되어야 비즈니스 로직을 구현할 수 있으니 말이다.

기대효과는 무엇일까? application logic은 자신의 책임에 알맞는 만큼의 역할을 수행한다. (두터운 로직을 작성할 필요가 없다.) 실제 도메인 로직은 도메인 객체가 수행하므로, 각자 오염되지 않은 깔끔한 상태를 유지 할 수 있다. 결국 이해하기 쉬운 코드가 작성되고, 변경에 탄력적으로 대응 할 수 있을 것으로 기대 된다.

Value Object

  • 각 속성들이 똘똘 뭉쳐져서 개념적으로 하나의 완전한 의미를 가져야 한다. ex) 사용자 정보내에 있는 주소 정보를 Address VO로 만드는 것
  • 식별성이 필요 없다.
  • 객체내 필드 값을 이용한 동등성 테스트를 한다. 따라서 equals와 hashcode 를 구현해야 한다.
  • 불변 객체로 만들어라. (모든 속성은 생성자를 통해서 초기화가 된다. getter 메소드만 존재 하여 속성 수정 불가!)
  • 자신의 속성에 변경이 가해지는 기능이 제공 된다면? 그 메소드에서는 결과를 새로이 반영한 새로운 VO 객체를 만들어서 제공해야 한다. 이는 불변 객체이어야 하기 때문이다.
  • 보통 Reference Object의 생명주기에 종속적이다.

Entity, VO on ORM (2016.06.25 추가)

소프트웨어로 구현해야 할 업무 도메인에서 모델을 식별 하고 그 결과, Entity와 VO들이 도출 되었다. 그리고, ORM기술을 이용 하기로 했다. 모든 것이 완벽하다. 하지만 위에서도 조금 언급 했지만, 객체는 관계를 맺고 있다. 아주 복잡하게.

ORM 기술을 사용한다고 해서, 특히 Entity 객체간 연관 관계를 어떤 기준(DDD에 나오는 Aggregate) 없이 맺으면 안 된다. 이건 아마 기술의 과함 내지 오용 이라고 생각한다.

다 떠나서, 객체는 관계를 맺는다. 관계(association)에는 composition, aggregation 관계가 있다.

composition 관계는 한 객체가 다른 객체의 생명 주기를 완벽히 제어 한다.

생명 주기 제어 객체는 Entity이고, 종속 객체는 VO일 확률이 크다.
종속 객체는 행위적 측면 보다, 데이터 관리 측면이 더 높을 것이다.
제어 객체에서 종속 객체를 제어 할 것이다.

만약 종속객체가 VO라면, 이 VO는 그리 단순한 VO가 아닐 수도 있다.
무슨 말이냐 하면, 다른 Entity에서 몇가지 속성만 의도 적으로 도출 시킨 VO 형태 일 것이다.

aggregation 관계는 각 객체의 생명 주기가 독립적이다. 그냥 관계 정도만 맺는다.

생명 주기가 독립적인데, 관계는 맺고 있다. 왜일까? 일단 데이터 관리는 해야 겠다.
여기서는, 관계 맺은 객체에 행위적 기능을 사용하고자 하는 의도가 있을 것이라고 생각한다.
즉, 객체 협력을 통해 내가 어떤 이득을 취하기 위함 일 것이다. 위임을 통해서...

관계에는 다중성, 방향성도 있다.

방향성은 1차적으로 단방향 관계만 맺어 놓자. 나중에 필요 할 경우, 반때쪽 단방향 관계를 만들자. 서로 다른 방향 두개가 합쳐져 양방향이 되고, 이는 관계의 복잡성 증가와 관리 해야 할 기능 즉 코드가 많이 늘어난다. 조심히 다뤄야 겠다.

내가 지금 까지 이해한 수준에서, 정말 내가 잘 이해 하고 있는지, 정리 하기 위해 적어 본다.

역시 원리나 개념은 기술따위와 비교 할 수 없을 정도로 생명주기가 긴것 같다.

단 원리나 개념은 내가 처한 상황에 따라 조금 다르게 해석 되거나 적용 시킬 수 있음을 명시 해야 한다.

본글

Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 1부

Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 2부

Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 3부

Domain-Driven Design의 적용-1.VALUE OBJECT와 REFERENCE OBJECT 4부

이 글은 조영호님 블로그 DDD 편의 글 을 읽고, 정리한 글임을 밝힌다.

Aggregate와 Repository


출처

내용중에 아래와 같은 비즈니스 룰이 있다.

    주문 시 고객은 정해진 한도 내에서 상품을 구매할 수 있다.
    한도 초과 여부를 검증하는 책임을 어떤 도메인 객체에게 할당할까?

내가 관심갖는 부분은 Order와 Customer의 관계이다. Order와 Customer는 다대일 관계이다. 반대로 이야기하면, Customer와 Order는 일대다 관계이다. 과거에 내가 ORM 책에서 읽었던 내용을 되짚어 보면, 양방향관계는 복잡해 진다는 언급되어 있다. 특히 양방향 연관관계에서 일대다쪽의 관계는 특별한 요구사항이 있을 경우에만 그 관계를 맺으라는 언급이 있다. 더 강력하게 표현한 다면, 이 경우 일대다 연관관계는 맺지 말아야 한다!

다시 돌아와서, 글의 내용에는 위에서 구현해야 할 비즈니스 룰의 책임은 Order에 있다고 친절하게 설명되어 있다. 협력하는 객체들 속에서 비즈니스 로직 구현 책임을 어떤 객체에 할당 해야 할까? 협력 객체내 관계를 곰곰히 생각한 후 그 책임이 어울리는 객체 쪽으로 할당해 주는 많은 연습이 필요 하겠다. 본문에서는 책임을 잘못 위치시킬 경우, 다른 객체의 내부 상태를 알고 있어야 하고, 이에 따라 결합도가 높아지는 폐해가 발생한다고.

이와 별개로, 주문은 Customer로 부터 시작된다. 왜? 도메인이 그러하니까…

글 내용에 등장하는 코드를 보면, 집요할 만큼 객체간 협력이 이루어지고 있다. 협력이 이뤄지고 있을때는 INFORMATION EXPERT 패턴을 준수하도록 하자.

Order와 OrderLineItem

주문객체와 주문항목 객체들은 구매액이 고객의 주문 한도액을 초과할 수 없다는 불변식을 공유하는 하나의 논리적 단위

주문항목객체의 상태를 변경할 수 있는 책임은 주문밖에 없다. 다른 객체는 주문항목객체를 제어 해서는 안된다. 또한 주문객체는 주문항목객체를 노출해서는 안된다. 결론은, 주문객체는 주문항목객체를 캡슐화 해야 한다.

Aggregate

  • 협력하는 객체들의 논리적 묶음, 단위
  • Aggregate에는 불변식이라 불리우는 어떤 규칙을 수행하는 행위(business logic)와 범위가 있다 (트랙잭션 처리 대상이 될 수 있다)
  • Root와 Boundary 개념이 있다.
  • Aggregate Root는 어떤 행위를 하기 위한 협력 객체 묶음 중 특정 Entity 객체 하나를 가르킨다.
  • Aggregate Boundary는 경계 안의 객체는 Aggregate 내부 객체 끼리 서로 참조 가능하지만, 경계 밖의 객체는 Root만 참조 가능 하고, Aggregate 객체 내부는 참조 할 수 없다.
  • 값만 가지고 있는 조금은 단순한 Entity도 Aggregate인가? 당연하다!!! 오해 하지 말자!!!

Factory

객체를 생성하고자 할 때는 생성자를 이용하면 된다. 생성자를 통한 객체 생성을 통해, 그 객체의 기능을 사용한다면야 상관 없겠지만 보다 복잡한 객체를 생성해야 한 다면, Factory를 사용해야 겠다. 객체를 사용하는 쪽에서 복잡한 객체 생성에 대한 방식이 노출 되는 순간 이건 벌써 오염이다. 그리고, 이 순간 객체 생성이라는 책임의 개념이 나타났다는 것을 인지 하도록 노력해야 겠다. 생성되는 객체가 Type이라는 관점에서 다형성 개념과도 물려 있을 수 있다. 복잡할 수도 있겠구나. Factory는 객체 생성시점과 database와 같은 저장소에 있는 정보를 가지고 재구성 할 때도 사용된다. 사용되는 시점에 따라 특징이 다르다. 재구성 될 때 가장 큰 차이점은 Entity생성을 하는 Factory의 경우, 이미 존재 하는 식별자를 Entity에 적용 시킨다. 이러한 기능은 아래 Repository가 담당하도록 하자!!!

Factory도 중요하겠지만 객체 생성 구현의 관점에서, 생성자 구현에도 좀 더 신경을 쓰도록 하자.

  • 의미 없는 public 접근자를 사용하지 않는가?
  • 의미 없는 default 생성자를 사용하지 않는가?
  • 객체가 객체 내부 상태를 관리 한다면, 객체 생성시 반드시 초기화가 필요한 상태에 대해, 생성자에서 적절히 초기화 해 주는가? 그에 따른 규칙이 잘 이루어 지는가? NotNull 체크 같은…

Repository

Repository는 application에 존재하는 Reference Object 혹은 Aggregate Object를 저장소에서 불러오거나, 저장할 책임을 지고 있다. 주의할 점은 Aggregate 내부에 존재하는 객체를 재구성하는 기능은 없어야 겠다.

GRASP

General Responsibility Assignment Software Patterns OOD의 핵심 문제는 개별 객체에 역할 즉 책임을 부여하는 것이다.

  1. Information Expert : 역할을 수행할 수 있는 정보를 가지고 있는 객체에 부여하자. 객체 자신의 상태는 그 객체 스스로가 처리하도록 하여 자율성을 부여하자. 외부에는 그 기능, 역할을 제공한다. 쓸데 없는 getter, setter 메소드는 구현하지 말자. 좀 제발.

  2. Creator : 객체의 생성은 생성되는 객체의 컨텍스트를 알고 있는 다른 객체가 있다면, 컨텍스트를 알고 있는 객체에 부여하자. A 객체와 B 객체의 관계의 관계가 다음 중 하나라면 A의 생성을 B의 역할로 부여하라.
    • B 객체가 A 객체를 포함하고 있다.
    • B 객체가 A 객체의 정보를 기록하고 있다.
    • A 객체가 B 객체의 일부이다.
    • B 객체가 A 객체를 긴밀하게 사용하고 있다.
    • B 객체가 A 객체의 생성에 필요한 정보를 가지고 있다.
  3. Controller : 시스템 이벤트(사용자 요청)를 처리할 객체를 만들자. 시스템, 서브시스템으로 들어오는 외부 요청을 처리하는 객체를 만들어 사용하자. 만약 어떤 서브시스템안에 있는 각 객체의 기능을 사용할 때, 직접적으로 각 객체에 접근하게 된다면 서브시스템과 외부간의 coupling이 증가되고, 서브시스템의 어떤 객체를 수정할 경우, 외부에 주는 충격이 크다. 서브시스템을 사용하는 입장에서 보면, 이 Controller 객체만 알고 있으면 된다.

  4. Low Coupling : 객체들간, 서브시스템들간의 상호 의존도가 낮게 역할으르 부여하자. 객체세계에서는 객체간 관계를 맺어 협력을 한다. 따라서, 객체간 Coupling이 존재하지 않을 수 없다. 이 패턴은 요구사항은 충족시키면서도 각 객체들, 각 서브시스템 간의 Coupling을 낮은 수준으로 유지하는 방향으로 디자인하라고 한다. 역시, 양방향 연관관계는 필요악이로구나.

  5. High Cohesion : 각 객체가 밀접하게 연관된 역할들만 가지도록 역할을 부여하자. Low Coupling과 비슷하면서 다른 개념이지만 땔수 없는 표현이다. 쓸데 없는 다른 객체를 참조 하지 말고, 필요한 만큼의 협력객체와 자기 자신에게 부여 받은 역할을 잘 수행하자.

  6. Polymorphism : 객체의 분류, 타입에 행동 양식이 바뀐다면, Polymorphism 기능을 사용하자. 만약 객체의 분류, 타입에 따라 행동이 바뀐다면, 객체의 타입을 체크하는 조건문을 사용하지 말고 Polymorphism 을 사용하자.

  7. Pure Fabrication : Information Expert 패턴을 적용하면 Low Coupling과 High Cohesion의 원칙이 깨어진다면, 기능적인 역할을 별도로 한 곳으로 모으자. 데이터베이스 정보를 저장하거나, 로그 정보를 기록하는 역할에 대해 생각해 보자. 각 정보는 각각의 객체들이 가지고 있을 것이다. 이 때 Information Expert 패턴을 적용하면, 각 객체들이 정보를 저장하고, 로그를 기록하는 역할을 담당해야 하지만, 실제로 그렇게 사용하는 사람들은 없다. 이것은 그 기능들이 시스템 전반적으로 사용되고 있기 때문에 각 객체에 그 기능을 부여하는 것은 각 객체들이 특정 데이터베이스에 종속을 가져오거나, 로그을 기록하는 매커니즘을 수정할 경우, 모든 객체를 수정해야 하는 결과를 가져온다. 즉 Low Coupling의 원칙이 깨어지게 된다. 이럴 경우에는 공통적인 기능을 제공하는 역할을 한 곳으로 모아서 가상의 객체, 서브시스템을 만들어라.

  8. Indirection : 두 객체 사이의 직접적인 Coupling을 피하고 싶으면, 그 사이에 다른 객체를 사용하라. 다른 객체는 대부분 interface인 경우가 많다. 그런 특별한 경우는 아래 Protected Variations 패턴이라한다.

  9. Protected Variations : jdbc interface와 그 interface를 구현한 각 vendor의 상황

spring-boot with spring-data-jpa

spring boot로 모든 예제가 변경되어 많이 혼란 스럽다.
spring-boot-autoconfigure.jar/META-INF/spring.factories 파일을 기반으로 동작 되는 것으로 생각된다.
org.springframework.boot.autoconfigure.EnableAutoConfiguration 에 등록된 XXXAutoConfiguration 이 실행 된다.
각 클래스는 @Conditional or @ConditionalXXX 가 실행 되어 각 조건별 bean 등록이 이루어 지는 것으로 일단 생각 된다.
이 부분은 좀 더 학습을 진행 해 볼 필요가 있다.

JpaRepositoriesAutoConfiguration

위 클래스는 spring-boot-autoconfigure.jar 에 있는 클래스다.
이 클래스에는 @Import(JpaRepositoriesAutoConfigureRegistrar.class) 구문이 작성되어 있고 JpaRepositoriesAutoConfigureRegistrar 클래스가 시작점이다.
즉, JpaRepositoriesAutoConfigureRegistrar 클래스가 spring-data-jpa 를 이용하여 작성한 bean을 spring bean으로 등록하는 가교 역할기능을 제공 하고 있다.
실질적인 bean 등록은 spring-data-commons.jar에 있는 RepositoryConfigurationDelegate 클래스가 책임을 맡고 있다.

@EnableJpaRepositories 를 사용한다면, JpaRepositoriesRegistrar 를 사용하게 되는데, 이 부분 역시 확인해 볼 필요성이 보인다.

RepositoryConfigurationDelegate

실제 bean 등록은 registerRepositoriesIn 메소드에서 실행 된다. 아래 코드가 핵심이다.

public List<BeanComponentDefinition> registerRepositoriesIn(BeanDefinitionRegistry registry,
		RepositoryConfigurationExtension extension) {
	...

	for (RepositoryConfiguration<? extends RepositoryConfigurationSource> configuration : extension
			.getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode)) {

		AbstractBeanDefinition beanDefinition = definitionBuilder.getBeanDefinition();
		String beanName = beanNameGenerator.generateBeanName(beanDefinition, registry);

		registry.registerBeanDefinition(beanName, beanDefinition);
		...
	}
	...
}

즉, spring bean 으로 등록이 될 후보 bean들을 찾고 찾아낸 후보들에 대해 bean name과 bean definition 정보를 registry에 등록 함을 알 수 있다.

JpaRepositoryConfigExtension

이 클래스는 spring-data-jpa 구현 기술을 이용한 spring candidate bean 찾기 기능을 담당하고 있다.

TO DO

사실 spring-data-jpa의 DRY 코드 억제 방안 코드를 찾아보고자 했다.
하지만 예상과 다르게 spring-boot로 부터 시작되는 코드와 annotaion, 빈 등록 전략의 코드를 보며,
학습해야 할 내용이 존재 한다.
아래 항목에 대한 학습 후, spring-data-jpa 좀 더 살펴 보도록 해야 겠다.

  1. @Conditional, @ConditionalOnBean, @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty, @ConditionalOnResource
  2. BeanDefinitionRegistry, ImportBeanDefinitionRegistrar
  3. @AutoConfigureBefore, @AutoConfigureAfter, @AutoConfigureOrder