24. 유일하지 않은 정렬 키의 키셋 페이지네이션: 경계 행 누락
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] 키셋(seek) 페이지네이션의 정렬 키가 유일하지 않은 경우
// ----------------------------------------------------------------------------
// 시나리오:
// 길드원 목록을 레벨 내림차순으로 페이지 단위로 조회한다. OFFSET 대신 키셋(seek)
// 페이지네이션을 써서, 다음 페이지는 "직전 페이지 마지막 행의 레벨보다 낮은" 행부터 가져온다.
// 길드에는 같은 레벨의 길드원이 여럿 있을 수 있다.
//
// 요구사항:
// - 전체 길드원이 페이지를 넘겨가며 정확히 한 번씩 조회되어야 한다(누락/중복 금지).
//
// 과제:
// MemberPageRepo.GetNextPage 의 (A) 쿼리 조건과 (B) 커서 갱신을 검토하라.
// 같은 레벨의 길드원이 페이지 경계에 걸치는 상황을 따라가며 결과와 수정 방향을 설명하라.
// ============================================================================
using System.Collections.Generic;
public readonly struct MemberRow
{
public readonly long MemberId;
public readonly int Level;
public MemberRow(long id, int lvl) { MemberId = id; Level = lvl; }
}
public sealed class Page
{
public IReadOnlyList<MemberRow> Rows { get; }
public int? NextCursorLevel { get; } // null 이면 마지막 페이지
public Page(IReadOnlyList<MemberRow> rows, int? next) { Rows = rows; NextCursorLevel = next; }
}
public sealed class MemberPageRepo
{
private readonly IDb _db;
public MemberPageRepo(IDb db) { _db = db; }
public Page GetNextPage(long guildId, int? afterLevel, int pageSize)
{
string sql = afterLevel is null
? "SELECT member_id, level FROM guild_members " +
"WHERE guild_id=@g ORDER BY level DESC LIMIT @n"
: "SELECT member_id, level FROM guild_members " +
"WHERE guild_id=@g AND level < @lvl " + // (A)
"ORDER BY level DESC LIMIT @n";
var rows = _db.Query(sql, guildId, afterLevel ?? 0, pageSize);
int? next = rows.Count == pageSize ? rows[rows.Count - 1].Level : (int?)null; // (B)
return new Page(rows, next);
}
}
public interface IDb { IReadOnlyList<MemberRow> Query(string sql, long guildId, int level, int limit); } 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] 키셋(seek) 페이지네이션의 정렬 키가 유일하지 않은 경우
// ----------------------------------------------------------------------------
// 시나리오:
// 랭킹 보드를 점수 내림차순으로 페이지 단위로 조회한다. OFFSET 대신 키셋(seek)
// 페이지네이션을 써서, 다음 페이지는 직전 페이지 마지막 행의 점수를 커서로 이어 조회한다.
// 여러 플레이어가 같은 점수를 가질 수 있다.
//
// 요구사항:
// - 전체 플레이어가 페이지를 넘겨가며 정확히 한 번씩 조회되어야 한다(누락/중복 금지).
//
// 과제:
// RankingPageRepo::nextPage 의 (A) 쿼리 조건과 (B) 커서 갱신을 검토하라.
// 같은 점수의 플레이어가 페이지 경계에 걸치는 상황을 따라가며 결과와 수정 방향을 설명하라.
// ============================================================================
#include <string>
#include <vector>
#include <cstdint>
struct RankRow { int64_t playerId; int64_t score; };
struct IDb {
// 파라미터 바인딩된 결과를 돌려준다(설명용 간략화).
virtual std::vector<RankRow> query(const std::string& sql, int64_t boardId, int64_t score, int limit) = 0;
virtual ~IDb() = default;
};
struct Page {
std::vector<RankRow> rows;
bool hasCursor = false;
int64_t cursorScore = 0; // hasCursor==false 이면 마지막 페이지
};
class RankingPageRepo {
public:
explicit RankingPageRepo(IDb& db) : db_(db) {}
Page nextPage(int64_t boardId, bool hasCursor, int64_t afterScore, int pageSize) {
std::string sql = !hasCursor
? "SELECT player_id, score FROM ranking "
"WHERE board_id=? ORDER BY score DESC LIMIT ?"
: "SELECT player_id, score FROM ranking "
"WHERE board_id=? AND score <= ? " // (A)
"ORDER BY score DESC LIMIT ?";
auto rows = db_.query(sql, boardId, afterScore, pageSize);
Page p;
p.rows = rows;
if ((int)rows.size() == pageSize) {
p.hasCursor = true;
p.cursorScore = rows.back().score; // (B)
}
return p;
}
private:
IDb& db_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.