40. 전투 중 리스펙으로 인한 스탯 불일치와 오버힐
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오 (서버-클라): 캐릭터는 리스펙(스탯 포인트 재분배) 기능을 통해
// strength, agility, vitality 세 기본 스탯에 배분한 포인트를 초기화하고
// 새로 재분배할 수 있다. 파생 스탯인 최대체력(MaxHp)과 공격력(Atk)은 세
// 기본 스탯으로부터 계산된다.
//
// 문제는, 캐릭터가 전투 중이어도 리스펙 요청을 막지 않는다는 점이다.
// 리스펙 처리 스레드가 strength, agility, vitality, 그리고 파생값
// MaxHp/Atk 를 순서대로 갱신하는 동안, 전투 계산 스레드는 언제든 같은
// 캐릭터 객체의 스탯 필드들을 읽어 데미지와 최대체력을 계산할 수 있다.
//
// 요구사항:
// - 전투 계산 로직이 특정 시점의 스탯을 읽을 때, 기본 스탯 세 개와 그로부터
// 파생된 MaxHp/Atk 는 항상 "같은 리스펙 결과" 기준으로 일관되어야 한다
// (일부는 새 값, 일부는 옛 값이 섞여서는 안 된다).
// - 리스펙으로 MaxHp 가 줄어드는 경우, 현재체력(CurHp)이 새로운 MaxHp 를
// 초과한 채로 남아 있어서는 안 된다(오버힐처럼 보이는 상태 방지).
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
using System.Threading.Tasks;
public sealed class CharacterStats
{
public int Strength;
public int Agility;
public int Vitality;
public int MaxHp;
public int Atk;
public int CurHp;
}
public interface IStatRepository
{
Task<(int str, int agi, int vit)> LoadAllocatedPointsAsync(ulong characterId);
}
public sealed class RespecService
{
private readonly IStatRepository _repo;
public RespecService(IStatRepository repo)
{
_repo = repo;
}
// 리스펙 요청 처리: 포인트를 초기화한 뒤 사용자가 새로 배분한 값을
// 서버가 검증해 저장소에서 다시 불러와 캐릭터 객체에 반영한다.
public async Task ApplyRespecAsync(CharacterStats stats, ulong characterId)
{
var (str, agi, vit) = await _repo.LoadAllocatedPointsAsync(characterId);
// (A) 기본 스탯 세 필드를 하나씩 순차적으로 갱신한다.
// 세 대입 사이사이에 다른 스레드가 이 객체를 읽을 수 있다.
stats.Strength = str;
stats.Agility = agi;
stats.Vitality = vit;
// 파생 스탯은 기본 스탯이 모두 갱신된 뒤 별도로 재계산한다.
RecalculateDerivedStats(stats);
}
private void RecalculateDerivedStats(CharacterStats stats)
{
int newMaxHp = 100 + stats.Vitality * 10;
int newAtk = 5 + stats.Strength * 2 + stats.Agility;
stats.MaxHp = newMaxHp;
stats.Atk = newAtk;
// (B) CurHp 가 새로운 MaxHp 를 초과하는지는 여기서 확인하지 않는다.
// 또한 이 시점 이전까지 MaxHp/Atk 와 CurHp 는 서로 다른 시점의
// 값을 섞어서 참조당할 수 있다.
}
}
public static class CombatCalculator
{
// 전투 계산 스레드에서 호출된다. 데미지 산정에 Atk 와 MaxHp 를 함께 쓴다.
public static int CalculateDamage(CharacterStats attacker, CharacterStats defender)
{
int atk = attacker.Atk;
int defenderMaxHp = defender.MaxHp;
// MaxHp 대비 비율로 고정 데미지 보정을 주는 등, 두 파생값이 같은
// 스냅샷 기준이라고 가정하고 계산이 이루어진다(구현 생략).
int baseDamage = atk;
return baseDamage;
}
} 결함 코드 · C++
// ============================================================================
// 시나리오 (서버-클라)
// ----------------------------------------------------------------------------
// 캐릭터는 리스펙(스탯 포인트 재분배) 기능을 통해 strength, agility,
// vitality 세 기본 스탯에 배분한 포인트를 초기화하고 새로 재분배할 수 있다.
// 파생 스탯인 최대체력(maxHp)과 공격력(atk)은 세 기본 스탯으로부터 계산된다.
//
// 리스펙 요청은 게임 로직 스레드에서, 전투 데미지 계산은 별도의 전투
// 워커 스레드에서 처리될 수 있다. 같은 캐릭터 객체를 두 스레드가 동시에
// 접근할 수 있다.
//
// 요구사항:
// - 전투 계산 로직이 특정 시점의 스탯을 읽을 때, 기본 스탯 세 개와 그로부터
// 파생된 maxHp/atk 는 항상 같은 리스펙 결과 기준으로 일관되어야 한다.
// - 리스펙으로 maxHp 가 줄어드는 경우, curHp 가 새로운 maxHp 를 초과한 채로
// 남아 있어서는 안 된다.
//
// 과제: (A)(B) 지점을 검토해 위 요구사항이 깨지는 경로를 찾고 수정하라.
// ============================================================================
#include <cstdint>
#include <algorithm>
struct CharacterStats
{
int strength = 0;
int agility = 0;
int vitality = 0;
int maxHp = 100;
int atk = 5;
int curHp = 100;
};
class RespecService
{
public:
// 리스펙 처리: 새로 배분된 포인트를 캐릭터 스탯에 반영한다.
// 게임 로직 스레드에서 호출되며, 같은 시각 전투 워커 스레드가
// 동일한 CharacterStats 를 읽고 있을 수 있다.
void ApplyRespec(CharacterStats& stats, int newStr, int newAgi, int newVit)
{
// (A) 기본 스탯 세 필드를 하나씩 순차적으로 대입한다.
// 이 사이사이에 다른 스레드가 이 객체를 읽을 수 있고,
// 아무 동기화도 걸려 있지 않다.
stats.strength = newStr;
stats.agility = newAgi;
stats.vitality = newVit;
RecalculateDerivedStats(stats);
}
private:
void RecalculateDerivedStats(CharacterStats& stats)
{
int newMaxHp = 100 + stats.vitality * 10;
int newAtk = 5 + stats.strength * 2 + stats.agility;
stats.maxHp = newMaxHp;
stats.atk = newAtk;
// (B) curHp 가 새로운 maxHp 를 초과하는지 확인하는 보정이 없다.
// 또한 이 대입 자체도 다른 스레드의 읽기와 순서를 보장받지 못한다.
}
};
class CombatCalculator
{
public:
// 전투 워커 스레드에서 호출된다. atk 와 maxHp 를 함께 사용해 데미지를
// 계산한다.
static int CalculateDamage(const CharacterStats& attacker, const CharacterStats& defender)
{
int atk = attacker.atk;
int defenderMaxHp = defender.maxHp;
// maxHp 대비 비율 보정 등, 두 파생값이 같은 스냅샷 기준이라고
// 가정하고 계산이 이루어진다(구현 생략).
int baseDamage = atk;
(void)defenderMaxHp;
return baseDamage;
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.