도메인 계층, 서비스 계층

sion Jeong·2024년 9월 6일
0

자바 자바 자바!! 자바를 잘하자!! 오늘은 도메인 계층과 서비스 계층이란 무엇인가에 대해서 알아보겠습니다.

🛠오늘의 핵심!!
도메인 계층: 각 엔티티만 있는 비즈니스 로직을 처리한다.
서비스 계층: 여러 도메인과 상호작용하는 비즈니스 로직을 처리한다.

도메인 계층

  • 각 엔티티의 핵심 비즈니스 규칙을 담당합니다.(비즈니스 로직이 다 들어가 있어야 한다.)
  • 엔티티, 값 객체, 도메인 서비스 등을 통해 비즈니스 규칙을 정의하고 실행합니다.

도메인 계층의 역할과 중요한 원칙!!

다른 계층(예: 데이터 접근 계층, 프레젠테이션 계층)과는 독립적으로,
순수하게 비즈니스 로직에 집중합니다.

  1. 비즈니스 로직의 집중
    • 도메인 계층은 비즈니스 규칙을 응집하여 처리
    • 비즈니스 로직이 외부 의존성 없이 처리될 수 있어야 합니다.
    • 예시: "고객이 주문할 때 재고가 부족하면 주문이 실패한다"는
      규칙을 도메인 계층에서 정의합니다.
  2. 독립성 유지
    • 데이터베이스, 사용자 인터페이스, 외부 시스템과는 직접적으로 상호작용하지 않음
    • 상호작용은 애플리케이션의 다른 계층이 처리하고, 도메인 계층은 오직 비즈니스 로직에만 집중
  3. 테스트 용이성
    • 외부 시스템과 독립적으로 동작하기 때문에, 단위 테스트를 쉽게 할 수 있다.
    • 비즈니스 규칙이 잘 작동하는지 테스트할 때, 실제로 데이터베이스나 외부 API를 호출할 필요가 없다.

도메인 계층의 구성 요소

  1. 엔티티
    • 도메인 내에서 중요한 데이터와 그 데이터에 관련된 행동을 정의하는 객체
    • 데이터베이스 테이블과 매핑되며, 상태와 행동을 가짐
  2. 값 객체(Value Object)
    • 고유한 식별자를 가지지 않고, 주로 불변의 속성을 가지고 있는 객체
    • 두 값 객체가 동일한 데이터를 가지면 동일한 것으로 취급
  3. 도메인 서비스
    • 여러 엔티티에 걸쳐 있는 비즈니스 로직을 처리하는 데 사용
    • 특정 엔티티 하나에 국한되지 않는 비즈니스 로직이 필요할 때 사용
    • 예시: "특정 고객에게 특별 할인을 적용한다"는 로직은 고객 엔티티와 주문 엔티티 모두에 관련이 있을 수 있는데, 이 경우 도메인 서비스에서 처리할 수 있다.
  4. 도메인 이벤트
    • 도메인 내에서 중요한 사건이 발생했을 때 이를 기록하거나 다른 시스템에 알리는 역할
    • 예시: 주문이 완료되었을 때, "주문 완료" 이벤트가 발생하여 배송 시스템에 알림을 보낼 수 있다.

질문!!
도메인 계층은 다른 계층과 독립정으로 비즈니스 로직에 집중한다고한다
“고객이 주문할 때 재고가 부족하면 주문이 실패한다"는 규칙을 도메인 계층에서 정의한다고 하는데
근데 재고가 부족한지 알려면 결국 외부 시스템(DB)에 한번 조회를 해야 하는거 아닌가??
→ 그럼 결국 독립성이 깨지는것 아닌가?

답변!!
"재고가 부족하면 주문을 실패한다"와 같은 규칙을 처리할 때, 재고 상태를 알기 위해 데이터베이스나 외부 시스템과 상호작용이 필요할 수 있다.
하지만, 도메인 계층은 직접적으로 데이터베이스나 외부 시스템과 상호작용하지 않는 방식으로 설계되어야 한다.

도메인 계층이 외부 시스템과 상호작용이 필요할 때는, 리포지토리 또는 서비스 인터페이스를 사용하여 그 상호작용을 추상화한다.
인터페이스를 통해 필요한 데이터를 조회하거나 조작할 수 있으며,
구체적인 구현(예: 데이터베이스와의 상호작용)은 애플리케이션 계층이나 인프라 계층에서 처리된다.

의존성 역전 원칙 역시 도메인 계층이 구체적인 인프라(데이터베이스, API)와 의존하지 않도록 인터페이스를 통해 상호작용하는 것을 의미합니다.
이를 통해 도메인 계층은 외부 의존성에 영향을 받지 않고 독립적으로 동작할 수 있습니다.

