하고자하는 것은 위와 같이 게시글에 대하여 조회수를 증가시키는 로직을 구성하는 것입니다. 이글은 조회수 증가기능을 구현하면서 했던 고민들과 방법을 작성한 글입니다.
기존에는 게시글을 조회할 때 +1 해주는 방식을 사용했었습니다. 코드는 아래와 같습니다.
BoardRepositoryImpl.java
@Override
public BoardResponseDTO getBoardWithTag(Long id) {
// TODO 중복 조회를 어떻게 예방할 것인지 고민해야함
queryFactory.update(board)
.set(board.viewCount, board.viewCount.add(1))
.where(board.id.eq(id))
.execute();
Board board1 = queryFactory.select(board)
.from(board)
.leftJoin(board.category, category).fetchJoin()
.leftJoin(board.writer, member).fetchJoin()
.where(board.id.eq(id))
.fetchOne();
if (board1 == null) {
throw new NotFoundException("Could not found board id : " + id);
}
return BoardResponseMapper.INSTANCE.toDto(board1);
}
이렇게 코드를 작성하게 된다면 게시글이 조회될 때마다 조회수 증가가 가능하지만 같은 사용자에 한해서도 무한정 조회수가 증가될 수 있게됩니다. 그래서 조회수 중복 count를 어떻게 방지할 수 있을지 고민해보았습니다.
정확히 어떤 방식으로 조회수가 카운트 되는 것인지 생각해보았습니다.
예시로는 youtube의 조회수 카운트 방식이 있습니다.
게시글의 특성상, 그리고 다양한 레퍼런스들을 참고했을 때 유튜브처럼 조회수에 대해 복잡한 로직을 가지고 있지는 않았습니다. 하지만 간단한 조회수 증가 기능을 구현하더라도 다양한 것을 고려해야할 수 있다는 것을 말씀드리고 싶었습니다.
그래서 제가 생각한 제 게시글의 기능은 아래와 같습니다.
조회 수 중복 count를 방지하는 것에는 다양한 방법이 있을 것으로 예상됩니다.
정도가 생각이 났습니다.
IP로 처리한다고는 작성했지만 사실 상 DB를 이용했을 때의 방법과 비슷합니다.
장점
1. 조작이 불가합니다.(해킹하지않는한)
단점
1. IP는 장소에 따라 유동적으로 변할 수 있는 문제점이 있습니다.
2. Mac Address는 같은 유저라도 다른 기기라면 다른 유저로 식별됩니다.
3. IP와 Mac Address는 값이 길기 때문에 수많은 유저와 수많은 게시글과 날짜를 함께 저장하기에는 문제점이 있습니다.
우선 세션의 특징은 사용자 정보를 서버에서 관리하는 것입니다.
장점
1. 사용자정보를 서버에 둔다는 뜻은 쿠키보다 보안에는 좋다는 것을 의미합니다.
2. 저장데이터에 제한이 없습니다.(서버 성능이 무한히 좋다는 가정이 있을 시)
단점
1. 서버에 데이터를 저장하므로 서버의 리소스를 사용하기 때문에 세션양이 많아진다면 서버에 부하가 커집니다. -> 비용, 성능과 직결될 수 있는 문제가 발생할 수 있습니다.
쿠키는 웹사이트에 접속할 때 생성되는 정보를 담은 임시 파일입니다.
쿠키의 데이터 형태는 Key와 Value로 구성되고 String 형태로 이루어져있습니다.
장점
1. 서버의 공간을 절약할 수 있습니다.
단점
1. 개인의 정보가 기록된다면 사생활을 침해할 소지가 있습니다.
2. 서버가 가지고 있는 것이 아니라 사용자에게 저장되기 때문에, 임의로 고치거나 지울 수 있고, 가로채기도 쉬워 보안에 취약합니다. 이는 곧 조회수를 조작할 수 있다는 것을 의미합니다.
쿠키로 조회수 증가 로직을 구현하기로 하였습니다.
이유는 아래와 같습니다.
1. 쿠키의 단점이었던 개인정보가 들어가있다면 보안에 취약하여 사생활 침해의 소지가 있다는 것 -> 제가 생성할 쿠키에는 개인정보가 들어가 있지 않았고 단순 조회수 증가로직에만 사용할 것이기 때문에 큰 문제가 되지 않을 것이라고 생각했습니다.
2. 조회 수 조작이 가능하다는 것 -> 우선 조작해도된다...라고 생각했습니다. 공부용 프로젝트 이기도 했고 session을 사용하거나 DB를 사용하려면 따로 서버를 생성해야하는데 비용이 부족한 상태이기도 했습니다.
전체코드는 chu-chu github에서 확인하실 수 있습니다.
BoardController.java
@GetMapping("/detail/{id}")
public ResponseResult<BoardResponseDTO> getOne(@PathVariable(value = "id") Long id, HttpServletRequest req, HttpServletResponse res) {
viewCountUp(id, req, res);
return success(boardService.getBoardWithTag(id));
}
...
private void viewCountUp(Long id, HttpServletRequest req, HttpServletResponse res) {
Cookie oldCookie = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("boardView")) {
oldCookie = cookie;
}
}
}
if (oldCookie != null) {
if (!oldCookie.getValue().contains("[" + id.toString() + "]")) {
boardService.viewCountUp(id);
oldCookie.setValue(oldCookie.getValue() + "_[" + id + "]");
oldCookie.setPath("/");
oldCookie.setMaxAge(60 * 60 * 24);
res.addCookie(oldCookie);
}
} else {
boardService.viewCountUp(id);
Cookie newCookie = new Cookie("boardView","[" + id + "]");
newCookie.setPath("/");
newCookie.setMaxAge(60 * 60 * 24);
res.addCookie(newCookie);
}
}
[1]_[2] 처럼 값이 담기게 한 이유는 101번글과 10번글의 구분을 위해서 이렇게 작성하였습니다.
BoardService.java
@Transactional
public void viewCountUp(Long boardId) {
Board board = findById(boardId);
board.viewCountUp(board);
}
Board.java
public void viewCountUp(Board board) {
board.viewCount++;
}
우선 위와 같이 게시글이 2개가 있다고 가정해보겠습니다.
1번과 2번글을 조회했을 때
위와 같이 Cookie가 생성된 것을 볼 수 있습니다. 이제는 1번과 2번글을 조회하더라도 조회수가 count되지 않습니다.
위와같은 상황이 반복된다면 하루 이틀 일주일이 지나더라도 1번 글을 조회할 때 조회수가 count 되지 않는 문제가 발생하게 됩니다.
이 문제를 해결하기 위해서는 위에서 작성했던 코드처럼 작성하는 것이 아니라 게시글 각각 cookie를 생성하는 방법을 생각해볼 수 있었습니다.
하지만 이것도 한가지 문제가 있었습니다. 클라이언트에는 쿠키의 저장용량, 저장개수가 한정되어있다는 것입니다.
보통 아래에 적혀있는 것이 표준안 이라고 합니다.
게시글마다 cookie를 생성하게 된다면 21개 이상의 글을 조회하는 순간 쿠키가 생성되지 않게됩니다.
그래서 우선은 제가 작성한 방법대로 cookie를 사용하기로 하고 더 효율적인 방법이 생각나거나 떠오른다면 수정해보도록하겠습니다.
cookie를 사용하여 조회수 증가 로직, 중복 count를 방지해보았습니다. 더 좋은 방법이 생기거나 수정하게 된다면 글을 업데이트 하도록 하겠습니다. 감사합니다!
제가 잘못이해하고 있거나 잘못 작성한 부분이 있다면 지적, 비판, 피드백 뭐든 해주시면 감사하겠습니다!
참고블로그
https://guiyomi.tistory.com/92
https://ssdragon.tistory.com/118
https://audiencegain.net/%EC%9C%A0%ED%8A%9C%EB%B8%8C-%EC%A1%B0%ED%9A%8C%EC%88%98%EB%A5%BC-%EC%B9%B4%EC%9A%B4%ED%8A%B8%ED%95%9C-%EB%B0%A9%EB%B2%95/
https://velog.io/@juwonlee920/Spring-%EC%A1%B0%ED%9A%8C%EC%88%98-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-%EC%A1%B0%ED%9A%8C%EC%88%98-%EC%A4%91%EB%B3%B5-%EB%B0%A9%EC%A7%80
사실 큰 서비스에서는 자본에 상관없이 DB에 log를 남겨 처리하는 곳도 있는것 같지만,
간단하게는 Redis라는 캐싱을 사용하여도 좋을 것 같아요. 만료기한을 주는 것이죠