← SEARCHABLE ARCHIVE
C# · 2026-08-12

C# 기초부터 마스터까지 — 10화

테스트 가능한 게임 구조로 C# 정리하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

문법을 많이 아는 것과 변경하기 쉬운 프로그램을 만드는 것은 다르다. 마지막에는 게임 규칙, 저장, 화면을 분리해 작은 단위로 검증할 수 있는 구조를 만들어보자.

규칙을 순수한 객체에 둔다

public sealed class CombatService
{
    public int CalculateDamage(int attack, int defense)
    {
        return Math.Max(1, attack - defense);
    }

    public void Attack(Character attacker, Character target)
    {
        int damage = CalculateDamage(attacker.Attack, target.Defense);
        target.TakeDamage(damage);
    }
}

전투 규칙이 UI나 엔진 객체를 직접 참조하지 않으면 콘솔과 테스트에서도 같은 코드를 사용할 수 있다. 화면은 결과를 표시하고 입력을 전달하는 역할만 맡는다.

외부 기능은 인터페이스 뒤에 둔다

public interface ISaveStore
{
    Task SaveAsync(SaveData data, CancellationToken token);
    Task<SaveData?> LoadAsync(CancellationToken token);
}

public sealed class GameSession
{
    private readonly ISaveStore saveStore;

    public GameSession(ISaveStore saveStore)
    {
        this.saveStore = saveStore;
    }
}

파일이나 서버 구현을 인터페이스 뒤에 두면 실제 저장소 대신 테스트용 가짜 구현을 넣을 수 있다. 이것은 기능을 숨기기 위한 계층이 아니라 변경 가능성이 다른 코드를 분리하는 경계다.

동작을 테스트한다

[Fact]
public void DamageNeverDropsBelowOne()
{
    var combat = new CombatService();

    int damage = combat.CalculateDamage(5, 100);

    Assert.Equal(1, damage);
}

테스트는 private 메서드의 구현을 그대로 따라가기보다 외부에서 보이는 규칙을 확인한다. 그러면 내부 구조를 정리해도 같은 동작이 유지되는지 알 수 있다.

엔진 경계 정하기

Unity에서는 MonoBehaviour가 입력과 생명주기를 받고 일반 C# 객체가 규칙을 담당하게 나눌 수 있다. 모든 클래스를 MonoBehaviour로 만들면 에디터 연결은 편하지만 생성과 테스트가 엔진에 묶인다.

좋은 구조는 파일 수가 많은 구조가 아니다. 상태의 소유자, 비동기 작업의 수명, 이벤트 구독 해제, 외부 자원 경계가 코드에서 분명한 구조다.

C#의 타입, 인터페이스, 제네릭, 이벤트, LINQ, async는 각각 따로 쓰는 기술이 아니다. 변경 가능한 부분을 나누고 잘못된 상태를 줄이기 위해 함께 사용하는 도구다.