public class OrderService {

   private final StockChecker stockChecker;

   public OrderService(StockChecker stockChecker) {
       this.stockChecker = stockChecker;
   }

   public void placeOrder(Order order) {
       // 비즈니스 규칙: 재고가 부족하면 주문 실패
       if (!stockChecker.isStockAvailable(order)) {
           throw new OutOfStockException("재고가 부족합니다.");
       }

       // 재고가 충분하면 주문을 처리
       order.completeOrder();
   }
}

비즈니스 규칙인 "재고가 부족하면 주문이 실패한다"를 정의중 입니다.
재고 시스템과 직접 상호작용하지 않고.
대신, StockChecker라는 인터페이스를 통해 외부 시스템과 상호작용 (의존성 역전 원칙)
도메인 계층(OrderService)은 이 구체적인 구현에 의존하지 않고, StockChecker 인터페이스만 사용하여 재고를 확인함

외부 의존성 처리하기 → 리포지토리 또는 인프라 계층
StockChecker 인터페이스의 구체적인 구현은 인프라 계층에서 처리

public interface StockChecker {
    boolean isStockAvailable(Order order);
}
//------------------------------------------------------------

public class StockCheckerImpl implements StockChecker {

    private final StockRepository stockRepository;

    public StockCheckerImpl(StockRepository stockRepository) {
        this.stockRepository = stockRepository;
    }

    @Override
    public boolean isStockAvailable(Order order) {
        // 실제로 DB나 외부 시스템에서 재고를 확인하는 로직
        return stockRepository.findAvailableStock(order.getProductId()) >= order.getQuantity();
    }
}
@Service
public class ReviewService {
 
	  private final ReviewRepository reviewRepository;
    private final Validator validator;
    private final DtoMapper dtoMapper;
    private final FinderService finderService;

    public ReviewService(ReviewRepository reviewRepository, Validator validator, DtoMapper dtoMapper, FinderService finderService) {
        this.reviewRepository = reviewRepository;
        this.validator = validator;
        this.dtoMapper = dtoMapper;
        this.finderService = finderService;
       
	    //후기 등록
	    public ReviewCreateResponse createReview(ReviewCreateRequest request, String phoneNumber) {

       // 회원 찾기
       User user = finderService.findUserByPhoneNumber(phoneNumber);

        // 주문 찾기
        Orders orders = finderService.findOrderById(request.getOrderId());

        // 상품 찾기
        Product product = finderService.findProductById(request.getProductId());

        //검증 로직 사용 (주문의 유저아이디와, 토큰에서 가져온 유저의 아이디가 같은지 검증)
        validator.validate(orders, user);

        // 후기 생성
        Review review = new Review(request, orders, product);
        Review savedReview = reviewRepository.save(review);

        //dto로 변환 후 반환
        return dtoMapper.toReviewCreateResponse(savedReview);
    }

    }

질문2
도메인 계층에는 비즈니스 로직이 전부 들어가 있어야 한다고 했는데
그럼 후기등록 메서드도 비즈니스 로직 아닌가?? → 그렇다면 도메인 계층인가??
하지만 ReviewService서비스 계층(Service Layer)에 속하며,
도메인 계층과 구체적인 인프라(데이터베이스, 외부 서비스) 사이의 중개 역할을 한다고 한다.

왜???
중요한 점은 비즈니스 로직을 어디에서 구현하고 관리하는지 라고한다
서비스 계층과 도메인 계층의 역할 차이 때문에 혼동이 올 수 있다.
후기 등록은 비즈니스 로직에 속한다.
도메인 계층은 이러한 비즈니스 로직을 처리하는 곳이지만,
모든 비즈니스 로직이 도메인 계층에만 있어야 하는 것은 아니라고 한다.

그렇다면 서비스 계층과 도메인 계층의 차이는?
서비스 계층: 여러 도메인 객체와 상호작용하여 복잡한 비즈니스 프로세스를 조율하는 계층
도메인 계층: 각 엔티티의 핵심 비즈니스 로직을 처리

그렇다!!!!
후기등록은 비즈니스 로직이지만
→ 서비스 계층인 것은 회원 찾기, 주문 찾기, 상품 찾기등 다양한 도메인계층과 상호작용하여 복잡한 로직을 만들어 내기 때문이다!!!


서비스 계층

