■ SOLID 원칙

프로그래밍에서 널리 사용되는 디자인 패턴(Design Patterns)은 Gang of Four(GOF) 라 불리는 4명의 저자가 정리한 책에서 시작되었다.
디자인 패턴을 공부해본 사람이라면 알겠지만, 대부분의 책은 SOLID 원칙부터 소개하고, 대부분의 디자인 패턴 역시 이 SOLID 원칙을 준수하도록 설계되어 있다.
▶ SOLID 원칙
- Single responsibility : 단일 책임 원칙
- Open-closed : 개방-폐쇄 원칙
- Liskov substitution : 리스코프 치환 원칙
- Interface segregation : 인터페이스 분리 원칙
- Dependency inversion : 의존 역전 원칙
위 5가지 원칙의 앞 글자를 따서 “SOLID” 원칙이라 부른다.
물론 실제 프로그래밍에서 이 모든 원칙을 완벽하게 지키며 설계하는 것은 쉽지 않다.
하지만 가능한 한 SOLID 원칙을 기반으로 구조를 설계하면 코드의 유지보수성이 높아지고, 읽기 좋은 깔끔한 구조를 만들 수 있다.
1. Single responsibility principle : 단일 책임 원칙

- 모든 클래스는 오직 하나의 책임만 가진다. (대표적인 예 : Unity의 Component 구조)
- 클래스는 그 책임을 완전히 캡슐화한다.
Unity의 컴포넌트를 보면 Audio Source는 오디오 출력을 담당하고, Mesh Renderer는 메쉬를 그리는 역할만 담당한다.
이처럼 하나의 클래스가 “하나의 역할(책임)”만 수행하도록 하는 것이 바로 “단일 책임 원칙”이다.
또한 클래스는 자신이 맡은 책임을 완전히 캡슐화해야 하는데, 이는 다음을 의미한다.
- 책임을 수행하는 데 필요한 데이터(변수)를 외부에 노출하지 않는다. (private 선언)
- 책임과 관련된 동작(메서드)을 클래스 내부에서 직접 수행한다.
- 외부에는 필요한 기능만 공개하고, 내부 구현 방식을 감춘다.
😄 단일 책임 원칙의 장점
- 하나의 클래스가 하나의 책임만 수행하므로, 클래스의 길이가 짧아지고 읽기 쉬워진다.
- 클래스가 작고 역할이 명확하므로 상속이나 확장이 쉬워지고, 재사용성이 높아진다.
public class Player : MonoBehaviour
{
[SerializeField] private string 이동입력;
[SerializeField] private float 이동관련변수_1;
private float _이동관련변수_2;
private AudioSource _오디오변수;
private void Start()
{
_오디오변수 = GetComponent<AudioSource>();
}
private void Update()
{
// 입력 로직...
// 이동 관련 로직...
}
private void OnTriggerEnter(Collider other)
{
_오디오변수.Play();
}
}
[단일 책임 원칙을 지키지 않은 코드]
위 코드를 보면 Player 클래스가 이동, 입력, 오디오 재생을 모두 한 번에 담당하고 있다.
즉, 하나의 클래스 안에서 “서로 다른 여러 책임”이 섞여있으므로, 단일 책임 원칙을 지키지 않은 예시라 볼 수 있다.
public class Player : MonoBehaviour
{
[SerializeField] private PlayerAudio playerAudio;
[SerializeField] private PlayerInput playerInput;
[SerializeField] private PlayerMovement playerMovement;
private void Start()
{
//...
}
}
public class PlayerAudio : MonoBehaviour
{
//...
}
public class PlayerInput : MonoBehaviour
{
//...
}
public class PlayerMovement : MonoBehaviour
{
//...
}
[단일 책임 원칙을 지킨 코드]
각 역할을 담당(이동, 입력, 오디오)하는 클래스를 만들고, Player 클래스가 이들을 의존(사용)하도록 만들었다. 이렇게 책임을 독립된 클래스로 나누면 구조가 명확해지고, 각 클래스는 자신만의 역할을 수행한다.
또한 PlayerAudio 같은 클래스는 NPC나 다른 오브젝트에도 재사용할 수 있어서 확장성이 크게 높아진다.
2. Open-closed principle : 개방-폐쇄 원칙
“확장에 대해 열려 있어야 하고”
- 요구 사항이 변경될 때, 새로운 기능을 추가하여 모듈을 확장할 수 있어야 한다.
- 즉, 기존 기능을 바꾸지 않고 동작을 덧붙여 확장할 수 있어야 한다.
“수정에 대해서는 닫혀 있어야 한다.”
- 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경 가능해야 한다.
말이 좀 모호할 수 있는데, 간단히 설명하자면 “상속”을 통해 기능을 확장하고, 기존에 잘 굴러가는 코드는 수정하지 말라는 얘기이다.
public class Rectangle
{
public float width;
public float height;
}
public class Circle
{
public float radius;
}
public class AreaCalculator
{
public float GetRectangleArea(Rectangle rect)
{
//...
}
public float GetCircleArea(Rectangle rect)
{
//...
}
}
[나쁜 예시]
AreaCalculator에서 각 도형의 부피를 계산하지만, 위와 같이 코드를 작성하면 도형을 추가할 때마다 원본 코드(AreaCalculator)를 수정해야 한다.
또한, 도형이 늘어날수록 AreaCalculator의 함수도 계속 추가해야 한다.
public abstract class Shape
{
public abstract float CalculateArea();
}
public class Rectangle : Shape
{
public float width;
public float height;
public override float CalculateArea()
{
//...
}
}
public class Circ : Shape
{
public float radius;
public override float CalculateArea()
{
//...
}
}
public class AreaCalculator
{
public float GetArea(Shape shape)
{
return shape.CalculateArea();
}
}
[좋은 예시]
CalculateArea()라는 추상 메서드를 가진 Shape 클래스를 만들고, 각 도형 클래스가 이를 상속받아 자신만의 방식으로 면적을 계산하도록 구현했다.
이렇게 구현하면 새로운 도형이 추가되더라도, AreaCalculator 클래스는 전혀 수정할 필요가 없다.
새로운 도형이 기존 코드를 건드리지 않고 자연스럽게 확장되기 때문에, 이러한 구조가 개방-폐쇄 원칙을 잘 지킨 사례이다.
3. Liskov substitution principle : 리스코프 치환 원칙

