์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

์กฐํšŒ์ˆ˜ ์ค‘๋ณต ์ฆ๊ฐ€ ๋ฐฉ์ง€ ๋ฐ ์ผ๊ด„ ์—…๋ฐ์ดํŠธ

Seung__Yong 2024. 2. 19. 14:34

๐Ÿ˜์ •๋ฆฌ

  • ์‚ฌ์šฉ์ž ๊ตฌ๋ถ„์€ viewCount๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €๋ณ„ random ๊ฐ’์„ ๊ฐ€์ง„ ์ฟ ํ‚ค๋ฅผ ์ด์šฉ
  • ์ƒํ’ˆ์— ๋Œ€ํ•œ ์ค‘๋ณต ์ ‘๊ทผ ์ฒดํ‚น์€ ๋ธŒ๋ผ์šฐ์ €๋ณ„ ๋žœ๋ค ๊ฐ’์„ Key๋กœ Redis์˜ SET์ด์šฉ(itemId์ €์žฅ)
  • ์‚ฌ์šฉ์ž ๊ตฌ๋ถ„์„ ์œ„ํ•œ ์ฟ ํ‚ค์™€ ์ค‘๋ณต ์ ‘๊ทผ์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์ €์žฅํ•˜๋Š” set์˜ Key๊ฐ’์€ ์ž์ •์— ๋งŒ๋ฃŒ
    Redis์—์„œ itemId๋กœ ์กฐํšŒ์ˆ˜๋ฅผ ์ €์žฅ -> ์ผ์ • ์‹œ๊ฐ„ ํ›„ DB๋ฐ˜์˜ 

 

1. ์ €๋Š” ํ˜น์‹œ ๋ชจ๋ฅผ ์ƒํ™ฉ์— ๋Œ€ํ•ด ๊ตฌํ˜„์—์„œ์˜ ํŽธ์˜์„ฑ์„ ๊ฐ€์ ธ๊ฐ€๊ธฐ ์œ„ํ•ด Redis๋กœ ์ค‘๋ณต์ ‘๊ทผ์„ ์ฒดํ‚นํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜๋‚˜์˜ ์ฟ ํ‚ค์— ์ค‘๋ณต์ ‘๊ทผ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์•˜์„ ๋•Œ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฒŒ์‹œ๊ธ€ 1000๊ฐœ ์ด์ƒ์„ ์ฝ๋Š”๋‹ค๋ฉด ๋ฌธ์ œ ๋ฐœ์ƒ
-> ์ฟ ํ‚ค๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์ด์šฉํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ™•์žฅํ•ด์•ผ ํ•จ 

๊ทธ๋Ÿฌ๋‚˜ ํ•˜๋ฃจ์— 1000๊ฐœ ์ด์ƒ์˜ ๊ฒŒ์‹œ๊ธ€์„ ์ฝ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ฐ๋ณด๋‹ค ๋งŽ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์„๊ฒƒ์ด๊ณ  ๊ทธ๋ ‡๋‹ค๋ฉด ์„œ๋ฒ„์—์„œ ๊ตณ์ด ์ž์›์„ ์†Œ๋ชจํ•  ํ•„์š”๊ฐ€ ์—†๊ธดํ•ฉ๋‹ˆ๋‹ค..!! 

2. ๋งŽ์€ ์‚ฌ์šฉ์ž๋“ค์ด ์ด์šฉํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒํ•˜๊ณ  ์ผ๊ด„ ์—…๋ฐ์ดํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.
10000๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ 10๋ถ„ ๋™์•ˆ ๊ฒŒ์‹œ๊ธ€์„ 100๊ฐœ์”ฉ ์กฐํšŒํ•˜๋ฉด 10๋ถ„๋™์•ˆ 100๋งŒ๋ฒˆ์˜ ์—…๋ฐ์ดํŠธ ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ€๊ฒ ๋„ค!?

