티스토리 뷰

반응형

0. 이 글을 쓰는 이유

“그래서 실제로 언제 쓰게 되느냐”
“안 쓰면 뭐가 얼마나 불편해지느냐”

이 글은
DDD를 이미 알고 있는 사람이 아니라,
적용해야 할지 고민 중인 개발자를 위한 글이다.


1. 대부분의 서비스는 DDD 없이 시작한다 (나도 그랬다)

나도 처음부터 DDD를 염두에 두고 개발하지 않았다.

초기 구조는 아주 흔했다.

Controller → Service → Repository
  • CRUD 위주
  • Service에 조건 분기 몇 개
  • 빠른 개발, 빠른 검증

이 단계에서는
DDD를 안 쓰는 게 정답에 가깝다고 생각한다.

솔직히 말하면,
이 시점에 DDD를 얹으면 이런 말이 나온다.

“이거 너무 과한 거 아니에요?”

맞는 말이다.


2. “Service가 망가지고 있다”는 느낌이 처음 든 순간

문제는 서비스가 커지면서 생겼다.

처음엔 단순히 Service 파일이 커졌다.
200줄 → 400줄 → 600줄.

그런데 어느 순간부터 성격이 바뀌었다.

  • 같은 정책 로직이 여러 Service에 복붙된다
  • 조건 하나 바꾸려면 파일 여러 개를 열어본다
  • 기능 추가보다 기존 로직 수정이 더 무섭다

그때 실제로 있었던 코드가 이런 형태였다.

(Before) 흔한 Service 비대화 코드

@Transactional
public PolicyResult getPolicyForLearning(Long userId) {

    User user = userRepository.findById(userId).orElseThrow();

    Subscription sub = subscriptionRepository.findActiveByUserId(userId);
    ParentSetting setting = parentSettingRepository.findByUserId(userId);

    boolean isPremium = sub != null && sub.getExpireAt().isAfter(Instant.now());
    int childAge = user.getChildAge();
    boolean isBlocked =
        setting != null && setting.getBlockedCategories().contains("GAME");

    if (childAge < 7) {
        return PolicyResult.denied("age_limit");
    }
    if (isBlocked) {
        return PolicyResult.denied("blocked_category");
    }
    if (!isPremium) {
        return PolicyResult.limited("not_premium");
    }

    return PolicyResult.allowed();
}

이 코드 자체가 “나쁜 코드”는 아니다.
문제는 이 정책 로직이 여기저기 흩어지기 시작했다는 것이다.


3. 그때 깨달은 문제의 본질

처음엔 이렇게 생각했다.

  • “Service를 더 쪼개야 하나?”
  • “클래스를 나누면 해결될까?”

그런데 쪼개도 해결이 안 됐다.

왜냐하면,
문제는 크기가 아니라 책임이었기 때문이다.

  • 이 정책 로직의 주인은 누구인가?
  • 이 규칙을 고치면 어디까지 영향이 가는가?

코드만 봐서는 답이 없었다.

이때 처음으로 DDD를
아키텍처가 아니라 ‘규칙 정리 방법’으로 보기 시작했다.


4. 내가 적용한 DDD는 “라이트 DDD”였다

여기서 중요한 포인트가 있다.

DDD를 적용했다고 해서
구조가 갑자기 복잡해지지는 않았다.

내가 한 건 딱 하나였다.

👉 “규칙을 Service에서 도메인 객체로 옮기기”


5. (After) 규칙의 주인을 만든다 — Policy 도메인

Domain: 규칙의 주인

public final class UserPolicy {

    private final SubscriptionStatus subscriptionStatus;
    private final ChildAge childAge;
    private final ParentRestrictions restrictions;

    public UserPolicy(SubscriptionStatus subscriptionStatus,
                      ChildAge childAge,
                      ParentRestrictions restrictions) {
        this.subscriptionStatus = subscriptionStatus;
        this.childAge = childAge;
        this.restrictions = restrictions;
    }

    public PolicyResult evaluateForLearning() {
        if (childAge.isUnder(7)) {
            return PolicyResult.denied("age_limit");
        }
        if (restrictions.isCategoryBlocked("GAME")) {
            return PolicyResult.denied("blocked_category");
        }
        if (!subscriptionStatus.isPremiumActive()) {
            return PolicyResult.limited("not_premium");
        }
        return PolicyResult.allowed();
    }
}

Value Object: 규칙을 읽기 쉽게 만든다

public final class ChildAge {
    private final int value;

    public ChildAge(int value) {
        this.value = value;
    }

    public boolean isUnder(int limit) {
        return value < limit;
    }
}
public final class SubscriptionStatus {
    private final Instant expireAt;

    public SubscriptionStatus(Instant expireAt) {
        this.expireAt = expireAt;
    }

    public boolean isPremiumActive() {
        return expireAt != null && expireAt.isAfter(Instant.now());
    }
}

여기서 핵심은
조건문이 줄었냐가 아니다.

👉 “정책 로직이 UserPolicy로 고정됐다”는 점이다.


6. Service는 이렇게 얇아졌다

