Project/WithFestival

조회수 중복 증가 방지(Redis + IP검증)

Seung__Yong 2023. 9. 26. 02:26

개요

- 프로젝트를 진행하며 각 게시물 별로 조회수를 관리하기로 결정했습니다.

- 문제점은 같은 사용자에 의해 조회수가 중복으로 증가하는 상황이었습니다.

- 여러 검증 방식을 고민하다가 정확한 측정에 중점을 두고 IP를 이용하기로 결정했습니다.

 

 

검증 방식

세션 검증

  • 장점
    • 서버에서 관리하고 값을 직접 세션 저장소에 저장하면서 검증을 할 수 있으므로 다루기 편하고 보안성이 높습니다.
    • 사용자의 세션 정보를 바탕으로 중복 조회를 정확하게 파악할 수 있습니다.
  • 단점
    • 서버에 데이터를 저장하므로 서버의 리소스를 사용하기 때문에 세션 양이 많아진다면 서버에 부하가 커집니다. -> 비용, 성능과 직결될 수 있는 문제가 발생할 수 있습니다.
    • 세션은 휘발성이라 만료되거나 사용자가 다른 컴퓨터로 사용하면 중복 체크하기가 까다로울 수 있습니다.

쿠키 검증

  • 장점
    • 서버에 부담을 주지 않으면서 사용자의 중복 접근을 감지할 수 있습니다.
    • 최대 용량이 4KB라 무겁지 않고 가볍게 사용 가능합니다.
    • 사용자 별로 정보를 저장하기 때문에 중복 체크하기에 용이합니다.
  • 단점
    • 서버가 가지고 있는 것이 아니라 사용자에게 저장되기 때문에, 임의로 고치거나 지울 수 있고, 가로채기도 쉬워 보안에 취약합니다. 이는 곧 조회수를 조작할 수 있다는 것을 의미합니다.
    • 또한 중복 접근을 완전히 막기에는 어렵습니다.

Local Storage 검증

  • 장점
    • 쿠키보다 큰 데이터를 저장할 수 있으며, 사용자가 Local Storage를 지우지 않는 한 데이터는 브라우저가 끄더라도 데이터가 지속됩니다.
    • 서버에 부담이 없고 클라이언트 측에서 조회수 조작을 방지할 수 있습니다.
  • 단점
    • 사용자의 브라우저에 저장이 되기 때문에 사용자가 저장소를 지우면 중복 조회가 발생합니다.
    • 만료 시간이 따로 없어 임의로 제거 작업을 추가로 진행 해줘야 해서 그에 따른 비용이 발생합니다.

IP 검증

  • 장점
    • 간단히 중복 체크를 감지할 수 있습니다.
    • 서버 측에서 처리하기 때문에 사용자가 쉽게 조작하기 어려워 보안성이 높습니다.
  • 단점
    • 여러 사용자가 같은 IP 주소를 공유하는 경우(ex. 공공장소, 학교, 회사 등) 정확한 조회수를 파악하기 어려울 수 있습니다.
    • 서버 측에서 관리하기 때문에 서버에 부담이 생깁니다
    • IP 주소 저장에 대한 개인정보 보호 이슈가 생길 수 있습니다.

 

 

구현 및 최적화

기존 방식

 

처음 시도 했던 흐름은 중복 접근 체크를 한 뒤에 조회수를 바로 증가 시켜주는 방법이었습니다.

위 방법은 매번 조회수를 증가 시키는 쿼리를 날려야 하기 때문에 DB I/O과정에서 부하가 걸립니다.

(실제로는 IP로 구분도 했고 이용자가 그렇게 많지 않아서 크게 영향을 받지 않지만 그래도 최적화해보고 싶어서 도전해봤습니다.)

 

@Transactional
public GuideRes getGuide(Long id, String ipAddress){
    Guide guide = guideRepository.findById(id).orElseThrow(() -> new NotFoundException(NOT_FOUND_GUIDE));
    if(!isDuplicateAccess(ipAddress, guide.getId())) {
        guide.increaseViewCount();
    }
    return GuideRes.of(guide);
}

private boolean isDuplicateAccess(String ipAddress, Long guideId) {
    ValueOperations<String, Object> redisRepository = redisTemplate.opsForValue();
    if(redisRepository.get(ipAddress + "_" + guideId) == null) {
        redisRepository.set(ipAddress + "_" + guideId, "TRUE");
        return false;
    }
    return true;
}

최적화 이후

 

따라서 리팩토링을 진행하였는데,

일정 시간동안 각 게시물 별 조회수를 IP주소_도메인이름_ID 형태로 Redis에 저장시켜두고, Scheduler를 통해 DB에 redis 키에 대한 값들을 증가시켜주는 방식으로 구현하였습니다.

 

// 1 hour
@Scheduled(fixedDelay = 3600000)
public void updateViewCount()
{
    Set<String> keySet = redisService.getKeySet("*Id*");
    for(String key : keySet){
        String[] splitKey = key.split("_");
        switch (splitKey[0]) {
            case "Booth" -> {
                boothService.increaseBoothViewCount(redisService.getData(key), Long.parseLong(splitKey[2]));
                redisService.deleteData(key);
            }
            case "Guide" -> {
                guideService.increaseGuideViewCount(redisService.getData(key), Long.parseLong(splitKey[2]));
                redisService.deleteData(key);
            }
            case "Program" -> {
                programService.increaseProgramViewCount(redisService.getData(key), Long.parseLong(splitKey[2]));
                redisService.deleteData(key);
            }
        }
    }
}

// 서비스
public void increaseBoothViewCount(Long id, Long viewCount) {
    Booth booth = boothRepository.findById(id).orElseThrow(() -> new NotFoundException(NOT_FOUND_BOOTH));
    booth.increaseViewCount(viewCount);
}

이를 통해서 서비스의 확장성도 높이면서 Redis의 싱글 스레드 형태 덕분에 동시에 접근해도 중복으로 조회수가 증가하지 못하도록 하였습니다.

 

최종 흐름