반응형
service(CartService, ProductItemService, ProductSearchService, ProductService)
CartService
package com.zerobase.cms.order.service;
import com.zerobase.cms.order.client.RedisClient;
import com.zerobase.cms.order.domain.product.AddProductCartForm;
import com.zerobase.cms.order.domain.redis.Cart;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class CartService {
private final RedisClient redisClient;
public Cart getCart(Long customerId) {
Cart cart = redisClient.get(customerId, Cart.class);
return cart != null ? cart : new Cart();
}
public Cart putCart(Long customerId, Cart cart) {
redisClient.put(customerId, cart);
return cart;
}
public Cart addCart(Long customerId, AddProductCartForm form) {
Cart cart = redisClient.get(customerId, Cart.class);
if (cart == null) {
cart = new Cart();
cart.setCustomerId(customerId);
}
// 이전에 같은 상품이 있는지 확인
Optional<Cart.Product> productOptional = cart.getProducts().stream()
.filter(product -> product.getId().equals(form.getId()))
.findFirst();
if(productOptional.isPresent()) {
Cart.Product redisProduct = productOptional.get();
// 요청한 아이템
List<Cart.ProductItem> items = form.getItems().stream()
.map(Cart.ProductItem::from).collect(Collectors.toList());
Map<Long, Cart.ProductItem> redisMap = redisProduct.getItems().stream()
.collect(Collectors.toMap(it -> it.getId(), it->it));
if(!redisProduct.getName().equals(form.getName())) {
cart.addMessage(redisProduct.getName() + "의 정보가 변경되었습니다. 확인 부탁드립니다.");
}
for (Cart.ProductItem item : items) {
Cart.ProductItem redisItem = redisMap.get(item.getId());
if(redisItem == null) {
redisProduct.getItems().add(item);
} else {
if (redisItem.getPrice().equals(item.getPrice())) {
cart.addMessage(redisProduct.getName() + item.getName()
+ "의 가격이 변경되었습니다. 확인 부탁드립니다.");
}
redisItem.setCount(redisItem.getCount() + item.getCount());
}
}
} else {
Cart.Product product = Cart.Product.from(form);
cart.getProducts().add(product);
}
redisClient.put(customerId, cart);
return cart;
}
}
1. 클래스 선언
- @Service: Spring 프레임워크에게 이 클래스가 서비스(Service) 역할을 한다는 것을 알려줍니다. 서비스는 비즈니스 로직을 처리하는 역할을 합니다.
- @Slf4j: Lombok 어노테이션입니다. 이 클래스에서 로깅을 사용할 수 있도록 log 필드를 자동으로 생성해줍니다.
- @RequiredArgsConstructor: Lombok 어노테이션입니다. 이 클래스의 final 필드인 redisClient를 매개변수로 받는 생성자를 자동으로 생성해줍니다.
- public class CartService: CartService라는 이름의 클래스를 정의합니다.
- public: 이 클래스는 어디서든 접근 가능합니다.
2. 필드 선언
- private final RedisClient redisClient: Redis 데이터베이스와 통신하는 클라이언트 객체를 저장하는 redisClient 필드를 선언합니다.
- private: 이 필드는 CartService 클래스 내에서만 접근 가능합니다.
- final: 이 필드는 한 번 값이 할당되면 변경할 수 없습니다. 즉, CartService 객체가 생성된 후에는 redisClient 필드를 변경할 수 없습니다.
- RedisClient: Redis 데이터베이스와 통신하는 클라이언트 클래스입니다.
3. 장바구니 조회 메서드
- public Cart getCart(Long customerId): 고객 ID를 받아서 해당 고객의 장바구니 정보를 조회하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- Cart: 이 메서드는 Cart 객체를 반환합니다.
- Long customerId: 고객 ID를 나타내는 매개변수입니다.
- Cart cart = redisClient.get(customerId, Cart.class): redisClient를 사용하여 Redis 데이터베이스에서 customerId에 해당하는 장바구니 데이터를 가져옵니다.
- redisClient.get(customerId, Cart.class): Redis 데이터베이스에서 customerId를 키로 사용하여 Cart 클래스 타입의 데이터를 가져오는 메서드입니다.
- Cart.class: 가져올 데이터의 타입을 지정합니다.
- return cart != null ? cart : new Cart(): 가져온 장바구니 데이터가 존재하면 해당 데이터를 반환하고, 존재하지 않으면 새로운 Cart 객체를 생성하여 반환합니다.
- cart != null ? cart : new Cart(): 삼항 연산자입니다. cart가 null이 아니면 cart를 반환하고, null이면 new Cart()를 반환합니다.
4. 장바구니 저장 메서드
- public Cart putCart(Long customerId, Cart cart): 고객 ID와 장바구니 데이터를 받아서 Redis 데이터베이스에 저장하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- Cart: 이 메서드는 Cart 객체를 반환합니다.
- Long customerId: 고객 ID를 나타내는 매개변수입니다.
- Cart cart: 저장할 장바구니 데이터를 나타내는 매개변수입니다.
- redisClient.put(customerId, cart): redisClient를 사용하여 Redis 데이터베이스에 customerId를 키로 하여 cart 데이터를 저장합니다.
- redisClient.put(customerId, cart): Redis 데이터베이스에 데이터를 저장하는 메서드입니다.
- return cart: 저장된 장바구니 데이터를 반환합니다.
5. 장바구니에 상품 추가 메서드
- public Cart addCart(Long customerId, AddProductCartForm form): 고객 ID와 상품 추가 폼 데이터를 받아서 장바구니에 상품을 추가하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- Cart: 이 메서드는 Cart 객체를 반환합니다.
- Long customerId: 고객 ID를 나타내는 매개변수입니다.
- AddProductCartForm form: 상품 추가 폼 데이터를 나타내는 매개변수입니다.
- Cart cart = redisClient.get(customerId, Cart.class): redisClient를 사용하여 Redis 데이터베이스에서 customerId에 해당하는 장바구니 데이터를 가져옵니다.
- if (cart == null): 가져온 장바구니 데이터가 존재하지 않으면 새로운 Cart 객체를 생성하고, 고객 ID를 설정합니다.
- Optional<Cart.Product> productOptional = cart.getProducts().stream() ... findFirst(): 장바구니에 이미 같은 상품이 있는지 확인합니다.
- cart.getProducts().stream(): 장바구니에 있는 상품 목록을 스트림으로 변환합니다.
- .filter(product -> product.getId().equals(form.getId())): 스트림에서 상품 ID가 폼 데이터의 상품 ID와 같은 상품만 필터링합니다.
- .findFirst(): 필터링된 상품 중에서 첫 번째 상품을 Optional 객체로 반환합니다. Optional은 값이 있을 수도 있고 없을 수도 있는 컨테이너 클래스입니다.
- if(productOptional.isPresent()): 같은 상품이 이미 장바구니에 있으면 해당 상품의 수량을 증가시키고, 상품 정보가 변경되었는지 확인합니다.
- Cart.Product redisProduct = productOptional.get(): Optional 객체에서 Cart.Product 객체를 가져옵니다.
- List<Cart.ProductItem> items = form.getItems().stream() ... collect(Collectors.toList()): 폼 데이터에서 상품 아이템 목록을 추출합니다.
- form.getItems().stream(): form에서 아이템 목록을 가져와 스트림으로 변환
- .map(Cart.ProductItem::from): 각 아이템을 Cart.ProductItem.from() 메서드를 사용하여 Cart.ProductItem 객체로 변환합니다.
- .collect(Collectors.toList()): 스트림의 결과들을 리스트로 수집합니다.
- Map<Long, Cart.ProductItem> redisMap = redisProduct.getItems().stream() ... collect(Collectors.toMap(it -> it.getId(), it->it)): 이미 저장된 상품 아이템 목록을 맵으로 변환합니다.
- redisProduct.getItems().stream(): redis에 저장된 상품의 아이템 목록을 스트림으로 변환
- .collect(Collectors.toMap(it -> it.getId(), it->it)): 스트림의 결과들을 맵으로 수집합니다. 키는 아이템의 ID, 값은 아이템 객체입니다.
- if(!redisProduct.getName().equals(form.getName())): 상품 이름이 변경되었으면 장바구니에 메시지를 추가합니다.
- for (Cart.ProductItem item : items): 폼 데이터의 상품 아이템 목록을 순회하면서, 이미 저장된 상품 아이템이 있는지 확인하고, 없으면 추가하고, 있으면 수량을 증가시킵니다.
- Cart.ProductItem redisItem = redisMap.get(item.getId()): 맵에서 아이템 ID에 해당하는 아이템을 가져옵니다.
- if(redisItem == null): redis에 해당 아이템이 없다면 redisProduct에 해당 item을 추가합니다.
- else: redis에 해당 item이 있다면 가격을 확인하고, 가격이 변경되었다면 장바구니에 메시지를 추가하고, 수량을 증가시킵니다.
- else: 같은 상품이 장바구니에 없으면 새로운 Cart.Product 객체를 생성하고, 장바구니에 추가합니다.
- Cart.Product product = Cart.Product.from(form): 폼 데이터를 사용하여 Cart.Product 객체를 생성합니다.
- cart.getProducts().add(product): 장바구니에 새로운 Cart.Product 객체를 추가합니다.
- redisClient.put(customerId, cart): redisClient를 사용하여 Redis 데이터베이스에 변경된 장바구니 데이터를 저장합니다.
- return cart: 변경된 장바구니 데이터를 반환합니다.
요약
이 코드는 사용자의 장바구니 기능을 관리하는 CartService 클래스입니다. Redis 데이터베이스를 사용하여 장바구니 데이터를 저장하고 관리하며, 장바구니 조회, 상품 추가 등의 기능을 제공합니다. 특히, 스트림 API를 활용하여 장바구니에 상품을 추가하는 로직을 간결하게 구현했습니다. Lombok 라이브러리를 사용하여 코드의 보일러플레이트(반복적인 코드)를 줄이고, 가독성을 높였습니다.
- Redis 데이터베이스: Redis는 "Remote Dictionary Server"의 약자로, 인메모리 데이터 저장소입니다. 데이터를 RAM에 저장하기 때문에 디스크에 저장하는 데이터베이스보다 훨씬 빠릅니다. 장바구니 데이터와 같이 자주 접근하는 데이터를 저장하는 데 적합합니다.
- 스트림 API: 자바 8부터 도입된 스트림 API는 컬렉션 데이터를 처리하는 새로운 방법을 제공합니다. 스트림 API를 사용하면 데이터를 필터링, 정렬, 변환하는 등의 작업을 더 간결하고 효율적으로 수행할 수 있습니다.
- Lombok 라이브러리: Lombok은 자바 개발 시 반복적으로 작성해야 하는 코드를 자동으로 생성해주는 라이브러리입니다. getter/setter 메서드, 생성자, toString() 메서드 등을 자동으로 생성해줌으로써 코드의 양을 줄이고, 가독성을 높일 수 있습니다.
ProductItemService
package com.zerobase.cms.order.service;
import com.zerobase.cms.order.domain.model.Product;
import com.zerobase.cms.order.domain.model.ProductItem;
import com.zerobase.cms.order.domain.product.AddProductItemForm;
import com.zerobase.cms.order.domain.product.UpdateProductItemForm;
import com.zerobase.cms.order.domain.repository.ProductItemRepository;
import com.zerobase.cms.order.domain.repository.ProductRepository;
import com.zerobase.cms.order.exception.CustomException;
import com.zerobase.cms.order.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ProductItemService {
private final ProductRepository productRepository;
private final ProductItemRepository productItemRepository;
@Transactional
public ProductItem getProductItem(Long id) {
return productItemRepository.getById(id);
}
@Transactional
public Product addProductItem(Long sellerId, AddProductItemForm form) {
Product product = productRepository.findBySellerIdAndId(sellerId, form.getProductId())
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_PRODUCT));
if (product.getProductItems().stream()
.anyMatch(item -> item.getName().equals(form.getName()))) {
throw new CustomException(ErrorCode.SAME_ITEM_NAME);
}
ProductItem productItem = ProductItem.of(sellerId, form);
product.getProductItems().add(productItem);
return product;
}
@Transactional
public ProductItem updateProductItem(Long sellerId, UpdateProductItemForm form) {
ProductItem productItem = productItemRepository.findById(form.getId())
.filter(pi -> pi.getSellerId().equals(sellerId))
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ITEM));
productItem.setName(form.getName());
productItem.setCount(form.getCount());
productItem.setPrice(form.getPrice());
return productItem;
}
@Transactional
public void deleteProductItem(Long sellerId, Long productItemId) {
ProductItem productItem = productItemRepository.findById(productItemId)
.filter(pi -> pi.getSellerId().equals(sellerId))
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ITEM));
productItemRepository.delete(productItem);
}
}
클래스 선언
- @Service: Spring 프레임워크에게 이 클래스가 서비스(Service) 역할을 한다는 것을 알려줍니다. 서비스는 비즈니스 로직을 처리하는 역할을 합니다.
- @RequiredArgsConstructor: Lombok 어노테이션입니다. 이 클래스의 final 필드인 productRepository와 productItemRepository를 매개변수로 받는 생성자를 자동으로 생성해줍니다.
- public class ProductItemService: ProductItemService라는 이름의 클래스를 정의합니다.
- public: 이 클래스는 어디서든 접근 가능합니다.
4. 필드 선언
- private final ProductRepository productRepository: 상품 데이터를 데이터베이스에 접근하는 ProductRepository 객체를 저장하는 productRepository 필드를 선언합니다.
- private: 이 필드는 ProductItemService 클래스 내에서만 접근 가능합니다.
- final: 이 필드는 한 번 값이 할당되면 변경할 수 없습니다. 즉, ProductItemService 객체가 생성된 후에는 productRepository 필드를 변경할 수 없습니다.
- ProductRepository: 상품 데이터를 데이터베이스에 접근하는 인터페이스입니다.
- private final ProductItemRepository productItemRepository: 상품 아이템 데이터를 데이터베이스에 접근하는 ProductItemRepository 객체를 저장하는 productItemRepository 필드를 선언합니다.
- private: 이 필드는 ProductItemService 클래스 내에서만 접근 가능합니다.
- final: 이 필드는 한 번 값이 할당되면 변경할 수 없습니다. 즉, ProductItemService 객체가 생성된 후에는 productItemRepository 필드를 변경할 수 없습니다.
- ProductItemRepository: 상품 아이템 데이터를 데이터베이스에 접근하는 인터페이스입니다.
5. 상품 아이템 조회 메서드
- @Transactional: 이 메서드를 트랜잭션으로 묶어줍니다. 트랜잭션은 데이터베이스의 상태를 변화시키는 작업의 단위입니다. 트랜잭션은 ACID (Atomicity, Consistency, Isolation, Durability) 속성을 보장해야 합니다.
- Atomicity: 트랜잭션 내의 모든 작업은 완전히 성공하거나 완전히 실패해야 합니다.
- Consistency: 트랜잭션이 완료된 후에도 데이터베이스의 무결성이 유지되어야 합니다.
- Isolation: 동시에 실행되는 트랜잭션은 서로 영향을 미치지 않아야 합니다.
- Durability: 트랜잭션이 성공적으로 완료되면 그 결과는 영구적으로 데이터베이스에 저장되어야 합니다.
- public ProductItem getProductItem(Long id): 상품 아이템 ID를 받아서 해당 상품 아이템 정보를 조회하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- ProductItem: 이 메서드는 ProductItem 객체를 반환합니다.
- Long id: 상품 아이템 ID를 나타내는 매개변수입니다.
- return productItemRepository.getById(id): productItemRepository를 사용하여 데이터베이스에서 ID에 해당하는 상품 아이템 정보를 가져와서 반환합니다.
- productItemRepository.getById(id): ProductItemRepository 인터페이스에 정의된 getById 메서드를 호출하여 데이터베이스에서 상품 아이템 정보를 가져옵니다.
6. 상품 아이템 추가 메서드
- @Transactional: 이 메서드를 트랜잭션으로 묶어줍니다.
- public Product addProductItem(Long sellerId, AddProductItemForm form): 판매자 ID와 상품 아이템 추가 폼 데이터를 받아서 상품 아이템을 추가하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- Product: 이 메서드는 Product 객체를 반환합니다.
- Long sellerId: 판매자 ID를 나타내는 매개변수입니다.
- AddProductItemForm form: 상품 아이템 추가 폼 데이터를 나타내는 매개변수입니다.
- Product product = productRepository.findBySellerIdAndId(sellerId, form.getProductId()) ...: 판매자 ID와 상품 ID를 사용하여 데이터베이스에서 상품 정보를 가져옵니다.
- productRepository.findBySellerIdAndId(sellerId, form.getProductId()): ProductRepository 인터페이스에 정의된 findBySellerIdAndId 메서드를 호출하여 데이터베이스에서 상품 정보를 가져옵니다.
- .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_PRODUCT)): 상품 정보를 가져오지 못하면 NOT_FOUND_PRODUCT 에러 코드를 사용하여 CustomException 예외를 발생시킵니다.
- if (product.getProductItems().stream() ...): 상품에 이미 같은 이름의 상품 아이템이 있는지 확인합니다.
- product.getProductItems().stream(): 상품에 속한 상품 아이템 목록을 스트림으로 변환합니다.
- .anyMatch(item -> item.getName().equals(form.getName())): 스트림에서 상품 아이템 이름이 폼 데이터의 상품 아이템 이름과 같은 상품 아이템이 하나라도 있는지 확인합니다.
- throw new CustomException(ErrorCode.SAME_ITEM_NAME): 상품에 이미 같은 이름의 상품 아이템이 있으면 SAME_ITEM_NAME 에러 코드를 사용하여 CustomException 예외를 발생시킵니다.
- ProductItem productItem = ProductItem.of(sellerId, form): 판매자 ID와 폼 데이터를 사용하여 새로운 ProductItem 객체를 생성합니다.
- ProductItem.of(sellerId, form): ProductItem 클래스에 정의된 of 메서드를 호출하여 ProductItem 객체를 생성합니다.
- product.getProductItems().add(productItem): 상품에 새로운 상품 아이템을 추가합니다.
- return product: 변경된 상품 정보를 반환합니다.
7. 상품 아이템 수정 메서드
- @Transactional: 이 메서드를 트랜잭션으로 묶어줍니다.
- public ProductItem updateProductItem(Long sellerId, UpdateProductItemForm form): 판매자 ID와 상품 아이템 수정 폼 데이터를 받아서 상품 아이템을 수정하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- ProductItem: 이 메서드는 ProductItem 객체를 반환합니다.
- Long sellerId: 판매자 ID를 나타내는 매개변수입니다.
- UpdateProductItemForm form: 상품 아이템 수정 폼 데이터를 나타내는 매개변수입니다.
- ProductItem productItem = productItemRepository.findById(form.getId()) ...: 상품 아이템 ID를 사용하여 데이터베이스에서 상품 아이템 정보를 가져옵니다.
- productItemRepository.findById(form.getId()): ProductItemRepository 인터페이스에 정의된 findById 메서드를 호출하여 데이터베이스에서 상품 아이템 정보를 가져옵니다.
- .filter(pi -> pi.getSellerId().equals(sellerId)): 가져온 상품 아이템의 판매자 ID가 폼 데이터의 판매자 ID와 같은지 확인합니다.
- .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ITEM)): 상품 아이템 정보를 가져오지 못하거나 판매자 ID가 일치하지 않으면 NOT_FOUND_ITEM 에러 코드를 사용하여 CustomException 예외를 발생시킵니다.
- productItem.setName(form.getName()): 상품 아이템 이름을 폼 데이터의 상품 아이템 이름으로 변경합니다.
- productItem.setCount(form.getCount()): 상품 아이템 수량을 폼 데이터의 상품 아이템 수량으로 변경합니다.
- productItem.setPrice(form.getPrice()): 상품 아이템 가격을 폼 데이터의 상품 아이템 가격으로 변경합니다.
- return productItem: 변경된 상품 아이템 정보를 반환합니다.
8. 상품 아이템 삭제 메서드
- @Transactional: 이 메서드를 트랜잭션으로 묶어줍니다.
- public void deleteProductItem(Long sellerId, Long productItemId): 판매자 ID와 상품 아이템 ID를 받아서 상품 아이템을 삭제하는 메서드입니다.
- public: 이 메서드는 어디서든 호출 가능합니다.
- void: 이 메서드는 반환값이 없습니다.
- Long sellerId: 판매자 ID를 나타내는 매개변수입니다.
- Long productItemId: 상품 아이템 ID를 나타내는 매개변수입니다.
- ProductItem productItem = productItemRepository.findById(productItemId) ...: 상품 아이템 ID를 사용하여 데이터베이스에서 상품 아이템 정보를 가져옵니다.
- productItemRepository.findById(productItemId): ProductItemRepository 인터페이스에 정의된 findById 메서드를 호출하여 데이터베이스에서 상품 아이템 정보를 가져옵니다.
- .filter(pi -> pi.getSellerId().equals(sellerId)): 가져온 상품 아이템의 판매자 ID가 폼 데이터의 판매자 ID와 같은지 확인합니다.
- .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ITEM)): 상품 아이템 정보를 가져오지 못하거나 판매자 ID가 일치하지 않으면 NOT_FOUND_ITEM 에러 코드를 사용하여 CustomException 예외를 발생시킵니다.
- productItemRepository.delete(productItem): productItemRepository를 사용하여 데이터베이스에서 상품 아이템을 삭제합니다.
- productItemRepository.delete(productItem): ProductItemRepository 인터페이스에 정의된 delete 메서드를 호출하여 데이터베이스에서 상품 아이템을 삭제합니다.
요약
이 코드는 상품 아이템을 관리하는 ProductItemService 클래스입니다. 상품 아이템 조회, 추가, 수정, 삭제 기능을 제공하며, 데이터베이스 연동 및 예외 처리를 수행합니다. Spring 프레임워크의 @Service 어노테이션을 사용하여 서비스 클래스로 등록하고, @Transactional 어노테이션을 사용하여 트랜잭션을 관리합니다. 또한, Lombok 라이브러리를 사용하여 코드의 보일러플레이트(반복적인 코드)를 줄이고, 가독성을 높였습니다.
- 트랜잭션(Transaction): 트랜잭션은 데이터베이스의 상태를 변화시키는 작업의 단위입니다. 트랜잭션은 ACID (Atomicity, Consistency, Isolation, Durability) 속성을 보장해야 합니다. 트랜잭션을 사용하면 데이터베이스의 무결성을 유지하고, 오류 발생 시 롤백(rollback)하여 데이터베이스의 상태를 이전 상태로 되돌릴 수 있습니다.
- 예외 처리(Exception Handling): 예외 처리는 프로그램 실행 중에 발생할 수 있는 예외 상황을 처리하는 방법입니다. 예외 처리를 통해 프로그램이 비정상적으로 종료되는 것을 방지하고, 사용자에게 유용한 오류 메시지를 제공할 수 있습니다.
- Repository: Repository는 데이터베이스에 접근하는 인터페이스입니다. Spring Data JPA를 사용하면 Repository 인터페이스만 정의하면 Spring이 자동으로 구현체를 생성해줍니다. Repository를 사용하면 데이터베이스 연동 코드를 간결하게 작성할 수 있고, 데이터베이스를 쉽게 교체할 수 있습니다.
반응형