Anemic Domain model 을 탈출하자! 내 프로젝트 도메인 모델에 수혈하기

신원규·2024년 11월 24일
2
post-thumbnail
  • 예상독자: 초중급 개발자.
  • 목표: 타성적으로 나눈 계층구조에서, rich domain model 이 무엇인지 이해할 수 있고,
    이를 어떻게 유지하고 발전시킬지 이해할 수 있다.

혹시 Anemic Domain model 이라는 말을 들어본 적 있으신가요?
이를 들어보지 못했다면, 지금 여러분이 작성하고 계시는 프로젝트의 소스코드는 빈혈에 걸려 시름시름 앓아가고 있을 수도 있답니다!

저와 함께 오늘 아키텍처의 도메인 모델중, 어떤 도메인 모델이 Rich domain model, 즉 풍부한 도메인 모델이고 어떤 모델이 Anemic domain model 인지 정리해보시죠.

빈약한 도메인 모델 (Anemic domain model)

이 단어는, 마틴 파울러가 2003 년 동명의 블로그 포스트를 작성하며 처음 소프트웨어 공학 업계에 이야기되기 시작하였습니다. 포스트 링크

이는 적합한 도메인 모델에 대한 충분한 경험과 이해가 부족한 상황에서,
피상적으로 계층구조의 아키택처를 적용할 때, 흔히 발생 할 수 있는 대표적인 안티패턴이라고 설명됩니다.

도메인 모델이 결국 여러 클래스의 선언문정도의 역할만 수행하게 되고, 다른 모든 비즈니스 로직은 도메인 모델 외부로 전파되어 수행되는거죠.

예를 들자면, 커머스 서비스에서는 필연적으로 주문 상태를 표현하기 위한 도메인 클래스가 있을겁니다.

class Payment {
  final PayemtnState state;
  final Decimal amount;
  final Decimal discountAmount;
  final List<Coupons> appliedCoupons;
}

class Order {
  final String uid;
  final OrderState state;
  final List<Product> products;
  final List<PaymentInfo> paymentInfos;
  ...
}

이 때, 주문 정보를 바탕으로 상품목록을 기반해
적절한 쿠폰을 하나 추천해야한다는 요구사항이 들어온다고 가정하면, 다음과 같은 코드를 작성 할 겁니다.

/// 쿠폰 목록과 주문의 상품목록을 모두 순회하며, 가장 할인률이 큰 쿠폰을 찾아냅니다.
final Coupon? properCoupon = User.coupons.fold(
		(final (Coupon, Deicimal)? prev, final Coupon coupon) {
      final Decimal couldBeBestDiscount =  Order.products.fold(
      	(final Decimal pre, final Product curr) {
          if (coupon.canUse(curr)) {
            return max(pre, coupon.apply(curr));
          }
          return pre;
        }
      , Deicmal.zero);
      
      if ((prev?.$2?? Deicmal.zero) < couldBeBestDiscount ) {
        return (coupon, couldBeBestDiscount);
      }
    }, null).$1;

문제는 이 코드가 서비스 계층의 의 viewModel 혹은 model 에 작성되기 쉽다는 겁니다.

코드에는 생략되었지만
쿠폰 목록을 조회해 불러온 뒤, 이를 주문 정보와 조합해 가장 적절한 쿠폰으로 찾아내고, 이를 UI로 표시하는 코드가 추가로 작성되어야 할 것이니까요.

관성적으로 코드를 작성할 땐, 이러한 성질의 코드는 서비스 계층에 몰리기 쉽습니다.

위의 주문과 쿠폰 목록에 대한 상호작용과 같은 도메인 로직 (혹은 비즈니스 로직, Computation logic, ..etc 어떻게 부르던)이 모두 도메인 계층에서 서비스 계층으로 추출되어 버린다면,

도메인 계층에 남은 코드는 단순한 클래스 선언문 정도만 남게 될 입니다.

이러한 상태의 도메인 모델을 바로 빈약한 도메인 모델 (Anemic Domain Model, ADM) 이라 부릅니다.

ADM의 단점

프로젝트의 구조가 빈약한 도메인 모델을 바탕으로 하고 있다면, 발생하게되는 문제점에 대해 설명하겠습니다.

동일한 동작의 코드가 복제되어 관리되어야 합니다.

위의 적절한 쿠폰을 찾는 용례가 확장되어, 여러 화면에서 보여주어야하거나,
아니면 버튼만 누르면 결제가 바로 완료되고, 이후에 어떤 쿠폰을 적용해 할인 혜택을 받았는지 설명해주는 UX가 신설된다면 어떨까요?

이 모든 케이스가 병행되어 모두 동작해야하는데, 버전에 따라 쿠폰의 추천 로직이 남은 일자와 할인량을 모두 고려하는 등으로 변경된다면, 여러분은 이러한 변경이 있을 때 마다 프로젝트에서 내가 모든 유즈케이스를 다 찾아서 수정한게 맞는지 기도하고 있게 될 겁니다.

뷰의 맥락과 강하게 결부되었다면, 테스트 용이성이 보장되기 어렵습니다.

도메인 로직으로 풀이되어야 할 코드가 view 단에 작성되어서, BuildContext 와 엮이게된다면,
이 로직을 테스트하기 위해선 unit test로 테스트를 진행 할 수 없습니다.
적어도 Headless 형태로 Flutter app 을 띄워 실제로 앱을 구동시켜 테스트하는 통합 테스트의 형태로 기능을 증명해야합니다.

혹은, E2E 테스트를 작성해야지만 동작의 검증을 할 수 있는 상황도 발생할 수 있습니다.

테스트 코드는 Uint -> 통합 -> E2E 로 진행할 수록 테스트코드의 작성과 유지보수에 보다 많은 비용을 투자해야하는 경향이 있습니다.

소스코드의 구조 설계를 통해서 훨씬 유지비용을 절감 할 수 있는데, 비효율적으로 많은 노력과 시간을 들어 테스트코드를 유지보수 하는 그 상황 자체도 그리 좋지 못하고,

이 비용을 감당할 수 없어서 결국 관련 기능의 테스트 코드가 작성되지 않거나 삭제된다면, 이는 앱의 안정성을 보장 할 수 없어지므로 가장 최악의 결과가 발생 할 수 있습니다.

OOP 의 장점을 잘 활용하지 못하는 형식의 코드입니다.

Class 가 C 의 구조체와 다른점중 하나는, method 를 통해 데이터 뿐만이 아닌, 동작을 정의할 수 있다는 점이겠죠. (C 의 구조체도 function pointer 를 사용해 일부분 가능하긴 합니다.)

빈약한 도메인 모델에서는 OOP 의 이러한 특장점을 잘 살리지 못하고, OOP이전의 절차적 기반의 코드 스타일과 비슷한 특징을 가지게 된다고 볼 수 있습니다.

결론

서비스 계층, 혹은 데이터계층에서 도메인 모델을 이용해 계산, 변환, 판별을 내리는 로직이 반복적으로 보인다면, 이를 도메인계층으로 이동시키고, 테스트를 만들어 서비스의 사양으로 병합합시다.

profile
생존형 개발자. 어디에 던져져도 살아 남는것이 목표입니다.

0개의 댓글