Search

SOLID

class
구조
상태
완료
날짜
목차

SOLID

SOLID는 모든 객체 지향 언어가 준수해야 하는 다섯 가지 원칙을 의미합니다.
해당 원칙들은 유지보수를 위해 설계되었습니다. 따라서 해당 원칙이 무엇인지보다는 왜 사용하는지가 중요합니다. 이곳에서는 그 원칙들을 어떤 이유로 사용하는지에 대해 설명하겠습니다.
SOLID의 원칙은 다음과 같습니다.
1.
Single Responsibility Principle (단일 책임 원칙)
2.
Open/Closed Principle (개방/폐쇄 원칙)
3.
Liskov Substitution Principle (리스코프 치환 원칙)
4.
Interface Segregation Principle (인터페이스 분리 원칙)
5.
Dependency Inversion Principle (의존성 역전 원칙)
SOLID에 대해 설명하기 전에, 개인적으로 이 원칙을 과도하게 지키며 코딩할 필요는 없다고 생각합니다. 예를 들어, 싱글톤 패턴을 사용하면 단일 책임 원칙을 어기기 쉽습니다. 그런 상황에서 원칙을 어기는 패턴을 작성하는 것이 좋은지 고려해보아야 합니다. 또한, 싱글톤 패턴이 단일 책임 원칙을 어긴다는 것은 주관적인 의견일 수 있습니다.

Single Responsibility Principle (단일 책임 원칙)

