대규모 애플리케이션에서 풍부한 도메인 모델 vs 빈약한 도메인 모델: 실제 성능 분석과 트레이드오프

1. 문제 상황

대규모 전자상거래 플랫폼의 주문 처리 시스템 개발 과정에서, 풍부한 도메인 모델과 빈약한 도메인 모델의 트레이드오프가 발생했습니다.

대량 주문(시간당 100만 건 이상)을 처리해야 하는 상황에서, 초기 설계의 한계와 성능 병목 현상이 드러났습니다.

2. 풍부한 도메인 모델 설계

다음은 초기 설계에 사용된 풍부한 도메인 모델의 예입니다.

public class Order {
    private List<OrderItem> items;
    private OrderStatus status;
    private Money totalAmount;

    public void addItem(OrderItem item) {
        validateItem(item);
        this.items.add(item);
        recalculateTotal();
    }

    public void cancel() {
        validateCancellable();
        this.status = OrderStatus.CANCELLED;
        refundPayment();
        notifyCustomer();
    }

    private void validateCancellable() {
        if (status == OrderStatus.SHIPPED) {
            throw new OrderCannotBeCancelledException();
        }
    }

    private void recalculateTotal() {
        this.totalAmount = items.stream()
            .map(OrderItem::getPrice)
            .reduce(Money.ZERO, Money::add);
    }
}

주요 특징

  • 풍부한 비즈니스 로직: 도메인 객체 내부에 핵심 로직이 포함되어 있습니다.
  • 모델 재사용성 증가: 일관된 비즈니스 로직 처리로 코드 중복을 최소화합니다.

3. 발생한 문제점

성능 병목

  • 객체 생성 비용 증가: 대량 주문 처리 시, 도메인 객체의 복잡한 초기화로 인해 GC(가비지 컬렉션) 부하 발생.

  • 메모리 사용량 증가: 모든 비즈니스 로직이 메모리 상에 유지됨으로써 시스템 리소스 소모 증가.

확장성 문제

  • 도메인 비대화: 새로운 요구사항 추가 시, 도메인 객체가 지나치게 복잡해짐.
  • 테스트 복잡도 증가: 도메인 모델에 종속된 비즈니스 로직의 테스트가 어려워짐.

성능 측정 데이터

4. 빈약한 도메인 모델 전환

다음은 빈약한 도메인 모델로 전환한 코드 예입니다.

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;

    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId);
        if (order.getStatus() == OrderStatus.SHIPPED) {
            throw new OrderCannotBeCancelledException();
        }

        order.setStatus(OrderStatus.CANCELLED);
        paymentService.refund(order.getPaymentId());
        notificationService.notifyCustomer(order.getCustomerId());
        orderRepository.save(order);
    }
}

개선된 성능 측정 데이터

5. 하이브리드 접근법

도메인 모델과 서비스 레이어의 장점을 모두 활용하기 위해 하이브리드 접근법을 도입했습니다.

설계 코드

public class Order {
    // 핵심 비즈니스 로직만 유지
    public void addItem(OrderItem item) {
        validateItem(item);
        this.items.add(item);
    }

    @Getter
    private OrderStatus status;
}

@Service
public class OrderProcessingService {
    public void processLargeOrderBatch(List<Order> orders) {
        // 대량 처리 최적화 로직
        var orderGroups = orders.stream()
            .collect(Collectors.groupingBy(Order::getStatus));

        // 병렬 처리
        orderGroups.forEach((status, orderList) -> {
            CompletableFuture.runAsync(() -> processOrderGroup(orderList));
        });
    }
}

성능 개선 데이터

6. 성능 비교와 분석 결과

성능 비교 그래프

아래 그래프는 모델별 성능 데이터를 시각화한 것입니다.

7. 실제 적용 사례와 교훈

실제 적용 사례

  • 주문 처리 시스템: 하이브리드 접근법 도입 후 초당 처리 주문이 50% 증가하고 메모리 사용량이 25% 감소했습니다.

교훈

1. 유연한 선택의 중요성

  • 단순 CRUD 작업: 빈약한 도메인 모델.
  • 복잡한 비즈니스 로직: 풍부한 도메인 모델.
  • 대량 처리: 하이브리드 접근법.

