디자인 패턴

디자인 패턴에 대해 알아보자 #1 Solid 원칙

근본넘치는개발자 2024. 10. 7. 23:41

오늘은 Solid 원칙에 대해 다루고자 합니다. 

이전에도 공부하면서 관련 내용이 나와 한번 시간 내서 정리해 보고자 했는데,

프로젝트를 진행하면서 그 중요성을 더욱 느낀바 글을 쓰게 되었습니다.

 

모든 패턴의 내용을 다 다룰지는 아직 모르겠지만 

최대한 할 수 있는 선에서 해보겠습니다.

 

디자인 패턴이란?

소프트웨어 개발에서 자주 발생하는 특정 문제들에 대해

재사용 가능하도록 패턴화한 해결 방식을 말합니다.  

 

수많은 디자인 패턴이 있지만 그 중 GoF(Gang of Four)가 정리한

23가지 패턴이 가장 유명하고 또 자주 사용되고 있습니다.

이를 기준으로 정리하고자 합니다. 

 

GoF의 디자인 패턴은 보통 기능에 따라서

생성 패턴, 구조 패턴, 행위 패턴으로 구분합니다.

 

생성 패턴 :

객체 생성을 다루는 패턴입니다.

 

Singleton, Factory Method, Abstract Factory, Builder, Prototype 

 

5개로 구성되어 있습니다.

 

구조 패턴 :

클래스와 객체를 더 큰 구조로 만드는 패턴입니다.

 

Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy

 

7개로 구성되어 있습니다.

 

행위(행동) 패턴 : 

객체 간의 상호작용과 책임 분배를 다루는 패턴입니다.

 

Observer, Strategy, Command, State, Iterator, Mediator,

Template Method, Visitor, Strategy, Memento

 

11개로 구성되어 있습니다.

 

대부분의 디자인 패턴은

객체 지향 프로그래밍의 원칙에 기반을 두고 있습니다.

 

그래서 오늘은 우선 객체 지향 프로그래밍 및 설계의

가장 기본이 되는 SOLID 원칙을 정리해 봤습니다.

SOLID?

단일책임 원칙(Single Responsibility Principle / SRP)

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

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

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

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

 

의 앞 글자를 하나씩 따서 SOLID 원칙이라고 합니다.

 

단일책임 원칙(Single Responsibility Principle / SRP)

모든 클래스는 하나의 책임만 가져야 한다는 원칙입니다.

 

클래스는 그 책임을 완전히 캡슐화해야 하며,

클래스가 제공하는 모든 기능은 이 책임과 부합해야 합니다.

 

//Player 코드를 기능별로 나누어 단일 책임원칙을 준수한 예시
// Unity 환경 코드

public class Player : MonoBehaviour {
[SerializeField] private PlayerAudio playerAudio;
[SerializeField] private PlayerInput playerInput;
[SerializeField] private PlayerMovement playerMovement;

    private void Start() {
         playerAudio = GetComponent<PlayerAudio>();
         playerInput = GetComponent<PlayerInput>();
         playerMovement = GetComponent<PlayerMovement>();
    }
}

public class PlayerAudio : MonoBehaviour {
... }

public class PlayerInput : MonoBehaviour {
... }

public class PlayerMovement : MonoBehaviour {
... }

 

 

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

확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다는 원칙입니다.

 

//Shape라는 추상 클래스를 통해 새로운 도형도 쉽게 추가 가능

public abstract class Shape {
    public abstract float CalculateArea();
}

public class Rectangle : Shape {
    public float width;
    public float height;
    public override float CalculateArea() {
        return width * height;
 }
}

public class Circle : Shape {
    public float radius;
    public override float CalculateArea()
    {
    return radius * radius * Mathf.Pl;
    }
}

public class AreaCalculator {
    public float GetArea(Shape shape) {
        return shape.CalculateArea();
    }
}

 

 

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

파생 클래스는 기본 클래스를 대체할 수 있어야 한다는 원칙입니다.

상속받은 클래스는 부모 클래스의 방향성을 지켜 주어야 한다고 이해하면 편합니다.

 

// 자동차와 기차는 둘 다 탈것으로 분류.
// 자동차는 왼쪽, 오른쪽 가지만 기차는 불가능 
// 탈것에서 이를 정의한 경우 기차는 왼쪽, 오른쪽 기능을 제거해야 
// => 리스코프치환원칙 위배
//
// 현실의 분류가 항상 클래스 계층구조로 변환되는 게 아니기 때문.
// 별도의 interface에서 상속받는 게 더 합리적

public interface ITurnable {
    public void TurnRight();
    public void TurnLeft();
}

public interface IMovable {
    public void GoForward(),
    public void Reverse();
}

public class RoadVehicle : IMovable, ITurnable {
    public float speed = 100f;
    public float turnSpeed = 5f;
    public virtual void GoForward() { ... }
    public virtual void Reverse() { ... }
    public virtual void TurnLeft() { ... }
    public virtual void TurnRight() { ... }
}

public class RailVehicle : IMovable {
    public float speed = 100;
    public virtual void GoForward () { ... }
    public virtual void Reverse() { ... }
}

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

클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다.

 

큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리하여 

꼭 필요한 메서드들만 이용할 수 있게 해야 한다고 이해하시면 편합니다.

 

public interface IUnitStats
{
    public float Health { get; set; }
    public int Defense { get; set; }
    public void Die();
    public void TakeDamage();
    public void RestoreHealth();
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }
    public void GoForward();
    public void Reverse();
    public void TurnLeft();
    public void TurnRight();
    public int Strength { get; set; }
    public int Dexterity { get; set; }
    public int Endurance { get; set; }
}

//다음을 아래처럼 분리
======================

public interface IMovable {
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }
    public void GoForward();
    public void Reverse();
    public void TurnLeft();
    public void TurnRight();
}

public interface IDamageable { ... }

public interface IUnitStats { ... }

public interface IExplodable { ... }

public class ExplodingBarrel :
MonoBehaviour, IDamageable, IExplodable { ... }

public class EnemyUnit :
MonoBehaviour, IDamageable, IMovable, IUnitStats{....}

 

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

소프트웨어 모듈들을 분리하는 특정 형식으로서,

상위 모듈은 하위 모듈의 것을 직접 가져오면 안 되고,

둘 다 추상화에 의존해야 한다는 원칙입니다.

 

// 스위치(상위 레벨)가 도어(하위 레벨)에 의존하는 경우
// 스위치 확장을 통해 전등을 끄는 기능을 추가할 때 일일이 코드를 수정해야 하는 문제 발생 
// 인터페이스 상속을 통해 손쉽게 기능 추가 가능


public interface ISwitchable {
    public bool IsActive { get; }
    public void Activate();
    public void Deactivate();

}

public class Door : MonoBehaviour, ISwitchable
{
    private bool isActive;
    public bool IsActive => isActive;
    public void Activate() {
    isActive = true;
    Debug.Log("The door is open.");
    }

    public void Deactivate() {
    isActive = false;
    Debug.Log("The door is closed.");
	}
}

public class Switch : MonoBehaviour
{
    public ISwitchable client;
    public void Toggle() {
        if (client.IsActive) {
           client.Deactivate();
        } 
        else {
            client.Activate();
        }
    }
}

 

 

 

참고한(혹은 참고하면 좋을) 자료

https://youtu.be/J6F8plGUqv8?si=RA1BHwZBlNS5dIWP

유니티 코리아 채널 SOLID원칙 영상

https://youtu.be/wGWrOpRdu40?si=PuADXl_QbA6Q8eLg