Dot Programming/Spring

[Spring] 스프링 프로젝트 계층 구조 설계하기 (레이어 아키텍처)

루지 2021. 6. 24. 09:40

    스프링 프로젝트 계층 구조 설계하기 (레이어 아키텍처)

    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();
        }
    
    }
    

     

    다양한 의존성 주입 방법

    1. @Autowired
    2. setter
    3. 생성자 주입

    → 이 중 가장 권하는 방식은 생성자로 주입받는 방식이다.  (이유 : 객체 불변성 확보, 테스트 코드 작성 용이)

     

    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();
        }
    }
    

    ※참고

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