본문 바로가기
Concepts/SW Design

DDD - Side effect free function

by ocwokocw 2022. 10. 22.

- 출처: 도메인 주도 설계 - 에릭 에반스

- 연산과 함수

연산은 명령(command, modification)과 질의(query)로 나눌 수 있다.
 
  • 명령(command, modification): 변수값을 변경해서 시스템의 상태를 변경
  • 질의(query): 변수안에 저장된 데이터에 접근하고, 저장된 데이터를 기반으로 계산을 수행
 
함수는 부수효과를 일으키지 않고 결과를 반환하는 연산이다. 여러번 호출 해도 무방하며 매번 동일한 값을 반환한다.
 

- 명령과 질의의 분리

명령과 질의는 서로 엄격하게 다른 연산으로 분리해야 한다. 변경을 발생시키는 메서드는 도메인 데이터를 반환하지 않고 단순하게 유지해야 한다. 또는 명령과 질의를 분리하는 대신 연산의 결과를 표현하는 새로운 VO(Value-Object)를 생성 후 반환한다.
 
상태 변경을 수반하는 로직과 계산 로직이 혼합되어 있다면 이를 리팩토링해서 두개의 연산으로 분리해야 한다. 부수 효과를 단순한 명령 메서드 내부로 격리시키는 작업은 당연히 ENTITY에 대해서만 적용한다.
 
수정과 질의를 분리하는 리팩토링을 완료했다면 계산을 처리하는 책임을 VO로 옮길지에 대한 고민을 시작한다.

- 예제

위에서 언급했던 말이 너무 추상적이라 이해하기 힘들 수 있다. 페인트를 혼합한 결과를 표현해주는 앱을 통해 명료하게 이해해보자.
 
type Paint struct {
	volume            float32
	red, yellow, blue int
}

func (p *Paint) mixIn(other Paint) {
	p.volume += other.volume
	// 새로운 빨강, 노랑, 파랑 값을 할당하는
	// 복잡한 색상 혼합 코드
}
 
위의 코드를 기반으로 아래의 객체가 생성되었다고 가정해보자.
 
  • Paint1: 노란색, 1/2 갤론
  • Paint2: 파란색, 1/2 갤론
 
이제 Paint1.mixIn(Paint2) 호출을 통해 페인트를 혼합하면 객체는 아래와 같은 상태로 변경될것이다.
 
  • Paint1: 초록색 1 갤론
  • Paint2: 파란색 1/2 갤론
 
mixIn 메서드는 질의로부터 명령을 분리해야한다는 규칙을 준수하고 있다.
 
다만 한 가지 걸리는 부분이 있는데 mixIn 인자의 Paint2 용량이 불확실한 상태로 방치되어 있다는 점이다. 이는 Paint2의 용량이 변하지 않는게 맞는가 라는 관점에서 봤을 때 납득하기 어려운 부분이다. 이 설계안에서는 mixIn 연산 수행 수 Paint2의 상태에 대한 고려가 없다고 할 수 있다. 이 부분은 추후 Assert 부분에서 다루기로 하고 지금은 색상 계산 부분에 집중하자.
 
현재 우리가 작성하려고 하는 Paint 도메인에서 색상은 중요한 개념이다. 색상을 명시적인 객체로 분리해보자.
 
type Paint struct {
	volume float32
	color  PigmentColor
}

type PigmentColor struct {
	red, yellow, blue int
}

func (p *Paint) mixIn(other Paint) {
	p.volume += other.volume
	// 새로운 빨강, 노랑, 파랑 값을 할당하는
	// 복잡한 색상 혼합 코드
}
 
색상을 구성하는 3 요소를 각 속성으로 갖고 있을 때 보다 색상이라는 개념이 더 명시적으로 드러났다. 하지만 여전히 계산은 mixIn 에서 처리되고 있다. 색상과 관련된 데이터를 분리하기로 했다면 그에 관련된 행위도 분리되어야 맞다.
 
Paint는 혼합하면 객체 자체가 변경되므로 Entity 이다. 하지만 PigmentColor의 경우 노란색이라면 노란 색조 그 자체만을 표현하고 있으므로 VO이다. PigmentColor의 경우 혼합해도 그 상태가 변경된다기 보다 새로운 색조를 표현하는 새로운 PigmentColor가 생성된다고 할 수 있다.
 
type Paint struct {
	volume float32
	color  PigmentColor
}

func (p *Paint) mixIn(other Paint) {
	p.volume += other.volume

	// 색상 혼합 행위를 VO로 위임했다. (행위가 분리됨)
	ratio := other.volume / p.volume
	p.color = p.color.mixInWith(other.color, ratio)
}

type PigmentColor struct {
	red, yellow, blue int
}

func (p PigmentColor) mixInWith(other PigmentColor, ratio float32) PigmentColor {
	// 새로운 빨강, 노랑, 파랑 값을 할당하는
	// 복잡한 색상 혼합 코드
	return PigmentColor{}
}
 
위의 코드를 이용하여 color의 객체 상태를 살펴보면 아래와 같다.
 
  • color1: 노란 색조, color2: 파란 색조
  • color1.mixInWith(color2, ratio)을 수행하여 새로운 color3 생성
  • 기존의 color1, color2는 변경되지 않음.
 
새로운 PigmentColor 개념을 도입함으로써 도메인의 지식을 한층 더 잘 표현하게 되었으며, 결과 예측이 쉬우므로 테스트 작성도 용이해졌다.
 
 

'Concepts > SW Design' 카테고리의 다른 글

DDD - 개념적 윤곽  (0) 2022.10.31
DDD - Assertion  (0) 2022.10.22
DDD - Inteface  (0) 2022.10.01
DDD - Specification  (0) 2022.09.25
DDD - 불명확한 개념  (0) 2022.09.19

댓글