
게시글 상세 API가 호출될 때마다 숫자를 하나 올리는 것만으로 조회수를 구현할 수 있을까? TRIPFEED의 게시판을 설계하면서 먼저 고민한 것은 구현 방법이 아니라 이 서비스에서 조회수가 무엇을 의미해야 하는가였습니다.
단순 요청 횟수를 보여주면 새로고침만으로도 숫자를 올릴 수 있습니다. 그렇게 만들어진 숫자는 게시글이 실제로 얼마나 읽혔는지 보여주지 못합니다. 그래서 코드를 작성하기 전에 유효한 조회의 기준부터 정하기로 했습니다.

게시판마다 조회수의 의미가 달랐다
TRIPFEED에는 여행 정보, 후기, 질문, 동행 모집 게시판이 있습니다. 모두 같은 게시글이지만 사용자가 글을 찾고 소비하는 방식은 서로 다릅니다.
특히 여행 정보글에서 조회수는 많은 사람이 해당 정보를 읽고 참고했다는 관심도 지표가 됩니다. 반면 기간이 지나면 의미가 줄어드는 동행 모집글에서는 조회수보다 모집 상태와 최신성이 더 중요합니다. 이 차이를 확인하고 나니 조회수를 모든 게시판에서 가장 중요한 숫자처럼 다룰 필요는 없지만, 적어도 집계되는 숫자 자체는 신뢰할 수 있어야 한다고 판단했습니다.
API 호출이 아니라 ‘유효한 조회’ 세기
이 서비스의 조회수를 다음과 같이 정의했습니다.
회원 또는 비회원에게 게시글 상세 내용이 정상적으로 제공된 유효 조회 횟수. 같은 사용자가 같은 게시글을 짧은 시간 안에 반복해서 조회한 경우에는 한 번만 집계한다.
삭제되었거나 접근할 수 없는 게시글, 응답에 실패한 요청은 사용자가 내용을 읽은 것이 아니므로 집계하지 않습니다. 작성자가 자신의 글을 확인하는 경우도 글의 실질적인 소비량과는 거리가 있다고 보아 제외했습니다.
재방문은 인정하고, 반복 새로고침은 제외하기
여행 정보는 같은 사용자가 다시 찾아볼 가능성이 큽니다. 하루 동안의 모든 재방문을 하나로 처리하면 실제로 다시 참고한 행동까지 사라집니다. 반대로 API가 호출될 때마다 증가시키면 짧은 시간 동안의 새로고침만으로 조회수를 부풀릴 수 있습니다.
두 행동을 구분할 시간 기준이 필요했습니다. 여행 커뮤니티로 많이 사용되는 네이버 카페의 조회수 집계 기준을 참고하고, 의도적인 반복 새로고침은 막으면서 실제 재방문은 지나치게 제한하지 않도록 같은 사용자의 같은 게시글 조회는 1분 동안 한 번만 집계하기로 했습니다.
이 시간은 기술적인 정답이 아니라 서비스 정책입니다. 운영 이후 사용 패턴이 달라진다면 다시 조정할 수 있어야 합니다.
검색으로 들어온 비회원도 포함하기
여행 정보글은 검색을 통해 처음 들어오는 사용자가 많을 수 있습니다. 로그인한 사용자만 집계하면 실제 콘텐츠 소비량과 조회수 사이의 차이가 커집니다. 따라서 게시글 상세 조회는 비회원에게도 열어두고, 회원과 비회원의 조회를 모두 포함하기로 했습니다.
- 회원은 계정의 사용자 ID로 구분합니다.
- 비회원은 서버가 브라우저에 발급한 방문자 식별 쿠키로 구분합니다.
- 비회원 상태에서 글을 본 뒤 로그인해 다시 보는 경우는 서로 다른 조회로 허용합니다.
마지막 경우까지 하나의 사용자로 연결하려면 로그인 전후의 식별 정보를 별도로 병합해야 합니다. 현재 서비스 규모에서 얻는 정확도에 비해 구조가 과도하게 복잡해진다고 판단해 이 정도의 중복은 허용했습니다.
IP를 사용자 식별 기준에서 제외한 이유
처음에는 IP 주소를 이용하는 방법도 생각했습니다. 하지만 여행 서비스의 사용자는 이동 중에 네트워크가 자주 바뀔 수 있고, 공항·숙소·카페처럼 하나의 네트워크를 여러 사람이 공유할 수도 있습니다.
같은 사람을 여러 명으로 보거나 여러 사람을 한 명으로 보는 상황이 모두 발생할 수 있기 때문에, IP는 중복 조회를 판단하는 식별 기준으로 사용하지 않기로 했습니다.
최종 조회수 정책
- 회원과 비회원의 정상적인 게시글 상세 조회를 모두 집계합니다.
- 작성자가 자신의 게시글을 조회한 경우는 집계하지 않습니다.
- 같은 사용자가 같은 게시글을 1분 안에 다시 조회하면 중복으로 처리합니다.
- 삭제되었거나 접근 권한이 없는 게시글과 조회에 실패한 요청은 집계하지 않습니다.
- 회원은 사용자 ID, 비회원은 방문자 식별 쿠키를 사용합니다.
- 이동과 공용 네트워크 환경을 고려해 IP 주소는 식별 기준에서 제외합니다.
정책을 정한 뒤에야 구현 문제가 보였다
집계 기준을 정하고 나니 다음 문제는 명확해졌습니다. 여러 요청이 동시에 들어와도 증가분을 잃지 않아야 했고, 조회 기록의 존재 여부를 확인하고 저장하는 과정 역시 경쟁 조건에서 안전해야 했습니다.
이 두 문제는 각각 Lost Update를 DB 원자적 연산으로 해결한 과정, Check-Then-Act를 조건부 UPDATE와 INSERT IGNORE로 바꾼 과정으로 나누어 기록했습니다. 조회수라는 하나의 기능 안에서도 서비스 정책과 데이터 정합성 문제를 분리하니 각 선택의 이유를 더 분명하게 설명할 수 있었습니다.