끝없는 계층구조 지옥에서 도메인 단위 모듈 마이그레이션 여행기

tony·2025년 6월 1일
0

판단여행기

목록 보기
11/13

Background


하나의 프로젝트가 장기화 되면 될수록 유지보수는 커녕 점점 더러워지는 코드베이스에 신물이 나기 시작했다.

이를 해결하기 위해서는 항상 외부 요인을 찾곤했었는데 — EDA 라던지, 모듈구조 등등 —

진정한 해결책은 그게 아닌 내부의 복합적인 문제였다.

어떤 문제들이 있는지 살펴보고, 어떻게 해결할 수 있는지 정리해보고자 한다.

무엇이 문제인가?


1. 흩어진 도메인 로직

  • 문제: 도메인 로직이 v1·v2로 나뉘어 여기저기 퍼져 있음
  • 원인: 도메인 클래스와 DAO 클래스를 똑같이 사용해서 구분이 안 됨
  • 해결:
    • 도메인 모델과 DAO를 분리

2. 낮은 인프라 유연성

  • 문제: 외부 인프라(예: DB, 메시지 큐 등)를 바꾸기 어려움
  • 원인: 인프라 코드가 도메인 코드에 단단히 붙어 있음
  • 해결:
    1. 도메인과 인프라 계층 분리
    2. 도메인에서 인프라 상세를 모르게 설계
    3. 인프라 인터페이스와 DTO 정의
    4. 유스케이스(Use Case) 레이어가 인터페이스를 통해 도메인 오브젝트 생성·조회

3. 정책 수정에 따른 코드 수정

  • 문제: 기획(정책) 바뀔 때마다 도메인 로직이 여기저기 흩어져 있어 손이 많이 감
  • 원인: 서비스 레이어가 실제 도메인 로직을 직접 처리
  • 해결:
    • 서비스 레이어에서 도메인 로직 삭제
    • 인프라 전용 레이어에서만 외부 시스템 호출 처리

4. 로직 구현 위치 판단 어려움

  • 문제: 어떤 도메인에 어떤 로직을 넣어야 할지 모호
  • 원인: 도메인 경계가 불명확
  • 해결:
    • 모듈 단위로 도메인 경계 정의 (Modular Monolith)
    • 모듈 간 의존성 명확히 관리

5. 지켜지지 않는 컨벤션

  • 문제: 코드 컨벤션 문서를 만들어도 실제 코드가 다르게 작성됨
  • 원인:
    • 일정이 바빠 코드 품질 검토를 못 함
    • 기획 변경이 잦아 코드리뷰 효율이 떨어짐
  • 해결:
    1. 자체 호스팅 정적 분석 도구 도입
    2. 외부 IaaS 기반 코드 검사 도구 사용
    3. CI/CD 파이프라인에 코드 분석 자체 구현하여 추가

6. 확장하기 어려운 도메인 코드

  • 문제:
    • Facade·Service·Repo 계층이 많아지고, Read·Command 분리 후에도 겹치는 로직 재사용이 어려움
    • 도메인 수가 늘어날수록 응용 서비스가 폭발, 코드 복잡도 상승
  • 원인: 빈약한 도메인 모델에 과도한 응용 레이어 집중
  • 해결:
    1. 도메인 모델 풍부화 (행위와 상태를 함께 표현)
    2. 응용 레이어는 오직 흐름(Use Case)만 담당
    3. 도메인 내부에서 필요한 정책·검증은 도메인 객체에 위임
    4. 공통 필터(차단·삭제 등)는 횡단 관심사(Cross-cutting)를 별도 모듈로 처리

해결방안 💡


논리적 구조 (방법론)

명령 모듈

페이징 모듈

Presentation Layer

  • 역할

    • HTTP, gRPC, SSE 프로토콜들에 대한 외부 API 처리
  • 규칙

    1. 페이지네이션과 아닌 것을 분리할 것

    2. 액터 별로 분리할 것

Usecase Layer

Infra Layer

  • 역할

    • 데이터 및 외부 분산 시스템 처리
      • Data : MySQL, Redis, Mongo ,,,
      • Cloud : Aws, Azure, ,,,,
    • 각각 시스템에 대한 별도의 DAO Model 처리
  • 규칙

    1. 시스템 별로 DAO 를 선언/활용

    2. 최대한 의미있는 함수를 사용할 것 (특정 조건의 도메인을 가져오는 로직을 짜야하다보니 최대한 의미있는 함수를 활용하는 게 좋음)

