스프링 프로젝트 계층 구조 설계하기 (레이어 아키텍처)
API를 만들기 위해 총 3개의 클래스가 필요하다.
- Request 데이터를 받을 Dto
- API 요청을 받을 Controller
- 트랜잭션, 도메인 기능 간의 순서를 보장하는 Service
여기서 많이 오해하고 있는 부분이 Service에서 비즈니스 로직을 처리해야 한다는 것이다. 하지만 전혀 그렇지 않다. Service는 트랜잭션, 도메인 간 순서 보장의 역할만 한다.
그럼 비즈니스 로직은 누가 처리할까?
바로, 도메인 Domain이다.
Spring 웹 계층 구조
Web 계층
- 흔히 사용하는 컨트롤러(@Controller)와 JSP/ Freemarker 등의 뷰 템플릿 영역이다.
- 이외에도 필터(@Fiilter), 인터셉터, 컨트롤러 어드바이스(@Controller Advice)등 외부 요청과 응답에 대한 전반적인 영역을 나타낸다.
Service 계층
- @Service가 사용되는 서비스 영역이다.
- 일반적으로 Controller와 Dao의 중간 영역에서 사용된다.
- @Transactional이 사용되어야 하는 영역이기도 하다.
Repository 계층
- Database와 같이 데이터 저장소에 접근하는 영역이다.
- Dao(Data Access Object)영역이라고 불리기도 한다.
Dtos
- Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체를 이야기하며 Dtos는 이들의 영역을 말한다.
- ex) 뷰 템플릿 엔진에서 사용될 객체나 Repositoy 계층에서 결과로 넘겨준 객체 등이 Dto이다.
Domian Model
- 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고 공유할 수 있도록 단순화 시킨 것을 도메인 모델이라고 한다.
- 비즈니스 로직을 처리한다.
- @Entity가 사용된 영역 역시 도메인 모델이다.
- 다만, 무조건 데이터베이스의 테이블과 관계가 있어야 하는 것은 아니다. (VO처럼 값 객체들도 도메인 모델에 해당하기 때문)
Spring 계층 간 흐름도
비즈니스 로직을 도메인에 넣는 이유
기존에 Service로 비즈니스 로직을 처리하던 방식은 트랜잭션 스크립트라고 한다. 모든 로직이 서비스 클래스 내부에서 처리하면 서비스 계층이 무의미하며, 객체란 단순히 데이터 덩어리 역할만 하게 된다.
Service에서 비즈니스 로직 처리
@Transactional
public Order cancelOrder(int orderId){
OrdersDto order = ordersDao.selectOrders(orderId);
BillingDto billing = billingDao.selectBilling(orderId);
DeliveryDto delivery = deliveryDao.selectDelivery(orderId);
String deliveryStatus = delivery.getStatus();
// "배송 취소" 해야 하는지 확인
if("IN_PROGRESS".equals(deliveryStatus)){
// 배송 취소로 변경
delivery.setStatus("CANCEL");
deliveryDao.update(delivery);
}
// 각 테이블 취소 상태 update
order.setStatus("CANCEL");
ordersDao.update(order);
billing.setStatus("CANCEL");
billingDao.update(billing);
return order;
}
반면 도메인에서 처리할 경우
order, billing, delivery가 각자 본인의 취소 이벤트 처리를 하며, 서비스 메소드는 트랜잭션과 도메인 간의 순서만 보장해준다.
@Transactional
public Order cancelOrder(int orderId){
OrdersDto order = ordersRepository.findById(orderId);
BillingDto billing = billingRepository.findByOrderId(orderId);
DeliveryDto delivery = deliveryRepository.findByOrderId(orderId);
// 배송 취소 로직
delivery.cancel();
// 각 테이블에 취소 update
order.cancel();
delivery.cancel();
return order;
}
예시 - 게시글 저장하기 코드
PostsApiController
@RestController
@RequiredArgsConstructor
public class PostsApiController {
private final PostsService postsService;
/**
* 게시글 등록
*/
@PostMapping("/api/v1/posts")
public Long save(@RequestBody PostsSaveRequestDto requestDto){
return postsService.save(requestDto);
}
}
PostsService
@Service
@RequiredArgsConstructor
public class PostsService {
private final PostsRepository postsRepository;
/**
* 게시글 저장 (트랜잭션 처리)
*/
@Transactional
public Long save(PostsSaveRequestDto requestDto){
return postsRepository.save(requestDto.toEntity()).getId();
}
}
다양한 의존성 주입 방법
- @Autowired
- setter
- 생성자 주입
→ 이 중 가장 권하는 방식은 생성자로 주입받는 방식이다. (이유 : 객체 불변성 확보, 테스트 코드 작성 용이)
PostsSaveRequestDto
/**
* 게시글 저장 요청 Dto
*/
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
@Builder
public PostsSaveRequestDto(String title, String content, String author){
this.title = title;
this.content = content;
this.author = author;
}
public Posts toEntity(){
return Posts.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
※참고
'Dot Programming > Spring' 카테고리의 다른 글
[Spring] 스프링으로 OAuth2 로그인 구현하기2 - 네이버 (0) | 2021.06.25 |
---|---|
[Spring] 스프링으로 OAuth2 로그인 구현하기1 - 구글 (0) | 2021.06.25 |
[Spring] Spring Data JPA란 무엇인가? (0) | 2021.06.24 |
[Spring/ AWS] S3에 있는 파일(CSV) 다운로드없이 바로 읽기 (0) | 2021.05.22 |
[Spring/ AWS] Spring Boot S3를 이용하여 File 다운로드하기 (0) | 2021.05.22 |