์‚ฌ์šฉ์ž๊ฐ€ ์ ๋‹ค๋ฉด ์ค‘๋ณต ์ ‘๊ทผ์„ ์ œ๊ฑฐํ•œ ๋งˆ๋‹น์— ํฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ค‘๋ณต ์กฐํšŒ๋ฅผ ์ œํ•œํ•œ ์ˆœ๊ฐ„๋ถ€ํ„ฐ๋Š” ๋ถˆํ•„์š”ํ•œ ๊ณผ์ •์ด๋ผ๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค Redis์˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์žก์•„๋จน๊ธฐ๋„ ํ• ๊ฑฐ๊ตฌ์š”

 

๐Ÿ˜๋ชฉํ‘œ

์ œ๊ฐ€ ์ด๋ฃจ๊ณ ์ž ํ–ˆ๋˜ ๋ชฉํ‘œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

 

1. 00:00 ๋ถ„์„ ๊ธฐ์ค€์œผ๋กœ ํ•˜๋ฃจ ๋™์•ˆ ํ•œ ๋ช…์˜ ์‚ฌ์šฉ์ž๋Š” ํ•˜๋‚˜์˜ ์กฐํšŒ์ˆ˜๋งŒ ์˜ฌ๋ฆฐ๋‹ค.

2. ์ƒํ’ˆ ๋ณ„ ์กฐํšŒ์ˆ˜๋Š” ํŠน์ • ์‹œ๊ฐ„๋งˆ๋‹ค ์ผ๊ด„ ๋ฐ˜์˜ํ•œ๋‹ค.

 

 

๐Ÿ˜์‚ฌ์šฉ์ž๋Š” ์–ด๋–ป๊ฒŒ ๊ตฌ๋ถ„ํ•˜์ง€?

๊ตณ์ด ์„œ๋ฒ„ ์ž์›์„ ์“ฐ๋ฉด์„œ ์œ ์ง€ํ•  ๋ฐ์ดํ„ฐ๋Š” ์•„๋‹ˆ๋ผ๊ณ  ํŒ๋‹จํ–ˆ๊ณ  IP์™€ ์ฟ ํ‚ค ์ค‘์— ๊ณ ๋ฏผํ–ˆ๊ณ  ์กฐ์ž‘์˜ ์œ„ํ—˜์„ฑ ๋ณด๋‹ค๋Š” ์ •ํ™•ํ•œ ์‹๋ณ„์— ๋” ์ค‘์ ์„ ๋‘์–ด ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฟ ํ‚ค IP
์กฐ์ž‘ ๊ฐ€๋Šฅ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ™์€ IP์ฃผ์†Œ ๊ณต์œ ํ•˜๋Š” ๊ฒฝ์šฐ(ํ•™๊ต, ๊ณต๊ณต์žฅ์†Œ, ํšŒ์‚ฌ ๋“ฑ)
์‹๋ณ„์ด ์–ด๋ ค์›€

 

๐Ÿ˜์‚ฌ์šฉ์ž๋งŒ ๊ตฌ๋ณ„ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ ์‚ฌ์šฉ์ž์˜ ์ƒํ’ˆ ๋ณ„ ์ ‘๊ทผ์„ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•˜๋„ค!?

IP๋ฅผ ์ด์šฉ   -  IP + itemId์ •๋ณด๋ฅผ ์ €์žฅ(์„œ๋ฒ„์— ์ €์žฅํ•ด์•ผํ•จ)

cookie์ด์šฉ -  Random๊ฐ’ + itemId์˜ ํ˜•ํƒœ๋กœ ์ €์žฅ

 

๐Ÿ˜์ฟ ํ‚ค...๋„ˆ ์ข‹์€ ๋ฐฉ๋ฒ• ๋งž๋ƒ๊ณ 

IP๋Š” ์„œ๋ฒ„์— ์ €์žฅ์ด ๋ถˆ๊ฐ€ํ”ผํ–ˆ์ง€๋งŒ ์ฟ ํ‚ค๋Š” ์„ ํƒ์ง€๊ฐ€ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ณ ๋ฏผ ๊ณผ์ •์„ ์ •๋ฆฌํ•˜์ž๋ฉด ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

