앞장에서는 비교적 간단한 비즈니스 로직을 다루는 두 가지 패턴에 대해 논의했다. 이번 장에서는 계속해서 비즈니스 로직의 구현에 대해 다루는데, 복잡한 비즈니스 로직에 사용되는 도메인 모델 패턴이 있다.
class Color{
int _red;
int _green;
int _blue;
}유비쿼터스 언어
아래 코드는 유효성 검사 로직이 중복되기 쉽다. 그리고 값이 사용되기 전에 유효성 검사 로직을 호출하기 어렵다.
다른 엔지니어가 미래를 대비한 유지보수가 더 어렵다.
static void main(String[] args){
Person hong = new Person(3321, "Hong", "zxc123@gmail.com", "LA");
}
value object로 타입을 정해서 변수명을 짧게해도 의도를 명확하게 전달 가능하다.
유효성 검사 로직이 이미 value object 자체에 들어 있기 때문 값을 할당하기 전에 유효성 검사를 할 필요가 없다.
값을 조작하는 비즈니스 로직을 한곳에 모을 때 더욱 진가를 발휘한다.
→ 테스트 하기 쉽다.
코드에서 유비쿼터스 언어를 사용하게 하므로 코드에서 비즈니스 도메인의 개념을 표현하게 된다.
class Person{
private PersonId _id;
private Name _name;
private EmailAddress _email;
private CountryCode _country;
// 생성자 생략
}
static void main(String[] args){
Person hong= new Person(
new PersonId(23232),
new Name("Hong", "GILDONG"),
Email.Parse("zxc123@gmail.com"),
CountryCode.Parse("LA")
);
}
구현
public class Color {
public final byte Red;
public final byte Green;
public final byte Blue;
public Color(byte red, byte green, byte blue) {
Red = red;
Green = green;
Blue = blue;
}
public Color MixWith(Color other) {
return new Color((byte) Math.min(this.Red + other.Red, 255),
(byte) Math.min(this.Green + other.Green, 255),
(byte) Math.min(this.Blue + other.Blue, 255));
}
}사용하는 경우
class Person{
public Name Name // getter & setter
public Person(Name name){
this.Name = name;
}
}class Person{
public final PersonId id; // 식별 필드
public Name Name // getter & setter
}public class Message {
private PersonId from;
private String body;
public Message(PersonId from, String body) {
this.from = from;
this.body = body;
}
public void Append(Message message) {
this.body += message.getMessage();
}
}
// Aggregate 객체에 평범한 public method로 구현
public class Ticket {
public void AddMessage(PersonId from, String body){
Message message = new Message(from, body);
message.Append(message);
}
}
// command 실행에 필요한 모든 입력값을 포함하는 파라미터 객체로 표현하는 것 -> 다형적
public class Ticket {
public void Execute(AddMessage cmd){
Message message = new Message(cmd.from, body);
message.Append(message);
}
}
public ExecutionResult Escalate(TickeId id, EscalationReason reason){
try{
Ticket ticket = _ticketRepository.Load(id);
Command cmd = new Escalate(reason);
ticket.Execute(cmd);
_ticketRepository.Save(ticket);
return ExecutionResult.Success();
}
catch(ConcurrencyException ex){
return ExecutionResult.Error(ex);
}
}class Ticket{
TicketId _id;
int _version;
//...
}UPDATE tickets
SET ticket_status = @new_status,
agg_version = agg_version + 1
WHERE ticket_id=@id and agg_version=@expected_version; # 버전이 같을때만 변경둘 다 동시에 변경되거나 객체 하나가 다른 객체의 상태에 의존하는 비즈니스 규칙인 것 처럼 여러 객체가 하나의 트랜잭션 경계를 공유하는 비즈니스 시나리오가 있다.
DDD에서는 비즈니스 도메인이 시스템의 설계를 주도해야 한다고 규정한다.
→ Aggregate도 마찬가지로 엔티티 계층 구조와 비슷하게 모든 트랜잭션을 공유해서 일관성을 유지한다.
이 계층은 entity와 VO를 모두 담고있다.
→ 이 요소들이 도메인의 비즈니스 로직 경계내에 엤으면 동일한 Aggregate에 속한다.
public class Ticket{
//...
List<Message> _messages;
//...
public void Execute(EvaluateAutomaticActions cmd){
// 1. 남은 처리 시간이 정의된 50% 임계치 아래인지 확인하기 위해 티켓의 값을 검사
// 2. 현재 에이전트가 메시지를 아직 읽기 전인지 검사한다.
// 3. 현재 에이전트가 메시지를 아직 읽기 전인지 검사한다.
if(this.IsEscalated && this.RemainingTimePercentage < 0.5
&& GetUnreadMessagesCount(for: AssignedAgent) > 0)
{
_agent = AssignNewAgent();
}
}
public int GetUnreadMessagesCount(User id){
return _messages.Where(x -> x.To == id && !x.WasRead).Count();
}
~~}~~
위 메서드는 일관된 데이터에 대해 모든 조건을 엄격하게 검사하도록 확인한다.
→ Aggregate 데이터의 모든 변경이 원자적인 단일 트랜잭션으로 수행되도록 보장하여 점검이 완료된 후 수정되지 못하게 한다.
public class Ticket{
private UserId _customer;
private List<ProductId> _products;
private UserId _assignedAgent;
private List<Message> _messages;
// ...
}public class Ticket{
...
List<Message> _message;
...
public void Execute(AcknowledgeMessage cmd){
Message message = _message.Where(x-> x.id == cmd.id).First();
message.WasRead = true;
}
...
}
//언제, 무슨 이유로 특정 티켓이 상부에 보고됬는지 설명함.
{
"ticket-id": "asdasddssaew-dasdsa-dasdas-dsadsadqe23e2",
"event-id": 133,
"event-type": "ticket-escalated",
"escalation-reason": "missed-sla",
"escalation-time" 16289714577
}public class Ticket{
...
private List<DomainEvent> _domainEvents;
public void Execute(RequestEscalation cmd){
if(!this.IsEscalated && this.RemainingTimePercentage <= 0){
this.IsEscalated = true;
// 새로운 도메인 이벤트가 인스턴스화 된다.
Event escalatedEvent = new TicketEscalated(_id, cmd.Reason);
// 티켓의 도메인 이벤트 모음에 추가된다.
_domainEvents.Append(esacalatedEvent);
}
}
}향후 Aggregate에도 VO에도 속하지 않거나 복수의 Aggregate에 관련된 비즈니스 로직을 다루게 될 것이다.
→ 이 경우 DDD에서는 도메인 서비스로 로직을 구현할 것을 제안한다.
도메인 서비스는 비즈니스 로직을 구현한 상태가 없는 객체(stateless Object)다.
→ MSA, SOA(서비스 지향 아키텍처)에서 '서비스'와 다른 의미이다.
어떤 계산이나 분석을 수행하기 위한 다양한 시스템 구성 요소의 호출을 조율한다.
public class ResponseTimeFrameCalculationService{
...
public ResponseTimeframe CalculateAgentResponseDealline(UserId agentId,
var priority, boolean escalated, DateTime startTime){
var policy = _departmentRepository.GetDepartmentPolicy(agentId);
var maxProcTime = policy.GetMaxResponseTimeFor(priority);
if(escalated){
maxProcTime = maxProcTime * policy.EscalationFactor;
}
var shifts = _departmentRepository.GetUpcomingShifts(agentId,
startTime, startTime.Add(policy.MaxAgentResponseTime));
return CalculateTargetTime(maxprocTime, shifts);
}
...
}
→ 여러 Aggregate 작업들을 쉽게 조율할 수 있다.
→ 1개의 DB 트랜잭션으로 Aggregate 인스턴스의 수정할 수 있다는 패턴의 한계를 명심해야 함
→ 여러 개의 Aggregate 데이터를 읽는 것이 필요한 계산 로직을 구현하는 것을 도와준다.