개발/Unity 3D

[Unity] Unity에서의 SOLID 원칙

귀뚜래미 2025. 6. 9. 13:10
728x90

 

SOLID 원칙

  • 코드의 유지보수성과 확장성을 극대화하기 위한 다섯 가지 디자인 원칙
  • 클래스나 모듈을 어떻게 나누고 구성해야 유지보수·확장성이 좋은지에 대한 가이드라인

 

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

정의

  • 하나의 클래스는 하나의 책임만 가진다.
  • 변경 이유도 하나여야 한다.

특징

  • 클래스가 작아지고 명확해진다.
  • 버그 방생 시 원인 파악이 쉬워진다.

사용 예시

/// <summary>
/// 플레이어 입력 처리와 이동 로직을 분리하여 각각 책임을 분리
/// </summary>

// 플레이어 입력만 처리하는 클래스
public class PlayerInputHandler : MonoBehaviour
{
    public Vector2 MovementInput { get; private set; }

    void Update()
    {
        // 플레이어의 수평/수직 입력값을 가져와서 저장
        MovementInput = new Vector2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
    }
}

// 플레이어 이동만 처리하는 클래스
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMover : MonoBehaviour
{
    [SerializeField] private float speed = 5f;
    private Rigidbody2D rb;
    private PlayerInputHandler inputHandler;

    void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        inputHandler = GetComponent<PlayerInputHandler>();
    }

    void FixedUpdate()
    {
        // 입력 핸들러로부터 받아온 값을 사용해 물리 이동 수행
        Vector2 move = inputHandler.MovementInput * speed;
        rb.velocity = move;
    }
}

 

 

 

 

2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

정의

  • 기능 확장에는 열려 있어야(Open)하고, 기존 코드를 수정하는 데에는 닫혀 있어야(Closed) 한다.

특징

  • 새로운 기능 추가 시 기존 코드를 변경하지 않고 확장 클래스나 인터페이스를 구현한다.
  • 기존 기능에 대한 안정성을 유지한다.

사용 예시

/// <summary>
/// 적의 행동을 확장할 때, 추상 클래스를 통해 새로운 행동을 추가
/// </summary>


// 적 행동 추상 클래스 (OCP 적용)
public abstract class EnemyBehavior : MonoBehaviour
{
    // 적 행동 수행
    public abstract void Act();
}


// 단순 추격 행동을 구현한 클래스 (OCP 적용)
public class ChasePlayerBehavior : EnemyBehavior
{
    [SerializeField] private float chaseSpeed = 3f;
    private Transform player;

    void Start()
    {
        player = GameObject.FindWithTag("Player").transform;
    }

	// 상속받은 EnemyBehavior의 Act 함수 오버라이딩
    public override void Act()
    {
        // 플레이어 방향으로 추격
        transform.position = Vector2.MoveTowards(transform.position, player.position, chaseSpeed * Time.deltaTime);
    }
}


// 대각선 이동을 구현한 새로운 행동 클래스(확장 예시) (OCP 적용)
public class ZigZagBehavior : EnemyBehavior
{
    [SerializeField] private float speed = 2f;
    private float zigzagAngle = 45f;

	// 상속받은 EnemyBehavior의 Act 함수 오버라이딩
    public override void Act()
    {
        // 지그재그 이동 로직
        float angle = zigzagAngle * Mathf.Sin(Time.time);
        Vector2 dir = Quaternion.Euler(0, 0, angle) * Vector2.right;
        transform.Translate(dir * speed * Time.deltaTime);
    }
}

 

 

 

 

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

정의

  • 자식 클래스는 부모 클래스를 대체해도 프로그램이 정상 동작해야 한다.

특징

  • 상속받은 메서드의 동작을 변경하지 않고, 계약(사전/사후 조건)을 지켜야 한다.
  • 잘못된 오버라이드는 예기치 않은 버그를 일으킨다.

사용 예시

/// <summary>
/// 무기 베이스 클래스를 상속받아도 동일하게 발사 동작이 이뤄지도록 구현
/// </summary>


// 무기 공통 기능 정의하는 추상 클래스 (LSP 적용)
public abstract class WeaponBase : MonoBehaviour
{
    protected int ammoCount;

    // 초기 탄약 설정
    public virtual void Initialize(int initialAmmo)
    {
        ammoCount = initialAmmo;
    }

