조회수 기준
- 비회원도 조회수가 올라야 한다.
- 상세 페이지 바로 접근 시 조회수가 올라야 한다.
- 새로고침 시 조회수를 올리지 않아야 한다.
- 일정 시간 주기로 조회 가능 여부를 초기화한다. → 10분 설정
방안
- 조회수 테이블로의 UpdateOrInsert
- 트래픽이 많아질 수록 DB 부하가 예상된다.
- 개발 공수가 적다.
- 중복 조회 이슈가 생긴다.
- Redis를 이용한 Wirte Back 전략
- Redis에 먼저 적재해두고 일정 주기로 DB에 insert 하는 배치 실행
- 중복 조회를 막을 수 있다.
- 개발 공수가 적지 않다.
- 일관성 이슈가 발생할 수 있다.
- Redis 이용한 Write Through 전략
- Redis에 적재 후 DB에 적재
- 일정 시간 동안 Redis 에 있는 key 값으로는 DB insert 못하게 튕겨냄
중복 조회 이슈
중복 조회를 막기 위한 전략을 생각한다.
- ip 주소 기반
- 동일한 네트워크에 있는 사용자들이 동일한 ip를 사용하는 경우 사용자 식별에 어려움
- Mac Address 기반
- 저장 값이 매우 길다.
- 쿠키 기반
- 고의 삭제 가능
- 세션 기반
- 사용자별로 조회 기록 관리 가능
- 서버가 여러 개인 분산 환경에서는 세션이 여러개 생겨날 수 있어 적절하지 않음.
- 로그인 사용자 기반
- 비회원 접속이 가능하기 때문에 적용하기 어려움
중복 조회가 서비스에 영향을 미치는 정도는 낮다.
구현
// Controller
@GetMapping("")
fun getPostByPostSeq(
@Valid @ParameterObject postReq: PostReq,
request: HttpServletRequest,
): PostDetailRes? {
// 게시물을 조회하고 결과 반환
val postRes = postQueryService.findPostByPostSeq(postReq) ?: return null
// 환경 변수가 설정된 경우에만 조회수 체크 기능 활성화
System.getenv(POST_VIEW_FEATURE_TOGGLE_ENV)?.takeIf { it.isNotBlank() }?.let {
val clientIp = getClientIp(request)
postViewCommandService.updatePostViews(clientIp, postReq)
}
return postRes
}
// Service
@Transactional
fun updatePostViews(
clientIp: String,
req: PostReq,
) {
// Redis 캐시에서 중복 확인 및 저장
if (clientIp.isNotBlank() && req.postSeq != null) {
// 이미 10분 내 동일한 조회가 있었다면 종료
if (!redisService.insertViewIfNotRecent(clientIp, req, postViewExpireMinutes)) return
// todo Redis 정상적으로 테스트 되면 삭제해도 되지 않을까
// DB에서 한번 더 중복 확인 후 데이터 삽입
val exists =
repository.existsByPostSeqAndInfoIpAndInsDateAfter(
req.postSeq,
clientIp,
LocalDateTime.now().minusMinutes(postViewExpireMinutes),
)
if (!exists) { // 중복이 없으면 DB에 삽입
repository.save(mapper.toEntity(clientIp, req))
}
}
}
@Service
class RedisService(
private val redisRepository: RedisRepository,
) {
/**
* @param postId 게시물의 고유 번호
* @param clientIp 클라이언트 IP 주소
* @param expireMinutes 캐시 만료 시간 (기본값 10분)
* @return Boolean 해당 키가 이미 존재하면 false, 새로 저장되면 true
*/
fun insertViewIfNotRecent(clientIp: String, req: RideReq, expireMinutes: Long): Boolean {
val postId = req.postId ?: return false
val key = "postView:$postId:$clientIp"
// Redis에서 키 존재 여부 확인
if (redisRepository.existsKey(key)) {
// 키가 이미 존재하면 중복된 조회로 간주
return false
}
val postViewDto = RideViewDto(
postId = postId,
infoIp = clientIp,
paxId = req.paxId
)
// 키가 없으면 Redis에 저장하고 만료 시간 설정
return redisRepository.saveKeyWithExpiration(postViewDto, expireMinutes)
}
환경변수를 가지고 피쳐토글로 구현했다.
'Back-end' 카테고리의 다른 글
실시간 게시글 추천 초간단 구현 (0) | 2024.10.23 |
---|---|
Protobuf와 Map Struct 친해지길 바래 (0) | 2024.10.15 |
[Redis] Redis 데이터 저장 근데 protobuf를 곁들인 (3) | 2024.10.14 |
[gRPC] Armeria + gRPC 띄워보기 (7) | 2024.10.07 |
[gRPC] gRPC란 (3) | 2024.10.02 |