5.06 도메인 서비스 구현

5.06 도메인 서비스 구현

사용자 도메인에서는 도메인 서비스를 최소화하고, 가능한 한 많은 로직을 도메인 모델과 명세(Specification)에 구현합니다. 이를 통해 도메인 로직의 응집도를 높이고 재사용성을 향상시킬 수 있습니다.

명세(Specification) 패턴 활용

명세 패턴을 사용하여 검증 로직을 구현합니다.

package net.badnom.library.user.domain.user.specification;

public class UniqueEmailSpecification implements Specification<Email> {
    private final UserRepository userRepository;

    public UniqueEmailSpecification(UserRepository userRepository) {
        this.userRepository = Objects.requireNonNull(userRepository);
    }

    @Override
    public Result<Boolean> isSatisfiedBy(Email email) {
        return Result.success(!userRepository.existsByEmail(email))
                .flatMap(isUnique -> 
                    isUnique ? Result.success(true)
                            : Result.failure(ErrorCodes.DUPLICATE_EMAIL, email));
    }
}

package net.badnom.library.user.domain.user.specification;

public class UniqueUsernameSpecification implements Specification<Username> {
    private final UserRepository userRepository;

    public UniqueUsernameSpecification(UserRepository userRepository) {
        this.userRepository = Objects.requireNonNull(userRepository);
    }

    @Override
    public Result<Boolean> isSatisfiedBy(Username username) {
        return Result.success(!userRepository.existsByUsername(username))
                .flatMap(isUnique -> 
                    isUnique ? Result.success(true)
                            : Result.failure(ErrorCodes.USERNAME_ALREADY_EXISTS, username));
    }
}

도메인 서비스 구현

비밀번호 해싱과 같이 기술적인 요구사항을 처리하는 서비스만 도메인 서비스로 구현합니다.

package net.badnom.library.user.domain.service;

public interface PasswordHasher {
    HashedPassword hash(String password);
    
    default HashedPassword hash(Password password) {
        return hash(password.value());
    }
    
    boolean matches(String inputPassword, String hashedPassword);
}

package net.badnom.library.user.infrastructure.security;

@Service
@RequiredArgsConstructor
public class BCryptPasswordHasher implements PasswordHasher {
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    @Override
    public HashedPassword hash(String password) {
        String hashedValue = encoder.encode(password);
        return HashedPassword.of(hashedValue)
                .orElseThrow(() -> new InternalServerException(
                    ErrorCodes.PASSWORD_HASHING_FAILED));
    }
    
    @Override
    public boolean matches(String inputPassword, String hashedPassword) {
        return encoder.matches(inputPassword, hashedPassword);
    }
}

패키지 구조

도메인 서비스와 관련된 패키지 구조는 다음과 같습니다:

net.badnom.library.user
├── domain
│   ├── user
│   │   ├── specification      // 도메인 명세들
│   │   │   ├── UniqueEmailSpecification
│   │   │   └── UniqueUsernameSpecification
│   └── service               // 도메인 서비스 인터페이스
│       └── PasswordHasher
└── infrastructure
    └── security             // 도메인 서비스 구현체
        └── BCryptPasswordHasher

명세와 도메인 서비스의 사용

Member 엔티티에서 명세와 도메인 서비스를 활용하는 방법:

package net.badnom.library.user.domain.user;

public final class Member extends User {
    public static Result<Member> create(Username username,
                                      HashedPassword password,
                                      Email email,
                                      Specification<Email> uniqueEmailSpec,
                                      Specification<Username> uniqueUsernameSpec,
                                      FullName fullName) {
        return uniqueEmailSpec.isSatisfiedBy(email)
                .flatMap(__ -> uniqueUsernameSpec.isSatisfiedBy(username))
                .map(__ -> new Member(
                        UserId.newId(),
                        username,
                        password,
                        email,
                        fullName,
                        MembershipTier.getDefaultTier()))
                .onSuccess(member -> 
                    member.registerDomainEvent(new MemberRegisteredEvent(member.getId())));
    }
}

이러한 접근 방식의 장점은 다음과 같습니다:

  1. 도메인 로직의 높은 응집도 유지
  2. 명확한 패키지 구조
  3. 재사용 가능한 검증 로직
  4. 테스트 용이성
  5. 도메인 규칙의 명시적 표현

도메인 서비스는 정말 필요한 기술적인 서비스로 최소화하고, 대부분의 비즈니스 로직은 도메인 모델과 명세를 통해 구현함으로써 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.