    // 발사 동작: 자식 클래스에서 구체 구현
    public abstract void OnFire();
}


// 레이저 건 구현 (LSP 적용)
public class LaserGun : WeaponBase
{

    // 초기 탄약 설정 (부모 구현 재사용)
    public override void Initialize(int initialAmmo)
    {
        base.Initialize(initialAmmo);
    }


    // 레이저 발사 시 탄약 감소
    public override void OnFire()
    {
        if (ammoCount <= 0) return;
        // 레이저 발사 이펙트 재생 (생략)
        ammoCount--;
    }
}

 

 

 

 

 

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

정의

  • 범용 인터페이스보다는 구체적인 역할별 인터페이스를 여러 개 만들어야 한다.

특징

  • 하나의 큰 인터페이스를 구현할 때 불필요한 메서드 구현 부담이 사라진다.
  • 클라이언트는 자신이 사용하는 메서드만 의존하게 된다.

사용 예시

/// <summary>
/// 상호작용 가능한 오브젝트 인터페이스를 용도별로 분리
/// </summary>


// 클릭 가능한 UI 요소용 인터페이스 (ISP 적용)
public interface IClickable
{
    void OnClick();
}


// 데미지를 받을 수 있는 오브젝트용 인터페이스 (ISP 적용)
public interface IDamageable
{
    void TakeDamage(int amount);
}


// 클릭하면 데미지를 주는 함정 오브젝트 예시
public class SpikeTrap : MonoBehaviour, IClickable, IDamageable
{
    [SerializeField] private int damage = 20;

    public void OnClick()
    {
        // 클릭 시 자신에게도 데미지
        TakeDamage(damage);
    }

    public void TakeDamage(int amount)
    {
        // 데미지 적용 로직
        Debug.Log($"SpikeTrap received {amount} damage.");
    }
}

 

 

 

 

 

5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)

정의

  • 고수준 모듈이 저수준 모듈에 의존하면 안 된다.
  • 둘 다 추상화에 의존해야 한다.

특징

  • 구체적인 클래스보다 인터페이스(추상화)에 의존하여 결합도를 낮춘다.
  • 테스트나 확장 시 유리하다.

사용 예시

/// <summary>
/// 오디오 재생을 인터페이스로 분리하고, 런타임에 구현체를 주입
/// </summary>


// 오디오 서비스 추상화 인터페이스 (DIP 적용)
public interface IAudioService
{
    void PlaySound(string clipName);
}


// 실제 오디오 매니저 구현 (DIP 적용)
public class AudioManager : MonoBehaviour, IAudioService
{
    public void PlaySound(string clipName)
    {
        // Resources 폴더에서 오디오 클립 로드 후 재생
        AudioClip clip = Resources.Load<AudioClip>($"Sounds/{clipName}");
        AudioSource.PlayClipAtPoint(clip, Vector3.zero);
    }
}


// 플레이어 발사 소리를 재생하는 클래스 (DIP 적용)
public class PlayerShooting : MonoBehaviour
{
    private IAudioService audioService;

    void Awake()
    {
        // 의존성 주입 (Inspector나 DI 컨테이너로도 가능)
        audioService = FindObjectOfType<AudioManager>();
    }

    void Fire()
    {
        // 총 발사 로직 생략...
        audioService.PlaySound("LaserShot");
    }
}

 

 

 

interface와  abstract 구분

다중 상속 가능: 여러 인터페이스 동시 구현 불가능: C# 기준으로 단일 상속만 허용
멤버 구성 (C# 8.0 이전) 선언만 (void Foo();) 구현(필드·메서드) + 선언 혼합 가능
접근 제한자 암묵적으로 public (모든 멤버) public/protected/private 등 자유롭게
생성자 정의 불가 정의 가능 (하위에서 base() 호출)
목적 “무엇을 할 수 있는지” 계약(contract) 정의 “공통 기능” + “미완성 기능” 틀(template) 제공
버전 대응성 (C# 8.0+) 기본 구현(default method) 지원 언제든 새로운 구현 메서드 추가 가능

 

언제 interface?

  • 완전한 다형성이 필요할 때
  • 서로 전혀 관계없는 클래스에 같은 역할(계약)만 부여하고 싶을 때

언제 abstract class?

  • 공통된 필드·메서드 구현을 물려주고, 일부 메서드는 서브클래스에 반드시 구현하게 강제하고 싶을 때

 

728x90