본문 바로가기

DDD/도메인 주도 설계 철저 입문

Specification 객체 정의와 활용 - 「도메인 주도 설계 철저 입문」 13장 (1)

 

Specifiction 객체란?

한국말로 명세 객체는 어떤 객체가 특정 조건을 만족하는지 평가하기 위한 객체로, 엔티티나 값 객체의 메서드로 담기에는 복잡한 경우에 사용되는 클래스이다.

 

 

복잡한 도메인 규칙 예시

여러명의 User로 이루어진 Circle이 있을 때, Circle의 최대 인원은 30명이며 프리미엄 User가 10명 이상 속하는 Circle은 최대 인원이 50명인 규칙이 있다고 치자. 도메인 규칙이 다소 복잡하여 Specification으로 구현하는 것이 적절하다.

 

 

Specification을 활용하지 않으면 어떻게 될까 (1) - Repository 사용하기

Circle 클래스에 `isFull`이라는 메서드가 UserRepository를 가지고 circle에 속한 user 수를 셀 수도 있겠지만 이 경우 Circle 엔티티가 Repository를 다룸으로써 도메인에 집중하지 못하게 된다. 즉, DDD 원칙에 어긋난다.

 

 

Specification을 활용하지 않으면 어떻게 될까 (2) - AppService에 구현하기

그렇다고 도메인이 아닌 서비스 레이어인 CircleApplicationService에 이를 구현하기에는 엄연한 도메인 규칙을 서비스 레이어에 구현하게 되는 오류가 생긴다. 도메인 규칙은 도메인에 구현해야 한다.

 

 

Specification 구현

따라서 이 경우에 활용할 수 있는 것이 Specification 클래스이다. Specification 클래스는 도메인 객체로, 객체가 도메인 규칙을 잘 따르는지 판단하는데 사용된다.

import java.util.List;
import java.util.stream.Collectors;

public class CircleSpecification {

    private final IUserRepository userRepository;

    public CircleSpecification(IUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public boolean isSatisfiedBy(Circle circle) {
        List<User> users = userRepository.find(circle.getMembers());
        long premiumUserNumber = users.stream().filter(User::isPremium).count();
        int circleUpperLimit = premiumUserNumber < 10 ? 30 : 50;
        return circle.countMembers() >= circleUpperLimit;
    }
}

 

 

Specification 활용

public class CircleApplicationService {

    (...생략...)
    
    public void join(CircleJoinCommand command) throws CircleFullException, UserNotFoundException {
        CircleId circleId = new CircleId(command.getCircleId());
        Circle circle = circleRepository.find(circleId);

        CircleSpecification circleSpecification = new CircleSpecification(userRepository);
        if (circleSpecification.isSatisfiedBy(circle)) {
            throw new CircleFullException(circleId);
        }

        UserId memberId = new UserId(command.getUserId());
        User member = userRepository.find(memberId);
        if (member == null) {
            throw new UserNotFoundException(memberId, "사용자 없음");
        }

        circle.join(member);
        circleRepository.save(circle);
    }
}