[SpringBoot] 조회 수 카운팅 중복 방지 - ver1
현재 회사에서 운영하고 있는 레거시 시스템에 조회수 어뷰징으로 의심되는 로그가 발견되었다.
(참고로 내가 운영하고 있는 서비스는 B2C라고 하기엔 좀 애매한 서비스임.)
특정 동영상 조회수를 조작하는걸로 의심되었고, 확인해보니 페이지 진입(혹은 새로고침)시 조회수를 +1로 DB에 업데이트를 치는 로직인데,
그게 한꺼번에 트래픽이 몰리니 DB가 락이 걸려버리는 이슈였다.
레거시라 누굴 탓할수 없고 ㅋㅋ 어쩌겠느냐, 수정해야지!!!
일단 조회수 중복을 막는 방법으로 3가지 정도를 생각했다.
1. 새로고침을 막기위해 페이지 진입시, 새로고침 체크용 파라미터 추가.
⇒ 하지만 해당건은 새로고침만 막을 수 있을뿐 URL을 이용한 매크로를 막을순 없음. (탈락)
2. Redis 를 이용하여 동시성제어
- 특정영상(ID) 를 조회
- MediaID 라는 Key를 가진 List에 해당 ID 확인 → 없다면 List에 ID를 Add 한 후 조회수 +1 증가
- 동일한 영상(ID)를 조회
- MediaID List에서 조회 → ID를 보유 하고 있으므로 Pass
- Key 별로 TTL설정가능 (ex.24시간)
⇒ 사실 이것 외에도 캐시의 용도나 세션저장소로 유용하게 쓰일 수 있을듯 하여 겸용하여 쓸 수 있다. 하지만 비용의 문제로 이것도 탈락
3. 쿠키를 이용
쿠키는 웹사이트에 접속할 때 생성되는 정보를 담은 임시 파일이다.
쿠키의 데이터 형태는 Key와 Value로 구성되고 String 형태로 이루어져있다.
장점으론
서버의 공간을 절약할 수 있고
단점으론 사용자에게 저장되기 때문에, 임의로 고치거나 지울 수 있고, 가로채기도 쉬워 보안에 취약하다.
✔️ 쿠키로 가자
쿠키의 단점이었던 개인정보가 들어가 보안에 취약하다?
어차피 내가 생성할 쿠키에는 개인정보가 들어가 있지 않았고 단순 조회수 증가로직에만 사용할 것이라 딱히 문제가 되지 않는다 생각했다.
중복을 막기 위함이므로 TTL을 타이트하게 30초 정도로 둘 거라...쿠키로 해도 여러 동영상을 조회했을 경우 성능저하 이슈도 없어 보였다.(참고로 쿠키 용량은 4KB)
public void handleViewCount(HttpServletRequest request, HttpServletResponse response, Long mediaId) {
log.info("[media viewCount] Media id : {} / login : {} / ip : {}", mediaId, SecurityUtils.getCurrentUserLogin(), request.getHeader("x-forwarded-for"));
String cookieName = "vodView";
try {
Cookie userCookie = getCookieByName(request.getCookies(), cookieName); // 쿠키 GET
if (userCookie == null) {
log.info("[media viewCount] User Cookie X");
log.info("[media viewCount] view count up");
jsonPlaceholderService.increaseMediaViewCount(mediaId).execute();
setNewCookie(response, cookieName, "[" + mediaId + "]", MAX_AGE); // 쿠키 SET
} else {
log.info("[media viewCount] User Cookie O");
}
} catch (Exception e){
log.error("[media viewCount] EXCEPTION:"+e.getMessage(),e);
}
}
1) 클라이언트로부터 요청이 들어옴
2) 요청에 Cookie가 없고 동영상을 조회한다면 [vodView]의 값을 추가하여 Cookie생성 (기간은 30초로 설정)
3) 요청에 Cookie가 있고 동영상을 조회한 기록이 있다면 pass
✔️ 주의해야할 점
만약에 TTL이 길어질 경우 문제가 생길수 있다.
클라이언트에는 쿠키의 저장용량, 저장개수가 한정되어있다는 것이다.
보통 아래에 적혀있는 것이 표준안임
- 총 300개
- 하나의 도메인당 20개
- 하나의 쿠키당 4kb(=4096byte)
동영상마다 cookie를 생성하게 된다면 21개 이상의 동영상을 조회하는 순간 쿠키가 생성되지 않게된다.
✔️ 결론
어차피 나는 TTL이 짧기 때문에 가능했던거고, 길어지게 된다면 2안으로 가야 하지 않을까 싶다.
+ 추가내용
이렇게 작업을 했더니 Postman에서 Get 요청했을 경우 쿠키가 있어 체크가 가능했으나,
매크로프로그램이 없어 JAVA에서 HttpURLConnection 으로 While문을 돌렸다.
public static void main(String[] args) {
try {
while (true) {
sendGetRequest("http://localhost:9090/vod/play?id=4974");
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sendGetRequest(String url) {
try {
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
// GET 요청 설정
con.setRequestMethod("GET");
// 응답 코드 확인
int responseCode = con.getResponseCode();
System.out.println("GET Response Code :: " + responseCode);
// 응답이 성공적일 때 (HTTP OK)
if (responseCode == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
// 출력 결과
System.out.println("Response :: " );
} else {
System.out.println("GET request not worked");
}
} catch (Exception e) {
e.printStackTrace();
}
}
이렇게 테스트했더니.... 쿠키체크가 안되서 똑같이 +1 카운팅이 적용되는게 아닌가... ㅠㅠ....
아 어떡하지 하고 고민하다가... 문득 어차피 브라우저에서 접속을 해야하는거니,
브라우저인지만 판단하면 될 꺼 아닌가? 싶어 체크로직을 하나 더 추가하였다.
1. Header값에 있는 User-Agent로 판단
public boolean isBrowser(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
// 간단한 브라우저 User-Agent 검사
return userAgent.contains("Mozilla") || userAgent.contains("Chrome") ||
userAgent.contains("Safari") || userAgent.contains("Firefox") ||
userAgent.contains("Edge") || userAgent.contains("Opera");
}
return false;
}
이렇게 하고 생각해보니 , 우리 서비스는 모바일도 있고, 모바일브라우저는 또 여러개가 아닌가? 이건 또 어떻게 판단하지 싶어.. 혹시 지원되는 라이브러리가 있나 찾아봤다.
역시 갓 구글과 GPT....UserAgentUtils 라는 라이브러리가 있다고 ㅋㅋㅋ 이걸쓰기로 하였다.
2. UserAgentUtils 라이브러리 이용
<!-- User Agent 체크 추가-->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
public boolean isBrowser(HttpServletRequest request) {
String userAgentString = request.getHeader("User-Agent");
if (userAgentString != null) {
UserAgent userAgent = UserAgent.parseUserAgentString(userAgentString);
Browser browser = userAgent.getBrowser();
return browser != Browser.UNKNOWN && browser != Browser.BOT && browser != Browser.BOT_MOBILE;
}
return false;
}
브라우저에 BOT과 BOT_MOBILE을 추가하여 매크로로 돌리는것들도 방지하였다.
3. 브라우저 && 쿠키있을 경우
// 브라우저 && 쿠키있음
Cookie userCookie = getCookieByName(request.getCookies(), cookieName); // 쿠키 GET
if(isBrowser(request)) {
log.info("[viewCount] Is broswer : " + isBrowser(request));
if (userCookie == null) {
setNewCookie(response, cookieName, "[" + mediaId + "]", MAX_AGE); // 쿠키 SET
} else {
log.info("[viewCount] User Cookie 있음");
}
}
일단 테스트 해보니 잘된다.
참조
https://yooseong12.tistory.com/47
https://velog.io/@korea3611/Spring-Boot%EA%B2%8C%EC%8B%9C%EA%B8%80-%EC%A1%B0%ED%9A%8C%EC%88%98-%EC%A6%9D%EA%B0%80-%EC%A4%91%EB%B3%B5%EB%B0%A9%EC%A7%80-%EA%B8%B0%EB%8A%A5-%EB%A7%8C%EB%93%A4%EA%B8%B0