본문 바로가기
Books/도메인 주도 개발 시작하기

Chapter3. 애그리거트

by YoonJong 2024. 4. 7.
728x90

  • 애그리거트 : 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만드려면 상위 수준에서 모델을 조망할 수 있는 방법

  • 애그리거트는 도메인 주도 설계(DDD)에서 사용되는 중요한 개념
  • 애그리거트는 함께 변경되는 일관성 있는 엔터티들의 집합
  • 애그리거트 내 엔터티들은 서로 밀접하게 연관되어 있으며, 외부 엔터티는 애그리거트 루트를 통해서만 애그리거트 내 엔터티에 접근 가능

  • 애그리거트 루트 : 애그리거트를 식별하고 외부 엔터티와 상호 작용하는 엔터티
    • 모든 객체가 일관된 상태를 유지하려면 애그리거트 전체를 관리할 주체가 애그리거트 루트의 역할
    • 식별: 애그리거트를 식별하는 고유한 ID를 가진다.
    • 접근 제어: 외부 엔터티가 애그리거트 내 엔터티에 접근하도록 허용하는 역할을 한다.
    • 변경 관리: 애그리거트 내 엔터티의 변경을 관리한다.
  • 애그리거트 루트에서는 애그리거트가 제공해야 할 도메인 기능을 구현한다.
public class Order {

    private String id;
    private List<OrderItem> orderItems;
    private Customer customer;
    private ShippingAddress shippingAddress;
    private OrderStatus orderStatus;

    public void addOrderItem(OrderItem orderItem) {
        // 비즈니스 로직: 주문 항목 추가 시 수행해야 할 비즈니스 로직 구현
        // 예: 재고 검사, 가격 계산 등

        this.orderItems.add(orderItem);
    }

    public void changeOrderStatus(OrderStatus newOrderStatus) {
        // 비즈니스 로직: 주문 상태 변경 시 수행해야 할 비즈니스 로직 구현
        // 예: 결제 승인, 배송 준비, 배송 완료 등

        this.orderStatus = newOrderStatus;
    }
}

  • 트랜잭션 범위 선택 시 고려 사항
    • 작업 범위: 트랜잭션 범위는 작업 범위를 최대한 포함
    • 성능: 트랜잭션 범위가 클수록 성능 저하가 발생 가능
    • 일관성: 트랜잭션 범위는 데이터 일관성을 유지할 수 있는 최소한의 범위로 설정
public class OrderService {

	@Transactional
  public void createOrder(Customer customer) {
      // 주문 엔터티 생성
      Order order = new Order();
      order.setCustomer(customer);

      // 주문 저장
      orderRepository.save(order);

      // 별도의 트랜잭션에서 고객 정보 수정
      new CustomerService().updateCustomerName(customer.getId(), "변경된 이름");
  }
}

---

public class CustomerService {

    @Transactional(propagation = Propagation.REQUIRES_NEW) // 새로운 트랜잭션 시작 
    public void updateCustomerName(Long customerId, String newName) {
        // 고객 엔터티 조회
        Customer customer = customerRepository.findById(customerId);

        // 고객 이름 변경
        customer.setName(newName);

        // 고객 정보 저장
        customerRepository.save(customer);
    }

}

  • 애그리거트 간의 참조는 필드를 통해 쉽게 구현 가능하다.
    • 결합성이 강화된다 (단점)
    • 편한 탐색 오용 (단점) → 다른 애그리거트의 상태를 변경할 수 있다.
    • 성능 고민 (단점)
    • 확장의 어려움 (단점)
// ID 를 사용한 객체 참조 방식
public class Order {

    private Long id;
    private List<OrderItem> orderItems;
    private Customer customer;
}

public class OrderItem {

    private Long id;
    private Order order;
    private Product product;
}

public class Customer {

    private Long id;
    private String name;
}
  • N+1 문제가 발생할 수 있는데, 아래와 같이 한번에 데이터를 불러오는 것으로 해결할 수 있다.
public class OrderRepository {

    public Order findByIdWithFetchJoin(Long orderId) {
        // JPQL을 사용하여 주문 엔터티, 주문 항목, 고객 정보를 한 번에 조회
        return entityManager.createQuery(
                "SELECT o FROM Order o " +
                        "LEFT JOIN FETCH o.orderItems " +
                        "LEFT JOIN FETCH o.customer " +
                        "WHERE o.id = :id", Order.class)
                .setParameter("id", orderId)
                .getSingleResult();
    }

}
  • 상품과 카테고리가 있다고 할때, 카테고리에 속한 상품을 구하고 싶을 때는 카테고리에 접근해서 상품을 구하는게 성능상 좋다.
public class Product {

    private Long id;
    private String name;
    private Category category;
}

public class Category {

    private Long id;
    private String name;
    private List<Product> products;
}

// 상품에서 카테고리 접근 -> N+1문제 발생 가능성, 성능 저하
Product product = productRepository.findById(productId);
Category category = product.getCategory();

	// 카테고리에서 상품 접근 -> N+1문제 해결, 성능 향상 기대 
Category category = categoryRepository.findById(categoryId);
List<Product> products = category.getProducts();
  • 애그리거트를 팩토리로 사용
    • 중요한 도메인 로직 처리가 응용 서비스에 노출되지 않도록 하기
// 계좌생성 하는 로직에 사용자가 존재하는지에 대한 로직이 노출

public void createAccount(@RequestBody UserDto userDto) {
    // 중요한 도메인 로직: 사용자 이름이 이미 존재하는지 확인
    boolean isUsernameExists = userService.isUsernameExists(userDto.getUsername());
    if (isUsernameExists) {
        throw new UsernameAlreadyExistsException();
    }
}

--
// 사용자 생성 가능여부를 userService 에서 확인  

public void createAccount(@RequestBody UserDto userDto) {
    // 도메인 서비스 사용
  userService.createUser(userDto);
}

public class UserService {
  public void createUser(UserDto userDto) {
      // 중요한 도메인 로직: 사용자 이름이 이미 존재하는지 확인 및 사용자 생성
      userDomainService.createUser(userDto);
  }
}

public class UserDomainService {

  private UserRepository userRepository
  
  public void createUser(UserDto userDto) {
      boolean isUsernameExists = userRepository.findByUsername(userDto.getUsername()).isPresent();
      if (isUsernameExists) {
          throw new UsernameAlreadyExistsException();
      }
}

728x90

댓글