@Transactional(readOnly = true)
public PolicyResult getPolicyForLearning(Long userId) {

    User user = userRepository.findById(userId).orElseThrow();
    Subscription sub = subscriptionRepository.findActiveByUserId(userId);
    ParentSetting setting = parentSettingRepository.findByUserId(userId);

    UserPolicy policy = new UserPolicy(
        new SubscriptionStatus(sub == null ? null : sub.getExpireAt()),
        new ChildAge(user.getChildAge()),
        new ParentRestrictions(
            setting == null ? null : setting.getBlockedCategories()
        )
    );

    return policy.evaluateForLearning();
}

Service는 이제:

  • 데이터 조합
  • 도메인 호출
  • 흐름 제어

만 담당한다.


7. 이때 처음으로 체감한 DDD의 효과

이 구조로 바꾸고 나서
팀에서 이런 말이 나왔다.

“아, 정책은 여기만 보면 되는구나.”

그 한마디로
DDD 적용의 목적은 달성됐다고 느꼈다.

 


 

8. DDD가 오히려 독이 되었던 경험

DDD를 한 번 적용해보고 나면,
이런 생각이 들기 쉽다.

“이 도메인도 중요해 보이는데,
처음부터 DDD 구조로 가면 더 깔끔하지 않을까?”

나도 그랬다.

한 번은 비교적 단순한 영역에
미리 도메인을 만들어두고 시작했다.

  • Entity
  • Policy
  • Validator
  • Domain Service

겉보기엔 그럴듯했다.
그런데 시간이 지나면서 문제가 생겼다.

  • 실제 규칙은 거의 늘지 않았고
  • 대부분 CRUD였고
  • 도메인 객체는 껍데기가 됐다

그때 팀에서 이런 말이 나왔다.

“이거… 그냥 Service에 있으면 되는 거 아닌가요?”

맞는 말이었다.

이 경험 이후로 생긴 기준은 하나다.

DDD는 ‘중요해 보이는 영역’이 아니라,
‘실제로 복잡해진 영역’에만 써야 한다.


9. DDD는 왜 항상 “중간에” 들어오는가

돌아보면,
DDD는 항상 리팩토링의 결과로 등장했다.

한 번도 이런 적은 없었다.

“이번 프로젝트는 처음부터 DDD로 갑시다.”

대신 항상 이런 흐름이었다.

  1. 빠르게 만들고
  2. 로직이 쌓이고
  3. 규칙이 얽히고
  4. Service가 버거워지고
  5. 구조를 다시 보게 된다

그리고 그때서야
“도메인”이라는 개념이
코드로 드러나기 시작한다.

재밌는 건,
이 과정을 겪기 전엔 DDD가 잘 와닿지 않았다는 거다.

책으로 볼 땐 과해 보였던 구조가,
막상 겪고 나면 “아, 이걸 말하는 거였구나”가 된다.


10. DDD를 했다고 말할 수 있는 현실적인 기준

누군가 나에게 묻는다.

“이 정도면 DDD 했다고 말해도 되나요?”

내 대답은 늘 비슷하다.

아래 질문에 “예”라고 답할 수 있다면 충분하다

  • 규칙이 한 곳에 모여 있는가?
  • 규칙의 이름이 코드에 드러나는가?
  • 정책 수정 시 고칠 위치가 명확한가?
  • 팀원이 “이 로직 어디 보면 되죠?”라고 묻지 않는가?

이걸 만족하면,

  • 패키지가 단순해도
  • 레이어가 많지 않아도

DDD를 적용했다고 말해도 무리 없다고 본다.


11. DDD의 진짜 효과는 “테스트”에서 나온다

DDD를 적용하고 나서
가장 체감이 컸던 건 테스트였다.

이전에는:

  • Service 테스트 = DB 세팅
  • 조건 하나 테스트하려고
    • 데이터 여러 개 생성

DDD 이후에는:

  • 도메인 객체 하나로
  • 규칙 테스트 가능

실제로 생긴 도메인 테스트

class UserPolicyTest {

    @Test
    void under_age_should_be_denied() {
        UserPolicy policy = new UserPolicy(
            new SubscriptionStatus(Instant.now().plusSeconds(3600)),
            new ChildAge(6),
            new ParentRestrictions(Set.of())
        );

        PolicyResult result = policy.evaluateForLearning();

        assertThat(result.isDenied()).isTrue();
        assertThat(result.getReason()).isEqualTo("age_limit");
    }

    @Test
    void blocked_category_should_be_denied_even_if_premium() {
        UserPolicy policy = new UserPolicy(
            new SubscriptionStatus(Instant.now().plusSeconds(3600)),
            new ChildAge(10),
            new ParentRestrictions(Set.of("GAME"))
        );

        PolicyResult result = policy.evaluateForLearning();

        assertThat(result.isDenied()).isTrue();
        assertThat(result.getReason()).isEqualTo("blocked_category");
    }
}

이 테스트가 생긴 순간,
팀에서 이런 말이 나왔다.

“아, 이제 정책 고치는 게 무섭지 않네요.”


12. DDD는 ‘코드를 예쁘게 만드는 기술’이 아니다

DDD를 해보면서 가장 크게 느낀 건 이거다.

DDD는 구조를 멋있게 만드는 게 아니라,
변경을 덜 무섭게 만드는 선택이다.

  • 규칙이 바뀔 때
  • 예외가 추가될 때
  • 정책이 복잡해질 때

DDD가 있으면,
“어디를 고쳐야 하는지”가 보인다.


반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함