본문으로 건너뛰기
Leo Jung
뒤로 가기

[DDD Intro #18] Specification과 Policy: 조건과 정책을 모델링하기

페이지 수정

도메인 로직은 처음에는 작은 조건문으로 시작한다. 특정 상태이면 처리하지 않고, 특정 날짜가 지나면 만료시키고, 특정 요금제이면 할인하지 않는다. 처음에는 if 몇 개면 충분하다.

문제는 같은 조건이 여러 곳에 반복되기 시작할 때다. 조건문이 늘어나면 도메인 정책은 코드 곳곳에 흩어진다. 어느 순간 “무료 체험 가능 조건”이나 “갱신 가능 조건”을 정확히 설명할 수 있는 사람이 줄어든다. 코드를 찾아봐도 조건이 한곳에 있지 않다.

Specification과 Policy는 이런 조건과 정책을 모델링하는 방법이다. 핵심은 조건문을 없애는 것이 아니라, 도메인에서 중요한 조건과 정책에 이름을 부여하는 것이다.

조건문은 숨어 있는 도메인 개념일 수 있다

다음 코드를 보자.

if (subscription.getStatus() == ACTIVE
    && subscription.getNextBillingDate().isBefore(now.plusDays(1))
    && !subscription.hasOverduePayment()
    && member.isVerified()) {
    subscription.renew();
}

이 코드는 갱신 가능한 구독인지 확인하는 것처럼 보인다. 하지만 그 의미는 조건문 안에 숨어 있다. 같은 조건이 배치, 관리자 화면, 수동 갱신 API에 반복될 가능성이 높다.

이 조건에 도메인 이름을 붙일 수 있다.

if (renewalEligibility.isSatisfiedBy(subscription, member, now)) {
    subscription.renew();
}

이제 코드는 “갱신 가능 조건”을 말한다. 조건의 세부 구현은 내부에 숨겨진다. 중요한 것은 이 조건이 도메인에서 의미 있는 개념으로 승격되었다는 점이다.

Specification은 조건을 객체로 표현한다

Specification은 어떤 객체가 특정 조건을 만족하는지 판단하는 패턴이다.

public interface Specification<T> {
    boolean isSatisfiedBy(T candidate);
}

구독 도메인에서는 다음과 같은 Specification을 만들 수 있다.

public class ActiveSubscriptionSpecification implements Specification<Subscription> {
    public boolean isSatisfiedBy(Subscription subscription) {
        return subscription.isActive();
    }
}

하지만 너무 단순한 조건까지 모두 Specification으로 만들면 오히려 복잡해진다. Specification은 조건이 도메인적으로 중요하거나, 여러 곳에서 재사용되거나, 조합이 필요한 경우에 가치가 있다.

예를 들어 무료 체험 가능 여부는 좋은 후보가 될 수 있다.

public class TrialAvailableSpecification {
    public boolean isSatisfiedBy(Member member, List<SubscriptionHistory> histories) {
        return member.isVerified()
            && histories.stream().noneMatch(SubscriptionHistory::wasTrial)
            && !member.hasPaymentHistory();
    }
}

이제 무료 체험 조건은 코드 곳곳의 if가 아니라 이름 있는 도메인 개념이 된다.

Policy는 판단과 결정을 표현한다

Specification이 주로 “만족하는가?”를 묻는다면, Policy는 더 넓은 판단이나 결정을 표현할 때 유용하다.

예를 들어 요금제 변경 수수료를 계산한다고 해보자. 단순히 가능 여부만 판단하는 것이 아니라, 얼마를 청구해야 하는지, 즉시 변경할지 다음 결제일부터 적용할지 결정해야 한다.

PlanChangeDecision decision = planChangePolicy.decide(subscription, targetPlan, now);

PlanChangePolicy는 조건뿐 아니라 결과를 만든다.

public record PlanChangeDecision(
    boolean allowed,
    Money additionalCharge,
    SubscriptionPeriod nextPeriod,
    PlanChangeTiming timing
) {}

이런 정책을 Application Service에 직접 넣으면 유스케이스 흐름과 도메인 판단이 섞인다. Policy 객체로 분리하면 도메인 판단이 명시적으로 드러난다.

이름을 붙이면 대화가 가능해진다

Specification과 Policy의 가장 큰 가치는 코드 구조보다 언어에 있다. 조건에 이름이 생기면 도메인 전문가와 대화하기 쉬워진다.

무료 체험 가능 조건은 무엇인가요?
갱신 가능 조건에서 미납 사용자는 제외하나요?
요금제 변경 정책은 즉시 적용인가요, 다음 결제일부터 적용인가요?
환불 정책에서 사용 이력은 어떻게 판단하나요?

이 질문들은 코드의 조건문을 설명하는 것이 아니다. 도메인 정책을 설명하는 질문이다.

코드에도 같은 이름이 있으면 대화와 구현이 이어진다.

trialAvailabilityPolicy.isAvailableFor(member, histories);
renewalEligibilityPolicy.canRenew(subscription, paymentHistory, now);
refundPolicy.decide(subscription, payment, refundRequest, now);

좋은 이름은 설계의 일부다. 이름이 생긴 정책은 문서화되고, 테스트되고, 변경될 수 있다.

모든 조건을 객체로 만들 필요는 없다

Specification과 Policy를 배우면 모든 if를 객체로 바꾸고 싶어질 수 있다. 하지만 그것은 좋은 방향이 아니다.

다음과 같은 단순한 조건은 굳이 별도 객체가 필요 없을 수 있다.

if (subscription.isExpired()) {
    throw new CannotRenewException();
}

이 조건은 Subscription 자신이 알아도 충분하다. 오히려 별도 Specification으로 빼면 코드를 읽기 어려워질 수 있다.

Specification이나 Policy를 고려할 때는 다음 질문을 해보는 것이 좋다.

이 조건이 여러 곳에서 반복되는가?
도메인 전문가가 이름 붙여 말하는 정책인가?
조건이 자주 바뀌는가?
여러 조건을 조합해야 하는가?
결과가 단순 boolean을 넘어서는가?
Entity 하나에 넣기에는 책임이 어색한가?

이 질문에 해당하지 않는다면 단순한 메서드나 Entity 내부 조건으로 충분할 수 있다.

정책 객체는 도메인 테스트의 중심이 된다

정책을 객체로 만들면 테스트하기 좋아진다. 특히 정책은 비즈니스 규칙이 자주 바뀌는 곳이므로 테스트가 중요하다.

예를 들어 환불 정책을 테스트할 수 있다.

결제_후_7일_이내이고_사용_이력이_없으면_전액_환불된다
결제_후_7일이_지나면_환불되지_않는다
부분_사용_이력이_있으면_부분_환불된다
이미_해지된_구독은_중복_환불할_수_없다

이 테스트 이름들은 도메인 정책 문서처럼 읽힌다. 정책 객체가 없으면 이런 규칙은 여러 서비스 메서드에 흩어지고, 테스트도 유스케이스 단위로만 작성되어 세부 규칙을 확인하기 어려울 수 있다.

Policy와 Domain Service의 관계

Policy는 넓게 보면 Domain Service의 한 형태로 볼 수 있다. 특정 Entity나 Value Object에 자연스럽게 들어가지 않는 도메인 판단을 표현하기 때문이다.

다만 Policy라는 이름은 “정책”이라는 도메인 의미를 더 분명하게 드러낸다. RefundService보다 RefundPolicy가 나을 때가 많다. 전자는 무엇이든 할 수 있는 서비스처럼 보이지만, 후자는 환불 규칙을 판단하는 객체처럼 보인다.

이름은 책임을 제한한다. 책임을 제한하는 이름이 좋은 설계를 만든다.

마치며

조건문은 나쁜 것이 아니다. 모든 도메인 규칙은 결국 어떤 조건과 판단을 포함한다. 문제는 중요한 조건이 이름 없이 흩어질 때다.

Specification과 Policy는 조건과 정책에 이름을 붙인다. “이 사용자가 무료 체험 대상인가?”, “이 구독은 갱신 가능한가?”, “이 환불 요청은 얼마를 돌려줘야 하는가?” 같은 질문을 코드 안에 명시적으로 남긴다.

하지만 모든 조건을 객체로 만들 필요는 없다. 작은 조건은 Entity나 Value Object 안에 두는 것이 더 낫다. Specification과 Policy는 도메인적으로 중요하고, 반복되며, 변경 가능성이 높은 판단에 사용해야 한다.

DDD에서 좋은 모델은 명사만 잘 찾는 것이 아니다. 좋은 모델은 조건과 정책에도 이름을 붙인다. 도메인에서 중요한 판단이 코드 안에서 중요한 개념으로 보일 때, 모델은 더 선명해진다.


페이지 수정
공유하기:

이전 글
[DDD Intro #19] Layered Architecture: 도메인 계층을 보호하기
다음 글
[DDD Intro #17] Factory: 생성에도 도메인 지식이 있다