5.03 사용자 애그리거트 설계

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));
    }
}

불변성과 캡슐화 전략

사용자 애그리거트는 다음과 같은 원칙으로 불변성과 캡슐화를 보장합니다:

  1. 값 객체를 사용하여 데이터의 유효성을 보장합니다
  2. 생성자를 private으로 선언하고 팩토리 메서드를 사용합니다
  3. 상태 변경은 명시적인 메서드를 통해서만 가능하도록 합니다

이러한 설계를 통해 다음과 같은 이점을 얻을 수 있습니다:

  1. 명확한 생성 규칙과 검증
  2. 캡슐화를 통한 도메인 규칙 강제
  3. 상태 변경의 추적 가능성
  4. 확장 가능하면서도 제어된 상속 구조

사용자 애그리거트는 도메인의 핵심 개념을 명확하게 표현하며, 관련된 모든 비즈니스 규칙을 일관되게 강제합니다. 이를 통해 도메인 모델의 무결성을 유지하고, 시스템의 확장성과 유지보수성을 향상시킬 수 있습니다.