Domain Layer

  • 역할

    • 도메인 데이터에 대한 도메인 로직

    • 데이터 조회가 아닌 데이터 변경사항은 모두 해당 컴포넌트를 거쳐야 함

  • 규칙

    1. 도메인 경계 벗어나지 말 것

    2. Functional + 객체지향을 지향

    Service Function 을 인자로 받는 도메인 로직 (예시)

    public PriceV2 getPriceV2(
        ChocoDiscountPolicyServiceV2 chocoDiscountPolicyServiceV2,
        ChocoTruncatePolicyServiceV2 chocoTruncatePolicyServiceV2
    ){
        return this.chocoPremiumV2.getDiscountedPrice(
            this.urgentJobPostingPriceV2,
            chocoDiscountPolicyServiceV2,
            chocoTruncatePolicyServiceV2
        );
    }

    Infra Function 을 인자로 받는 도메인 로직 (예시)

    public static List<ChocoHistoryV2> consumeChoco(
        Supplier<List<ChocoHistoryV2>> accumulatedChocoFetcher,
        Supplier<Long> balanceFetcher,
        Long choco
    ) {
        List<ChocoHistoryV2> accumulatedChoco = accumulatedChocoFetcher.get(); // 사용 전인 적립된 (+) 유효기간 마감 임박 순으로 find
        Long balance = balanceFetcher.get(); // 회원의 초코 총액 계산
    
        // 초코 사용 여부 검증
        if (!isConsumable(accumulatedChoco,choco,balance)){
            throw new CustomException(ErrorCode.LACK_OF_CHOCO);
        }
    
        // 충전 초코 사용
        AtomicLong initialPrice = new AtomicLong(choco);
        accumulatedChoco
            .stream()
            .takeWhile(ch -> initialPrice.get() > 0) // 초코가격이 완전히 사용될 때까지
            .forEach(ch -> ch.updateConsumedChoco(initialPrice)); // 충전초코들을 차감
        return accumulatedChoco;
    }

Further work


💡

위 구조에 대해 규칙을 강제화하는 방안들이다.

선택적인 사항이므로 개선사항으로 간주하였다.

  • 도메인 모임 형성, 도메인 영역 규정
    • Modular Monothlic → 도메인 영역을 침범 시 컴파일 되지 않도록 처리
  • 도메인 모임 간 대화
    • Message Broker 사용 → EDA 를 통해 도메인끼리 몰라도 되게끔 처리
  • 도메인 쿼리는 도메인 안에
    • @QueryDelegate 를 활용하여 QClass 에 위임
      // User Jpa Model
      @Entity
      @QueryEntity
      public class User {
             
          String name;
             
          User manager;
          
          @QueryDelegate(User.class)
      		public static BooleanPath isManagedBy(QUser user, User other){
      		    return user.manager.eq(other);
      		}
      }
      
      // QUser is a Querydsl query type for User
      @Generated("com.querydsl.codegen.DefaultEntitySerializer")
      public class QUser extends EntityPathBase<User> {
      
          private static final long serialVersionUID = -1825801311L;
      
          private static final PathInits INITS = PathInits.DIRECT2;
          
          public static final QUser user = new QUser("user1");
          
      
      		public BooleanPath isManagedBy(QUser other) {
      		    return User.isManagedBy(this, other);
      		}
      		
      		,,,
      }
    • 무조건 필터는 도메인 QClass 함수를 호출하도록 규제
      // ❌
      JPAQuery<T> query = select(select)
                  .from(memberCompanyV2)
                  .where(memberCompanyV2.mcIsShutDown.eq(0))
                  ,,,
      						;
      // ✅
      JPAQuery<T> query = select(select)
                  .from(memberCompanyV2)
                  .where(memberCompanyV2.isShutDown())
                  ,,,
      						;

Show me the code ⌨️


도메인 단위 패키지

https://github.com/vanillacake369/otlp-demo/tree/divide-per-domain

기능 단위 패키지

https://github.com/vanillacake369/otlp-demo/tree/divide-per-usecase

Reference


https://medium.com/@inzuael/anemic-domain-model-vs-rich-domain-model-78752b46098f

https://www.cnblogs.com/aspiration2016/articles/13306649.html

http://querydsl.com/static/querydsl/3.2.2/reference/html/ch03s03.html#:~:text=3.3.4.%C2%A0Delegate%20methods

https://vaadin.com/blog/ddd-part-3-domain-driven-design-and-the-hexagonal-architecture?utm_source=chatgpt.com

https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/?utm_source=chatgpt.com

https://lannex.github.io/blog/2022/ddd-hexagonal-onion-clean-cqrs/

https://github.com/hgraca/explicit-architecture-php/tree/master

https://khalilstemmler.com/articles/typescript-domain-driven-design/aggregate-design-persistence/#:~:text=A%20repository%20will%20save(),of%20the%20complex%20persistence%20code

https://vaadin.com/blog/ddd-part-3-domain-driven-design-and-the-hexagonal-architecture

profile
내 코드로 세상이 더 나은 방향으로 나아갈 수 있기를

0개의 댓글