ํ•˜๋‚˜์˜ ๋ธŒ๋ผ์šฐ์ €๋Š” 300๊ฐœ์˜ ์ฟ ํ‚ค๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๊ณ  ํ•˜๋‚˜์˜ ๋„๋ฉ”์ธ ๋‹น 20๊ฐœ์˜ ๊ฐ’๋งŒ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.

 

 

-> ์ฆ‰, cookie_item_1๊ณผ ๊ฐ™์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ฟ ํ‚ค๋กœ ๋‚˜๋ˆ ์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅํ•  ๋ฟ๋”๋Ÿฌ ๊ฐ€๋Šฅํ•ด๋ดค์ž ๋น„ํšจ์œจ์ 

-> ๊ทธ๋ ‡๋‹ค๋ฉด ํ•˜๋‚˜์˜ ์ฟ ํ‚ค๋กœ ์—ฌ๋Ÿฌ๊ฐœ์˜ id๋ฅผ ๊ด€๋ฆฌํ•ด์•ผํ•˜๋ฉด ๋˜๊ฒ ๋‹ค!

-> ์ฟ ํ‚ค์˜ ์šฉ๋Ÿ‰์€ 4KB์ด๊ธฐ ๋•Œ๋ฌธ์— 1/2/3/4/5/6/7/8/9/ ์™€ ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์ €์žฅํ•œ๋‹ค๋ฉด ์ˆซ์ž๋„ ์ž๋ฆฌ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜๊ณ  / ๋„ ํฌํ•จํ•ด์•ผํ•ด์„œ 1500๊ฐœ์ •๋„..?

 

์‹ค์ œ๋กœ ๋กœ์ง์„ ์ž‘์„ฑํ•ด๋ณด์ง€๋Š” ์•Š์•˜์ง€๋งŒ ๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ถ€๋ถ„๋“ค์ด ๊ฝค ๋ณด์˜€๊ณ  ์„ฑ๋Šฅ์ ์ธ ๋ถ€๋ถ„์ด๋‚˜ ์กฐ๊ธˆ ๋” ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„์ด ๊ฐ€๋Šฅํ•œ Redis์˜ set์„ ์ด์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.  

  • ๋งค๋ฒˆ ์ฟ ํ‚ค ๊ฐ’์„ splitํ•œ List์— ์ ‘๊ทผํ•˜๋ ค๋Š” ์ƒํ’ˆ์˜ id๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ ๋ฐ ์—†์œผ๋ฉด ์ฟ ํ‚ค์— ์ž…๋ ฅ 
  • ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ 1500๊ฐœ ์ด์ƒ์„ ๋ณผ ์ผ์€ ๊ฑฐ์˜ ์—†๊ฒ ์ง€๋งŒ ๋„˜์–ด๊ฐ„๋‹ค๋ฉด ์ฟ ํ‚ค๋ฅผ ๋Š˜๋ ค์•ผ ํ•˜๊ณ  ์ด ๋•Œ๋ถ€ํ„ฐ ์ด์— ๋Œ€ํ•œ ์ถ”๊ฐ€์ ์ธ ํ™•์ธ์ด ํ•„์š” 

 

๐Ÿ˜๊ตฌํ˜„ ๋ฐฉ์‹

์‚ฌ์šฉ์ž์˜ ๊ตฌ๋ถ„๋งŒ ์ฟ ํ‚ค๋กœ ์ง„ํ–‰ํ•˜๊ณ  ์ดํ›„ ์ค‘๋ณต์— ๋Œ€ํ•œ ์ฒดํ‚น์„ Redis์˜ Set์„ ์ด์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

 

 