2. 성능 테스트와 모니터링의 필요성

  • 초기 설계 단계부터 성능 요구사항을 지속적으로 점검해야 합니다.

1. 아키텍처 다이어그램

2. 성능 측정 방법론

대규모 애플리케이션에서 성능 테스트는 시스템의 안정성과 확장성을 보장하기 위해 필수적입니다.

1. 성능 테스트의 목적 정의

성능 테스트를 수행하기 전에, 명확한 목표를 정의해야 합니다.

  • 초당 처리량: 초당 몇 건의 요청을 처리할 수 있는가?
  • 응답 시간: 평균 응답 시간과 최악의 응답 시간은 얼마인가?
  • 리소스 사용량: CPU, 메모리, 네트워크 사용량은 적정한가?
  • 확장성: 부하가 증가할 때 성능이 선형적으로 증가하는가?
2. 테스트 환경 설정

성능 테스트는 실제 운영 환경과 최대한 비슷한 환경에서 진행해야 합니다.

  • 테스트 서버 구성: 운영 환경과 동일한 CPU, 메모리, 네트워크 대역폭을 확보.
  • 데이터 준비: 실제 운영 데이터를 기반으로 테스트 데이터를 생성.
  • 부하 분산: 여러 지역에서 부하를 생성하는 시뮬레이션.

3. 성능 테스트 도구

다양한 성능 테스트 도구를 활용할 수 있습니다.

4. 성능 테스트 시나리오 설계

테스트 시나리오는 실제 사용자 행동을 기반으로 설계합니다.

1. 대량 주문 처리 테스트

  • 초당 1000건의 주문 생성 요청.
  • 5분 동안 지속적인 부하 생성.

2. 트래픽 피크 시나리오

  • 평소 트래픽의 3배를 초당 10초간 생성.
  • 트래픽 급증 시 성능 확인.

3. 장시간 안정성 테스트

  • 24시간 동안 지속적인 요청을 통해 메모리 누수나 리소스 문제 식별

5. 성능 테스트 스크립트 예제

아래는 JMeter와 k6를 활용한 예제입니다.

1) JMeter 스크립트

<TestPlan>
    <ThreadGroup>
        <stringProp name="ThreadGroup.num_threads">100</stringProp>
        <stringProp name="ThreadGroup.ramp_time">10</stringProp>
        <stringProp name="ThreadGroup.duration">300</stringProp>
        <HTTPsamplerProxy>
            <stringProp name="HTTPSampler.domain">example.com</stringProp>
            <stringProp name="HTTPSampler.path">/api/orders</stringProp>
            <stringProp name="HTTPSampler.method">POST</stringProp>
            <elementProp name="HTTPsampler.arguments">
                <collectionProp>
                    <elementProp>
                        <name>body</name>
                        <stringProp name="Argument.value">{ "item": "product1", "quantity": 10 }</stringProp>
                    </elementProp>
                </collectionProp>
            </elementProp>
        </HTTPsamplerProxy>
    </ThreadGroup>
</TestPlan>

2) k6 스크립트

import http from 'k6/http';
import { sleep } from 'k6';

export let options = {
  stages: [
    { duration: '10s', target: 100 }, // 10초 동안 100명 동시 접속
    { duration: '5m', target: 1000 }, // 5분 동안 부하 지속
    { duration: '1m', target: 0 }, // 부하 감소
  ],
};

export default function () {
  const url = 'https://example.com/api/orders';
  const payload = JSON.stringify({
    item: 'product1',
    quantity: 10,
  });

  const params = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  http.post(url, payload, params);
  sleep(1);
}

6. 테스트 결과 분석

성능 테스트 후에는 결과 데이터를 분석해야 합니다.

  • Throughput: 초당 처리량이 목표 수준을 만족하는가?
  • Latency: 평균/최대 응답 시간이 목표 수준을 넘지 않는가?
  • 에러율: 요청 중 실패율은 1% 이하인가?

결과 보고서 예시

7. 추가 도구와 통합

성능 테스트는 CI/CD 파이프라인과 통합하여 자동화할 수 있습니다.

  • Jenkins, GitHub Actions와 연계하여 배포 전 성능 테스트 자동 실행.
  • Grafana와 Prometheus로 테스트 결과 실시간 모니터링.
profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글