파생 클래스는 기본 클래스를 대체할 수 있어야 한다.
리스코프(Liskov)에 별다른 뜻이 있는 것은 아니고, 이 원칙을 개발한 사람의 이름을 따서 만들어졌다.
이 원칙 역시 조금 애매한 표현인데, 쉽게 얘기하면 상속받은 클래스(하위 클래스)는 부모 클래스의 방향성을 지켜줘야 한다.

Vehicle 클래스는 속도와 방향을 나타내는 변수와 전, 후, 좌, 우로 움직이는 기능을 갖추고 있다. 이 클래스를 상속받아 Car와 Truck 클래스를 만들었고, 두 클래스 모두 동일한 방식으로 이동할 수 있다.
자동차와 트럭 모두 같은 방식으로 움직이며, 다른 탈것을 만든다고 해도 Vehicle 클래스를 상속받으면 기본적인 이동 기능을 그대로 사용할 수 있기에, 아직까지 문제가 있는 코드는 아니다.

여기에 레일을 따라 움직이는 새로운 탈것인 Train 클래스를 추가해 보자.
자연스럽게 Vehicle 클래스를 상속받았지만, 문제는 부모 클래스에 있는 TurnLeft(), TurnRight() 메서드가 Train에서는 전혀 사용되지 않는다.
이처럼 부모 클래스의 기능이 자식 클래스에서 무효화되거나 사용하지 않는 구조는 부모 클래스로서의 방향성을 지키지 못한다는 의미이다.
- 이러한 설계는 리스코프 치환 원칙에 위배된다.

