5.03 사용자 애그리거트 설계
사용자 애그리거트는 도서관 시스템에서 사용자와 관련된 모든 정보와 동작을 캡슐화하는 핵심 애그리거트입니다. 이 섹션에서는 사용자 애그리거트의 설계와 구현 방법을 살펴보겠습니다.
User 추상 클래스 설계
사용자 도메인의 중심에는 User 추상 클래스가 있습니다. 이 클래스는 sealed 키워드를 사용하여 상속을 Member와 Librarian으로 제한함으로써, 사용자 유형의 확장을 엄격하게 통제합니다.
public abstract sealed class User extends AbstractAggregateRoot<UserId>
permits Member, Librarian {
private final UserId id;
private final Username username;
private final HashedPassword password;
private final Email email;
private final FullName fullName;
private final UserStatus status;
protected User(UserId id,
Username username,
HashedPassword password,
Email email,
FullName fullName) {
// 모든 사용자는 PENDING 상태로 시작합니다.
this.id = Objects.requireNonNull(id, "User ID must not be null");
this.username = Objects.requireNonNull(username, "Username must not be null");
this.password = Objects.requireNonNull(password, "Password must not be null");
this.email = Objects.requireNonNull(email, "Email must not be null");
this.fullName = Objects.requireNonNull(fullName, "Full name must not be null");
this.status = UserStatus.PENDING;
}
// 상태 확인 메서드들
public boolean isActive() {
return status == UserStatus.ACTIVE;
}
public boolean isPending() {
return status == UserStatus.PENDING;
}
public boolean isWithdrawn() {
return status == UserStatus.WITHDRAWN;
}
}Member 클래스 구현
Member 클래스는 도서관 서비스를 이용하는 회원을 표현합니다. 회원은 등급(MembershipTier)을 가지며, 등급 변경과 관련된 도메인 로직을 포함합니다.
public final class Member extends User {
private MembershipTier currentTier;
private Member(UserId id,
Username username,
HashedPassword password,
Email email,
FullName fullName,
MembershipTier initialTier) {
super(id, username, password, email, fullName);
this.currentTier = Objects.requireNonNull(initialTier,
"Initial membership tier must not be null");
}
// 팩토리 메서드를 통한 생성
public static Result<Member> create(Username username,
HashedPassword password,
Email email,
Supplier<Boolean> isEmailUnique,
FullName fullName) {
return ContractValidator.start()
.require(isEmailUnique.get(), ErrorCodes.DUPLICATE_EMAIL, email)
.validate()
.map(__ -> new Member(
UserId.newId(),
username,
password,
email,
fullName,
MembershipTier.getDefaultTier()))
.onSuccess(member ->
member.registerDomainEvent(new MemberRegisteredEvent(member.getId())));
}
// 등급 변경 로직
public Result<Void> changeTier(MembershipTier newTier) {
return ContractValidator.start()
.requireNotNull(newTier, ErrorCodes.ERROR_NULL_NEW_TIER)
.require(!newTier.equals(currentTier), ErrorCodes.SAME_TIER)
.validate()
.onSuccess(__ -> {
MembershipTier previousTier = this.currentTier;
this.currentTier = newTier;
registerDomainEvent(new MemberTierChangedEvent(
this.getId(),
previousTier,
newTier,
newTier.isHigherThan(previousTier)
));
});
}
}Librarian 클래스 구현
Librarian 클래스는 도서관 직원을 표현하며, 직원 ID와 같은 추가적인 속성을 가집니다.
public final class Librarian extends User {
private String employeeId;
private Librarian(UserId id,
Username username,
HashedPassword password,
Email email,
FullName fullName,
String employeeId) {
super(id, username, password, email, fullName);
this.employeeId = Objects.requireNonNull(employeeId,
"Employee ID must not be null");
}
public static Result<Librarian> create(Username username,
HashedPassword password,
Email email,
Supplier<Boolean> isEmailUnique,
FullName fullName,
String employeeId) {
return ContractValidator.start()
.require(isEmailUnique.get(), ErrorCodes.DUPLICATE_EMAIL, email)
.validate()
.map(__ -> new Librarian(
UserId.newId(),
username,
password,
email,
fullName,
employeeId));
}
}불변성과 캡슐화 전략
사용자 애그리거트는 다음과 같은 원칙으로 불변성과 캡슐화를 보장합니다:
- 값 객체를 사용하여 데이터의 유효성을 보장합니다
- 생성자를 private으로 선언하고 팩토리 메서드를 사용합니다
- 상태 변경은 명시적인 메서드를 통해서만 가능하도록 합니다
이러한 설계를 통해 다음과 같은 이점을 얻을 수 있습니다:
- 명확한 생성 규칙과 검증
- 캡슐화를 통한 도메인 규칙 강제
- 상태 변경의 추적 가능성
- 확장 가능하면서도 제어된 상속 구조
사용자 애그리거트는 도메인의 핵심 개념을 명확하게 표현하며, 관련된 모든 비즈니스 규칙을 일관되게 강제합니다. 이를 통해 도메인 모델의 무결성을 유지하고, 시스템의 확장성과 유지보수성을 향상시킬 수 있습니다.