그림과 같이 클릭했던 상품들을 "최근 본 상품"이라는 목록에 보여주는 기능을 추가하게 되었다.
이 기능을 구현하기 위해서는 Cookie와 Session을 사용해 구현할 수 있는데, 여기서 고민에 빠졌다.
고민하게 된 이유는 다음과 같다.
Cookie와 Session방식을 통해 "최근 본 상품"기능을 구현했을 때의 장단점
먼저 Cookie는 기본적으로 문자열 형태로 데이터를 저장하며, 복잡한 데이터 타입인 리스트(List)와 같은 자료구조를 그대로 Cookie에 저장하는 것은 지원되지 않는다.
그렇기 때문에 복잡한 데이터 타입을 저장하려면 추가적인 로직이 필요하다. (필자는 objectMapper로 JSON 데이터로 변환하여 저장하였다.)
게다가 최근 본 상품에는 이미지URL로 해당 상품의 이미지를 보여준다.
여기서 Cookie를 사용한 방식이 더 복잡해지는데 쿠키는 ASCII 문자만 허용하기 때문에
imageURL을 URLEncoder, URLDecoder로 인코딩 디코딩해서 저장하고 꺼내야 하는 복잡성이 추가된다.
반면에 Session은 복잡한 데이터 타입인 리스트(List)와 같은 자료구조를 그대로 Session에 저장할 수 있고, imageUrl을 인코딩, 디코딩을 하지 않고도 저장할 수 있다.
이렇게 까지만 보면 Session 방식으로 하는게 더 유리해보이지만 또 그렇지도 않다.
그 이유는 Cookie와 Session의 특징에서 알수있다.
Cookie와 Session의 특징
쿠키는 클라이언트에 저장된다.
그렇기 때문에 서버의 자원을 사용하지 않는다는 이점이 있다.
하지만 쿠키는 보안에 약해 민간함 정보를 담아서는 안된다.
쿠키는 만료시간 동안 파일로 저장되어 브라우저를 종료해도 정보가 남아있다.
세션은 서버에 저장된다. 그렇기 때문에 보안면에서 쿠키보다 세션이 더 우수하다.
하지만 서버에 요청을 보내는 사용자가 많을 경우 세션은 부하가 심할수 있다.
또한 만료 시간을 설정하더라도 세션 쿠키로 저장되어 브라우저 종료시 제거된다.
특히나 "최근 본 상품"이라는 기능은 해당 상품을 클릭하는 순간 서버에 요청을 보내게 되므로 더 치명적일수있다.
이러한 장단점들을 바탕으로 결정한 방식은 쿠키를 사용해 구현하는 방식이였다.
그 이유는 "최근 본 상품"은 민간함 정보가 포함되어 있지 않아 이 부분에 대해 걱정할 필요는 없다고 생각했기 때문이다.
또 타 쇼핑몰을 보더라도 브라우저 종료 시에도 최근 본 상품 목록을 유지하는 것을 볼 수 있고 이는 사용자 편의성 측면에서 유리하다 생각했다.
그리고 결론적으로 "최근 본 상품" 기능은 사용자별 클릭 빈도가 있을 것으로 판단되어 해당 상품을 클릭하는 순간 서버에 요청을 보내게 되므로 더 치명적일 수 있다.
따라서 Cookie를 사용한 방식이 더 적합하다고 판단하여 Cookie를 사용한 방식으로 구현하기로 하였다.
Cookie를 사용해 구현한 코드
@Controller
@RequiredArgsConstructor
public class RecentProductController {
private final ImageService imageService;
private final RecentProductService recentProductService;
@PostMapping("/recent-products/{productId}")
@ResponseBody
public String addRecentProduct(@PathVariable("productId") Long productId, HttpServletResponse response, HttpServletRequest request) throws IOException {
String recentImage = imageService.selectProductImageUrlByProductId(productId);
recentProductService.saveRecentProductToCookie(productId, response, request, recentImage);
return "success";
}
}
@Service
public class RecentProductService {
private ObjectMapper objectMapper = new ObjectMapper();
public static final String RECENT_PRODUCTS = "recentProducts";
private static final int MAX_RECENT_PRODUCTS_SIZE = 3;
public void saveRecentProductToCookie(Long productId, HttpServletResponse response, HttpServletRequest request, String recentImage) throws IOException {
List<RecentProduct> recentProducts = getRecentProductsFromCookie(request);
if (!recentProducts.contains(recentImage)) {
recentProducts.add(0, new RecentProduct(recentImage, productId));
if (recentProducts.size() > MAX_RECENT_PRODUCTS_SIZE) {
recentProducts.remove(recentProducts.size() - 1);
}
String recentProductsJson = objectMapper.writeValueAsString(recentProducts);
String encodedRecentProductsJson = URLEncoder.encode(recentProductsJson, StandardCharsets.UTF_8.toString());
Cookie cookie = new Cookie(RECENT_PRODUCTS, encodedRecentProductsJson);
cookie.setPath("/");
cookie.setMaxAge(24 * 60 * 60); // Cookie 유효기간 1일로 설정
response.addCookie(cookie);
}
}
public List<RecentProduct> getRecentProductsFromCookie(HttpServletRequest request) throws IOException {
Cookie[] cookies = request.getCookies();
String recentProductCookieValue = getCookieValue(cookies, RECENT_PRODUCTS);
if (recentProductCookieValue != null) {
String decodedCookieValue = URLDecoder.decode(recentProductCookieValue, StandardCharsets.UTF_8.toString());
return objectMapper.readValue(decodedCookieValue, new TypeReference<List<RecentProduct>>() {});
}
return new ArrayList<>();
}
private String getCookieValue(Cookie[] cookies, String cookieName) {
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(cookieName)) {
return cookie.getValue();
}
}
}
return null;
}
}
Session을 사용해 구현한 코드
비교를 위해서 Session을 사용해 구현해보기도 했다.
확실히 Cookie에 비해 구현하기 쉬운것을 느낄수 있었다.
@Controller
@RequiredArgsConstructor
public class RecentProductController {
private final ImageService imageService;
public static final String RECENT_PRODUCTS = "recentProducts";
private static final int MAX_RECENT_PRODUCTS_SIZE = 3;
@PostMapping("/recent-products/{productId}")
@ResponseBody
public String addRecentProduct(@PathVariable("productId") Long productId, HttpSession session) {
List<RecentProduct> recentProducts = (List<RecentProduct>) session.getAttribute("recentProducts");
if (recentProducts == null) {
recentProducts = new ArrayList<>();
}
String recentImage = imageService.selectProductImageUrlByProductId(productId);
if (!recentProducts.contains(recentImage)) {
recentProducts.add(0, new RecentProduct(recentImage, productId));
if (recentProducts.size() > MAX_RECENT_PRODUCTS_SIZE ) {
recentProducts.remove(recentProducts.size() - 1);
}
session.setAttribute(RECENT_PRODUCTS, recentProducts);
}
return "success";
}
}
'◼ Spring' 카테고리의 다른 글
[Spring] 아임포트(import)로 결제 기능 구현하기 (클라이언트 + 서버 코드 포함) (7) | 2023.09.01 |
---|---|
[Spring] 로그인 어노테이션으로 세션 정보가져오기 (Argument Resolver 활용) (0) | 2023.08.31 |
[Spring] 공통 기능을 @Aspect를 사용해 적용하기 (0) | 2023.08.30 |
[Spring] GET요청으로 url 쿼리 파라미터에 포함된 JSON 데이터 사용하기 (0) | 2023.08.30 |
@ModelAttribute가 객체에 바인딩하는 과정 및 방법 (0) | 2023.07.29 |