Controller

    @GetMapping("/{itemId}")
    public Response<ItemDetailResponse> getItemDetail(@Valid @PathVariable(value = "itemId") Long itemId, HttpServletRequest request, HttpServletResponse response) {
        ItemDetailResponse itemDetailResponse = itemService.getItemDetail(itemId);

        /**
         * @Description
         * 1. ์ฟ ํ‚ค๊ฐ€ ์—†๋‹ค๋ฉด ๋งŒ๋“ค๊ณ  ์žˆ๋‹ค๋ฉด value Return
         * 2. ์‚ฌ์šฉ์ž์˜ ์•„์ดํ…œ์— ๋Œ€ํ•œ ์ ‘๊ทผ ํ”์ ์ด ์—†๋‹ค๋ฉด ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€
         */
        String viewCountCookieValue = CookieUtils.getViewCountCookieValue(request, response);
        itemService.handleViewCount(itemId, viewCountCookieValue);


        return Response.success(HttpStatus.OK, String.format("%d๋ฒˆ ์ƒํ’ˆ ์ •๋ณด์ž…๋‹ˆ๋‹ค.",itemId), itemDetailResponse);
    }
  • ์‚ฌ์šฉ์ž๊ฐ€ ์ฒซ ์ ‘๊ทผํ–ˆ์„ ๋•Œ viewCount ์ฟ ํ‚ค ๋ฐœํ–‰ํ•ด ์ค๋‹ˆ๋‹ค(์‚ฌ์šฉ์ž ๊ตฌ๋ถ„์„ ์œ„ํ•จ)

 

CookieUtils, TimeUtils

@Component
public class CookieUtils {

    public static String getViewCountCookieValue(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            Optional<Cookie> viewCountCookie = Arrays.stream(cookies)
                    .filter(cookie -> cookie.getName().equals("viewCountCookie"))
                    .findFirst();
            if(viewCountCookie.isPresent())
                return viewCountCookie.get().getValue();
        }
        return createAndAddViewCountCookie(response);
    }

    private static String createAndAddViewCountCookie(HttpServletResponse response) {
        String randomCookieValue = UUID.randomUUID().toString();
        ResponseCookie cookie = ResponseCookie.from("viewCountCookie", randomCookieValue)
                .path("/")
                .sameSite("None")
                .httpOnly(true)
                .secure(true)
                .maxAge(60 * 60 * 24) 
                .build();

        response.addHeader("Set-Cookie", cookie.toString());
        return randomCookieValue;
    }
}

public class TimeUtils {

    public static long getRemainingTimeUntilMidnight() {
        LocalDateTime now = LocalDateTime.now();

        // ํ˜„์žฌ ์‹œ๊ฐ„๋ถ€ํ„ฐ ๋‚ด์ผ 00:00:00๊นŒ์ง€์˜ ๋‚จ์€ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
        Duration duration = Duration.between(now, now.plusDays(1).withHour(0).withMinute(0).withSecond(0));

        // ๊ณ„์‚ฐ๋œ ์‹œ๊ฐ„์„ ์ดˆ๋กœ ๋ณ€ํ™˜
        return duration.getSeconds();
    }
}
  • ์‚ฌ์šฉ์ž ๊ตฌ๋ถ„์„ ์œ„ํ•œ ์ฟ ํ‚ค๋Š” 00:00 ๋งˆ๋‹ค ๋งŒ๋ฃŒ๋˜๋„๋ก ๊ตฌํ˜„(์‚ฌ์šฉ์ž์˜ ์ ‘๊ทผ ๊ธฐ๋ก ์ดˆ๊ธฐํ™”)
    -> ์ž์ •์ด ๋„˜์–ด๊ฐ”๋Š”๋ฐ ์ฟ ํ‚ค๊ฐ€ ๋‚จ์•„์žˆ๋‹ค๋ฉด ์กฐํšŒ์ˆ˜ ์ค‘๋ณต์ฒดํ‚น์— ์˜ํ–ฅ์„ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ์ž์ •๋งˆ๋‹ค ๋งŒ๋ฃŒ๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค!

 

ItemRedisService, RedisService