Single Responsibility Principle (단일 책임 원칙) : 하나의 클래스는 하나의 책임만 가져야 한다.
게임 개발에서 이 원칙을 따르면 각 클래스는 명확한 역할을 가질 수 있고, 이는 코드 유지보수를 훨씬 쉽게 만듭니다.
예를 들어, 플레이어 이동, AI 행동, 인벤토리 관리 등의 기능이 각각 분리되어 있으면, 한 영역의 변경이 다른 영역에 미치는 영향을 최소화할 수 있습니다.
단일 책임 원칙 위반 사례
아래와 같이 캐릭터 매니저를 사용하면, 이동과 데미지 처리를 동시에 담당하게 되어, 단일 책임 원칙을 위반하게 됩니다.
하지만 앞서 언급한 것처럼 단일 책임 원칙은 주관적인 판단입니다. 아래의 프로그램을 작성한 개발자는 해당 매니저가 캐릭터의 이동과 HP를 관리하는 것이 당연하며, 단일 책임 원칙을 어기지 않았다고 생각할 수 있습니다. 여기서 중요한 점은, 해당 책임을 가진 로직들이 서로에게 큰 영향을 미치어 유지보수가 어려워지거나, 대부분의 개발자들이 분리하는 것이 더 좋다고 판단한다면, 이는 단일 책임 원칙을 위반하는 것이라고 생각합니다.
public class CharacterManager : MonoBehaviour { public float speed = 5.0f; public int maxHealth = 100; private int currentHealth; private void Start() { currentHealth = maxHealth; } private void Update() { MoveCharacter(); } private void MoveCharacter() { float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical); transform.Translate(movement * speed * Time.deltaTime); } public void TakeDamage(int amount) { currentHealth -= amount; // Check for character death } }
C#
복사
위 단일 책임 원칙 위반을 고친 코드
public class CharacterManager : MonoBehaviour { private CharacterMovement _characterMovementController = null; private Health _healthController = null; // 위 부분은 다른 곳에서 받아 와야 함. private void Update() { if(_characterMovementController.IsNull() == false) { _characterMovementController.Update(시간 조정값); } } public void TakeDamage(int amount) { if(_healthController.IsNull()) return; _healthController.TakeDamage(amount); } } public class CharacterMovement { public float speed = 5.0f; public void Update(float timeDelta) { float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical); transform.Translate(movement * speed * timeDelta); } } public class Health { public int maxHealth = 100; private int currentHealth; // 이 부분은 다른 곳에서 받아 와야 함. public void TakeDamage(int amount) { currentHealth -= amount; // Check for character death } }
C#
복사

Open/Closed Principle (개방/폐쇄 원칙)

Open/Closed Principle (개방/폐쇄 원칙) : 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다.
이 원칙을 유니티 게임 개발에 적용하면, 새로운 기능을 추가할 때 기존 코드를 변경하지 않고도 확장할 수 있습니다.
예를 들어, 새로운 적 타입이나 아이템을 추가할 때 기존 시스템을 재사용하고 확장할 수 있어야합니다.
개방 / 폐쇄 원칙 위반 사례
다양한 타입의 도형을 그리기 위해 조건문을 사용하는 경우, 새로운 도형을 추가하려면 기존의 ShapeDrawer를 변경해야 합니다. 이는 개방-폐쇄 원칙을 위반하는 행위입니다.
public class Shape { public string Type { get; set; } } public class Circle : Shape { public Circle() { Type = "Circle"; } } public class Square : Shape { public Square() { Type = "Square"; } } public class ShapeDrawer { public void DrawShape(Shape shape) { if (shape.Type == "Circle") { DrawCircle((Circle)shape); } else if (shape.Type == "Square") { DrawSquare((Square)shape); } } private void DrawCircle(Circle circle) { // Circle drawing logic } private void DrawSquare(Square square) { // Square drawing logic } }
C#
복사
위 개방/폐쇄 원칙 위반을 고친 코드
public abstract class Shape { public abstract void Draw(); } public class Circle : Shape { public override void Draw() { // Circle drawing logic } } public class Square : Shape { public override void Draw() { // Square drawing logic } } public class ShapeDrawer { public void DrawShape(Shape shape) { shape.Draw(); } }
C#
복사

Liskov Substitution Principle (리스코프 치환 원칙)

Liskov Substitution Principle (리스코프 치환 원칙) : 하위 타입 인스턴스는 언제나 부모 타입 인스턴스를 대체할 수 있어야한다.
이 원칙은 다형성을 올바르게 사용하여, 서로 다른 타입의 객체를 같은 인터페이스로 다룰 수 있도록 합니다.
예를 들어, 다양한 종류의 무기나 적 캐릭터가 같은 인터페이스를 구현함으로써, 공통된 방식으로 이들을 처리할 수 있습니다. 이는 코드의 유연성을 크게 향상시킵니다.
이 부분은 개인적으로 다섯 가지 원칙 중에서 신입 개발자가 지키기도 어렵고 어기기도 어렵다고 생각합니다. 사실, 아래의 원칙인 '인터페이스 분리 원칙'만 잘 지키더라도 이 원칙은 잘 지켜질 것입니다. 그럼에도 불구하고 이 원칙이 중요한 이유는 하위 클래스에 과도한 자유를 주지 않는 것이 목적이라 생각합니다. 이에 관해선 아래 코드를 보며 설명하겠습니다.
리스코프 치환 원칙 위반 사례
아래는 기반 클래스에서 Fly 메소드를 상속받았지만 그 기능이 제대로 구현되지 않은 예시입니다.
public class Bird { public virtual void Fly() { // Fly logic } } public class BabyBird : Bird { public override void Fly() { throw new NotImplementedException("BabyBird can't fly."); } }
C#
복사
위 리스코프 치환 원칙 위반을 고친 코드 예시 1
public interface IBird { void Move(); } public class FlyingBird : IBird { public void Move() { // Implement flying logic } } public class BabyBird : IBird { public void Move() { // Implement walking logic } }
C#
복사
위 리스코프 치환 원칙 위반을 고친 코드 예시 2
public interface IFlyable { void Fly(); } public class Bird { } public class Duck : Bird, IFlyable { public void Fly() { Console.WriteLine("Duck flying."); } } public class BabyBird : Bird { // BabyBird 클래스는 IFlyable 인터페이스를 구현하지 않습니다. }
C#
복사
리스코프 치환 원칙 위반 사례
아래는 기반 클래스의 함수의 기대값과 다른 경우.
다른 개발자가 기반 클래스와 동일하게 동작할 것으로 예상했지만, 다르게 동작하는 경우도 리스코프 치환 원칙을 위반한 것으로 볼 수 있습니다.
public class Calculator { public virtual int Add(int a, int b) { return a + b; } } public class ScientificCalculator : Calculator { public override int Add(int a, int b) { return base.Add(a, b) + 1; // LSP 위반: 결과에 1을 더함 } }
C#
복사
위 리스코프 치환 원칙 위반을 고친 코드 예시
public class Calculator { public virtual int Add(int a, int b) { return a + b; } } public class ScientificCalculator : Calculator { // 기존 Add 메서드의 계약을 그대로 유지 // 추가 기능이 필요한 경우, 새로운 메서드를 사용 public int AddOne(int a, int b) { return base.Add(a, b) + 1; } }
C#
복사

Interface Segregation Principle (인터페이스 분리 원칙)

Interface Segregation Principle (인터페이스 분리 원칙) : 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.
게임 개발에서 이 원칙을 적용하면, 더 작고 목적에 맞는 인터페이스를 만들 수 있습니다.
이는 각 컴포넌트가 필요한 기능만을 가지고 있도록 하여, 코드의 가독성과 관리 용이성을 향상시킵니다.
인터페이스 분리 원칙 위반 사례
아래와 같이 NPC 캐릭터가 Attack과 Heal을 할 수 없음에도 인터페이스를 상속받아 구현해야 한다면, 이는 인터페이스 분리 원칙을 위반하는 사례입니다.
public interface ICharacterActions { void Move(); void Attack(); void Heal(); } public class NPC : ICharacterActions { public void Move() { /* 동작 */ } public void Attack() { /* NPC는 공격할 수 없음 */ } public void Heal() { /* NPC는 힐할 수 없음 */ } }
C#
복사
위 인터페이스 분리 원칙 위반을 고친 코드 예시
public interface IMovable { void Move(); } public interface IAttackable { void Attack(); } public interface IHealable { void Heal(); } public class NPC : IMovable { public void Move() { /* 동작 */ } }
C#
복사

Dependency Inversion Principle (의존성 역전 원칙)

Dependency Inversion Principle (의존성 역전 원칙) : 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.
게임의 로직이 데이터 저장 방식이나 외부 서비스에 직접적으로 의존하지 않도록 합니다.
이를 통해 게임의 핵심 로직을 데이터 저장 방법의 변경이나 외부 서비스의 교체에 영향받지 않도록 보호할 수 있습니다.
의존성 역전 원칙 위반 사례
다음과 같이 LocalFileSaveLoad(저수준 모듈)을 GameManager(고수준 모듈)에서 직접 참조하면, 저장 방식을 변경하려 할 때 어려울 수 있습니다.
public class PlayerData { public int score; // 플레이어 데이터에 대한 기타 필드... } public class LocalFileSaveLoad { public void SavePlayerData(PlayerData data) { // 로컬 파일 시스템에 데이터 저장 } public PlayerData LoadPlayerData() { // 로컬 파일 시스템에서 데이터 로드 return new PlayerData(); } } public class GameManager { // LocalFileSaveLoad 직접 참조 // DIP 위반 private LocalFileSaveLoad saveLoad = new LocalFileSaveLoad(); public void SaveGame() { PlayerData data = new PlayerData(); // 데이터 설정 saveLoad.SavePlayerData(data); } public void LoadGame() { PlayerData data = saveLoad.LoadPlayerData(); // 데이터를 사용하여 게임 상태 복원 } }
C#
복사
의존성 역전 원칙 위반 사례를 고친 코드 예시
public interface ISaveLoad { void SavePlayerData(PlayerData data); PlayerData LoadPlayerData(); } public class LocalFileSaveLoad : ISaveLoad { public void SavePlayerData(PlayerData data) { // 로컬 파일 시스템에 데이터 저장 } public PlayerData LoadPlayerData() { // 로컬 파일 시스템에서 데이터 로드 return new PlayerData(); } } public class CloudSaveLoad : ISaveLoad { public void SavePlayerData(PlayerData data) { // 클라우드에 데이터 저장 } public PlayerData LoadPlayerData() { // 클라우드에서 데이터 로드 return new PlayerData(); } } public class GameManager { private ISaveLoad saveLoad; // 게임 매니저를 만들 때, saveLoad를 설정했지만, // 세이브할 때, 변경하는 것도 가능. public GameManager(ISaveLoad saveLoad) { this.saveLoad = saveLoad; } public void SaveGame() { PlayerData data = new PlayerData(); // 데이터 설정 saveLoad.SavePlayerData(data); } public void LoadGame() { PlayerData data = saveLoad.LoadPlayerData(); // 데이터를 사용하여 게임 상태 복원 } }
C#
복사