서비스란?
- 레포지토리와 컨트롤러 사이에서 세부적인 비즈니스 로직을 처리하는 계층을 서비스 계층이라고 합니다.
- 컨트롤러에서 레포지토리를 호출하여 모든 로직을 처리할 수 있지만, 그러한 코드는 단일 책임 원칙을 위반하게 됩니다. 코드의 유지 보수를 위해서라도 코드는 단일 책임 원칙에 따르는 것이 좋습니다. 서비스층을 만들어 DB접근, DTO생성등을 행하게 만든다면 컨트롤러는 URL맵핑에 집중 할 수 있습니다.
서비스의 필요성
모듈화
- 서비스는 DB에 접근하여 데이터를 다루는 어떠한 기능인데, 만약 이를 서비스 파일에 저장해놓는다면 필요할 때 서비스를 호출하여 실행하면 될 것입니다. 하지만 만약 서비스 파일을 생성하지 않는다면 그 기능이 필요한 모든 컨트롤러마다 똑같은 로직을 반복하게 될 것입니다. 따라서 이러한 중복을 줄이기 위해 모듈화의 관점에서 서비스 파일은 필요합니다.
보안
- 만약 컨트롤러에 레포지토리를 다루는 로직을 남겨둔다면, 컨트롤러에 대한 접근을 탈취당했을 때 레포지토리 즉, DB에 대한 접근 또한 탈취당한 것입니다. 이러한 불상사를 막기 위해 컨트롤러에서 DB에 대한 직접적인 접근은 최대한 삼가는 것이 좋습니다.
단일책임원칙
- 앞서 잠깐 언급했듯이 컨트롤러에서 URL맵핑이외에 DTO생성, 비즈니스 로직, 트랜잭션 관리, 예외처리등을 하게 된다면 코드는 매우 복잡해지고 보안적인 측면에 좋지 않을 것입니다. 따라서 서비스 계층을 만들어 DB에 접근하는 로직들을 처리하게 만들어 각자의 책임을 명확하게 만드는 편이 좋습니다.
@Service
package com.example.kakao.cart;
import com.example.kakao._core.errors.exception.Exception400;
import com.example.kakao._core.errors.exception.Exception404;
import com.example.kakao.product.option.Option;
import com.example.kakao.product.option.OptionJPARepository;
import com.example.kakao.user.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.HashSet;
import java.util.Optional;
import java.util.Map;
import java.util.HashMap;
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class CartService {
private final CartJPARepository cartJPARepository;
private final OptionJPARepository optionJPARepository;
// 트랜잭션 관리
@Transactional
public void addCartList(List<CartRequest.SaveDTO> requestDTOs, User sessionUser) {
Set<Integer> uniqueOptionIds = new HashSet<>();
for (CartRequest.SaveDTO requestDTO : requestDTOs) {
Integer optionId = requestDTO.getOptionId();
Integer quantity = requestDTO.getQuantity();
// 1. null값이 들어왔을 때 예외처리
if (optionId == null || quantity == null) {
throw new Exception400("잘못된 요청입니다 : 인덱스:" + optionId + " 수량:" + quantity);
}
// 2. 애초에 요청할 때 동일한 옵션이 들어오면 예외처리
// [ { optionId:1, quantity:5 }, { optionId:1, quantity:10 } ]
if (!uniqueOptionIds.add(optionId)) {
throw new Exception400("중복된 요청입니다 : " + optionId);
}
// 3. 유저fk와 옵션fk가 일치하는 기존에 존재하는 장바구니에 대해서는 수량을 추가하는 방식으로 함.
Optional<Cart> existingCart = cartJPARepository.findByOptionIdAndUserId(optionId, sessionUser.getId());
if (existingCart.isPresent()) {
Cart cart = existingCart.get();
cart.setQuantity(cart.getQuantity() + quantity);
// DB 접근
cartJPARepository.save(cart);
}
// 4. 기존에 존재하지 않을 경우 새로운 cart생성
else {
Option optionPS = optionJPARepository.findById(optionId)
.orElseThrow(() -> new Exception404("해당 옵션을 찾을 수 없습니다 : " + optionId));
int price = optionPS.getPrice() * quantity;
Cart cart = Cart.builder().user(sessionUser).option(optionPS).quantity(quantity).price(price).build();
cartJPARepository.save(cart);
}
}
}
public CartResponse.FindAllDTO findAll(User user) {
List<Cart> cartList = cartJPARepository.findByUserIdOrderByOptionIdAsc(user.getId());
// DTO 생성
return new CartResponse.FindAllDTO(cartList);
}
}
- 스프링 부트는 편리하게도 클래스명 위에 @Service 어노테이션을 붙여주면 서비스 파일로 자동 인식합니다.
참조
'웹개발' 카테고리의 다른 글
@ResponseStatus를 이용하여 커스텀 예외를 만드는 방법 (0) | 2023.09.02 |
---|---|
타임리프를 통한 템플릿 상속 (0) | 2023.08.12 |
리다이렉트, 포워드 (0) | 2023.08.08 |
타임리프 템플릿에 데이터 전달하기 (0) | 2023.08.04 |
JPA와 H2서버 사용하기 (0) | 2023.06.18 |