-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
책 : 도메인 주도 설계 철저 입문 - 코드와 패턴으로 밑바닥부터 이해하는 DDD #1
Comments
DDD 도메인 주도설계 1장도메인 주도 설계란?도메인 : 모델 :
|
DDD 도메인 주도설계 2장시스템 특유의 값을 나타내기 위한 값 객체값 객체
class FullName : IEquatable<FullName> // IEquatable : 등가성 비교 기능
{
private readonly FirstName firstName; // 값의 불변성 (생성자에서만 정의 가능)
private readonly LastName lastName;
public FullName(FirstName firstName, LastName lastName)
{
if (firstName == null) throw new ArgumentNullException(nameof("firstName"));
if (lastName == null) throw new ArgumentNullException(nameof("lastName"));
if (!ValidateName(firstName)) throw new ArgumentException("허가되지 않은 문자가 사용됨", nameof(firstName));
if (!ValidateName(lastName)) throw new ArgumentException("허가되지 않은 문자가 사용됨", nameof(lastName));
this.firstName = firstName;
this.lastName = lastName;
}
public bool ValidateName(string name) => Regex.IsMatch(name, @"^[a-zA-Z]+$"); // 값에 규칙 추가
public bool Equals(FullName other) // 등가성 비교
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(firstName, other.firstName)
&& string.Equals(lastName, other.lastName); // 속성 쉽게 추가 가능
}
public override bool Equals(object obj) // 등가성 비교
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((FullName)obj);
}
// c#에서 Equals를 오버라이드 하려면 GetHashCode를 함께 오버라이드 해야 한다.
public override int GetHashCode()
{
unchecked
{
return ((firstName != null ? firstName.GetHashCode() : 0) * 397)
^ (lastName != null ? lastName.GetHashCode() : 0);
}
}
}
// -- change
class Name
{
private readonly string value;
public Name(string value)
{
if (value == null) throw new ArgumentNullException(nameof(value));
if (!Regex.IsMatch(value, @"^[a-zA-Z]+$")) throw new ArgumentException("허가되지 않은 문자가 사용됨", nameof(value));
if(value.Length <= 2) thorw new Exception("유효하지 않는 값"); // 무결성
this.value = value;
}
}
class FullName
{
private readonly Name firstName;
private readonly Name lastName;
public FullName(Name firstName, Name lastName)
{
if (firstName == null) throw new ArgumentNullException(nameof("firstName"));
if (lastName == null) throw new ArgumentNullException(nameof("lastName"));
this.firstName = firstName;
this.lastName = lastName;
}
} 객체 표현
C# DDD
???
|
DDD 도메인 주도설계 3장생애주기를 갖는 객체 - 엔티티
엔티티 성질
생애주기, 연속성
도메인 객체를 정의할 때의 장점
|
DDD 도메인 주도설계 4장소프트웨어에서 말하는 서비스
부자연스러움을 해결하는 도메인 서비스
도메인 서비스를 남용한 결과
|
1장 ~ 4장 내용 정리
|
DDD 도메인 주도설계 5장데이터와 관계된 처리를 분리하자 - 리포지토리
리포지토리 책임
리포지토리 인터페이스
테스로 구현 검증
객체-관계 매핑
리포지토리 정의 지침
|
1장 ~ 5장 내용 정리
|
DDD 도메인 주도설계 6장유스케이스를 구현하기 위한 '애플리케이션 서비스'
유스케이스 수립 (예시 제공)
도메인 규칙의 유출
애플리케이션 서비스와 프로그램의 응집도
애플리케이션 서비스의 인터페이스
|
1장 ~ 6장 내용 정리
|
DDD 도메인 주도설계 7장소프트웨어의 유연성을 위한 의존 관계 제어
Service Locator 패턴private readonly IUserRepository userRepository;
public UserApplicationService()
{
// ServiceLocator를 통해 필요한 인스턴스를 받음
this.userRepository = ServiceLocator.Resolve<IUserRepository>();
}
IoC Container 패턴
// IFootRepository 가 추가되었을 때 생성자에 추가
public UserApplicationService(IUserRepository userRepository, IFootRepository footRepository) { /*...*/ }
// 하지만 UserApplicationService를 생성하는 모든 코드를 교체해야 한다.
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IUserRepository, InMemoryUserRepository>();
serviceCollection.AddTransient<UserApplicationService>();
var provider = serviceCollection.BuildServiceProvider();
var userApplicationService = provider.GetService<UserApplicationService>(); |
DDD 도메인 주도설계 8장복잡한 객체 생성을 맡길 수 있는 '팩토리 패턴'팩토리
예시
public class User
{
private readonly UserId id;
private UserName name;
// 사용자를 최초 생성할 때 실행되는 생성자 메서드
public User(UserName name)
{
if (name = null)
throw new ArgumentNullException(nameof(name));
// 식별자로 GUID를 사용한다.
id = new UserId(Guid.NewGuid().ToString());
this.name = name;
}
// 사용자 객체를 복원할 때 실행되는 생성자 메서드
public User(UserId id. UserName name)
{
if (id = null)
throw new ArgumentNullException(nameof(id));
if (name = null)
throw new ArgumentNullException(nameof(name));
this.id = id;
this.name = name;
// (...생략...)
}
}
public class User
{
private readonly UserId id;
private UserName name;
// 사용자 객체를 복원할 때 실행되는 생성자 메서드
public User(UserId id. UserName name)
{
if (id = null)
throw new ArgumentNullException(nameof(id));
if (name = null)
throw new ArgumentNullException(nameof(name));
this.id = id;
this.name = name;
// (...생략...)
}
} public interface IUserFactory
{
User Create(UserName name);
} public class UserApplicationService
{
private readonly IUserFactory userFactory;
private readonly IUserRepository userRepository;
private readonly UserService userService;
// (...생략...)
public void Register(UserRegisterCommand command)
{
var userName = new UserName(command.Name);
// 팩토리를 이용해 인스턴스를 생성
var user = userFactory.Create(userName);
if (userService.Exists(user))
throw new CanNotRegisterUserException(user);
userRepository.Save(user);
}
} class InMemoryUserFactory : IUserFactory
{
// 마지막으로 발행된 식별자
private int currentId;
public User Create(UserName name)
{
// 사용자를 생성할 때마다 1씩 증가
currentId++;
return new User(new UserId(currentId.ToString()),name);
}
} 팩토리 존재감
팩토리와 레파지토리
|
DDD 도메인 주도설계 10장데이터 무결성 유지하기
무결성?
무결성 문제?
무결성 문제 예시public class UserApplicationService
{
private readonly IUserFactory userFactory;
private readonly IUserRepository userRepository;
private readonly UserService userService;
// (...생략...)
public void Register(UserRegisterCommand command)
{
var userName = new UserName(command.Name);
var user = userFactory.Create(userName);
if (userService.Exists(user))
throw new CanNotRegisterUserException(user);
userRepository.Save(user);
}
}
해결방법 1 유일키
해결방법 2 트랜잭션
해결방법 3 트랜잭션 - C# TransactionScope()
public class UserApplicationService
{
private readonly IUserFactory userFactory;
private readonly IUserRepository userRepository;
private readonly UserService userService;
// (...생략...)
public void Register(UserRegisterCommand command)
{
using(var transaction = new TransactionScope())
{
var userName = new UserName(command.Name);
var user = userFactory.Create(userName);
if (userService.Exists(user))
throw new CanNotRegisterUserException(user);
userRepository.Save(user);
transaction.Complete(); // 커밋
}
}
} 해결방법 4 트랜잭션 - AOP
해결방법 5 트랜잭션 - 유닛오브워크
|
11장 |
DDD 도메인 주도설계 12장도메인의 규칙을 지키는 ‘애그리게이트’
애그리게이트는 불변 조건을 유지하는 단위로 꾸려지며 객체 조작의 질서를 유지한다. 애그리게이트는 경계와 루트 를 갖는다. 경계는 말 그대로 애그리게이트에 포함되는 대상을 결정하는 경계 루트는 애그리게이트에 포함되는 특정한 객체 외부에서 애그리게이트를 다루는 조작은 모두 루트를 거쳐야만 한다. 애그리게이트에 포함되는 객체를 외부에 노출하지 않음으로써 불변의 조건을 유지 할 수 있다. 1.1 애그리게이트의 기본 구조 애그리게이트는 서로 연관된 객체를 감싸는 경계를 통해 정의 된다. 외부에서는 애그리게이트 내부에 있는 객체를 조작할 수 없다. 오직, 애그리게이트 루트(aggregate root)로만 조작이 가능하다. var userName = new UserName("NewName"); //NG // OK circle.Members.Add(member); 서클 애그리게이트에 포함되는 Members에 대한 조작은 애그리게이트의 루트인 Circle 객체를 통해야 한다. public class Circle (... 생략 ...) public void Join(User member) if(members.Count >= 29) members.Add(member); circle.Join(user); 이런 방법으로 불변 조건을 유지하면서도 직관과 좀 더 일치하는 코드를 만들 수 있다. 데메테르의 법칙으로 알려진 규칙이다. 1.2 객체를 다루는 조작의 기본 원칙 데메테르의 법칙은 객체 간의 메서드 호출에 질서를 부여하기 위한 가이드라인이다. 데메테르의 법칙은 어떤 컨텍스트에서 다음 객체의 메서드만을 호출할 수 있게 제한한다. 객체 자신 인자로 전달받은 객체 인스턴스 변수 해당 컨텍스트에서 직접 생성한 객체 if(circle.Members.Count >= 29) 메서드를 사용할 수 있는 객체의 범위 벗어나기 때문에 데메테르의 법칙을 위반한 코드이다. public class Circle (... 생략 ...) public bool IsFull() public void Join(User user) if(IsFull()) members.Add(user); if(circle.IsFull()) public class Circle public bool IsFull() 데메테르의 법칙은 소프트웨어의 유지 보수성을 향상시키고 코드를 더욱더 유연하게 한다. 1.3 내부 데이터를 숨기기 위해 객체 내부의 데이터를 함부로 외부에 공개돼서는 안 된다. 그러나 데이터를 외부에 전혀 공개하지 않으면 리포지토리가 객체를 데이터스토에에 저장 할 수가 없다. public class EFUserRepository : IUserRepository UserDataModel 객체를 생성하려면 User 클래스의 Id와 Name에 접근해야 하므로 User 클래스가 Id와 Name을 완전히 비공개로 하면 이 코드는 컴파일 애러를 일으킨다. 가장 단순하고 일반적인 해결체는 팀 내에 규칙을 정해 리포티토리 객체외에는 애그리게이트의 내부 데이터에 접근하는 코드를 작성하지 말자고 할 수 있다. 또 다른 방법은 노티피케이션 객체를 이용하는 것이다. public interface IUserNotification public void Id(UserId id) public void Name(UserName name) // 전달 받은 데이터로 데이터 모델을 생성하는 메서드 public class User (...생략 ...) public void Notify(IUserNotification note) public class EFUserRepository : IUserRepository // 전달 받은 내부 데이터로 데이터 모델을 생성 // 데이터 모델을 ORM에 전달한다. (... 생략 ...)
애그리게이트의 경계를 정하는 원칙 중 가장 흔히 쓰이는 것은 변경의 단위이다. 서클과 사용자는 별개의 애그리게이트이므로 서클을 변경할 때는 서클 애그리게이트 내부로 변경이 제한돼야 하고, 사용자를 변경할 때도 사용자 애그리게이션 내부의 정보만 변경돼야 한다. 만약, 이러한 규칙을 위반하고 서클 애그리게이트에서 자신의 경계를 넘어 사용자 애그리게이트까지 변경하려고 하면 프로그램에 어떤 일이 일어날까? public class Circle (... 생략 ...) public void ChangeMemberName(UserId id, UserName name) 아래는 서클 애그리게이트의 퍼시스턴시 처리 코드이다. public class CircleRepository : ICircleRepository public void Save(Circle circle) command.Parameters.Add(new SqlParameter("@id", circle.Id.Value)); using(var command = connection.CreateCommand()) command.Parameters.Add(new SqlParameter("@circleid", circle.Id.Value)); foreach (var member in circle.Members) } 이 상태로는 경계 너머 사용자 애그리게이트를 변경한 내용이 저장되지 않는다. 그래서 리포지토리를 수정해야 한다. public class CircleRepository : ICircleRepository public void Save(Circle circle) foreach(var user in circle.Members) // 서클 애그리게이션에 대한 업데이트는 그 다음 더불어 서클 리포지토리에 새로 추가된 코드의 대부분이 사용자 리포지토리의 코드와 중복 된다. 애그리게이트에 대한 변경은 해당 애그리게이트 자신에게만 맡기고, 퍼시스턴시 요청도 애그리게이트 단위로 해야 한다. 해당 이유로 인해 리포지토리는 애그리게이트마다 하나씩 만든다. 2.1 식별자를 이용한 컴포지션 Circle 객체는 User 클래스의 인스턴스를 컬렉션 객체에 저장하고 프로퍼티를 통해 객체에 접근해 메서드를 호출 할 수 있다. 하지만, 이것 자체를 문제로 보는 시각이 많다. 애그리게이트의 경계를 넘지 않는다는 불문율을 만드는 것보다 더 나은 방법이 없을까? 인스턴스를 갖지 않도록 하면 된다. 인스턴스를 실제로 갖지는 않지만 그런 것처럼 보이게끔 하는것, 엔티티의 식별자를 사용하면 된다. public class Circle (... 생략 ...) 이러한 절차를 강제한다면 부주의하게 메서드를 호출해 애그리게이트 너머의 영역을 변경하는 일은 일어나지 않는다. 또한 메모리를 절약하는 효과가 있는데 아래는 서클명을 변경하는 처리 코드이다. public class CircleApplicationService (... 생략 ...) public void Update(CircleUpdateCommand command) if(command.Name != null)
} User 객체를 직접 포함하는 대신 UserId를 포함하면 소속 사용자는 모든 User 객체를 복원할 만큼의 처리 능력을 절약할 수 있을 것이고 인스턴스를 저장하기 위한 메모리도 절약될 것이다.
애그리게이트의 크기는 가능한 한 작게 유지하는 것이 좋다. 한 트랜잭션에서 여러 애그리게이트를 다루는 것도 가능한 한 피해야 한다. 여러 애그리게이션에 걸친 트랜잭션은 범위가 큰 애그리게이트와 마찬가지로 광범위한 데이터에 로크를 걸 가능성이 높다.
서클의 규칙 중 서클에 소속되는 인원은 서클장을 포함해 최대 30명까지 허용된다.이 있다. 30이라는 구체적인 수치가 규칙에 실려 있지만, 정작 코드에 나오는 수치는 29이다. public class Circle (... 생략 ...) public bool IsFull() 하지만 코드에 문제가 없다고 해서 언어 표현과의 모순을 그대로 두면 오해를 일으키기 쉽다. public class Circle (... 생략 ...) public bool IsFull() public int CountMembers() |
DDD 도메인 주도설계 13장복잡한 조건을 나타내기 위한 ‘명세’명세?
|
https://www.yes24.com/Product/Goods/93384475?pid=123487&cosemkid=go16027467066842708&gad_source=1&gclid=CjwKCAiAk9itBhASEiwA1my_6y9zasuRJ9uUlG6asaBq0KoJH6Au07jaYdTTweC3_aWAY29m0yLWaBoCF1IQAvD_BwE
The text was updated successfully, but these errors were encountered: