본문 바로가기
이론/개발 지식

객체지향 프로그래밍 설계 5원칙 - SOLID

by 퇴근후개발 2024. 3. 21.
반응형

객체지향 프로그래밍 설계 5원칙

객체지향 프로그래밍에서 코드를 더 효율적이고 유연하게 만들기 위해 SOLID 원칙을 따르는 것이 중요합니다. SOLID는 단일 책임 원칙(SRP), 개방-폐쇄 원칙(OCP), 리스코프 치환 원칙(LSP), 인터페이스 분리 원칙(ISP), 의존 역전 원칙(DIP)의 약자입니다. 이러한 원칙들을 잘 지키면 코드의 모듈화가 증가하고 유연성과 확장성이 향상됩니다. 이제 각 원칙을 자세히 살펴보겠습니다.

1. 단일 책임 원칙(SRP)

단일 책임 원칙은 클래스나 모듈은 하나의 책임만 가져야 한다는 원칙입니다. 이는 클래스가 변경되어야 하는 이유는 오직 한 가지여야 한다는 것을 의미합니다. 이를 위해 클래스는 한 가지 기능만 수행하고, 다른 기능에 대한 변경이 필요할 때마다 클래스를 수정하는 것을 피해야 합니다.

예를 들어, 파일을 읽고 쓰는 기능을 가진 클래스가 있다고 가정해봅시다. 이 클래스는 파일 입출력과 관련된 모든 기능을 수행해야 하고, 다른 책임을 맡지 않아야 합니다.

class FileHandler
{
    public void ReadFile(string filePath)
    {
        // 파일 읽기 로직
    }

    public void WriteFile(string filePath, string content)
    {
        // 파일 쓰기 로직
    }
}

단일 책임 원칙을 지키면 (= 각 클래스가 하나의 책임만 갖게 되면)

  • 클래스의 목적이 명확해집니다. 이는 코드를 이해하기 쉽고 읽기 쉽게 만듭니다.
  • 해당 책임을 변경할 때 다른 부분에 영향을 미칠 가능성이 줄어듭니다. 따라서 유지보수가 더욱 용이해집니다.
  • 해당 클래스를 다른 프로젝트나 모듈에서 쉽게 재사용할 수 있습니다. 이는 개발 시간을 단축하고 코드의 일관성을 유지하는 데 도움이 됩니다.

2. 개방-폐쇄 원칙(OCP)

개방-폐쇄 원칙은 클래스나 모듈은 확장에는 열려 있어야 하고 변경에는 닫혀 있어야 한다는 원칙입니다. 이는 기존의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 함을 의미합니다.

예를 들어, 도형을 그리는 클래스가 있다고 가정해봅시다. 이 클래스는 새로운 도형이 추가되더라도 코드를 수정하지 않고 새로운 도형을 그릴 수 있어야 합니다.

abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw()
    {
        // 원 그리기 로직
    }
}

class Rectangle : Shape
{
    public override void Draw()
    {
        // 사각형 그리기 로직
    }
}

개방-폐쇄 원칙을 지키면 새로운 요구 사항이나 변경 사항이 발생할 때 기존의 코드를 변경하지 않고도 대응할 수 있습니다. 이는 시스템이 변경에 대해 유연하게 대처할 수 있다는 것을 의미합니다.

3. 리스코프 치환 원칙(LSP)

리스코프 치환 원칙은 상속 관계에 있는 클래스들은 서로 대체 가능해야 한다는 원칙입니다. 즉, 상위 타입의 객체를 하위 타입의 객체로 교체해도 프로그램의 의도가 변하지 않아야 합니다.

예를 들어, Shape 클래스를 상속하는 Rectangle 클래스와 Square 클래스가 있다고 가정해봅시다. 아래 코드에서는 Rectangle 객체를 Shape 객체로 대체할 수 있어야 합니다. 즉, 코드에서 Shape 객체가 예상되는 곳에 Rectangle 객체를 넣어도 동작에 이상이 없어야 합니다. 마찬가지로 Square 객체도 마찬가지로 대체 가능해야 합니다.

using System;

class Shape
{
    public virtual double CalculateArea()
    {
        return 0;
    }
}