  • 도메인 로직을 호출하거나 조합하여, 복잡한 작업을 처리하는 계층입니다.
    • 여러 도메인 객체를 조합하여 하나의 비즈니스 프로세스를 처리합니다. 도메인 계층에서 개별 로직이 처리되는 반면, 서비스 계층은 전체 흐름을 제어합니다.
  • 주로 도메인 계층에서 정의된 비즈니스 로직을 조합하여 하나의 흐름이나 트랜잭션을 처리합니다.
  • 서비스 계층은 "애플리케이션의 흐름"을 다루며, 여러 도메인 객체를 조합하여 비즈니스 프로세스를 처리하는 역할을 합니다.
  • 예를 들어, "고객이 주문을 하면 재고를 차감하고 결제를 진행하며, 주문 확인 이메일을 발송하는 일련의 작업"은 서비스 계층에서 처리됩니다.

도메인 계층: 순수한 비즈니스 규칙을 처리하는 계층. 비즈니스 로직의 핵심 부분을 정의합니다.

서비스 계층: 여러 도메인 객체를 조합하여 복잡한 비즈니스 흐름을 관리합니다. 즉, 도메인 로직을 호출하고 외부 시스템(예: 데이터베이스, API 등)과 연결하는 역할을 담당합니다.

@Entity
@Getter
@NoArgsConstructor
public class Review {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String content;
    private String reviewImage;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "orders_id")
    private Orders orders;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;

    public Review(ReviewCreateRequest request, Orders orders, Product product) {
        this.content = request.getContent();
        this.reviewImage = request.getReviewImage();
        this.orders = orders;
        this.product = product;
        orders.markAsReviewed();// 리뷰 작성 시 주문의 reviewed 필드를 true로 설정 

    }
}

질문3
서비스 계층이 여러 도메인과 상호작용하는 로직을 처리하는 곳 이라고 한다면
Review를 생성하는 생성자도
Orders도메인 계층과, Product 도메인 계층이랑 상호작용 하는거 아닌가????
이러면 생성자도 서비스 계층으로 옮겨줘야하는거 아닌가??!!!! 하는 생각이 든다이…

답변
하지만! 생성자는 객체의 상태를 설정하는 것이지 비즈니스 로직이 아니다!!!!
따라서 서비스 계층에 생성자를 만드는 것이 아니라 도메인 계층에 만들 수 있는 것이다.

함정문제

public Review(ReviewCreateRequest request, Orders orders, Product product) {
	this.content = request.getContent();
	this.reviewImage = request.getReviewImage();
	this.orders = orders;
	this.product = product;
        orders.markAsReviewed();// 리뷰 작성 시 주문의 reviewed 필드를 true로 설정 
         // (이건 비즈니스 로직이다. 잘못된 설정) -> 따라서 얘는 서비스 계층으로 가야함

생성자에서 잘못된 부분이 있습니다. (객체의 상태 설정)
다른 도메인 객체직접 수정하거나 상태를 변경하는 것은 → 비즈니스 로직입니다.
따라서 Orders 객체의 상태를 변경하는 것은 도메인 객체 간의 상호작용을 초래하며, 이는 서비스 계층에서 처리하는 것이 더 바람직합니다.

즉!! 생성자의 역할은 해당 객체의 상태를 설정하는 것이며, 다른 도메인 객체의 상태를 변경하는 작업서비스 계층에서 이루어져야 합니다.
(생성자가 불필요하게 다른 도메인 객체의 상태를 변경하면, 도메인 객체 간의 강한 결합이 발생하기 때문)

개선된 설계를 해 보자

  • Review 엔티티 생성자Review 객체 자체의 상태를 설정하는 데 집중합니다.
  • Orders 객체의 상태 변경은 orders.markAsReviewed()서비스 계층에서 처리해야 합니다.
  • 이를 통해 도메인 객체 간의 의존성을 최소화할 수 있습니다.
수정된 Review 엔티티 생성자
public Review(ReviewCreateRequest request, Orders orders, Product product) {
    this.content = request.getContent();
    this.reviewImage = request.getReviewImage();
    this.orders = orders;
    this.product = product;
}
서비스 계층에서 Orders 상태 변경 처리
@Service
public class ReviewService {

    private final ReviewRepository reviewRepository;
    private final FinderService finderService;

    public ReviewCreateResponse createReview(ReviewCreateRequest request, String phoneNumber) {

        // 회원 찾기
        User user = finderService.findUserByPhoneNumber(phoneNumber);

        // 주문 찾기
        Orders order = finderService.findOrderById(request.getOrderId());

        // 상품 찾기
        Product product = finderService.findProductById(request.getProductId());

        // Review 엔티티 생성
        Review review = new Review(request, order, product);

        // 서비스 계층에서 Orders 상태 변경
        order.markAsReviewed(); // 주문의 상태를 서비스 계층에서 변경

        // 리뷰 저장
        Review savedReview = reviewRepository.save(review);

        return new ReviewCreateResponse(savedReview);
    }
}
profile
개발응애입니다.

0개의 댓글