서동우 튜터님 특강

 

  • 관계형 데이터베이스에서는 연관관계FK(외래키)를 통해 달성
    • 여러 테이블이 FK를 통해 연관관계를 맺고 있다면, 조인을 사용하여 데이터를 연결하고 원하는 정보를 가져올 수 있음

 

  • 그러나 객체 관점에서는? →객체 그래프 탐색으로 인한 n+1 문제 발생

 

 

🤔 RDBMS의 관점과 객체 관점이 맞지 않음

ORM : RDMBS와 객체, 둘의 구조가 달라서 미스매치가 발생하는 것을 해결하기 위해 SQL을 자동으로 생성하고, 객체와 데이터베이스를 연결

 

 

 

 

 

 

이전까지는 ERD설계를 통해 외래키를 설정하고, 외래키 기반으로 연관관계를 설정하여 엔티티를 구현했음

연관관계에서도 종속적인 관계가 있음

  • 영속성 전이(Cascade)가 가능
  • 라이프사이클을 공유
    • ex) 주문 아이템은 주문 없이는 존재할 수 없다
      •  

 

독립적인 관계도 있음

  •  주문아이템-상품 : 상품이 먼저 만들어 진 후 주문이 생성될때 상품을 가져옴 → 생명주기가 다름
  • 주문 - 유저 : 유저가 삭제된다고 해서 주문에 영향이 미치면 안됨

 

 

JPA는 종속적인 관계에 대한 기능을 지원하고 있음

ex) 주문을 생성할때 주문 아이템을 생성함, 주문이 삭제될 때 주문 아이템도 삭제함 등..

 

 

 

 

 

이렇게 도메인이 확장되어 복잡하다면 어떻게 해결할 수 있을까? → DDD 

 

 

 


 

도메인 주도 설계 (DDD)

도메인을 중심으로 소프트웨어를 설계하는 것 

 

 

바운디드 컨텍스트(Bounded Context)

큰 시스템을 여러 개의 작은 컨텍스트로 나누어 각 컨텍스트 내에서 특정 비즈니스 규칙과 데이터 모델이 적용하는 것을 의미

각 컨텍스트는 독립적으로 설계되고 구현 됨. 

각 컨텍스트가 독립적으로 동작하도록 설계 → 전체 시스템의 복잡성을 줄이고 유연성을 높임

컨텍스트 간에는 인터페이스를 통해 상호작용

 

 

패키지구조 - 4계층 구조

presentation - application - domain - infrastructure

: DDD에서 가져가는 기본적인 계층 구조

 

 

모놀리식 → MSA 전환을 위한 구조

 

주문이 공통모듈을 통해 상품을 호출하게 함

그 결과 주문-상품 의존하는 코드가 없음 → 서비스를 따로 분리할 수 있음

 

 

애그리거트(Aggregate)

도메인 모델에서 논리적으로 하나의 단위로 묶여야 하는 객체들의 집합

* 애그리거트 루트 : 애그리커드를 대표하는 객체, 애그리거트의 유일한 진입점(Entry Point) - ex)주문, 유저, 가게..

 

애그리거트는 라이프사이클을 공유함 → 애그리거트를 트랜잭션으로 묶음

 

 

🤔 애그리커드와 루트가 필요한 이유?

  • 데이터 일관성 유지
  • 변경 범위 (트랜잭션) 제어
  • 도메인 모델 명확하게 만들기
  • 엔티티 간 직접 참조 방지
  • 데이터 저장소(Repository)와의 관계 정리

 

데이터 저장소(Respository)와의 관계 정리

애그리거트 단위로 데이터 저장소 관리 - 데이터의 무결성 보장

* OrderRespositoryOrder를 저장, Order 내부의 OrderItem도 함께 저장 (Cascade.Persist) - 영속성 전이
→ 별도의 OrderItemRepository는 만들지 않음

* OrderitemRepository가 따로 있으면 Order 없이 OrderItem이 변경 될 가능성이 있음
→ 데이터가 엉켜서 일관성이 깨질 위험이 있음
public Order(List<OrderItem> orderItems) {
	validateOrderItems(orderItems);
    this.orderItems = orderItems;
}​

Order의 생성은 항상 OrderItems와 함께

public void validateOrderItems(List<OrderItem> orderItems) {
	if(orderItem == null || orderItems.isEmpty()) {
    	throw new IllegalArgumentException("주문 항목은 비어 있을 수 없습니다.");
	}
    if (orderItems.size() > MAX_ORDER_ITEMS) { // OrderItem 갯수가 10개를 초과하면 Order 자체를 만들면 안됨
    	throw new IllegalArgumentException("주문 상품 수가 " + MAX_ORDER_ITEMS + "개를 초과하였습니다.");
    }
}

 

 

 


 

 

 

'Back-End > DDD' 카테고리의 다른 글

