자바 자바 자바!! 자바를 잘하자!! 오늘은 도메인 계층과 서비스 계층이란 무엇인가에 대해서 알아보겠습니다.
🛠오늘의 핵심!!
도메인 계층:각 엔티티만 있는 비즈니스 로직을 처리
한다.
서비스 계층:여러 도메인과 상호작용하는 비즈니스 로직을 처리
한다.
핵심 비즈니스 규칙
을 담당합니다.(비즈니스 로직이 다 들어가 있어야 한다.)다른 계층(예: 데이터 접근 계층, 프레젠테이션 계층)과는 독립적으로,
순수하게 비즈니스 로직에 집중
합니다.
"특정 고객에게 특별 할인을 적용한다"
는 로직은 고객 엔티티와 주문 엔티티 모두에 관련이 있을 수 있는데, 이 경우 도메인 서비스에서 처리할 수 있다.질문!!
도메인 계층은 다른 계층과 독립정으로 비즈니스 로직에 집중한다고한다
“고객이 주문할 때 재고가 부족하면 주문이 실패한다"
는 규칙을 도메인 계층에서 정의한다고 하는데
근데 재고가 부족한지 알려면 결국 외부 시스템(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); } }