@Service
@RequiredArgsConstructor
public class ItemRedisService {
    private final RedisService redisService;

    // @Description ์ค‘๋ณต๋œ ์ ‘๊ทผ์ด ์•„๋‹ˆ๋ผ๋ฉด ์กฐํšŒ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค๊ณ  ์ ‘๊ทผ ์ฒ˜๋ฆฌ
    public void handleViewCount(String cookieValueOfViewer, Long itemId) {
        if (!redisService.isExistInSet(cookieValueOfViewer, itemId))
        {
            redisService.increaseData("viewCount_item_" + itemId);
            redisService.addToSet(cookieValueOfViewer, itemId);
        }
    }
}

@RequiredArgsConstructor
@Service
public class RedisService {

    private final RedisTemplate<String, Object> redisTemplate;

    public void increaseData(String key) {
        redisTemplate.opsForValue().increment(key);
    }

    public void addToSet(String key, Long itemId){
        if(!redisTemplate.hasKey(key)) {// ํ‚ค๊ฐ€ ์—†๋‹ค๋ฉด(set์ด ์—†๋‹ค๋ฉด)
            redisTemplate.opsForSet().add(key, String.valueOf(itemId)); // set์ƒ์„ฑ
            redisTemplate.expire(key, TimeUtils.getRemainingTimeUntilMidnight(), TimeUnit.SECONDS); // ๋งŒ๋ฃŒ๊ธฐ๊ฐ„ ์„ค์ •
        }
        else // ๊ธฐ์กด ํ‚ค ๊ฐ’์œผ๋กœ ๋œ set์— ์ถ”๊ฐ€
            redisTemplate.opsForSet().add(key,String.valueOf(itemId));

    }
    public boolean isExistInSet(String key, Long itemId){
        return Boolean.TRUE.equals(redisTemplate.opsForSet().isMember(key, String.valueOf(itemId)));
    }

}

 

-> ์ฟ ํ‚ค ๊ฐ’์„ ํ‚ค ๊ฐ’์œผ๋กœ Set์— itemId๋ฅผ ์ €์žฅ  ์ดํ›„ ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•ด ์ค‘๋ณต ์ ‘๊ทผ ์ฒดํ‚น ๋ฐ ์กฐํšŒ์ˆ˜ ์ฆ๊ฐ€ ์ง„ํ–‰

 

 - ์„œ๋ฒ„์— ์ €์žฅ๋˜์žˆ๋˜ ์ฟ ํ‚ค์— ๋Œ€ํ•œ set ์ดˆ๊ธฐํ™”(์‚ฌ์šฉ์ž์˜ ์•„์ดํ…œ ๋ณ„ ์ ‘๊ทผ ๊ธฐ๋ก ์ดˆ๊ธฐํ™”)

 

 

๐Ÿ˜์กฐํšŒ์ˆ˜ ๋งค๋ฒˆ ์—…๋ฐ์ดํŠธ ์‹œ์ผœ์•ผํ• ๊นŒ์š”?

์ค‘๋ณต ์ ‘๊ทผ์€ ์ œ๊ฑฐํ–ˆ์ง€๋งŒ ์ด์ œ ์„ฑ๋Šฅ์ ์ธ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒŒ์‹œ๊ธ€์ด ์กฐํšŒ๋  ๋•Œ๋งˆ๋‹ค ์—…๋ฐ์ดํŠธ ์ฟผ๋ฆฌ๊ฐ€ ๋‚˜๊ฐ„๋‹ค๋Š” ๋ฌธ์ œ์ ์ด ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ์ œ๊ฐ€ ์„ ํƒํ•œ ๋ฐฉ๋ฒ•์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

 

- Redis์—์„œ itemId๋กœ ์กฐํšŒ์ˆ˜๋ฅผ ์ €์žฅ -> ์ผ์ • ์‹œ๊ฐ„ ํ›„ DB๋ฐ˜์˜ 

 

๐Ÿ˜์ตœ์ข… ํ๋ฆ„