티스토리 뷰

SOLID Principle

SOLID 원칙이란, 객체지향 프로그래밍에서 소프트웨어 디자인을 위한 5가지 지켜야 하는 원칙이다. 이 원칙을 통해 소프트웨어의 유연성, 확장성, 유지보수성을 향상한다.

 

 


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

하나의 클래스는 하나의 책임만 가진다.

 

단일 책임 원칙은 하나의 클래스하나의 기능만을 책임지는 것이다.

 

클래스 하나가 다수의 기능을 구현하는 것은 단일 책임 원칙에 위배된다. 이는 캐릭터에 움직임, 사운드, 애니메이션 등을 하나의 클래스가 모두 통재하는 현상이다.

 

그렇다면 단일 책임 원칙을 준수하면 어떤 이점이 있을까?

 

단일 책임 원칙의 이점
이점 설명
가독성 증가 단일 기능 단위로 분리된 코드는 가독성이 늘어난다.
확장성 증가 하나의 기능만으로 이루어진 클래스들은 상속 및 확장에 용이하다.
재사용성 증가 단일 기능이 이루어져 모듈식으로 재사용하기 쉬워진다.

 

 

단일 책임 원칙 잘못된 유니티 예제
using UnityEngine;

public class CharacterController : MonoBehaviour
{
    private float speed;
    private Rigidbody rb;

    void Start()
    {
        speed = 5f;
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // 캐릭터 이동 로직
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
        rb.velocity = movement * speed;
    }

    // 캐릭터 충돌 처리
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Enemy"))
        {
            Destroy(gameObject);
        }
    }
}

 

위 코드에서 캐릭터 컨트롤러 클래스는 캐릭터의 움직임과 충돌처리를 같이 담당하고 있다.

이는 다음과 같이 분할하여 작성할 수 있다.

 

 

캐릭터 움직임 스크립트
using UnityEngine;

public class CharacterMovement : MonoBehaviour
{
    private float speed;
    private Rigidbody rb;

    void Start()
    {
        speed = 5f;
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // 캐릭터 이동 로직
        float moveHorizontal = Input.GetAxis("Horizontal");
        float moveVertical = Input.GetAxis("Vertical");
        Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical);
        rb.velocity = movement * speed;
    }
}
캐릭터 충돌 스크립트
using UnityEngine;

public class CharacterCollision : MonoBehaviour
{
    // 캐릭터 충돌 처리
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Enemy"))
        {
            Destroy(gameObject);
        }
    }
}
캐릭터 스크립트
using UnityEngine;

public class Character : MonoBehaviour
{
    private CharacterMovement characterMovement;
    private CharacterCollision characterCollision;

    void Start()
    {
        characterMovement = GetComponent<CharacterMovement>();
        characterCollision = GetComponent<CharacterCollision>();
    }

    void Update()
    {
        // 캐릭터 이동 로직
        characterMovement.MoveCharacter();
    }

    // 캐릭터 충돌 처리
    private void OnCollisionEnter(Collision collision)
    {
        characterCollision.HandleCollision(collision);
    }
}

 

위 3가지 스크립트는 기존 하나의 캐릭터 스크립트를 구성하는 요소들을 세분화하여 단일 책임 원칙에 준하여 나눈 것이다.

 

위 예시는 단일 원칙 책임을 준하는 방법을 보여주는 것일 뿐 항상 단일 책임 원칙이 옳은 것은 아니다. 프로젝트 크기나 복잡도에 따라 설계 패턴이나 구조가 달라질 수 있고 유니티에서는 MonoBehaviour 컴포넌트를 사용하여 오브젝트의 동작을 제어하기에, 항상 위와 같이 클래스로 나눌 필요도 없다.

 

 

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

 

클래스 확장에는 개방, 수정에는 닫다.

 

개방 폐쇄 원칙은 기존 코드를 수정하지 않고 확장에만 열어두어 새로운 기능을 추가할 수 있어야 한다.

이는 다형성, 추상화, 인터페이스를 통한 의존성 주입 등의 개념이 포함된다.

 

개방/폐쇄 원칙
using System;

// 도형을 나타내는 추상 클래스
abstract class Shape
{
    public abstract double Area();
}

// 사각형 클래스
class Rectangle : Shape
{
    private double width;
    private double height;

    public Rectangle(double width, double height)
    {
        this.width = width;
        this.height = height;
    }

    public override double Area()
    {
        return width * height;
    }
}

// 원 클래스
class Circle : Shape
{
    private double radius;

    public Circle(double radius)
    {
        this.radius = radius;
    }

    public override double Area()
    {
        return Math.PI * radius * radius;
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 사각형과 원의 넓이 계산
        Rectangle rectangle = new Rectangle(5, 4);
        Circle circle = new Circle(3);
    }
}

 

위 코드에서 사각형 클래스와 원 클래스는 도형 클래스를 상속받는다. 이때 도형 클래스는 추상 클래스로써 상속 받는 자식 클래스들이 반드시 재정의를 해야 한다.

 

이를 통해 부모 클래스는 수정이 필요 없는 폐쇄 상태가 되고, 자식 클래스는 부모가 정의해 둔 틀에 따라 자신만의 기능을 구현하여 확장할 수 있게 된다.

 

 

