게임 개발

상속으로 묶을까, 인터페이스로 나눌까

스위치와 문 예제로 상속·인터페이스·합성을 구분합니다. SOLID를 적용할 때 줄어드는 변경 범위와 늘어나는 복잡성을 함께 봅니다.

CodingJoa 수정 2026.09.22 3분 정도 걸립니다
화이트보드에 구조 다이어그램을 그리고 있는 손
코드를 치기 전에 관계부터 그려두면 나중에 갈아엎을 일이 줄어듭니다.

문을 여는 스위치 하나를 만들 때는 Switch가 Door를 직접 참조해도 별문제가 없습니다. 그런데 다음 스테이지에서는 같은 스위치로 조명을 켜고 싶습니다. 그다음에는 함정을 작동시키고 싶어집니다. 스위치 코드에 대상별 분기가 늘어나는 순간, 공통으로 요구하는 동작이 무엇인지 생각해 볼 만합니다.

SOLID를 볼 때 이런 장면부터 떠올리면 덜 추상적입니다. 원칙 다섯 개를 외웠는지보다 기능을 하나 추가할 때 어떤 파일들을 함께 수정해야 하는지가 더 잘 보입니다.

스위치는 상대가 문인지 몰라도 된다

스위치가 필요한 동작을 ‘활성 상태 변경’으로 정했다면 계약을 작게 만들 수 있습니다. 아래는 Unity 컴포넌트 연결을 제외한 일반 C# 예제입니다.

using System;

public interface IActivatable
{
    void SetActive(bool active);
}

public sealed class Door : IActivatable
{
    public bool IsOpen { get; private set; }

    public void SetActive(bool active)
    {
        IsOpen = active;
    }
}

public sealed class Switch
{
    private readonly IActivatable target;

    public Switch(IActivatable target)
    {
        this.target = target ?? throw new ArgumentNullException(nameof(target));
    }

    public void Press()
    {
        target.SetActive(true);
    }
}

Door를 만들고 Switch 생성자에 넣은 뒤 Press를 호출하면 IsOpen이 true가 됩니다. 같은 인터페이스를 구현한 조명 객체도 전달할 수 있습니다. 스위치 입장에서는 대상별 분기가 필요 없습니다.

다만 모든 기믹을 억지로 같은 계약에 맞추지는 않습니다. “한 번 폭발하고 사라짐”과 “켜고 끌 수 있음”은 다릅니다. 실제 요구가 다르면 Trigger() 같은 별도 계약이 더 알맞을 수 있습니다.

has-a는 인터페이스 구현의 다른 이름이 아니다

Door가 IActivatable을 구현한다는 것은 해당 동작의 계약을 제공한다는 뜻입니다. Switch가 target을 필드로 갖는 쪽이 has-a, 즉 다른 객체를 보유하는 관계에 가깝습니다. 이 예제는 인터페이스와 합성을 함께 사용합니다.

상속은 공통 상태나 구현을 공유하면서 부모가 약속한 동작을 유지할 때 사용할 수 있습니다. 인터페이스는 소비자가 필요한 계약을 분리하는 데 유용합니다. 어느 쪽을 선택하든 이름을 “~이다”로 읽을 수 있다는 이유만으로 설계가 끝나지는 않습니다.

부모 타입으로 사용할 때 자식이 같은 계약을 지키는지 살펴보세요. 빈 override도 계약에 따라 합법적인 no-op일 수 있습니다. 반대로 부모가 항상 지원한다고 약속한 연산을 자식이 예외로 막으면 호출자는 타입마다 분기해야 합니다.

인터페이스 기능도 C# 버전에 따라 다르다

“인터페이스에는 구현이나 static 멤버가 절대 없다”는 설명은 현대 C# 전체에 적용되지 않습니다. 기본 구현이나 여러 static 멤버를 지원하는 언어 기능이 있습니다. 다만 인스턴스 필드로 개체별 상태를 저장하는 클래스와는 다릅니다.

Unity에서는 사용하는 버전의 C#과 런타임 지원을 함께 봐야 합니다. 최신 .NET 문서의 예제를 그대로 붙이기보다 프로젝트에서 지원하는 범위를 확인하세요. 위 예제는 단순한 메서드 계약만 사용합니다. 또한 MonoBehaviour의 일반 인터페이스 필드가 Inspector에 자동으로 오브젝트 참조 슬롯처럼 나타난다고 가정하지 않습니다. 씬에서 연결하려면 별도 직렬화·주입 방식을 설계해야 합니다.

파일 수보다 변경 이유를 본다

단일 책임은 메서드가 하나여야 한다는 규칙이 아닙니다. 함께 바뀌는 책임과 서로 다른 이유로 바뀌는 책임을 구분하는 기준으로 보는 편이 유용합니다. 입력 방식 변경과 저장 포맷 변경이 같은 클래스를 계속 건드린다면 분리할 후보입니다.

반면 구현이 하나뿐이고 변경 가능성도 낮은 짧은 코드에 인터페이스를 계속 추가하면 읽는 경로만 길어질 수 있습니다. 추상화를 넣은 뒤에는 새 대상 하나를 연결해 보고 기존 호출 코드를 얼마나 수정했는지 확인합니다. 설명하기 쉬워졌는지, 테스트에서 대체 객체를 넣기 쉬워졌는지도 좋은 판단 기준입니다.

참고 문서

이 글 공유하기 X Facebook 네이버

이 글은 기술 설명과 참고 자료, 글쓴이의 판단을 담고 있습니다. 예제와 실제 사용 사례의 적용 조건은 본문을 참고해주세요. 도구의 버전·가격·동작은 시간이 지나면 달라질 수 있으니, 따라 하실 때는 공식 문서를 함께 확인해주세요. 자세한 내용은 이용약관에 정리해두었습니다.