4.8 도메인 완전성
이메일 중복 체크는 회원가입 시스템에서 매우 중요한 요구사항입니다. 동일한 이메일로 여러 계정이 생성되는 것을 방지해야 하기 때문입니다. 이러한 이메일 중복 체크를 구현하는 방식으로 도메인 모델과 도메인 서비스의 두 가지 접근법을 살펴보겠습니다. 각각의 특징과 장단점을 실제 시나리오를 통해 이해해보도록 하겠습니다.
먼저 도메인 모델에서 모든 검증을 처리하는 방식을 살펴보겠습니다. 이 방식에서는 Member 객체를 생성할 때 팩토리 메서드를 통해 모든 유효성 검사를 한 곳에서 수행합니다:
public class Member extends User {
// 팩토리 메서드를 통해 Member 객체 생성
public static Result<Member> create(Username username,
HashedPassword password,
Email email,
boolean isEmailUnique, // 외부 상태를 파라미터로 받음
FullName fullName) {
// ContractValidator를 사용해 모든 제약조건을 한 곳에서 검증
return ContractValidator.start()
// 불변식 검증
.require(email.isValid(), "Invalid email format")
// 집합에 대한 불변식 검증
.require(isEmailUnique, ErrorCodes.DUPLICATE_EMAIL, email)
.validate()
.map(__ -> new Member(UserId.newId(), username, password,
email, fullName));
}
// private 생성자로 객체 생성을 제한
private Member(UserId id, Username username, HashedPassword password,
Email email, FullName fullName) {
super(id, username, password, email, fullName);
}
}이러한 방식은 객체의 일관성을 보장한다는 장점이 있습니다. Member 객체가 생성된 이후에는 항상 유효한 상태를 유지할 수 있기 때문입니다. 하지만 도메인 모델이 외부 상태에 의존하게 되어 모델의 순수성을 해치는 단점이 있습니다.
반면 도메인 서비스에서 검증을 처리하는 두 번째 방식은 책임을 명확히 분리합니다:
// Member 엔티티는 자신의 상태에 대한 검증만 담당
public class Member extends User {
public static Result<Member> create(Username username,
HashedPassword password,
Email email,
FullName fullName) {
// 순수하게 객체 자체의 유효성만 검증
return ContractValidator.start()
.require(username != null, "Username is required")
.require(email.isValid(), "Invalid email format")
.validate()
.map(__ -> new Member(UserId.newId(), username, password,
email, fullName));
}
}
public class UserDomainService {
private final UserRepository userRepository;
private final PasswordHasher passwordHasher;
public Result<Member> registerMember(Username username,
Password password,
Email email,
FullName fullName) {
// 이메일 중복 검사
if (userRepository.existsByEmail(email)) {
return Result.failure(ErrorCodes.DUPLICATE_EMAIL, email);
}
// 비밀번호 해싱과 Member 생성
HashedPassword hashedPassword = passwordHasher.hash(password);
return Member.create(username, hashedPassword, email, fullName);
}
}
// 애플리케이션 서비스가 트랜잭션 경계를 설정하고 전체 흐름을 조율
public class UserApplicationService {
private final UserDomainService userDomainService;
private final UserRepository userRepository;
@Transactional
public Result<Member> registerNewMember(RegisterMemberCommand command) {
return userDomainService.registerMember(
command.getUsername(),
command.getPassword(),
command.getEmail(),
command.getFullName()
)
.flatMap(userRepository::save); // 저장도 애플리케이션 서비스에서 수행
}
}실무에서는 두 번째 방식이 더 실용적인 선택이 될 수 있습니다. 이메일 중복 방지는 개별 Member 엔티티의 책임이라기보다는 특정 시점에 검증해야 하는 비즈니스 규칙에 가깠습니다. 따라서 애그리게잇 루트는 자신의 범위 내 제약조건만 관리하고, 시스템 전체의 제약조건은 도메인 서비스가 관리하는 것이 더 적절합니다.
그러나 이는 절대적인 정답이 아니며, 시스템의 특성과 중요시하는 가치에 따라 적절한 방식을 선택할 수 있습니다. 가장 중요한 것은 팀 내에서 일관된 방식을 사용하는 것입니다. 마치 교통 신호와 같이, 어떤 규칙을 선택하든 모든 구성원이 동일한 규칙을 따르는 것이 혼란을 최소화할 수 있습니다.
null 체크하는것도 불변식에 포함될까요?
이는 매우 흥미로운 질문입니다. null 체크와 불변식의 관계를 이해하기 위해서는 먼저 불변식의 정의를 살펴볼 필요가 있습니다.
불변식(invariant)은 객체의 lifecycle 전체에 걸쳐 항상 참이어야 하는 규칙입니다. 예를 들어, ‘계좌 잔액은 항상 0 이상이어야 한다’와 같은 비즈니스 규칙이 전형적인 불변식입니다.
null 체크는 이와는 조금 다른 성격을 가집니다. null 체크는 객체의 기술적인 유효성을 검증하는 것으로, 비즈니스 규칙보다는 프로그래밍 언어 수준의 안전성을 확보하기 위한 것입니다. 따라서 엄밀히 말하면 도메인 모델의 불변식이라기보다는, 기술적인 전제조건(precondition)에 가깝습니다.
예를 들어 보겠습니다:
import java.util.Objects;
public class Member extends User {
public static Result<Member> create(Username username,
HashedPassword password,
Email email,
FullName fullName) {
// 기술적인 전제조건 검사
Objects.requireNonNull(username);
Objects.requireNonNull(password);
Objects.requireNonNull(email);
// 도메인 불변식 검사
if (!email.isValid()) {
return Result.failure(ErrorCodes.INVALID_EMAIL_FORMAT);
}
return Result.success(new Member(UserId.newId(), username, password,
email, fullName));
}
}여기서 email.isValid()는 이메일 형식이 올바른지 확인하는 도메인 불변식이지만, email == null 체크는 기술적인 전제조건입니다.
실제로 많은 현대 프로그래밍 언어들은 null 체크를 타입 시스템으로 해결하려고 합니다. 예를 들어 Kotlin은 nullable 타입을 명시적으로 구분하여, null 체크를 컴파일 시점에 처리합니다. 이는 null 체크가 도메인 모델의 불변식이라기보다는 기술적인 문제임을 보여주는 또 다른 증거입니다.
따라서, 도메인 모델에서는 실제 비즈니스 규칙과 관련된 검증에 집중하고, null 체크와 같은 기술적인 검증은 가능한 한 타입 시스템이나 유틸리티를 통해 처리하는 것이 좋습니다. 이는 도메인 모델의 표현력을 높이고, 진정한 비즈니스 규칙에 집중할 수 있게 해줍니다.