class Rectangle : Shape
{
    private double width;
    private double height;

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

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

class Circle : Shape
{
    private double radius;

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

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

class Program
{
    static void Main(string[] args)
    {
        Shape shape1 = new Rectangle(5, 10);
        Shape shape2 = new Circle(3);

        Console.WriteLine("Shape 1 area: " + shape1.CalculateArea());
        Console.WriteLine("Shape 2 area: " + shape2.CalculateArea());
    }
}

리스코프 치환 원칙을 지키면 코드의 일관성과 안정성을 유지할 수 있으며, 시스템의 확장성과 유연성을 향상시킬 수 있습니다.

4. 인터페이스 분리 원칙(ISP)

인터페이스 분리 원칙은 클라이언트가 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다. 한 인터페이스가 너무 많은 기능을 제공하면 해당 인터페이스를 사용하지 않는 클래스도 해당 기능에 의존하게 됩니다. 즉, 인터페이스는 클라이언트가 필요로 하는 작은 단위로 분리되어야 합니다.

간단한 예시를 통해 설명해보겠습니다. 가령, 우리가 자동차 인터페이스를 설계한다고 가정해봅시다.

interface ICar
{
    void Start();
    void Accelerate();
    void Brake();
    void Stop();
    void PlayMusic();
}

위의 인터페이스에는 자동차의 주요 기능들이 포함되어 있습니다. 그런데, 클라이언트 중에는 음악을 재생할 필요가 없는 경우가 있을 수 있습니다. 이 경우에는 인터페이스를 분리하여 음악 재생 기능을 제외할 수 있습니다.

interface ICar
{
    void Start();
    void Accelerate();
    void Brake();
    void Stop();
}

interface IMusicPlayer
{
    void PlayMusic();
}

이제 클라이언트는 자동차를 사용할 때 음악 재생 기능에 의존하지 않고, 필요한 경우에만 따로 음악 재생 인터페이스를 사용할 수 있습니다.

인터페이스 분리 원칙 지키면 (= 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않으면 )

  • 불필요한 의존성을 최소화할 수 있습니다.
  • 특정한 기능에 집중하므로, 해당 기능이 변경되거나 추가되더라도 다른 부분에 영향을 미치지 않습니다.
  • 작고 명확한 인터페이스는 코드의 의도를 명확하게 전달할 수 있으며, 코드를 이해하고 사용하기 쉽게 만듭니다.

5. 의존 역전 원칙(DIP)

의존 역전 원칙은 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다는 원칙입니다. 추상화에 의존해야 한다는 것을 의미합니다. 즉, 상위 수준의 모듈은 하위 수준의 모듈에 직접 의존하면 안 되고, 추상화된 인터페이스나 추상 클래스에 의존해야 합니다.

고수준 모듈은 보통 추상화된 정책이나 비즈니스 규칙을 구현하는 모듈입니다. 저수준 모듈은 실제 구현을 담당하는 모듈로, 데이터베이스 연결, 파일 시스템 접근 등의 저수준의 작업을 처리합니다.

가령, 자동차를 만드는 프로그램을 작성한다고 가정해봅시다. 이 프로그램에는 엔진(저수준 모듈)을 제어하는 Car 클래스(고수준 모듈)가 있습니다. 그리고 이 프로그램은 엔진을 제어하기 위해 Engine 클래스를 직접 사용합니다.

class Engine
{
    public void Start()
    {
        Console.WriteLine("Engine started");
    }

    public void Stop()
    {
        Console.WriteLine("Engine stopped");
    }
}

class Car
{
    private Engine _engine;

    public Car()
    {
        _engine = new Engine();
    }

    public void StartCar()
    {
        _engine.Start();
    }

    public void StopCar()
    {
        _engine.Stop();
    }
}

이 코드는 DIP를 지키지 않고 있습니다. Car 클래스가 Engine 클래스에 직접 의존하고 있기 때문에, 새로운 종류의 엔진을 사용하고 싶을 때 Car 클래스를 수정해야 합니다. 이는 코드의 유연성을 감소시키며, 새로운 기능 추가나 변경이 어려워집니다.

이제 이 코드를 DIP를 적용하여 리팩토링해보겠습니다.

using System;

interface IEngine
{
    void Start();
    void Stop();
}

class Engine : IEngine
{
    public void Start()
    {
        Console.WriteLine("Engine started");
    }

    public void Stop()
    {
        Console.WriteLine("Engine stopped");
    }
}

class NewEngine : IEngine
{
    public void Start()
    {
        Console.WriteLine("New engine started");
    }

    public void Stop()
    {
        Console.WriteLine("New engine stopped");
    }
}

class Car
{
    private IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public void StartCar()
    {
        _engine.Start();
    }

    public void StopCar()
    {
        _engine.Stop();
    }

    public void ChangeEngine(IEngine newEngine)
    {
        _engine = newEngine;
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 기존 엔진으로 Car 인스턴스 생성
        IEngine engine = new Engine();
        Car car = new Car(engine);

        // 기존 엔진으로 차량 시작 및 정지
        Console.WriteLine("Using the original engine:");
        car.StartCar();
        car.StopCar();

        // 새로운 엔진으로 변경
        IEngine newEngine = new NewEngine();
        car.ChangeEngine(newEngine);

        // 변경된 엔진으로 차량 시작 및 정지
        Console.WriteLine("\nUsing the new engine:");
        car.StartCar();
        car.StopCar();
    }
}

이제 Car 클래스는 엔진을 제어하는 데 어떤 구체적인 엔진을 사용하는지 알 필요가 없으며, 언제든지 새로운 엔진을 추가하거나 기존 엔진을 변경할 수 있습니다. 이는 코드의 유연성과 확장성을 높여줍니다.

이렇게 SOLID 원칙을 따르면 코드의 유지보수성과 확장성을 향상시킬 수 있습니다. 따라서 객체지향 설계를 할 때 이러한 원칙을 염두에 두고 코드를 작성하면 효율적으로 작업할 수 있습니다.

반응형