본문 바로가기
Spring/이론

DDD에서 AbstractAggregateRoot 이해하기

by JuNo_12 2025. 10. 30.

AbstractAggregateRoot

1. AbstractAggregateRoot란?

Spring Data에서 제공하는 도메인 이벤트 발행을 위한 추상 클래스입니다. 애그리거트 루트가 이를 상속받으면 도메인 이벤트를 쉽게 등록하고 자동으로 발행할 수 있습니다.

import org.springframework.data.domain.AbstractAggregateRoot;

public class Order extends AbstractAggregateRoot<Order> {
    public void pay() {
        this.status = PAID;
        registerEvent(new OrderPaidEvent(this.id));
    }
}

2. 핵심 역할

2.1 도메인 이벤트 저장소

public abstract class AbstractAggregateRoot<A> {
    private final List<Object> domainEvents = new ArrayList<>();
    
    protected <T> T registerEvent(T event) {
        this.domainEvents.add(event);
        return event;
    }
}

도메인 모델이 발생시킨 이벤트를 임시로 보관합니다.

2.2 Spring 이벤트 시스템과 자동 연결

// 도메인에서는 이벤트만 등록
order.pay();  // 이벤트 등록됨

// Repository save 시 자동 발행
orderRepository.save(order);  // 이벤트가 자동으로 발행됨

Spring Data JPA가 save() 호출 시 등록된 이벤트를 자동으로 발행합니다.

2.3 트랜잭션 안전성 보장

@Transactional
public void processOrder() {
    order.pay();  // 이벤트 등록만 됨
    // 여기서 예외 발생하면 이벤트 발행 안됨
    
    orderRepository.save(order);  // 커밋 시점에 이벤트 발행
}
```

이벤트 발행이 save()와 함께 트랜잭션 내에서 관리됩니다.

## 3. 동작 흐름
```
1. 도메인 로직 실행
   └─> registerEvent()로 이벤트 등록
   
2. Repository.save() 호출
   └─> Spring Data가 domainEvents 조회
   
3. ApplicationEventPublisher로 이벤트 발행
   └─> 등록된 모든 이벤트 발행
   
4. clearDomainEvents() 호출
   └─> 이벤트 목록 초기화

3. 실전 사용 패턴

단일 이벤트

public class Order extends AbstractAggregateRoot<Order> {
    public void pay() {
        validatePayment();
        this.status = PAID;
        registerEvent(new OrderPaidEvent(this.id));
    }
}

다중 이벤트

public class Order extends AbstractAggregateRoot<Order> {
    public void cancel() {
        validateCancellation();
        this.status = CANCELLED;
        
        registerEvent(new OrderCancelledEvent(this.id));
        registerEvent(new OrderStatusChangedEvent(this.id, CANCELLED));
    }
}

조건부 이벤트

public class Order extends AbstractAggregateRoot<Order> {
    public void complete() {
        this.status = COMPLETED;
        
        if (isFirstOrder()) {
            registerEvent(new FirstOrderCompletedEvent(this.customerId));
        } else {
            registerEvent(new OrderCompletedEvent(this.id));
        }
    }
}

4. Spring 의존성 논쟁

순수주의 관점

문제점

  • 도메인이 org.springframework.data.domain에 의존
  • 프레임워크 독립성 위배
  • 헥사고날 아키텍처 원칙 위반

대안: 순수 구현

// 도메인 (Spring 의존 없음)
public class Order {
    private List<DomainEvent> events = new ArrayList<>();
    
    public void pay() {
        this.status = PAID;
        events.add(new OrderPaidEvent(...));
    }
    
    public List<DomainEvent> getDomainEvents() {
        return events;
    }
}

// 인프라 (Spring은 여기서만)
@Component
public class OrderRepositoryAdapter {
    public Order save(Order order) {
        Order saved = jpaRepository.save(order);
        
        for (DomainEvent event : saved.getDomainEvents()) {
            eventPublisher.publishEvent(event);
        }
        
        saved.clearDomainEvents();
        return saved;
    }
}

실용주의 관점

AbstractAggregateRoot의 장점

  • 특정 기술에 종속되지 않는 일반적 추상화
  • DB 독립적 (JPA, MongoDB, Redis 모두 동작)
  • 도메인 개념(애그리거트 루트)을 정확히 표현
  • 이벤트 발행 자동화로 실수 방지

업계 전문가 의견

  • Martin Fowler: "완벽한 순수성보다 실용적인 타협이 더 나을 때가 많다"
  • Vaughn Vernon: "충분히 일반적인 추상화이며 대부분의 프로젝트에서 문제없다"
  • Eric Evans: "절대적인 규칙은 아니다. 실용적인 타협점을 찾아라"

5. 선택 기준

AbstractAggregateRoot 사용 (권장)

다음 상황에서 사용:

  • 일반적인 Spring 프로젝트
  • 팀 생산성이 중요
  • 프레임워크 전환 계획 없음
  • 실용성이 순수성보다 우선

순수 구현 사용

다음 상황에서 사용:

  • 멀티 프레임워크 지원 필요
  • 도메인 모델을 다른 프로젝트에서 재사용
  • 헥사고날 아키텍처 엄격 준수 필요
  • 완벽한 프레임워크 독립성 요구

6. 의존성 레벨 비교

의존성 심각도 설명
AbstractAggregateRoot 낮음 일반적 추상화, 도메인 개념 표현
@Entity, @Id 중간 JPA 표준이지만 기술 의존
Hibernate, MongoDB 클라이언트 높음 특정 구현체 강결합, 피해야 함

7. 결론

대부분의 Spring 프로젝트에서 AbstractAggregateRoot 사용을 권장합니다.

이유:

  • 충분히 일반적이고 안전한 추상화
  • 생산성 향상과 실수 방지
  • Spring 생태계의 사실상 표준
  • 실용적 이점이 이론적 순수성보다 중요

다만, 프레임워크 독립성이 핵심 요구사항인 특수한 경우에는 순수 구현을 고려할 수 있습니다.

핵심은 완벽한 순수성보다 실용적인 균형입니다.