Redis TTL로 조회수 중복 증가 막기
이 글은 Connectrip 개발 당시 조회수 기능 도입을 검토하면서 고민했던 설계 사고 과정을 정리한다. 해당 기능은 MVP에 포함되지 않았지만, 중복 조회 방지라는 일반적인 문제에 대해 Redis TTL을 활용한 접근 방식을 검토해봤던 경험을 기록한다.
문제 정의와 요구사항
조회수 기능 도입 시 고려해야 할 핵심 요구사항을 검토했다. Connectrip은 인증된 사용자만 접근 가능한 서비스로, 동일 사용자가 단시간 내에 반복 조회해도 조회수는 1회만 증가하도록 하는 중복 방지 메커니즘이 핵심이었다. 인증된 사용자 기반이므로 봇이나 크롤러 문제는 상대적으로 적지만, 사용자의 의도적인 새로고침이나 실수로 인한 반복 요청으로 조회수가 왜곡되는 것을 방지해야 했다. 또한 향후 서비스 확장 시 비회원 접근을 허용하게 될 가능성도 염두에 두고 확장 가능한 설계를 고려했다.
해결 방안 검토
인증된 사용자만 접근하는 서비스 특성상 여러 접근 방식을 비교 검토했다. 데이터베이스에 조회 이력 테이블을 생성하는 방식은 영구적이고 정확하지만, 조회는 매우 빈번한 작업이며 임시성 데이터를 영구 저장할 필요성이 낮다고 판단했다. 세션 기반 접근도 고려했는데, 인증된 사용자는 이미 세션을 가지고 있어 구현이 간단할 수 있지만, 분산 환경에서의 세션 공유와 로드 밸런서 설정의 복잡성이 우려되었다. 최종적으로 일정 시간 후 자동 삭제가 필요한 데이터 특성과 확장성을 고려하여 Redis TTL 기능이 가장 적합하다고 결론지었다.
Redis TTL 기반 해결 방안
엔티티 설계
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@RedisHash(timeToLive = 10) // 10초
public class PostViews {
@Id
private String id;
}
핵심은 @RedisHash
의 timeToLive
속성이다. 10초로 설정한 것은 임의의 값으로, 실제 서비스에서는 사용자 행동 패턴 분석을 통해 최적값을 찾아야 한다.
Repository 구성
@Repository
public interface PostViewsRedisRepository extends CrudRepository<PostViews, String> {
}
Spring Data Redis를 사용하면 JPA와 동일한 방식으로 Repository 패턴을 구현할 수 있다. 별도의 복잡한 설정 없이 CRUD operations을 제공한다.
비즈니스 로직 구현
@Service
@RequiredArgsConstructor
public class PostService {
private final PostMemoryRepository postMemoryRepository;
private final PostViewsRedisRepository postViewsRedisRepository;
public Post findById(Long memberId, Long postId) {
// 게시글을 찾는다
Post foundPost = postMemoryRepository.findById(postId)
.orElseThrow(PostNotFoundException::new);
// Connectrip은 인증 필수이므로 memberId는 항상 존재
// (향후 확장 시 비회원 접근 허용 고려)
// 일정 시간 내 조회하지 않았다면 조회수 증가
String viewKey = memberId + ":" + postId;
if (postViewsRedisRepository.findById(viewKey).isEmpty()) {
postViewsRedisRepository.save(new PostViews(viewKey));
foundPost.incrementViews();
}
return foundPost;
}
}
핵심 아이디어는 memberId:postId
형태의 복합 키를 통한 중복 검사다. Redis에 해당 키가 존재하면 최근 조회로 판단하여 조회수를 증가시키지 않고, 존재하지 않으면 새로운 조회로 간주해 조회수를 증가시킨다. TTL 설정으로 10초 후 자동 삭제되어 재조회가 가능해진다.
참고로 Post 엔티티에는 다음과 같은 조회수 증가 메서드가 필요하다.
@Getter
@Entity
public class Post {
// 기타 필드들...
private int views;
public void incrementViews() {
this.views++;
}
}
Spring Boot 자동 설정의 편의성
Spring Boot의 Redis 자동 설정 기능으로 최소한의 구성만으로도 완전한 기능을 구현할 수 있었다.
환경 설정
spring:
data:
redis:
host: $REDIS_HOST
port: $REDIS_PORT
자동 제공 기능
- Repository 자동 검색:
@EnableRedisRepositories
어노테이션 없이도 자동으로 Redis Repository 검색 - ConnectionFactory 자동 생성: 환경 설정 기반으로
RedisConnectionFactory
자동 구성 - Serialization 자동 처리: 객체의 직렬화/역직렬화 자동 처리
기본 설정만으로도 완전한 Redis 연동이 가능하여 개발 시간을 크게 단축시킬 수 있다.
운영 고려사항
구현하지 않은 이유
조회수 기능을 최종적으로 구현하지 않은 것은 비즈니스 우선순위 때문이었다. Connectrip의 핵심 가치는 여행 계획 공유와 소통이었고, 조회수는 부가적인 기능으로 판단했다. 개발 리소스가 한정된 상황에서 사용자 인증, 게시글 작성/수정, 댓글 기능 등 필수 기능 구현에 집중하기로 했다. Redis 설정과 운영 복잡도를 추가하는 것보다는 서비스의 핵심 가치 구현이 더 중요하다고 결론지었다.
확장 가능한 활용 패턴
이 TTL 기반 중복 방지 패턴은 조회수 외에도 다양한 시나리오에 적용할 수 있다. 사용자별 API 호출 빈도를 제한하는 Rate Limiting이나 좋아요, 신고 등의 중복 실행을 차단하는 용도로 활용 가능하다. 또한 결제 진행 중이나 이메일 인증 대기 같은 임시 상태 관리, 그리고 특정 시간 동안 동일 요청에 대한 캐시를 제공하는 무효화 전략에도 적용할 수 있어 동일한 아키텍처 패턴으로 다양한 비즈니스 요구사항을 효율적으로 해결할 수 있다.
설계의 장점
Redis TTL 기반 해결책은 여러 측면에서 장점을 제공한다. 개발 효율성 측면에서는 Spring Boot의 자동 설정으로 구현 시간을 대폭 단축할 수 있고, 운영 안정성 면에서는 TTL 자동 만료로 수동 데이터 정리 작업이 불필요하다. 확장성 관점에서는 동일 패턴으로 다양한 중복 방지 시나리오에 대응할 수 있으며, 사용자 경험 측면에서는 조회수 왜곡을 방지하여 신뢰할 수 있는 지표를 제공한다.
설계 과정에서의 깨달음
Redis TTL 방식을 검토하면서 깨달은 것은, 기술적으로 우아한 해결책이 항상 최선은 아니라는 점이었다. Connectrip은 인증된 사용자만 접근하는 서비스이고, 이미 Spring Session을 사용하고 있었기 때문에 사용자 세션에 viewedPosts: Set<Long>
같은 필드를 추가하는 것만으로도 충분히 해결할 수 있었다.
하지만 당시에는 Redis TTL이라는 새로운 기술을 써보고 싶은 마음이 컸다. 결국 개발 리소스와 운영 복잡도를 고려했을 때 조회수 기능 자체가 필수가 아니라고 판단해서 구현하지 않았지만, 만약 정말 필요했다면 가장 간단한 세션 기반 방식을 택했을 것이다.
결론
Redis TTL을 활용한 중복 조회 방지 패턴을 설계해봤지만, 실제로는 구현하지 않았다. 비즈니스 우선순위와 개발 리소스, 운영 복잡도를 종합적으로 고려했을 때 필수 기능이 아니라고 판단했기 때문이다.
하지만 이런 설계 사고 과정 자체는 의미가 있었다고 생각한다. 중복 방지라는 일반적인 문제에 대해 여러 접근 방식을 비교 검토하고, 현재 프로젝트 맥락에서 가장 적절한 방법을 선택하는 사고 과정을 경험할 수 있었다. 실제 구현 경험은 없지만, 향후 비슷한 요구사항이 생겼을 때 참고할 수 있는 설계 레퍼런스로 남겨둔다.