DIP (Dependency Inversion Principle, 의존성 역전 원칙)  (0) 2025.03.15
아키텍처  (0) 2025.03.15
도메인  (0) 2025.03.14
고수준 모듈은 저수준 모듈에 의존하면 안 되고,
저수준 모듈이 고수준 모듈에 의존하도록 해야한다.
→ 둘 다 추상화(인터페이스)에 의존해야 한다.

 

  • 고수준 모듈 : 핵심 비지니스 로직 담당
  • 저수준 모듈 : 세부적인 구현 담당

 

"주문이 완료되면 사용자에게 알람을 보내는 시스템"

 

❌ DIP를 지키지 않은 코드

OrderService(고수준)가 EmailNotification(저수준)에 직접 의존

만약 문자메세지(SMS) 알림을 추가하려면? - OrderService 내부 코드 수정 → OCP 위반

 // 저수준 모듈
 class EmailNotification {
 	public void sendEmail(String message) {
    	System.out.println("이메일전송: " + message);
    }
 }
// 고수준 모듈
class OrderService {
	private EmailNotification emailNotification; // 직접 의존
    
    public OrderService() {
    	this.emailNotification = new EmailNotification();
    }
    
    public void completeOrder() {
    	System.out.println("주문 완료!");
        emailNotification.sendEmail("주문이 완료되었습니다");
    }
 }

 

 

✅ DIP를 적용한 코드

인터페이스(추상화)를 사용하여 의존성 역전

OrderService(고수준) → Notification 인터페이스(추상화)에만 의존

EmailNotification, SmsNotification을 자유롭게 교체 가능 → OCP 만족

새로운 알림 방식 추가 시 OrderService 수정 불필요

// 추상화(인터페이스)
public interface Notification {
	void send(String message);
}
// 저수준 모듈1
pulbic class EmailNotification implements Notification {
	pulbic void send(String message) {
    	System.out.println("이메일발송: " + message);
    }
}

// 저수준 모듈2
public class SmsNotification implements Notification {
	public void send(String message) {
    	System.out.println("문자발송: " + message);
    }
}

 

 

 


 

 

'가격 할인 계산' 입장에서는 어떻게 구현했는지 중요하지 않음

'고객정보와 구매 정보에 룰을 적용해서 할인 금액을 구한다' 만 중요함

// 룰을 이용해 할인 금액 계산
public interface RuleDiscounter {
	Money applyRules(Customer customer, List<OrderLine> orderLines);
}
public class CalculateDiscountService {
	
    private RuleDiscounter ruleDiscounter;
    
    public CalculateDiscountService(RuleDiscounter ruleDiscounter) {
    	this.ruleDiscounter = ruleDiscounter;
    }
    
    public Money calculateDiscount(List<OrderLine> orderLines, String customerId) {
    	Customer customer = findCustomer(customerId);
        
        return ruleDiscount.applyRules(customer, orderLines);
   	}

'가격 할인 계산'은 구현 기술인 Drools에 의존하지 않고

'룰을 이용한 할인 금액 계산'을 추상화한 RuleDiscounter 인터페이스에 의존

// RuleDiscounter를 상속받아 구현한 할인 규칙 적용 구현체
public class DroolsRuleDiscounter implements RuleDiscounter {
	
    ...
    
    @Override
    public Money applyRules(Customer customer, List<OrderLine> orderLines) {
    	KieSession kSession = kContainer.newKieSession("discountSession");
        ..
        return money.toImmutableMoney();
    }
 }

 

 

 

DIP를 적용하면 저수준 모듈이 고수준 모듈에 의존

 

 

* DIP 주의사항

저수준 모듈에서 인터페이스를 추출하는 경우가 있음

DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출해야 함

 

 

 

이메일 알람을 SMS으로 변경하거나 Jpa 대신 MyBatis를 구현 기술로 변경하고 싶다면

응용 영역과 도메인 영역은 수정 없이 구현체만 바꾸면 됨

'Back-End > DDD' 카테고리의 다른 글

도메인 주도 설계의 필요성  (0) 2025.03.27
아키텍처  (0) 2025.03.15
도메인  (0) 2025.03.14

 

이전까지 프로젝트 할 때는 
Contoller / Service / Entity / Repository / Dto 
이런 패키지 구조로 진행했음

도메인 중심 설계 (DDD, Domain-Driven Design)에 기반한 구조는
Presentation / Application(Service) / Domain / InfraStructure
이렇게 구성됨

이전에는 Service에서 비지니스 로직을 수행했는데 DDD 계층 구조에서는 Service에서는 로직을 직접 수행하지 않고 Domain 을 조합하거나 도메인 모델에 로직 수행을 위임함
핵심 로직은 도메인

 


Service : 로직을 직접 구현하지 않고 도매인 객체를 조합하는 역할

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final MemberRepository memberRepository;

    public Long createOrder(OrderRequest orderRequest) {
        // 1. 주문 정보를 생성
        Member member = memberRepository.findById(orderRequest.getMemberId())
                .orElseThrow(() -> new IllegalArgumentException("회원 정보를 찾을 수 없습니다."));
        
        Product product = productRepository.findById(orderRequest.getProductId())
                .orElseThrow(() -> new IllegalArgumentException("상품 정보를 찾을 수 없습니다."));

        Order order = new Order(member, product, orderRequest.getQuantity());
        
        orderRepository.save(order);

        return order.getId();
    }
}​

 

 