3. 리스코프 치환 원칙 (Liskov's Substitution Principle)

파생 클래스가 기본 클래스를 대체할 수 있어야 한다.

 

리스코프는 실제 소프트웨어 공학자이신 바바라 리스코프(Barbara Liskov)에 의해 제안되었다. 하여 이름이 생소할 수 있다.

 

리스코프 원칙의 가장 큰 정의는 "하위 클래스는 어떠한 경우라도 부모 클래스를 대체할 수 있어야 한다."이다.

 

이 말이 무엇인가 하니, 우선 탈것(Vehicle)에 대한 스크립트를 작성해야 한다고 생각해 보자.

현재 우리에게서 가장 대표적인 탈것은 자동차가 있다. 자동차는 전/후 방향으로 이동할 수 있고 좌/우 회전을 할 수 있다.

 

탈것 스크립트
public class Vehicle
{
    public void MoveFoward()   {}
    public void MoveBackward() {}
    public void TurnRight()    {}
    public void TurnLeft()     {}
}
public class Car : Vehicle {}
public class Horse : Vehicle {}
public class Bicycle : Vehicle {}
public class Train : Vehicle {}

 

위 스크립트에서 자동차, 말, 자전거 모두 운송수단으로써 역할을 할 수 있고 자동차와 동일한 동작을 하기에 문제가 없다.

하지만 위 Vehicle 클래스로 기차를 만든다면 어떻게 될까?

 

그렇게 된다면 기차 클래스는 전/후방 이동 함수만 사용할 것이고, 좌/우 회전 기능은 사용되지도 않고 정의되지도 않은 상태로 존재하게 된다. 이 부분이 리스코프 원칙에 어긋나게 되는 것이다. 즉, 부모 클래스의 의도에 명확하게 맞는 자식 클래스를 만들어야 한다.

 

이를 위해 추상 클래스를 더 간략하고 세부적으로 분류하거나, 인터페이스를 사용하여 조합하는 것이 리스코프 원칙을 지키기 더욱 좋다.

 

 

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

 

인터페이스는 클라이언트가 실제 사용하는 메서드로 분리하여 더 작고 구체적인 인터페이스를 생성해야 한다.

 

조금 더 쉽게 말하면, 인터페이스를 여러 개로 분리하여 필요한 인터페이스만 사용할 수 있도록 모듈화 하는 것이다. 이는 불필요한 메서드를 사용하지 않거나 필요에 따라 특정 기능을 추가하는데 도움이 된다.

 

인터페이스 예제
public interface IMove {}
public interface IState {}
public interface IFlying {}
public class Bird : IMove, IState, IFlying {}
public class Dog : IMove, IState {}

 

위 예제와 같이 인터페이스를 세분화하면 상속받는 클래스의 성격에 따라 쉽고 간편하게 기능을 추가해 줄 수 있게 된다.

 

 

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

상위 모듈은 하위 모듈에서 직접 사용되면 안 된다.

 

의존성 역전 원칙은 조금 헷갈리시 쉬운 애매한 설명들이 많다. 실제 사용할 만한 예제로는 유한 상태 기계(Finite State Machine : FSM)가 있다.

 

좀 더 예제를 사용하자면, 전구를 킬 수 있는 스위치가 있다고 가정할 때, 스위치는 전기가 통하는 모든 기기에서 전력을 공급하는 여부를 결정하는 도구이다.

 

전구 예제
class Bulb {
    public void turnOn() {
        // 전구를 켜는 동작
    }

    public void turnOff() {
        // 전구를 끄는 동작
    }
}

class Switch {
    private Bulb device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }

    public void turnOff() {
        device.turnOff();
    }
}

 

위 예제에서 스위치는 전구를 켜고 끄는 역할을 하게 된다.

문제는 여기서 발생하는데, 해당 스위치 클래스는 이제 전구만 키고 끌 수 있는 의존성이 발생하여 전구 외의 기기들에는 사용할 수 없는 클래스가 되어 버렸다.

 

즉, 전구의 스위치가 수정될 수도 없게 용접되어버린 상태인 것이다. 이제 이 스위치는 다른 어떠한 전자기기에도 사용할 수 없는 재활용이 불가능한 제품이 되었다.

 

하지만 이는 인터페이스로 간단하게 개선할 수 있다.

 

전구 예제 개선 방안
interface Switchable {
    void turnOn();
    void turnOff();
}

class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void turnOn() {
        device.turnOn();
    }

    public void turnOff() {
        device.turnOff();
    }
}

class Bulb : Switchable {
    public void turnOn() {
        // 전구를 켜는 동작
    }

    public void turnOff() {
        // 전구를 끄는 동작
    }
}

 

위 코드에서 Switchable이라는 인터페이스를 통해 해당 인터페이스를 상속받는 모든 클래스들을 스위치 클래스에서 사용이 가능하도록 만들었다.

 

이와 같이 하나의 클래스가 다른 클래스에 완벽히 커플링되는 것을 막아야 유지보수성과 확장성에 큰 역할을 한다.

'개발 > Unity' 카테고리의 다른 글

[Unity] Custom Editor  (0) 2023.09.03
[Unity] 인스펙터 레이아웃 생성 과정  (0) 2023.09.03
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함