
대부분의 웹 서비스에서 게시판은 필수적인 기능입니다. 그리고 게시판에는 언제나 페이징(Pagination) UI가 따라오죠.
<이전> 1 2 3 4 5 <다음> <마지막>
이 익숙한 UI를 만들기 위해 개발자들은 보통 두 가지 쿼리를 사용합니다.
SELECT * FROM board ORDER BY created_at DESC LIMIT 10 OFFSET 0 (현재 페이지 게시글 목록)SELECT COUNT(*) FROM board (전체 게시글 수)게시글 수가 몇 천, 몇 만 건일 때는 아무 문제가 없습니다. 하지만 게시글 수가 300만 건을 넘어가는 순간, COUNT(*) 쿼리는 조용히, 그리고 치명적으로 당신의 서비스를 망가뜨릴 수 있습니다. 이 글에서는 COUNT(*) 쿼리가 왜 위험한지, 그리고 이를 어떻게 극복할 수 있는지 다양한 해결책들을 심층적으로 분석해 보겠습니다.
COUNT(*) 쿼리의 문제점: 풀 스캔(Full Scan)의 악몽관계형 데이터베이스에서 SELECT COUNT(*) 쿼리는 보통 테이블의 모든 로우(Row)를 순회하는 풀 스캔을 수행합니다. 인덱스가 걸려 있더라도, 전체 개수를 정확히 세기 위해서는 인덱스를 처음부터 끝까지 읽어야 합니다.
COUNT(*) 쿼리가 동시에 수백 개씩 들어온다면 어떻게 될까요? DB 서버는 이 무거운 쿼리들을 처리하느라 다른 작업들을 할 수 없게 되고, 결국 DB 커넥션이 고갈되어 서비스 전체가 마비되는 최악의 상황에 놓이게 됩니다.즉, COUNT(*) 쿼리는 게시글 수가 적을 때는 문제가 없지만, 서비스가 성장할수록 발목을 잡는 성장통과 같은 존재입니다.
COUNT(*)의 문제를 해결하기 위한 방법들은 크게 세 가지로 나눌 수 있습니다. 각 방법은 장단점이 명확하므로, 서비스의 특성에 맞춰 신중하게 선택해야 합니다.
이 방법은 전체 게시글 수를 아예 세지 않고 COUNT(*) 쿼리 자체를 회피하는 방식입니다. 인스타그램이나 페이스북의 무한 스크롤이 대표적인 예입니다.
SELECT * FROM board WHERE id < [마지막 게시글 ID] ORDER BY id DESC LIMIT 10COUNT(*) 쿼리가 필요 없어 성능이 매우 빠릅니다.<1, 2, 3... 마지막>과 같은 페이지 번호 기반의 UI를 구현할 수 없습니다.따라서 페이지 번호가 중요하지 않은 '무한 스크롤' 방식에만 적합한 해결책입니다.
이 방법은 COUNT(*) 쿼리를 실시간으로 실행하는 대신, 별도의 통계 테이블에 전체 게시글 수를 미리 저장해두고 주기적으로 갱신하는 방식입니다.
board_count와 같은 통계 테이블을 만듭니다.SELECT COUNT(*) FROM board 쿼리를 한 번 실행하고, 그 결과를 board_count 테이블에 업데이트합니다.SELECT total_count FROM board_count 쿼리를 실행하여 매우 빠르게 총 개수를 가져옵니다.COUNT(*) 쿼리는 배치 작업으로 딱 한 번만 실행됩니다.INSERT)을 전혀 건드리지 않아도 되므로 개발 공수가 적습니다.이 방법은 데이터의 불일치를 어느 정도 허용할 수 있는 경우에 적합하며, 조회 조건이 단순하고 한정적일 때 효과적입니다.
가장 유연하고 강력한 방법 중 하나로, Redis의 In-memory DB를 활용하여 COUNT(*) 쿼리 결과를 캐싱하는 방식입니다.
board:count와 같은 키에 전체 게시글 수를 저장합니다.INCR 또는 DECR 명령어로 1씩 증가/감소시킵니다.TTL(Time-to-Live)을 설정하여 일정 시간(예: 5분)이 지나면 캐시가 만료되도록 합니다.board:count:notice 또는 board:count:rating:5와 같이 규칙에 따라 설정하면 다양한 필터링 조건에 쉽게 대응할 수 있습니다.COUNT(*) 쿼리를 날릴 수 있습니다. 이 순간의 DB 커넥션 폭주를 막기 위한 방어 로직(Mutex Lock 등)이 반드시 필요합니다.Redis 캐싱은 복잡도가 높지만, 성능과 유연성을 모두 잡을 수 있는 가장 이상적인 해결책입니다. 단, 발생할 수 있는 모든 예외 상황을 고려하고 대비책을 마련해야 합니다.
COUNT(*) 쿼리의 문제를 해결하는 방법에는 정답이 없습니다. 각 방법은 명확한 장단점을 가지고 있으며, 이는 곧 개발 비용, 성능, 데이터 일관성 사이의 트레이드오프 문제입니다.
무엇보다 중요한 것은 "A ms -> B ms로 개선했다"와 같은 단순한 수치 개선에만 집중하지 않고, 시스템 전체의 안정성과 데이터 일관성까지 고려하는 깊이 있는 설계입니다. 여러분의 서비스에 맞는 최적의 해결책을 찾아 현명하게 적용하시길 바랍니다.