[Spring] Cookie(쿠키)를 활용한 최근 본 상품 기능 구현 (Session과 비교)

그림과 같이 클릭했던 상품들"최근 본 상품"이라는 목록에 보여주는 기능을 추가하게 되었다.

이 기능을 구현하기 위해서는 Cookie와 Session을 사용해 구현할 수 있는데, 여기서 고민에 빠졌다.

고민하게 된 이유는 다음과 같다.

 

Cookie와 Session방식을 통해 "최근 본 상품"기능을 구현했을 때의 장단점

먼저 Cookie기본적으로 문자열 형태로 데이터를 저장하며, 복잡한 데이터 타입인 리스트(List)와 같은 자료구조를 그대로 Cookie에 저장하는 것은 지원되지 않는다.

그렇기 때문에 복잡한 데이터 타입을 저장하려면 추가적인 로직이 필요하다. (필자는 objectMapper로 JSON 데이터로 변환하여 저장하였다.)

게다가 최근 본 상품에는 이미지URL로 해당 상품의 이미지를 보여준다.

여기서 Cookie를 사용한 방식이 더 복잡해지는데 Cookie를 사용하면 해당 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";
    }
}