따라서 탈것의 기능을 “회전이 가능한가?”, “앞뒤로 이동이 가능한가?”처럼 역할 단위로 분리하고, 그 역할에 맞는 인터페이스를 정의한 뒤, 탈것이 필요한 기능만 조립하도록 만드는 방식이 더 올바르다.
이렇게 하면 불필요한 메서드가 자식 클래스에 강제로 들어오는 일을 막을 수 있어, 리스코프 치환 원칙을 위반하지 않은 구조로 설계할 수 있다.
- 역할에 따라 인터페이스를 분리하고 필요한 기능만 구현.
- 상속(Inheritance) 보다 구성(Composition)을 우선적으로 사용한다.
4. Interface segregation principle : 인터페이스 분리 원칙
클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.
앞서 말한 단일 책임 원칙과 리스코프 치환 법칙에서 연장되는 원칙인데, 필요한 인터페이스만 만들고, 큰 인터페이스를 구체적이고 작은 단위로 분리하라는 얘기이다.
- 시스템의 내부 의존성을 약화하고 유연성을 강화한다.
public interface IUnitState
{
public float HP { get; set; }
public float MP { get; set; }
public int Defense { get; set; }
public void Die();
public void TakeDamage(int damage);
public void RestoreHealth();
public void GoForward();
public void Reverse();
public void TurnLeft();
public void TurnRight();
// ....
}
[IUnitState]
게임에서 유닛이 가지는 상태와 행동은 매우 다양하다. 하지만 이 모든 기능을 하나의 인터페이스에 몰아넣어버리면, 유닛마다 필요하지 않는 기능까지 억지로 구현해야 한다.
이런 구조는 앞서 설명한 원칙들, 특히 “단일 책임 원칙”, “리스코프 치환 원칙”, “인터페이스 분리 원칙”을 위배할 가능성이 매우 높다.

이처럼 인터페이스를 역할 단위로 최대한 작게 분리하여 구성하면, 유닛은 필요한 기능만 선택적으로 구현할 수 있게 된다.
- 따라서 불필요한 기능을 억지로 구현할 필요가 없고,
- 기능과 확장 조합이 훨씬 쉬워지며,
- 앞선 원칙들도 자연스럽게 지킬 수 있다.
5. Dependency Inversion Principle : 의존 역전 원칙
의존 역전 원칙은 간단하게 말하면 소프트웨어 모듈들을 분리하는 특정 형식을 말한다.
- 상위(High - level) 모듈은 하위(Low-level) 모듈의 것을 직접 가져오면 안 된다.
- 상위, 하위 모듈 둘 다 추상화(abstraction)에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안 된다.
- 세부 사항이 추상화에 의존해야 한다.
- 클래스가 다른 클래스와 관계가 있으면 안된다.
- 클래스가 다른 클래스의 작동 방식을 많이 알고 있으면, 종속성(Dependency) 또는 결합(Coupling)이 발생한다.

코드를 작성하다 보면 자연스럽게 상위 레벨(Player)이 하위 레벨(KeyboardInput)에 의존하게 되고, 이것이 일반적인 “정방향 의존”이다.
하지만 의존 역전 원칙은 말 그대로, 이 관계를 뒤집어 상위 레벨과 하위 레벨 모두 추상화(IInput)에 의존하도록 만든다.

또한, 두 클래스의 결합도가 높을수록 A 클래스의 내용을 수정했을 때, B 클래스의 내용도 함께 수정해야 한다.
지금은 예시로 두 클래스만 놓고 봤지만, 실제 개발에선 여러 클래스가 서로 얽혀 있는 경우가 많기 때문에 한 클래스를 수정하면 관련 클래스까지 줄줄이 수정해야 하는 문제가 생기기 쉽다.
- 이렇게 되면 전체 구조의 유지보수가 매우 어려워진다.

