[UMC] 커서 페이징

Backend·2025-09-17·by 1000hyehyang

1주차 미션은 "진행 중, 완료한 미션을 모아서 보는 화면의 쿼리를 작성해보자"

기본적인 쿼리는 이미 작성해둔 상태였고, 처음에는 오프셋 기반으로 페이징을 처리해보고, 이어서 커서 페이징으로 최신순 정렬까지 구현해봤다. 그런데 시니어 미션에서는 요구사항이 조금 더 확장됐다. 단순히 최신순만이 아니라, 정렬 기준을 1순위는 포인트, 2순위는 최신순으로 바꿔야 했다.

보통 오프셋 기반 페이징은 간단하지만, 데이터가 많아질수록 성능이 떨어지는 문제가 있다. 앞쪽 데이터를 몽땅 스캔하고 버린 다음 원하는 위치에서 끊어오기 때문이다.

반면 커서 기반 페이징은 “내가 마지막으로 본 데이터 이후의 값”이라는 기준을 직접 커서로 넘겨주기 때문에, 훨씬 효율적으로 다음 데이터를 가져올 수 있다. 특히 미션처럼 점점 데이터가 쌓일 수 있는 도메인에서는 커서 방식이 더 자연스럽다.

여기서 중요한 건 정렬 조건을 포인트 → 최신순으로 잡는 거다. 즉, reward_point가 높은 미션이 우선 노출되고, 포인트가 같다면 최근에 참여한 순서대로 정렬되도록 해야 한다. 커서 역시 이 두 가지 기준을 동시에 고려해야 한다.

 

SELECT 
    um.user_mission_id,
    um.status,
    um.completed_at,
    m.mission_id,
    m.title,
    m.description,
    m.reward_point,
    s.store_id,
    s.name AS store_name,
    um.created_at
FROM user_mission um
JOIN mission m ON um.mission_id = m.mission_id
JOIN store s ON m.store_id = s.store_id
WHERE um.user_id = ?
  AND (
        m.reward_point < ? 
        OR (m.reward_point = ? AND um.created_at < ?)
        OR ? IS NULL
      )
ORDER BY m.reward_point DESC, um.created_at DESC
LIMIT 10;

 

자세히 뜯어보자

AND (
    m.reward_point < ? 
    OR (m.reward_point = ? AND um.created_at < ?)
    OR ? IS NULL
)

m.reward_point < ? → 직전 페이지 마지막 미션보다 포인트가 더 낮은 미션만 가져오겠다!

OR (m.reward_point = ? AND um.created_at < ?) → 포인트가 같다면, 커서 시각(created_at)보다 더 오래된 데이터만 가져오겠다!

OR ? IS NULL → 첫 페이지라면 커서 값이 없으므로, 이 조건으로 인해 제약을 무시하고 최신 데이터 10개를 가져오겠다.

 

ORDER BY m.reward_point DESC, um.created_at DESC

→ 먼저 reward_point가 높은 순으로 정렬하고, 포인트가 같다면 참여 시각이 최근인 순서로 정렬한다.

이 정렬 조건과 커서 조건이 짝을 이루기 때문에, 다음 페이지로 넘어갈 때도 안정적으로 데이터를 이어받을 수 있다.

즉, 단순히 최신순 정렬을 넘어, 포인트 → 최신순이라는 복합적인 정렬 조건을 안정적으로 처리할 수 있다. 무엇보다 대규모 데이터셋에서 오프셋 방식보다 성능상 이점이 크다.

댓글 로딩 중...