이를 구현하기 위해 다음과 같은 방식이 떠올랐다.
레디스를 사용하는게 간단하게 구현될 것이라 생각했지만 서버의 부담을 줄이고 쿠키만으로도 충분히 구현할 수 있을거라고 생각하였기에 세 가지의 방법 중 쿠키를 사용하여 구현하기로 결정하였다.
postView
라는 이름의 쿠키에 {게시글Id}:{날짜정보}의 Set을 담아 해당 게시글을 조회해도 되는지 안되는지 판단하는 로직을 작성하였다. 플로우차트는 아래와 같다.
쿠키를 읽어들여 해당 게시글의 조회수를 증가시킬지 판단하는 로직을 Service
레이어에서 처리하지 않고 별도의 ViewCountValidator
라는 클래스를 만들었다.
Controller
나 Service
가 아닌ViewCountValidator
에 조회수 증가 판별 로직 책임을 지움으로써 코드 가독성을 고려하였다.
ViewCountValidator
는 이미 조회를 했으면(조회수 유지) true
를 반환, 조회를 안했으면(조회수 +1) false
를 반환한다.
아래의 코드에 게시글을 Log라고 이름을 지어서 Log == 게시글이라고 보면 될 것 같다. (logId
= 게시글의 Id)
@Operation(summary = "id로 로그 조회")
@GetMapping("/{log_id}")
public ApiResponse<LogDetailResponseDto> readLog(HttpServletRequest request, HttpServletResponse response, @PathVariable(name = "log_id") String logId) {
boolean hasViewed = viewCountValidator.hasViewedInCoookie(request, response, logId);
return ApiResponse.onSuccess(logService.readLog(logId, hasViewed));
}
ViewCountValidator
에서 게시글을 조회해도 되는지의 여부를 판단하여 Service
단으로 넘기는 것을 확인할 수 있다. public LogDetailResponseDto readLog(String logId, boolean hasViewed) {
// 게시글 조회가 유효하지 않다면 조회수 증가
if(!hasViewed) logRepository.increaseViewCountByLogId(logId);
Log findLog = logRepository.findLogAndMemberById(logId).orElseThrow(() ->
new LogException(ErrorCode.LOG_NOT_EXIST));
return LogDetailResponseDto.of(findLog);
hasViewed
== false) logRepository.increaseViewCountByLodId(logId)
를 통해 조회수를 증가시킨다.repository
에서 찾아 반환한다.postViewed
라는 이름의 쿠키가 있는지 판단한다.
쿠키가 존재하지 않으면, postViewed
: logId:yyyyMMdd
라는 쿠키를 생성한다.
쿠키가 존재하면, 쿠키 값 안에 게시글의 Id가 있는지도 판단해야한다.
게시글의 Id가 없으면, 기존의 쿠키 값에 조회 정보를 추가한다.
postViewed
: {기존 값}/logId:yyyyMMdd
이때 {게시글Id}:{날짜정보}간의 구분자는 /
로 한다.
게시글의 Id가 존재하면, 기간이 지났는지를 체크한다.
게시글의 Id가 존재하지만 기간이 지났다면 index 0부터 해당 게시글Id까지 지운다. (날짜가 오래된 순서대로 쌓이기 때문에 해당 게시글Id의 기간이 지났다면 앞의 게시글Id 보도 기간이 지난 것) + 새로 날짜정보를 업데이트하여 값을 추가한다.
{ } / { } / {게시글 Id : 날짜정보 } / { }
{ } / { } / {게시글 Id : 날짜정보 } / { } / {게시글 Id : 새로운 날짜 정보}
게시글 Id의 기간이 지나지 않았다면 해당 게시글은 조회수를 늘려도 되므로 true
를 반환하게 된다.
이러한 로직을 구현하면 아래와 같다.
@Component
public class ViewCountValidator {
private static final String COOKIE_NAME = "postViewed";
private static final int COOKIE_MAX_AGE = 24 * 60 * 60; // 1 day in seconds
private static final String DATE_FORMAT = "yyyyMMdd-HHmm";
private final SimpleDateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);
// 해당 게시글을 조회한게 유효한지 체크 (조회한 날짜 내에서만 유효)
public boolean hasViewedInCoookie(HttpServletRequest request, HttpServletResponse response, String logId) {
Cookie viewedCookie = getCookie(request);
if (viewedCookie != null) {
if(isLogIdInCookie(viewedCookie, logId)) {
return checkAndUpdateCookie(viewedCookie, response, logId);
} else {
updateCookieWithNewLogId(viewedCookie, response, logId);
return false;
}
} else {
createNewCookie(response, logId);
return false;
}
}
// getCookies()에서 특정 이름의 쿠키를 찾음
private Cookie getCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
return (cookies == null) ? null : Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(COOKIE_NAME))
.findFirst()
.orElse(null);
}
// cookie에 특정 logId가 포함되어있는지 확인
private boolean isLogIdInCookie(Cookie cookie, String logId) {
return Arrays.stream(cookie.getValue().split("/"))
.anyMatch(entry -> entry.startsWith(logId + ":"));
}
// 쿠키 날짜를 확인하고 필요시 업데이트하여 조회수를 막는 로직
private boolean checkAndUpdateCookie(Cookie cookie, HttpServletResponse response, String logId) {
String today = getCurrentDate();
String[] entries = cookie.getValue().split("/");
for (String entry : entries) {
String[] parts = entry.split(":");
if(parts[0].equals(logId)) {
if(parts[1].equals(today)) return true;
else {
updateCookieWithLogId(cookie, response, logId);
return false;
}
}
}
return false;
}
// logId:yyyyMMdd-HHmm/
// 만료된 logId를 없애고 해당 logId 새로 추가하는 로직 (시간 순서대로 정렬되어있으므로 조회하지 않는 logId도 한꺼번에 정리 가능)
private void updateCookieWithLogId(Cookie cookie, HttpServletResponse response, String logId) {
System.out.println(getCurrentDate());
String deletedExpireLogIdInValue = cookie.getValue().split(logId)[1].substring(DATE_FORMAT.length() + 1);
String updateValue = deletedExpireLogIdInValue + "/" + logId + ":" + getCurrentDate();
addCookie(response, updateValue);
}
//새로 logId를 추가하여 쿠키 추가
private void updateCookieWithNewLogId(Cookie cookie, HttpServletResponse response, String logId) {
String updateValue = cookie.getValue() + "/" + logId + ":" + getCurrentDate();
addCookie(response, updateValue);
}
// 새로운 쿠키 생성하는 로직 (logId 처음으로 정의)
private void createNewCookie(HttpServletResponse response, String logId) {
addCookie(response, logId + ":" + getCurrentDate());
}
private void addCookie(HttpServletResponse response, String value) {
Cookie cookie = new Cookie(COOKIE_NAME, value);
cookie.setPath("/");
cookie.setMaxAge(COOKIE_MAX_AGE);
response.addCookie(cookie);
response.setCharacterEncoding("UTF-8");
}
// 현재 날짜(yyyyMMdd) 반환
private String getCurrentDate() {
return dateFormatter.format(new Date());
}
}
SimpleDateFormat
을 사용하여 시간 단위를 관리하였다./
를 기준으로 조회정보가 잘 들어갔음을 확인할 수 있었다.