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
'개발 > Unity 3D' 카테고리의 다른 글
[Unity] Unity 듀얼 모니터 확장 출력 (1) | 2025.06.13 |
---|---|
[Unity] Unity 프로그래밍에서의 응집도, 결합도, 의존성 (1) | 2025.06.11 |
[Unity] 언어 변경 설정 Localization (0) | 2025.02.07 |
[Unity]MySQL 관련 dll 다운받기 or NuGet 패키지 설치하기 (0) | 2023.08.25 |
[Unity] 스크롤뷰 기본 세팅 (0) | 2023.08.25 |