12. 소프트 삭제와 UNIQUE 제약의 충돌 (길드명 재사용 실패)
난이도 하 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 길드 삭제(해산)는 실제 행을 지우지 않고 is_deleted 플래그만 세우는
// 소프트 삭제로 구현되어 있다(감사/복구 목적). guild 테이블의 name 컬럼에는
// UNIQUE 제약이 걸려 있다: UNIQUE(name).
// 길드가 해산된 뒤, 플레이어가 같은 이름으로 새 길드를 만들려 하거나,
// 운영자가 실수로 삭제된 길드를 복구하려 할 때 문제가 생길 수 있다.
//
// 요구사항:
// - 해산(소프트 삭제)된 길드의 이름은 이후 재사용(같은 이름으로 새 길드 생성)이
// 가능해야 한다.
// - 살아있는(is_deleted=false) 길드 사이에서는 이름 중복이 있어서는 안 된다.
// - 소프트 삭제된 길드는 필요 시 복구(is_deleted=false 로 되돌림)할 수 있어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 현재 스키마/로직으로 위 요구사항 중 무엇이,
// 어떤 상황에서 깨지는지 밝히고 고쳐라.
using System.Threading.Tasks;
public interface IDb
{
Task<bool> ExistsAsync(string sql, string arg);
Task<int> ExecAsync(string sql, params object[] args);
}
public sealed class GuildRepo
{
private readonly IDb _db;
public GuildRepo(IDb db) => _db = db;
// guild 테이블: id, name (UNIQUE), owner_id, is_deleted (기본 false)
public async Task<bool> CreateGuildAsync(string name, ulong ownerId)
{
// (A)
if (await _db.ExistsAsync("SELECT 1 FROM guild WHERE name=@n AND is_deleted=false", name))
return false; // 살아있는 동명 길드가 있으면 거부
var rows = await _db.ExecAsync(
"INSERT INTO guild(name, owner_id, is_deleted) VALUES(@n, @o, false)",
name, ownerId);
// UNIQUE(name) 위반 시 여기서 예외가 나는데, 호출자는 "이미 존재"로만 인지하고
// 그것이 살아있는 길드 때문인지 해산된 길드 때문인지 구분하지 못한다.
return rows > 0;
}
// 운영자가 해산된 길드를 복구
public async Task<bool> RestoreGuildAsync(int guildId)
{
// (B)
var rows = await _db.ExecAsync(
"UPDATE guild SET is_deleted=false WHERE id=@id", guildId);
return rows > 0;
}
} 결함 코드 · C++
// 시나리오: 캐릭터 삭제는 실제 행을 지우지 않고 is_deleted 플래그만 세우는
// 소프트 삭제로 구현되어 있다(유예기간 후 실제 파기, 그 전까지 복구 가능).
// character 테이블의 name 컬럼에는 UNIQUE 제약이 걸려 있다: UNIQUE(name).
// 서버는 C++ 로 작성된 게이트웨이에서 캐릭터 생성/삭제/복구 요청을 처리한다.
//
// 요구사항:
// - 삭제(소프트 삭제)된 캐릭터의 이름은 이후 재사용(같은 이름으로 새 캐릭터 생성)이
// 가능해야 한다.
// - 살아있는(is_deleted=false) 캐릭터 사이에서는 이름 중복이 있어서는 안 된다.
// - 소프트 삭제된 캐릭터는 유예기간 내에 복구(is_deleted=false 로 되돌림)할 수
// 있어야 한다.
//
// 과제: (A)(B) 지점을 검토하라. 현재 스키마/로직으로 위 요구사항 중 무엇이,
// 어떤 상황에서 깨지는지 밝히고 고쳐라.
#include <string>
#include <optional>
class IDb {
public:
virtual ~IDb() = default;
virtual bool exists(const std::string& sql, const std::string& arg) = 0;
virtual int execUpdate(const std::string& sql, const std::string& name, unsigned long long ownerId) = 0;
virtual int execRestore(const std::string& sql, int characterId) = 0;
};
class CharacterRepo {
IDb& db_;
public:
explicit CharacterRepo(IDb& db) : db_(db) {}
// character 테이블: id, name (UNIQUE), owner_id, is_deleted (기본 false)
bool createCharacter(const std::string& name, unsigned long long ownerId) {
// (A)
if (db_.exists("SELECT 1 FROM character WHERE name=? AND is_deleted=0", name))
return false; // 살아있는 동명 캐릭터가 있으면 거부
int rows = db_.execUpdate(
"INSERT INTO character(name, owner_id, is_deleted) VALUES(?, ?, 0)",
name, ownerId);
// UNIQUE(name) 위반 시 여기서 삽입이 실패하는데, 원인이 "살아있는 동명 캐릭터"
// 때문인지 "소프트 삭제된 동명 캐릭터가 인덱스를 점유"한 때문인지 구분 못 한다.
return rows > 0;
}
// 유예기간 내 캐릭터 복구
bool restoreCharacter(int characterId) {
// (B)
int rows = db_.execRestore(
"UPDATE character SET is_deleted=0 WHERE id=?", characterId);
return rows > 0;
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.