Domain : 핵심 비지니스 로직

@Getter
public class Order {
    private final Long id;
    private final Member member;
    private final Product product;
    private int quantity;
    private OrderStatus status;

    public Order(Member member, Product product, int quantity) {
        validateStock(product, quantity);
        this.member = member;
        this.product = product;
        this.quantity = quantity;
        this.status = OrderStatus.PENDING;
    }

    private void validateStock(Product product, int quantity) {
        if (product.getStock() < quantity) {
            throw new IllegalStateException("재고가 부족합니다.");
        }
    }

    public void completeOrder() {
        if (this.status != OrderStatus.PLACED) {
            throw new IllegalStateException("주문이 완료될 수 없습니다.");
        }
        this.status = OrderStatus.COMPLETED;
    }

    public void cancel() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("이미 처리된 주문은 취소할 수 없습니다.");
        }
        this.status = OrderStatus.CANCELED;
    }
}

 

'Back-End > DDD' 카테고리의 다른 글

도메인 주도 설계의 필요성  (0) 2025.03.27
DIP (Dependency Inversion Principle, 의존성 역전 원칙)  (0) 2025.03.15
도메인  (0) 2025.03.14

 도메인 모델 패턴 / 도출 

 

아키텍처 구성

  • 표현(Presantation)  : 사용자 요청 처리, 사용자에게 정보 보여줌
  • 응용(Application) : 사용자가 요청한 기능 실행, 업무 로직 직접 구현x, 도메인 계층을 조합해 기능 실행
  • 도메인(Domain) : 시스템이 제공할 도메인 규칙 구현
  • 인프라스트럭처(Infrastructure) : 데이터베이스나 메시징시스템과 같은 외부 시스템 연동 처리

 

도메인

시스템이 제공할 도메인 규칙 구현

 

온라인 서점 서비스의 주문 도메인을 만든다고 할 때,

'출고 전 배송지를 변경할 수 있다' , '주문 취소는 배송 전에만 할 수 있다' 라는 규칙을 구현한 코드가 도메인 계층에 위치

public class Order {
	private OrderState state;
    private ShippingInfo shippingInfo;
    
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
    	if(!isShippingChangeable()) {
        	throw new IllegalStateException("cant' change shippingn int " + state);
        }
        	this.shippingInfo = newShippingInfo;
    }
    
    private boolean isShippingChangeable() {
    	return state == OrderState.PAYMENT_WAITING ||
        	state == OrderState.PREPARING;
    }
 }
 
 
 public enum OrderState {
 	PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
 }

배송지 변경 가능 여부를 판단하는 기능 - 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에 구현

 

핵심 규칙을 구현한 코드는 도메인 모델(도메인 계층의 객체 모델)에 위치

▶ 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 됨

 

 

 


 

 엔티티 

엔티티의 가장 큰 특징은 식별자를 가진다는 것

식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 

밸류 타입

ShippingInfo 클래스의 receiverName / reveiverPhoneNumber 필드는 다른 데이터를 담고 있지만 개념적으로는 받는 사람을 의미

즉 두 필드는 실제로 하나의 개념을 표현하고 있음

 

밸류타입은 개념적으로 완전한 하나를 표현할 때 사용

 

 

price와 amounts 는 '돈'을 의미 - Money타입

// 밸류타입 사용 전
public class OrderLine {
	private Product product;
    priavte int price;
    private int quantity;
    private int amounts;
    
    public OrderLine(Product product, int price int quantity) {
    	this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amounts = calculateAmounts();
    }
    
    private int calculateAmounts() {
    	return price * quantity;
    }
    ..
  }
// 밸류타입 사용 후 - 코드 가독성 향상
public class OrderLine {
	private Product product;
    priavte Money price;
    private int quantity;
    private Money amounst;
    
    public OrderLine(Product product, Money price int quantity) {
    	this.product = product;
        this.price = price;
        this.quantity = quantity;
        this.amounts = caluculateAmounts();
    }
    
    private int calculateAmounts() {
    	return price.multiply(quantity);
    }
    ..
  }

 

또한 밸류타입은 밸류 타입을 위한 기능을 추가할 수 있음

public class Money {
	private int value;
    
    ... 
    
    public Money add(Money money) {
    	return new Money(this.value + money.value);
    }
    
    // 정수 타입 연산이 아니라 돈 계산이라는 의므로 코드를 작성 할 수 있음
    public Money multiply(int multiplier) {
    	return new Money(value * multiplier);
    }
  }

 

 

식별자를 위한 밸류 타입을 사용하여 필드의 의미가 잘 드러나도록 할 수 있음

 

 

 

 

'Back-End > DDD' 카테고리의 다른 글

도메인 주도 설계의 필요성  (0) 2025.03.27
DIP (Dependency Inversion Principle, 의존성 역전 원칙)  (0) 2025.03.15
아키텍처  (0) 2025.03.15

+ Recent posts