티스토리 뷰

스프링 부트와 AWS로 혼자 구현하는 웹 서비스를 읽고 정리한 내용입니다.

프로젝트를 할 때 프로젝트의 패키지를 나누는 것은 중요합니다. 패키지 마다 구분된 역할을 하도록 설계하여 모듈마다 결합도는 낮추고 응집도를 높여 모듈은 각자의 역할만 해야되기 때문입니다. 스프링 웹 계층에는 Web, Service, Repository, DTOs, Domain Model 5가지 계층이 있습니다. 각 계층의 역할에 대해서 알아보겠습니다.

Web Layer

흔히 사용하는 Controller와 JSP등의 View 템플릿 영역입니다. 이외에도 Filter, Intercepter, ControllerAdvise등 외부 요청과 응답에 대한 전반적인 영역을 의미합니다. 웹 애플리케이션의 최상위에 존재한다고 생각하면 됩니다. 

 

Service Layer

Web Layer의 바로 아래에 존재하느느 층으로 @Service에 사용되는 서비스 영역입니다. 일반적으로 Controller와 DAO의 중간 영역에서 사용됩니다. @Transaction이 사용되는 영역이기도 합니다. 여기서 많은 사람들이 오해하는 것이, 서비스 계층에서 비즈니스 로직을 처리한다는 것입니다. 하지만 전혀 그렇지 않습니다. 서비스 계층은 트랜잭션, 도메인 간 순서 보장의 역할만 합니다.

서비스 계층에서 비즈니스 로직을 처리하는 코드를 보겠습니다. 배송을 주문하고 취소하는 로직이 전부 서비스 클래스 내부에서 처리됩니다. '서비스' 계층이 무의미해지며 객체는 단순히 데이터 덩 어리의 역할밖에 하지 못하는 상황이 됩니다. 프로젝트의 규모가 커지면 하나의 서비스에서 엄청나게 많은 모델을 읽어 로직이 구성된다면 서비스의 복잡도가 매우 높아지고 갈수록 유지보수 하기가 어려워 질 것 입니다.

그러면 비즈니스 로직은 어떤 계층에서 처리하는것이 바람직할까요?

@Transactional
public Order cancelOrder(int orderId){
	// 1) 데이터베이스로 부터 주문, 배송, 결제 정보 조회
	OrdersDto order = ordersDao.selectOrders(orderId);
	BillingDto billing = billingDao.selectBilling(orderId);
	DeliveryDto delivery = deliveryDao.selectDelivery(orderId);

	// 2) 배송 취소가 가능한지 확인
	String deliveryStatus = delivery.getStatus();

	// 3) 배송 상태 변경
	if("IN_PROGRESS".equals(deliveryStatus){
		delivery.setStatus("CANCEL");
		deliveryDao.update(delivery);
	}

	// 4) 각 테이블에 취소 상태 업데이트
	order.setStatus("CANCEL");
	ordersDao.update(order);

	billing.setStatus("CANCEL");
	deliveryDao.update(billing);

	return order;
}

 

Domain Model

도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 것을 도메인 모델이라고 합니다. 즉, 비즈니스 로직을 처리하는 영역입니다. 예를 들어 택시 앱이라고 하면 배차, 탑승, 요금 등이 모두 도메인이 될 수 있습니다. 서비스 계층에서 모든 비즈니스 로직을 처리하던 코드를 분리해보겠습니다. Order, Billing, Delivery 객체가 각자 본인 취소 이벤트를 처리하며, 서비스  메소드는 트랜잭션과 도메인 간의 순서만 보장해줍니다. @Entity가 사용된 영역 역시 도메인 모델이라고 이해하면 됩니다. 

public Order cancelOrder(int orderId){
	// 1)
	Order orders = ordersRepository.findById(orderId);
	Billing billing = billingRepository.findByOrderId(orderId);
	Delivery delivery= deliveryRepository.findByOrderId(orderId);

	// 2 & 3)
	delivery.cancel();

	// 4)
	order.cancel();
	billing.cancel();
	
	return order;
}

 

 Repository Layer

데이터베이스와 직접 연결되는 계층으로, 데이터 저장소에 접근하는 영역을 말합니다. DAO 혹은 @Repository를 사용한 DAO 구현 클래스가 이 계층에 속합니다. 즉 데이터베이스에 원하는 데이터를 Create, Read, Update, Delete하는 곳 입니다.

 

DTOs

DTO는 단순이 데이터를 저장하는 컨테이너, 즉 Data Transfer Object로, 다른 계층 간 교환을 위한 객체(Java Beans)입니다. 뷰 템플릿 엔진에서 사용될 객체, Repository Layer에서 결과로 넘겨 준 객체 등이 DTO에 해당합니다. 도메인 객체를 직접 View에 전달할 수도 있지만, 민감한 도메인 비즈니스 기능이 노출될 수 있으며 모델과 뷰 사이에 의존성이 생기기 때문입니다.

// User.java
public class User{
public Long id;
    public String name;
    public String email;
    public String password;	// 외부에 노출되면 안되는 정보
    public String detailInformation;	// 외부에 노출되면 안되는 정보

}



// UserController.java
@GetMapping("/{id}") // 예시 주소입니다.
public ResponseEntity<User> showArticle(@PathVariable long id){
    User user = userService.findById(id);
    return ResponseEntity.ok().body(user);
}

이 처럼 컨트롤러가 클라이언트의 요청에 대한 응답으로 도메인 객체인 User를 넘겨주면 몇 가지 문제점이 발생합니다.

1. 도메인 객체의 모든 속성이 외부에 노출됩니다.

2. 사용하지 않는 객체의 속성까지 전달하게 됩니다.

3. Model과 View가 서로 결합되어 View의 수정사항이 생길 때 Model에 영향을 미칠 수 있습니다.

4. 전달받은 객체를 이용해 다른 함수를 호출할 수 있습니다.

 

// UserDto.java
public class UserDto{
    public final long id;
    public final String name;
    public final String email;
    
    public static UserDto from(User user){
    	return new UserDto(user.getId(), user.getName(), user.getEmail());
    }
}


// UserController.java
@GetMapping
public ResponseEntity<UserDto> showArticle(@PathVariable long id){
    User user = userService.findById(id);
    return ResponseEntity.ok().body(UserDto.from(user));
}

반면 DTO를 사용하면 앞서 언급된 문제들을 해결할 수 있습니다. 도메인 객체를 캡슐화하고, 화면에서 사용하는 데이터만 선택적으로 보낼 수 있습니다. password, detailInformation 같은 정보는 전달하지 않는 것을 볼 수 있습니다.

즉, DTO는 클라이언트 요청에 포함된 데이터를 담아 서버에 전달하고, 서버로 부터 응답 데이터를 담아 클라이언트에 전달하는 계층간 전달자 역할을 하는 것입니다.

 

 


Reference

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 이동욱

https://tecoble.techcourse.co.kr/post/2021-04-25-dto-layer-scope/

 

댓글