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#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.