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())));
}
}이러한 접근 방식의 장점은 다음과 같습니다:
- 도메인 로직의 높은 응집도 유지
- 명확한 패키지 구조
- 재사용 가능한 검증 로직
- 테스트 용이성
- 도메인 규칙의 명시적 표현
도메인 서비스는 정말 필요한 기술적인 서비스로 최소화하고, 대부분의 비즈니스 로직은 도메인 모델과 명세를 통해 구현함으로써 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.