23. 실시간 랭킹 캐시와 원본 DB의 일관성 (C#)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 실시간 랭킹 캐시와 원본 DB의 일관성
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리 · 서버-서버/서버-클라):
// - 점수가 바뀔 때마다 권위 저장소(DB)에 기록하고, 실시간 조회를 빠르게 하려고
// 인메모리 정렬 캐시(랭킹)를 함께 유지한다(write-through).
// - 캐시는 두 구조로 구성: _scoreOf(playerId -> 현재 점수)와
// _ranking(정렬된 (점수, playerId) 집합; 상위 N 조회용).
// - UpdateScore 는 여러 워커 스레드에서 같은/다른 플레이어에 대해 동시에 불린다.
// - 캐시 미스(서버 재기동/축출)면 RebuildFromDb 로 DB를 읽어 캐시를 재구축한다.
//
// 요구사항:
// - 캐시의 점수/랭킹은 DB와 수렴해야 한다(영구 불일치 금지).
// - 같은 플레이어에 대한 동시/순서 뒤바뀐 갱신에도 캐시에 "더 낮은(옛) 점수" 가
// 최종으로 남으면 안 된다(out-of-order 역전 금지).
// - 랭킹 집합에서 한 플레이어는 항상 정확히 한 엔트리(중복/소실 금지).
// - 재구축이 동시 갱신을 덮어써 유실시키면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 캐시-DB 불일치·랭킹 중복/소실·순서 역전·
// 재구축 중 유실·동시 컬렉션 손상이 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은
// 설계를 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public interface IScoreDb
{
void WriteScore(long playerId, long score);
List<(long playerId, long score)> LoadAll();
}
public class RankingCache
{
private readonly IScoreDb _db;
private readonly Dictionary<long, long> _scoreOf = new(); // playerId -> 점수
private readonly SortedSet<(long score, long playerId)> _ranking = new(); // (점수, id) 오름차순
public RankingCache(IScoreDb db) { _db = db; }
// 점수 갱신: DB 기록 후 캐시 갱신 (여러 스레드 동시 호출)
public void UpdateScore(long playerId, long newScore)
{
// (A) DB 기록과 캐시 갱신의 순서/관계를 검토하라
_db.WriteScore(playerId, newScore);
// (B) 옛 엔트리 제거 → 새 엔트리 삽입 절차를 검토하라
if (_scoreOf.TryGetValue(playerId, out var old))
{
_ranking.Remove((old, playerId));
_scoreOf[playerId] = newScore;
}
else
{
_scoreOf[playerId] = newScore;
}
_ranking.Add((newScore, playerId));
}
// 상위 N 조회 (조회 스레드)
public List<long> TopN(int n)
{
var outList = new List<long>();
// (C) 이 순회를 동시 갱신과 함께 검토하라
foreach (var e in _ranking) // 오름차순; 실제로는 역순(상위)부터 필요하나 데모상 단순화
{
outList.Add(e.playerId);
if (outList.Count >= n) break;
}
return outList;
}
// 캐시 미스 시 DB에서 전체 재구축
public void RebuildFromDb()
{
// (D) 이 재구축 절차를 동시 갱신과 함께 검토하라
var all = _db.LoadAll();
_scoreOf.Clear();
_ranking.Clear();
foreach (var (pid, sc) in all)
{
_scoreOf[pid] = sc;
_ranking.Add((sc, pid));
}
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 실시간 랭킹 캐시와 원본 DB의 일관성
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리 · 서버-서버/서버-클라):
// - 점수가 바뀔 때마다 권위 저장소(DB)에 기록하고, 실시간 조회를 빠르게 하려고
// 인메모리 정렬 캐시(랭킹)를 함께 유지한다(write-through).
// - 캐시는 두 구조로 구성: scoreOf_(playerId -> 현재 점수)와
// ranking_(정렬된 (점수, playerId) 집합; 상위 N 조회용).
// - updateScore 는 여러 워커 스레드에서 같은/다른 플레이어에 대해 동시에 불린다.
// - 캐시 미스(서버 재기동/축출)면 rebuildFromDb 로 DB를 읽어 캐시를 재구축한다.
//
// 요구사항:
// - 캐시의 점수/랭킹은 DB와 수렴해야 한다(영구 불일치 금지).
// - 같은 플레이어에 대한 동시/순서 뒤바뀐 갱신에도 캐시에 "더 낮은(옛) 점수" 가
// 최종으로 남으면 안 된다(out-of-order 역전 금지).
// - 랭킹 집합에서 한 플레이어는 항상 정확히 한 엔트리(중복/소실 금지).
// - 재구축이 동시 갱신을 덮어써 유실시키면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 캐시-DB 불일치·랭킹 중복/소실·순서 역전·
// 재구축 중 유실·동시 순회 문제가 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은
// 설계를 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>
// DB 스텁: 권위 저장소(영속). 실제로는 원격/디스크.
struct ScoreDb {
virtual void writeScore(int64_t playerId, int64_t score) = 0;
virtual std::vector<std::pair<int64_t,int64_t>> loadAll() = 0; // (playerId, score)
virtual ~ScoreDb() = default;
};
class RankingCache {
public:
explicit RankingCache(ScoreDb& db) : db_(db) {}
// 점수 갱신: DB 기록 후 캐시 갱신 (여러 스레드 동시 호출)
void updateScore(int64_t playerId, int64_t newScore) {
// (A) DB 기록과 캐시 갱신의 순서/관계를 검토하라
db_.writeScore(playerId, newScore);
// (B) 옛 엔트리 제거 → 새 엔트리 삽입 절차를 검토하라
auto it = scoreOf_.find(playerId);
if (it != scoreOf_.end()) {
ranking_.erase({ it->second, playerId }); // 옛 (점수,id) 제거
it->second = newScore;
} else {
scoreOf_[playerId] = newScore;
}
ranking_.insert({ newScore, playerId }); // 새 (점수,id) 삽입
}
// 상위 N 조회 (조회 스레드)
std::vector<int64_t> topN(int n) {
std::vector<int64_t> out;
// (C) 이 순회를 동시 갱신과 함께 검토하라
for (auto rit = ranking_.rbegin(); rit != ranking_.rend() && (int)out.size() < n; ++rit)
out.push_back(rit->second);
return out;
}
// 캐시 미스 시 DB에서 전체 재구축
void rebuildFromDb() {
// (D) 이 재구축 절차를 동시 갱신과 함께 검토하라
auto all = db_.loadAll();
scoreOf_.clear();
ranking_.clear();
for (auto& [pid, sc] : all) {
scoreOf_[pid] = sc;
ranking_.insert({ sc, pid });
}
}
private:
ScoreDb& db_;
std::unordered_map<int64_t, int64_t> scoreOf_; // playerId -> 점수
std::set<std::pair<int64_t,int64_t>> ranking_; // (점수, playerId) 오름차순
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.