public class Switch : Monobehaviour
{
public Door door;
public bool isActivated;
public void Toggle()
{
if(isActivated)
{
isActivated = false;
door.Close();
return;
}
isActivated = ture;
door.Open();
}
[예시 - 1]
현재 구조에서는 Switch가 Door 클래스를 직접 참조하고 있으며, 스위치를 작동할 때 Door의 메서드를 그대로 호출하고 있다.
Door만 있는 상황은 괜찮지만, 기능을 확장시켜 스위치를 켜면 폭탄이 폭발하는 기능을 추가하거나, 전등을 키는 기능을 추가해야 한다고 생각해 보자.
이 경우, Switch 클래스 안에 폭탄용 ToggleBomb(), 전등용 ToggleLight()와 같이 새로운 메서드를 계속 추가해야 하는 상황이 생긴다.
- 즉, 기능을 확장할 때마다 Switch 코드를 수정해야 하므로 유지보수가 점점 어려워진다.

public interface ISwitchable
{
public bool IsActive {get;}
public void Activate();
public void Deavtivate()
}
// higher - level
public class Switch : MonoBehaviour
{
public ISwitchable client;
public Toggle()
{
//...
}
}
// lower - level
public class Door : MonoBehaviour, ISwitchable
{
private bool isActive;
public bool IsActive => isActive;
public void Activate() { // ... }
public void Deactivate() { // ... }
}
[의존성 역전 원칙]
스위치가 가능한 오브젝트에서의 역할을 인터페이스로 따로 빼두고, 상위-하위 레벨 모두 이 인터페이스를 의존하게 만든다.
이렇게 만들면 어떠한 ISwitchable 객체가 추가되더라도, Switch의 Toggle() 메서드는 수정할 필요가 없어지게 된다.
📒SOLID 원칙의 핵심
결국 SOLID 원칙의 목적은 코드를 더 읽기 쉽고, 유지보수하기 쉬운 구조로 만드는 것이다.
한 가지 원칙을 지키기 시작하면, 다른 원칙들도 자연스럽게 지켜지는 경우가 많다. 따라서 코드를 작성할 때 항상 SOLID 원칙을 염두하면 변경에 강하고, 이해하기 쉬우며, 확장성이 높은 깔끔한 구조를 만들 수 있다.
- 하나의 클래스와 인터페이스는 오직 하나의 역할만 수행하도록 설계한다.
- 구현해야 하는 기능은 잘게 분리하여 인터페이스 단위로 모듈화 한다.
- 상속과 구성을 상황에 맞게 적절히 활용한다.
■ abstract class VS Interface

추상 클래스를 구체 클래스(concrete class)가 상속받아 직접 내용을 구현하게 된다. 이렇게 직접적으로 상속을 받으면 “is” 관계가 된다.
- 추상 “클래스” 이기 때문에 필드, 스태틱 멤버, 전체 혹은 일부 구현된 메서드들을 가질 수 있다.
- C#은 다중 상속이 불가능하고 오로지 한 개의 “클래스”만 상속받을 수 있다.

따라서 C#에서 다중 상속을 구현하려면 Interface를 활용해야 한다. 인터페이스를 구현하게 되면 클래스와 인터페이스는 “can - do” 관계가 된다.
- is - a 관계(추상 클래스 상속) : 자식 클래스는 부모 클래스의 “종류”이다.
- can - do 관계(인터페이스 구현) : 클래스가 특정 능력이나 기능을 “할 수 있다”는 의미.
| Abstract class | Interface |
| 메서드를 완전히 또는 일부 구현. | 메서드를 선언만 가능. 구현 불가능. |
| 변수 및 필드 선언 / 사용. | 메서드와 프로퍼티 선언만 가능. |
| 스태틱 멤버 가능. | 스태틱 멤버 불가. |
| 생성자 사용 가능. | 생성자 사용 불가능. |
| 모든 액세스 한정자 가능. | 모든 멤버는 public으로 취급. |
최종 정리
→ Single responsibility (단일 책임 원칙)
- 클래스가 한 가지 작업만 수행한다.
→ Open-Closed (개방 폐쇄 원칙)
- 이미 작동하는 방식을 변경하지 않고도 클래스의 기능을 확장할 수 있어야 한다.
→ Liskov substitution (리스코프 치환 원칙)
- 하위 클래스는 기본 클래스를 대체할 수 있어야 한다. 기본 클래스의 방향성을 유지해야 한다.
→ Interface segregation (인터페이스 분리 원칙)
- 인터페이스를 작게 유지. 클라이언트는 필요한 것만 구현.
→ Dependency inversion (의존 역전 원칙)
- 추상화에 의존. 하나의 구체 클래스에서 다른 클래스로 직접 의존 금지.
'Unity,C# > Unity 정보' 카테고리의 다른 글
| [Unity] UI Toolkit 기본 사용법 (0) | 2025.11.21 |
|---|---|
| [Unity, C#] ScriptableObject - 스크립터블 오브젝트 (6) | 2025.08.02 |
| [Unity, C#] EditorWindow - 커스텀 에디터 윈도우 (1) | 2025.08.02 |
| [Unity, C#] Editor - 커스텀 인스펙터 (0) | 2025.08.02 |
| [C#] LINQ(Language Integrated Query) (3) | 2025.06.27 |