서비스를 만들 때 정말 자주 다양하게 사용되는 것이 있으니, 바로 조회수다.
만만해보여도, 막상 트래픽이 몰리면 성능 병목(Bottleneck) 의 주범이 되는 기능이기도 하다.
한번 간단하게 구현하는 로직부터, 점점 고도화해가며 구현 로직을 고민해보자.
1안: 조회 단 건을 저장하기
article 테이블과 별개로, 조회 단 건을 저장하는 article_view 테이블을 만드는 것이다. 게시글이 조회될 때마다, 이 테이블에 정보를 INSERT 해주기만 하면 된다.
class ArticleView {
private long id;
private long userId;
private long articleId;
// 방문한 유저 정보도 수집 가능
private String userAgent;
private String referrer;
private String ipAddress;
private LocalDateTime createdAt;
}누가 언제 봤는지 데이터가 남는다.
중복 방지 로직(하루에 1번만 카운트 등)을 짤 수 있다.
방문자 접속 정보 통계를 구할 수 있다.
정규화가 되어있기 때문에 추후에 나오겠지만, 데이터 정합성의 문제도 없다.
특정 구간의 조회수를 추출하기에도 좋다. (ex. 최근 한 달간 조회수 랭킹)
글의 총 조회수를 얻어오기 위해서는 매번 SELECT COUNT(*)를 해야 하므로 데이터가 쌓이면 서비스가 멈출 정도로 느려질 수 있다.
게시글 목록 API에서 조회수 기반 정렬을 하려면 Join, Group by 등을 매번 해야 하므로 현실적으로 불가능하다.
2안: 조회 수 컬럼 생성하기
게시글에 조회수 컬럼을 생성하는 방법이다. 글이 조회될 때마다 다음과 같이 viewCount를 +1 해주면 된다.
class Article {
private long id;
// ...
private long viewCount = 0;
}UPDATE article SET view_count = view_count + 1 WHERE id = ?물론 꼭 article 테이블에 생성하란 법은 없고, view_count라는 테이블을 만들고 거기에 +1 해주어도 된다.
구현이 가장 쉽고, 조회수 순 정렬 (
ORDER BY view_count DESC) 쿼리가 매우 빠르다.대신 글이 조회될 때마다 viewCount를 +1 해주어야 하는데, 게시글 하나에 트래픽이 몰리면 DB에 UPDATE 락(Lock)이 걸려 성능이 크게 저하된다.
누가 조회했는지를 저장하지 않으므로 중복 체크를 할 수 없어, 새로고침만 해도 조회수가 계속 오르는 문제가 있다.
특정 구간의 조회수를 구할 수 없다.
동시성 이슈로 인해 정합성 문제가 발생할 수 있다.
중복 체크를 돕는 트리키한 해결 방법
누가 조회했는지를 따로 저장하지는 않지만, 쿠키를 이용하면 중복으로 조회수가 막 오르는걸 방지할 수 있다.
viewed_articles 쿠키를 추가하여, 글을 조회했을 때 [1, 5, 10] 처럼 오늘 본 글 ID가 있는지 확인하고, 쿠키에 해당 ID가 있다면 조회수 증가 로직을 건너뛰고 글만 보여주면 된다.
마지막으로 글 조회 api의 응답 헤더에 Set-Cookie를 통해 현재 글 ID를 추가하면 되고.
하지만 같은 유저여도 다른 기기에서 접속했을 경우엔 쿠키가 남아있지 않으니, 아무래도 불완전한 방식이라고 할 수 있겠다. 그래도 최소한이라도 서비스 속도를 개선할 수 있는 효율적인 방법이다.
조회 단 건도 저장하며, 조회 수 컬럼도 생성하기
1안과 2안의 장점을 모두 취하는 방법이다.
글을 조회할 때마다 조회수도 +1 해주고, 조회 정보도 INSERT 하는 것이다.
하지만 글을 조회할 때마다 UPDATE할 경, 서비스가 커졌을 때 성능 상 문제가 발생하는 점은 여전하기 때문에, 서비스가 커질 경우 다음과 같은 대안이 필요하다.
비동기 스케줄링
DB에 바로 쓰지 않고, 중간 버퍼에 모았다가 한 번에 업데이트한다는 아이디어다.
조회가 됐을 때 조회수를 바로 올리는 것이 아니라 로그를 쌓아두고, 예를 들면 5분마다 그 동안의 조회수를 계산해서 조회수에 더해주는 배치 형식이다.
보통 서비스가 이 정도로 커졌다면 Redis를 사용해볼 수 있겠다. 물론 RDB에서도 엇비슷하게 가능하고.
구현 로직
Redis에 다음과 같이 두 개의 키를 생성한다.
조회수 카운터:
article:{id}:view_count(String 타입)중복 방지용:
article:{id}:viewers(Set 타입)
사용자 요청이 들어올 경우, SISMEMBER article:1:viewers {userId}로 이전의 조회 여부를 확인한다.
처음 접속한 유저라면 다음과 같이 처리해주면 된다.
INCR article:1:view_count(메모리에서 조회수 +1 하므로 빠르다)SADD article:1:viewers {userId}(viewer Set에 유저를 등록한다)Set의 만료시간(TTL)을 하루로 설정.
마지막으로, 주기적으로 스케줄러가 Redis를 스캔해서 변경된 조회수 (article:*:view_count)를 모두 가져온 후 DB에 Bulk Update 해주면 된다.
유명인의 인스타그램 처럼 1분에도 수백만 명이 조회하는 글일 경우, 1분에 수백만 번의 UPDATE를, 1분에 1번으로 획기적으로 줄일 수 있다.
특정 구간 조회수 구하기
지금까지 설명한 것들은 모두 총 조회수를 구하기 위한 방법이었다.

