[keyword] 9장 - 암시적인 개념을 명확하게
Closed this issue · 7 comments
P.240
특별한 목적을 위해 술어와 유사한 명시적인 VALUE OBJECT 를 만들어라. SPECIFICATION 은 어떤 객체가 특정 기준을 만족하는지 판단하는 술어다.
도메인 객체를 검사�, 선택, 생성하는 책임을 가진 객체를 따로 생성해서 사용하는게 Specification 의 주 내용이라고 이해했습니다.
하지만 이후에 10 장에서 Specification 을 AND, OR, NOT 등의 연산을 구현해 조합하는 내용이 나오는데, 이는 안티 패턴이라는 의견이 있더군요.
https://blog.ndepend.com/design-patterns-aged-poorly/
위 글에선 Specification 패턴을 그저 미화된 if 문이다 라고 표현합니다. and, or , not 연산을 굳이 구현할 필요 없이 Specification 을 확장 함수로 모아두고 && || 연산으로 조합하면 된다는 내용이더라구요.
저도 Specification 을 따로 정의하는 건 좋은 방법이라고 생각하지만 Specification 을 엮기 위해 불필요하게 AND, OR 연산 들을 추상화한게 아닌가라는 생각이 들었습니다.
Specification 은 명세로만 존재하고, 이를 조합하는 건 서비스 단에서 자유롭게 사용하는 방식이 더 유연하지 않을까 싶네요.
Specification
저번에도 잠깐 언급한 Specification 얘기가 나왔습니다.
SPECIFICATION을 이용하면 규칙을 도메인 계층에 유지할 수 있다. 아울러 완전한 객체를 사용해서 규칙을 표현하므로 설계가 모델을 더욱 명확하게 반영할 수 있다. (p. 241)
특별한 목적을 위해 술어와 유사한 명시적인 VALUE OBJECT 를 만들어라. SPECIFICATION 은 어떤 객체가 특정 기준을 만족하는지 판단하는 술어다. (p. 240)
VO를 만들음으로써 검증 로직 뿐만 아니라, 해당 값 객체의 책임을 VO에 위임할 수 있으므로 명시적 VO를 생성하는 것에 많은 공감을 하며 읽었습니다. 도메인 계층에 책임을 유지할 수 있다는 부분에도 공감이 되어 해당 문구가 가장 기억에 남았어요.
규칙이 더 복잡해지면 여느 암시적인 개념과 마찬가지로 규칙을 적용해야 하는 객체와 연산을 압도하기 시작한다. ~~ 규칙이 기존 객체에 존재하기에는 적절하지 않을지도 모른다. (p. 235)
검증 로직이 지나치게 많아지면 해당 객체의 기본 책임을 모호하게 만들거나, 코드의 가독성을 해치는 경우때문에 SPECIFICATION 관련 내용을 값 객체로 만들거나 별도의 utils 클래스 등으로 분리하는 패턴이 나온게 아닐까 싶습니다.
다만, 여기서 별도로 utils 클래스를 만드는 것에는 조금 의문이 있습니다.
이 부분은 Question 이슈로 남기겠습니다.
요약
- 심층 모델로 향하기 위해선 암시적으로 존재하는 개념을 인식하는 것으로부터 시작한다
- 점점 모델 내에 개념을 명확히 표현해가기 시작하면 본질적인 개념을 표현 가능해지고, 그 이후 지식탐구와 리팩터링으로 계속 정제해야함
- 때때로 추상적인 다른 중요한 범주의 개념도 모델링해야할 수 있음
P.236
객체는 절차(procedure)를 캡슐화함으로써 절차 대신 객체의 목표나 의도에 관해 생각하게 해야한다 (절차를 모델의 주요한 측면으로 삼고싶지 않다). 여기서 이야기하고자 하는 대상은 도메인에 존재하는 프로세스(process) 이며 우리는 모델 내에 프로세스를 표현해야한다.
프로시저는 캡슐화하여 객체의 목표나 의도(역할)에 집중하게 해야하고
도메인의 프로세스는 여러 객체와의 협력을 통해 비지니스 로직을 서술되는데 이는 SERVICE를 통해 주로 명시적으로 표현됨.
Specification Pattern
- 규칙을 명세(specification)라는 단일 단위로 캡슐화하여 다른 시나리오에서 재사용하도록 함
- 사용 시나리오 : 검증, 선택(또는 질의), 요청구축(생성)
"always-valid" domain model
- 모델의 자체의 불변성(invariants)과 행위에 대한 유효성 검사(validation)를 구분해야한다
- 전자의 경우 객체가 어떤 연산에 대해서도 항상 유효해야한다는 것을 얘기하는 게 아니라 항상 참이어야 하는 특정 수의 불변성이 있음
- 그렇다면 무엇을 유효하게 유지해야하는가? → "개념"(객체의 정의, 도메인에 따라 다름) : 뿔없는 유니콘, 꼭지점 세개인 사각형...
- 이러한 불변성을 사용하여 도메인 모델링을 하는 이유는 DRY 뿐만 아니라 인지적 과부화를 낮추는 데도 도움이 된다
- 후자의 경우는 개별 작업 시에 필요한 유효성 검사이므로, 상황(연산)에 따라 깨질 수 있음
- 객체 자체에서 수행되는 게 아니라 다른 객체가 해당 객체를 관찰하는 것으로 표현됨
- 이러한 경우 컨텍스트 기반으로 유효성 검사가 진행되므로 Specification Pattern 이 적용되는 건 이런 케이스임
http://jeffreypalermo.com/blog/the-fallacy-of-the-always-valid-entity/
http://codebetter.com/gregyoung/2009/05/22/always-valid/
Let's propose we now have a
SendUserCreationEmailService
that takes a UserProfile ... how can we rationalize in that service that Name is not null? Do we check it again? Or more likely ... you just don't bother to check and "hope for the best" you hope that someone bothered to validate it before sending it to you.
Of course using TDD one of the first tests we should be writing is that if I send a customer with a null name that it should raise an error. But once we start writing these kinds of tests over and over again we realize ... "wait if we never allowed name to become null we wouldn't have all of these tests"
불변성 : 모델은 상태를 가져야 한다. (혹은 ENUM에 대한 체크)
유효성 검사 : 모델의 상태에 따라 검증이 실패 혹은 성공할 수 있다.
gregyoung 의 반론
https://web.archive.org/web/20210920212449/http://codebetter.com/gregyoung/2009/05/22/always-valid/
case study를 해보면 좋을 것 같습니다
[메모] 추가 코멘트
- Craig
He's not writing procedural code. He's not saying entities shouldn't' have any behavior at all and just be dumb getter/setters. He's saying that validation is a separate concern and shouldn't be part of the domain objects, and that entities are valid or invalid in different times. Consider a workflow scenario where an object can be saved as draft, submitted, declined, approved, etc.
(그는 절차적 코드를 작성하는 것이 아닙니다. 엔티티가 아무런 동작도 하지 않고 멍청한 게터/세터만 되어서는 안 된다고 말하는 것이 아닙니다. 유효성 검사는 별도의 문제이며 도메인 객체의 일부가 되어서는 안 되며, 엔티티는 서로 다른 시기에 유효하거나 유효하지 않다고 말하는 것입니다. 개체를 초안, 제출, 거부, 승인 등으로 저장할 수 있는 워크플로 시나리오를 생각해 봅시다.)
In the draft status all or almost all properties might be allowed to be null. After all, the user should be able to save the object at any time, without having to fill out all the fields. But for submission or approval, much more of the properties must be filled out. When declining, the approver might be required to fill out comments which might even be in a separate collection.
(초안 상태에서는 모든 또는 거의 모든 속성이 null로 허용될 수 있습니다. 결국, 사용자는 모든 필드를 채울 필요 없이 언제든지 개체를 저장할 수 있어야 합니다. 그러나 제출 또는 승인을 위해서는 훨씬 더 많은 속성을 입력해야 합니다. 거절할 때는 승인자가 별도의 컬렉션에 있는 댓글을 작성해야 할 수도 있습니다.)
I've seen the validation rules change like this in many applications.
(많은 애플리케이션에서 유효성 검사 규칙이 이와 같이 변경되는 것을 보았습니다.)
- Greg
Your example does not discuss invariants of the object but instead discusses validations that are required for an action. These are two very different concepts should be viewed differently. The "always valid" camp is certainly not saying objects must always be in a state that they are valid for any operation (this would make no sense). They are however saying that there is a certain number of invariants for an object that should always be true (as an example that a customer object always has a name). In your workflow case you are just working with objects that have few if any invariants in terms of the business, it is illogical to extrapolate such an experience to all objects though.
(당신이 든 예제에서는 객체의 불변성에 대해 논의하지 않고 대신 작업에 필요한 유효성 검사에 대해 논의합니다. 이 두 가지 개념은 매우 다른 개념이므로 다르게 보아야 합니다. "항상 유효하다(always valid)"는 진영에서는 객체가 어떤 연산에 대해서도 항상 유효한 상태여야 한다고 말하는 것이 아닙니다(이는 말이 안 됩니다). 그러나 그들은 객체에 대해 항상 참이어야 하는 특정 수의 불변성이 있다고 말합니다(예: 고객 객체는 항상 이름이 있다는 것). 귀하의 워크플로 사례에서는 비즈니스 측면에서 불변성이 거의 없는 객체로 작업하는 것이므로 이러한 경험을 모든 객체로 외삽하는 것은 비논리적입니다.
추가 확장
- 이메일 모델 (개념 :
@
를 통해 아이디와 메일 서버를 나타내는 객체) - 고객모델 (개념 : 이름과 이메일을 갖는 사람에 대한 모델링된 객체)
- 이메일에
@
포함 여부 : invariant- 실제 객체의 행위나 협력 시에는 확인 불필요
- 대신 객체 생성 시점에 이 불변식이 참인 상태로 만들어져야함 (이게 거짓이면 더이상 해당 모델을 "이메일"이라 할 수 없음)
- 따라서 해당 메세지를 보내는 시점에 참인 상태가 되어, 내가 수신받을 때 해당 메세지로 엔터티를 받을 때 불변식에 대한 검증이 완료되어야 함
- 혹은 적어도 DTO -> ENTITY 매핑 시점에 해당 불변식이 참임이 보장되어야 함
- 이메일에
.edu
가 들어가 있는지.kr
이 들어가 있는지: validaiton- 이러한 검사는 연산 혹은 맥락에 따라 달라짐
문득 든 생각인데, 불변성은 유효성 검사로부터 만들어지는 개념이겠군요.
유효성 검사를 통해 유효한 불변 객체를 만들고 해당 객체를 이후 로직에서 사용함으로써 유효하지 않은 케이스를 아예 배제할 수 있겠죠.
이메일의 예시에서 유효성 검사를 어떻게 할 지에 따라 validation -> 불변성 객체 를 만드는 방법도 다양하게 있을 것 같네요.
case class Email(id: String, address: String) { // 이메일은 @ 가 항상 포함된다. 라는 유효성 검사로 만들어진 불변 객체
private val at = "@"
}
case class EmailForKR(id: String, addressFirst: String) { // 이메일은 @ 가 항상 포함되고, .kr 도메인의 이메일이다. 라는 유효성 검사로 만들어진 불변 객체
private val at = "@"
private val addressLast = ".kr"
}
관련하여 최근에 작성한 코드입니다.
taskStatus 는 7가지 정도의 상태가 있는데 각 상태마다 CXM 객체의 필수 파라미터가 존재해야 했고, 이를 불변 객체로 표현했습니다.
CREATED : segmentId 는 필수로 존재.
SOURCE_PREPARING : segmentId, segmentDataId 는 필수로 존재.
SOURCE_PREPARED : segmentId, segmentDataId, totalPage, totalCount 모두 필수로 존재.
def from(taskStatus: TaskStatus, cxm: CXM): CXMModel = (taskStatus, cxm) match {
case (TaskStatus.CREATED, CXM(segmentId, _, _, _)) => CreatedCXM(segmentId)
case (TaskStatus.SOURCE_PREPARING, CXM(segmentId, Some(segmentDataId), _, _)) =>
PreparingCXM(segmentId, segmentDataId)
case (
TaskStatus.SOURCE_PREPARED,
CXM(segmentId, Some(segmentDataId), Some(totalPage), Some(totalCount)),
) =>
PreparedCXM(segmentId, segmentDataId, totalPage, totalCount)
case _ => InValid
}