조회수 개발기

배경
관리자가 등록한 게시글을 사용자들이 얼마나 조회했는지 확인할 수 있는 기능이 필요했습니다. 주요 목표는 빠르고 단순하게 기능을 구현하는 것이었으며, 복잡한 데이터 분석보다는 얼마나 많은 사용자가 게시글을 조회했는지 간단히 확인할 수 있는 시스템을 만드는 것이 핵심이었습니다.
중요한 요구사항 중 하나는 조회수를 실시간으로 표시하는 것이었습니다. 사용자가 게시글을 클릭할 때마다 즉시 조회수가 증가하고, 이를 화면에 바로 반영해야 했습니다. 이는 사용자 경험을 향상시키고, 콘텐츠의 인기도를 실시간으로 파악할 수 있게 하는 중요한 기능이었습니다.
외부 서비스 사용하기 VS 직접 구현하기
조회수를 개발하는 과정에서 외부 서비스를 사용할지 직접 구현할지에 대한 고민이 있었습니다.
외부 서비스(Google Analytics)의 장단점
장점
- 다양한 기능: Google Analytics는 단순한 조회수 기능을 넘어 다양한 사용자 데이터를 수집하고 분석할 수 있는 기능을 제공합니다.
단점
외부 의존성: 외부 서비스에 데이터를 의존하는 것은 오히려 관리 부담을 증가시킬 수 있습니다.
전문 인력 부족: 내부에 Google Analytics를 능숙하게 다룰 수 있는 전문가가 부족합니다.
커스터마이징 한계: 특정 요구사항에 맞게 맞춤 설정하기 어렵거나 제한적일 수 있습니다.
직접 구현의 장단점
장점
빠른 개발: 요구사항에 맞춰 간단한 조회수 기능을 신속하게 개발할 수 있습니다.
높은 유연성: 필요에 따라 기능을 자유롭게 수정하거나 확장할 수 있습니다.
내부 데이터 관리: 모든 데이터를 내부에서 관리함으로써 요구사항에 맞는 데이터 활용을 용이하게 할 수 있습니다.
단점
- 기능 확장 시 어려움: 단순한 조회수 기능을 개발하는 것은 빠르지만, 추가적인 분석 기능이나 고급 기능을 개발할 때는 더 많은 시간이 소요될 수 있습니다.
직접 구현으로 결정
단순한 요구사항을 빠르게 충족시키고 관리와 유지의 효율성을 높이기 위해 직접 구현을 선택했습니다. 이 방식은 다음과 같은 장점들이 있었습니다:
신속한 개발이 가능했습니다.
내부에서 데이터를 효율적으로 관리할 수 있었습니다.
실시간 조회수 제공이라는 핵심 요구사항을 충족시킬 수 있었습니다.
특히, 실시간으로 조회수를 제공해야 하는 요구사항이 중요했습니다. 사용자가 게시글을 조회할 때마다 화면에 조회수가 즉시 업데이트되어야 했습니다. Google Analytics의 페이지뷰 기능은 주로 분석 용도로 사용되어, 이러한 실시간 요구사항을 충족시키기 어려웠습니다.
이러한 이유들로 인해, 조회수 기능을 직접 구현하는 것이 프로젝트의 요구사항과 목표를 가장 잘 충족시킬 수 있는 방법이라고 판단하였습니다.
구현 과정
최소한의 작업으로 빠르게 기능 구현
우선, 조회수 기능이 빠르게 동작하도록 하기 위해 Redis를 사용해 구현했습니다. Redis는 메모리 기반의 데이터 저장소로, 높은 성능을 제공하며 실시간으로 조회수를 집계하는 데 적합하다고 판단했습니다.
(앞으로 나오는 코드는 실제 코드가 아닌 이해를 돕기 위한 코드입니다.)
조회수 추가:
public void addPageView(String key) {
stringRedistemplate.opsForValue().increment(key);
}
조회수 조회:
public Integer getPageView(final String keyey) {
final String pageView = stringRedistemplate.opsForValue().get(key);
return Integer.valueOf(requireNonNullElse(pageView, "0"));
}
문제점 파악
빠르게 동작하는 조회수 기능을 구현하는 데 성공했지만, 이 과정에서 몇 가지 부족한 점이 드러났습니다.
데이터 안정성 부족: Redis는 메모리 기반 데이터 저장소이기 때문에 언제든지 데이터가 유실될 수 있다고 가정해야 합니다.
관리자와 사용자 간의 데이터 접근 분리 필요성: 현재 조회수 데이터는 엔드유저가 사용하는 Redis에만 적재되어 있습니다. 관리자가 데이터를 조회할 때, 엔드유저와 동일한 인프라를 사용하면 시스템 성능에 영향을 미칠 수 있고, 사용자 경험이 저하될 위험이 있습니다.
최종 구현
우선, 조회수 데이터의 안정성을 높이기 위해, Redis뿐만 아니라 RDB에도 데이터를 저장했습니다.
그리고 Redis에 조회수를 기록할 때마다 RDB에 직접 요청을 보내는 것은 성능상 문제가 될 수 있기 때문에 메시지 큐(Amazon SQS)를 활용했습니다.
Redis에 조회수를 기록합니다.
조회수 증가 이벤트를 메시지 큐(Amazon SQS)에 발행합니다.
별도의 리스너가 이벤트를 수신하여 RDB에 반영합니다.
추가적으로 트래픽이 몰릴 경우를 대비해서 리스너에 RateLimiter를 사용해서 RDB에 대한 요청수를 제어하도록 했습니다.
조회수 추가 및 이벤트 발행:
public void addPageView(String key) {
final Long views = getAndIncrease(pageViewKey);
pageViewPublisher.publish(PageViewEvent.of(pageViewKey));
}
private Long getAndIncrease(final String key) {
// lua script를 사용했고, key가 존재하면 1 증가하고 없으면 DB에서 값을 가져와서 1 증가하도록 구현
}
이벤트 수신:
class PageViewListener {
private final RateLimiter rateLimiter;
private final PageViewService service;
@SqsListener
void pageViewListener(final List<PageViewEvent> events, final BatchAcknowledgement<Object> acknowledgement) {
rateLimiter.acquire();
final PageViews pageViews = createPageViews(events);
service.save(pageViews);
acknowledgement.acknowledge();
}
}
마무리하며
조회수를 개발하며 인상적이었던 경험을 두 가지로 정리해 보려고 합니다.
첫째, 요구사항을 충족시키기 위해 다양한 방법을 검토하고 가장 적합한 해결책을 도출하는 과정이 매우 유익했습니다. 외부 서비스 사용과 자체 개발 사이에서 고민하며, 각 옵션의 장단점을 분석했고, 이러한 과정을 통해 단순히 기술적인 구현을 넘어, 프로젝트의 본질적인 목표와 제약 조건을 더 깊이 이해할 수 있었습니다.
둘째, Walking Skeleton 방식의 효과를 경험할 수 있었습니다. 적절한 방법을 결정한 후, 최소한의 작업으로 핵심 요구사항을 만족하는 시스템을 신속하게 구축하여 사용자에게 제공하고, 이후에 세부적인 부분들을 단계적으로 보완해 나갔습니다. 그 결과 불필요한 작업을 최소화하고, 사용자 피드백을 조기에 받아 시스템을 개선할 수 있었습니다. 또한, 실제 사용 환경에서의 문제점을 빠르게 파악하고 대응할 수 있었습니다.
Subscribe to my newsletter
Read articles from WENO directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