네이버 블로그, 티스토리 등에선 월간/주간/일간 방문자 통계를 보여주므로, 특정 구간의 조회수를 구해야 하는 요구사항이 존재할 수 있다.
view_count컬럼으로는 특정 구간 조회수를 구할 수 없고, 그렇다고 매번 수백만 건의 로그 테이블을 WHERE created_at > 한달전으로 COUNT를 때릴 수도 없는 노릇이다.
일별 통계 테이블 만들기
로그 데이터를 그대로 쓰는 게 아니라, "날짜별 조회수"를 미리 합산해 둔 테이블을 하나 더 만드는 수밖에 없다.
CREATE TABLE article_daily_stats (
article_id BIGINT NOT NULL,
stat_date DATE NOT NULL,
view_count INT DEFAULT 0,
PRIMARY KEY (article_id, stat_date), -- (게시글ID + 날짜)가 유니크 키
INDEX idx_date_count (stat_date, view_count) -- 날짜별 인기순 정렬을 빠르게 하기 위함
-- 날짜를 먼저 찾고, 그 안에서 조회수를 합산하기 위한 인덱스
INDEX idx_stats_date_article ON article_daily_stats (stat_date, article_id, view_count);
);데이터를 쌓는 방법은 위에서 설명한 방법을 비슷하게 사용하면 되겠다.
조회를 위해선 일별로 데이터를 쪼개면 좋다. 최근 1주, 1달, 1년 모두 대응하기 좋기 때문이다.
최근 일주일의 조회수 그래프를 보고 싶을 경우
SELECT stat_date, view_count
FROM article_daily_stats
WHERE article_id = 1
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) -- 오늘 기준 1주일 전부터
ORDER BY stat_date ASC;최근 1달 (30일) 간의 조회수를 보고 싶은 경우
SELECT
article_id,
SUM(view_count) AS monthly_view_count
FROM article_daily_stats
WHERE article_id = 1
AND stat_date >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) -- 오늘 기준 1달 전부터
GROUP BY article_id;이런 식으로 유연하게 대응할 수 있다.
조회 수 랭킹 구하기
velog에서는 메인 피드에서 게시글 월간/주간/일간 랭킹을 제공한다.
특정 구간 조회수를 구할 수 있게 됐으니, 특정 구간의 랭킹을 구해보자.
일별 통계 테이블 활용하기
최근 1달 (30일) 조회수 랭킹을 보고 싶을 경우 아까 만든 테이블을 이렇게 활용할 수 있다.
SELECT
s.article_id,
a.title,
SUM(s.view_count) as period_views
FROM article_daily_stats s
JOIN article a ON s.article_id = a.id
WHERE s.stat_date >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH) -- 최근 1달
GROUP BY s.article_id
ORDER BY period_views DESC -- 기간 내 조회수 순 정렬
LIMIT 10;Redis Sorted Set (ZSet) 활용하기
만약 DB 쿼리조차 아끼고 싶고, 실시간 랭킹이 중요하다면 Redis의 Sorted Set (ZSet)을 활용해 기간별 키를 따로 관리할 수 있다.
Sorted Set은 Redis에서
Score를 기준으로 정렬된Unique string을 관리하는 컬렉션이다.
다음과 같이 Redis에 랭킹 정보를 저장할 키를 만들 수 있다.
ranking:monthly:2026-01ranking:weekly:2026-W42
그러면 글이 조회될 때, 다음과 같이 해당 구간의 조회수를 올리면 된다.
# 총 조회수 증가
INCR article:1:view
# 이번 달 랭킹 점수 증가
ZINCRBY ranking:monthly:2026-01 1 "article:1"DB 쿼리 없이, Redis에서 빠르게 특정 구간 랭킹 TOP 10을 구할 수 있다.
ZREVRANGE ranking:monthly:2026-01 0 9 WITHCORES