객체 생성은 종종 단순한 기술 문제처럼 여겨진다. 생성자를 호출하고, 필요한 값을 넣고, 필드를 채우면 끝이라고 생각하기 쉽다. 하지만 도메인 모델에서는 생성도 중요한 행위일 수 있다.
어떤 객체는 아무 값으로나 만들어지면 안 된다. 생성되는 순간부터 유효해야 하고, 도메인 규칙을 만족해야 하며, 필요한 구성요소가 일관된 상태로 함께 만들어져야 한다.
Factory는 이런 생성 책임을 다루기 위한 패턴이다. Factory는 단순히 new를 감추는 도구가 아니다. 유효한 도메인 객체를 만드는 책임을 명시적으로 표현하는 방법이다.
생성자만으로 충분할 때
모든 객체에 Factory가 필요한 것은 아니다. 단순한 Value Object라면 생성자나 정적 팩토리 메서드로 충분할 수 있다.
Money fee = new Money(9900, Currency.KRW);
EmailAddress email = EmailAddress.of("user@example.com");
이 정도 생성에는 복잡한 절차가 없다. 객체 자신이 유효성만 확인하면 된다.
Entity도 단순한 경우에는 정적 팩토리 메서드로 충분하다.
Subscription subscription = Subscription.startTrial(memberId, trialPeriod, now);
이 메서드가 구독 생성에 필요한 규칙을 충분히 표현하고, 외부 협력이 많지 않다면 별도 Factory를 만들 필요는 없다.
Factory는 생성 로직이 복잡해지거나, 생성 책임을 Entity 안에 넣기 부담스러울 때 고려하면 된다.
생성에도 도메인 규칙이 있다
구독 서비스를 생각해보자. 새 구독을 시작하는 일에는 여러 규칙이 있을 수 있다.
무료 체험은 한 계정당 한 번만 가능하다.
기업 요금제는 최소 좌석 수를 만족해야 한다.
연간 구독은 할인 정책이 적용된다.
프로모션 코드는 특정 요금제에만 사용할 수 있다.
결제 완료 전에는 유료 구독을 활성화할 수 없다.
이 규칙들을 단순히 Application Service에서 값으로 조립하면 생성 과정이 흩어진다.
Subscription subscription = new Subscription();
subscription.setMemberId(memberId);
subscription.setPlanId(planId);
subscription.setStatus(ACTIVE);
subscription.setPeriod(period);
subscription.setPrice(price);
이 코드는 생성이 아니라 조립에 가깝다. 그리고 유효하지 않은 구독도 쉽게 만들어질 수 있다.
도메인에서 중요한 객체라면 생성 순간부터 유효해야 한다.
Subscription subscription = subscriptionFactory.createPaidSubscription(
member,
plan,
paymentResult,
promotionCode,
now
);
이 코드에서는 “유료 구독을 생성한다”는 도메인 행위가 드러난다. 생성 규칙도 Factory 안에 모을 수 있다.
Entity 내부의 정적 팩토리 메서드
생성 로직이 객체 자신의 책임으로 자연스럽다면 정적 팩토리 메서드가 좋은 선택이다.
public class Subscription {
public static Subscription startTrial(MemberId memberId, TrialPeriod trialPeriod, Instant now) {
return new Subscription(
SubscriptionId.newId(),
memberId,
PlanId.trial(),
SubscriptionStatus.TRIAL,
trialPeriod.toSubscriptionPeriod(now)
);
}
}
이 방식의 장점은 생성 의도가 명확하다는 것이다.
Subscription trial = Subscription.startTrial(memberId, trialPeriod, now);
생성자가 여러 개의 파라미터를 받는 것보다 훨씬 읽기 쉽다.
new Subscription(id, memberId, planId, TRIAL, startDate, endDate);
정적 팩토리 메서드는 생성 시나리오가 몇 가지로 명확하고, 외부 정책이나 Repository 조회가 필요하지 않을 때 잘 맞는다.
별도 Factory가 필요한 경우
별도 Factory는 생성 로직이 객체 하나에 넣기에는 너무 복잡하거나, 여러 도메인 객체와 정책이 필요할 때 유용하다.
예를 들어 유료 구독 생성에는 다음 정보가 필요하다고 해보자.
회원 정보
요금제 정보
프로모션 정책
결제 결과
무료 체험 이력
좌석 수 정책
이 모든 것을 Subscription의 정적 메서드 안에 넣으면 Subscription이 너무 많은 것을 알게 된다. 이때 SubscriptionFactory나 더 구체적인 이름의 Factory를 만들 수 있다.
public class SubscriptionFactory {
public Subscription createPaidSubscription(
Member member,
Plan plan,
PaymentResult paymentResult,
Promotion promotion,
Instant now
) {
if (!paymentResult.isSucceeded()) {
throw new CannotStartSubscriptionException("결제가 완료되지 않았습니다.");
}
Money price = promotion.applyTo(plan.price());
SubscriptionPeriod period = plan.billingCycle().periodFrom(now);
return Subscription.paid(member.id(), plan.id(), price, period, now);
}
}
여기서 Factory는 유효한 구독을 만드는 책임을 가진다. 단순한 객체 조립이 아니라 도메인 규칙을 만족하는 생성이다.
Factory와 Application Service의 경계
Application Service에서도 객체를 만들 수 있다. 하지만 생성 규칙이 복잡해지면 Application Service가 너무 많은 도메인 판단을 하게 된다.
if (!paymentResult.isSucceeded()) {
throw new IllegalStateException();
}
if (plan.isEnterprise() && seatCount < plan.minimumSeatCount()) {
throw new IllegalStateException();
}
Subscription subscription = new Subscription(...);
이런 코드가 Application Service에 쌓이면 생성 규칙이 유스케이스 흐름과 섞인다.
Application Service는 필요한 객체를 조회하고 Factory에 생성을 요청하는 편이 더 명확하다.
Subscription subscription = subscriptionFactory.create(command, member, plan, paymentResult, now);
subscriptionRepository.save(subscription);
이 구조에서는 Application Service가 흐름을 조율하고, Factory가 생성 규칙을 담당한다.
Factory는 불완전한 객체를 숨긴다
객체를 단계적으로 조립하다 보면 잠시 동안 불완전한 상태가 생길 수 있다.
Subscription subscription = new Subscription();
subscription.setMemberId(memberId);
subscription.setPlanId(planId);
subscription.setStatus(ACTIVE);
중간 단계의 객체는 유효하지 않다. 필수 값이 빠졌거나, 불변식을 만족하지 않을 수 있다. 이런 객체가 외부로 노출되면 버그가 생기기 쉽다.
Factory는 이 조립 과정을 내부에 숨기고, 완성된 유효한 객체만 반환한다.
Subscription subscription = subscriptionFactory.createTrial(member, now);
호출자는 생성 과정의 세부사항을 알 필요가 없다. 반환된 객체가 유효하다는 사실만 믿으면 된다.
이것이 Factory의 중요한 가치다. 복잡한 생성 과정을 캡슐화하고, 유효한 도메인 객체만 세상에 나오게 한다.
이름은 생성 의도를 드러내야 한다
Factory 메서드의 이름은 단순히 create보다 구체적인 편이 좋다. 도메인에서는 같은 객체도 여러 방식으로 생성될 수 있기 때문이다.
createTrialSubscription(...)
createPaidSubscription(...)
createEnterpriseSubscription(...)
restoreFromLegacy(...)
각 메서드는 다른 생성 시나리오를 나타낸다. 무료 체험 구독과 유료 구독은 같은 Subscription일 수 있지만, 생성 규칙은 다를 수 있다.
이름이 구체적이면 생성 규칙도 더 잘 보인다. 반대로 모든 생성을 create 하나로 처리하면 조건문이 많아지고, Factory 자체가 복잡해질 수 있다.
마치며
Factory는 객체 생성을 감추기 위한 기계적인 패턴이 아니다. 도메인에서 유효한 객체를 만드는 책임을 명확히 표현하는 방법이다.
생성이 단순하다면 생성자나 정적 팩토리 메서드로 충분하다. 하지만 생성에 여러 도메인 규칙이 얽혀 있고, 여러 객체나 정책을 함께 봐야 한다면 별도 Factory가 도움이 된다.
중요한 것은 객체가 태어나는 순간부터 유효해야 한다는 점이다. 잘못된 상태의 객체를 만든 뒤 나중에 고치는 방식은 도메인 모델을 불안정하게 만든다.
DDD에서 생성은 단순 조립이 아니다. 생성에도 도메인 지식이 있다. Factory는 그 지식을 코드 안에서 잃어버리지 않게 하는 도구다.