Q.커서 값을 클라이언트에 그대로 노출하면 어떤 문제가 있나요?
커서에 정렬 키 값이 그대로 담기면 내부 구조를 노출하고 나중에 바꿀 수 없게 됩니다.
| 문제 | 내용 |
|---|
| 구조 노출 | created_at=2026-08-14&id=1234 가 보이면 내부 정렬 방식이 드러난다 |
| 조작 가능 | 값을 바꿔 보내면 의도하지 않은 범위를 조회할 수 있다 |
| 변경 불가 | 정렬 기준을 바꾸면 옛 커서가 깨진다 |
| 권한 우회 | 다른 사용자의 커서를 써서 접근 범위를 넘볼 수 있다 |
| 정보 유출 | id 값 자체가 전체 규모나 순번을 알려준다 |
그래서 불투명한 커서로 만듭니다. 필요한 값을 인코딩해 하나의 문자열로 주고, 클라이언트는 그것을 그대로 되돌려 보냅니다.
| 처리 | 내용 |
|---|
| 인코딩 | 정렬 키와 방향, 필터 조건을 담아 base64 로 |
| 서명 | 변조를 막으려면 해시를 함께 담는다 |
| 버전 | 커서 형식을 바꿀 수 있게 버전을 넣는다 |
| 필터 포함 | 첫 요청의 필터를 담아 두면 도중에 조건이 바뀌는 것을 막는다 |
마지막이 실용적입니다. 커서에 필터를 담으면 클라이언트가 페이지마다 같은 필터를 보내는지 검증할 수 있습니다.
흔한 실수: base64 인코딩을 보안으로 여기는 것. 누구나 디코딩할 수 있어서 노출은 막지 못합니다. 조작을 막으려면 서명이 필요합니다.
Q.커서 기반 페이지네이션에서 정렬키가 중복될 때 어떤 문제가 생기나요?
같은 값이 여러 개면 어디까지 봤는지 특정할 수 없어 항목이 빠지거나 중복됩니다.
created_at 하나로 커서를 만든 경우입니다.
| 상황 | 결과 |
|---|
| 같은 시각에 3건이 있고 페이지 경계가 그 사이에 걸린다 | |
WHERE created_at < 마지막값 | 같은 시각의 나머지 2건을 건너뛴다 |
WHERE created_at <= 마지막값 | 이미 본 1건이 다시 나온다 |
어느 쪽으로도 정확하지 않습니다. 등호를 넣으면 중복, 빼면 누락입니다.
해결은 고유한 값을 정렬 키에 더하는 것입니다.
| 항목 | 내용 |
|---|
| 정렬 | ORDER BY created_at DESC, id DESC |
| 조건 | WHERE (created_at, id) < (마지막시각, 마지막id) |
| 인덱스 | 그 컬럼 조합으로 만든다 |
두 값을 묶어 비교하면 순서가 완전히 결정됩니다. 같은 시각이어도 id 로 갈리므로 경계가 애매해지지 않습니다.
주의할 것이 하나 있습니다. 이 비교를 created_at < ? OR (created_at = ? AND id < ?) 로 풀어 쓰면 인덱스를 제대로 못 타는 경우가 있어, 실행 계획을 확인해야 합니다.
흔한 실수: 정렬 키가 유일하다고 가정하는 것. 타임스탬프는 초 단위 저장이거나 대량 삽입이면 쉽게 겹칩니다. 정렬 키에 고유 컬럼을 항상 함께 두는 편이 안전합니다.
Q.무한 스크롤 중 새 글이 등록되면 offset 방식에서 무슨 일이 벌어지나요?
앞쪽에 항목이 끼어들면서 뒤로 밀린 항목을 다시 보게 됩니다.
| 순서 | 상황 |
|---|
| 1 | 1페이지로 최신 10건을 본다 |
| 2 | 새 글 3건이 등록된다 |
| 3 | OFFSET 10 으로 2페이지를 요청한다 |
| 4 | 새 글 때문에 모든 항목이 3칸 밀렸다 |
| 5 | 1페이지에서 본 8, 9, 10번째 글이 다시 나온다 |
반대 방향도 생깁니다. 글이 삭제되면 항목이 앞으로 당겨져 못 본 항목이 건너뛰어집니다. 이쪽이 더 나쁩니다. 사용자는 중복은 알아채지만 누락은 모릅니다.
커서 방식은 이 문제가 없습니다. 위치를 번호로 세지 않고 마지막으로 본 항목 자체를 기준으로 하기 때문입니다. 앞에 몇 건이 추가되든 "이 글보다 오래된 것"의 답은 변하지 않습니다.
| 대응 | 내용 |
|---|
| 커서 기반으로 전환 | 근본 해결 |
| 첫 요청 시각 고정 | WHERE created_at <= 조회시작시각 을 계속 붙인다 |
| 클라이언트 중복 제거 | id 로 걸러낸다. 누락은 못 막는다 |
두 번째는 offset 을 유지하면서 쓸 수 있는 우회입니다. 스크롤을 시작한 시점 이전 데이터만 보게 고정하면 새 글이 끼어들지 않습니다.
흔한 실수: 중복만 보고 클라이언트에서 걸러 해결했다고 보는 것. 누락은 그대로 남아 있고, 사용자는 그것을 알 수 없습니다.
Q.전체 개수를 함께 제공해야 한다면 어떻게 설계하겠습니까?
전체 개수 계산은 조건에 맞는 행을 모두 세는 일이라 데이터가 많으면 그 자체가 느립니다. 그래서 정말 필요한지부터 봅니다.
| 요구 | 대안 |
|---|
| 정확한 총 개수 | 별도 요청으로 분리해 첫 페이지 응답을 막지 않는다 |
| 대략적인 규모 | 추정치를 쓴다. 실행 계획의 예상 행수나 통계 |
| 다음 페이지 유무만 | 한 건 더 조회해 존재만 확인한다 |
| 상한 표시 | 1000+ 처럼 일정 개수까지만 세고 넘으면 표시만 바꾼다 |
세 번째가 가장 값싸고 대부분의 화면에 충분합니다. 페이지 크기 20이면 21건을 조회해 21번째가 있으면 다음 페이지가 있다고 판단하고 20건만 돌려줍니다.
네 번째는 검색 엔진들이 쓰는 방식입니다. 1,000건까지만 세고 그 이상은 정확히 세지 않습니다. 사용자도 정확한 숫자를 필요로 하지 않습니다.
| 성능을 얻는 방법 | 내용 |
|---|
| 캐시 | 개수를 짧게 캐시한다. 몇 초 지난 값이어도 된다 |
| 집계 테이블 | 자주 쓰는 조건의 개수를 미리 관리한다 |
| 근사치 | 통계 기반 추정 |
흔한 실수: 목록 조회와 개수 조회를 한 트랜잭션에서 함께 하는 것. 개수 계산이 느리면 목록도 그만큼 늦게 나갑니다. 개수를 나중에 채우면 사용자는 목록을 먼저 봅니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
API 설계 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.