SOLID 원칙: 객체 지향 설계의 5가지 핵심 원칙

2024. 10. 1. 00:24Design Pattern

SOLID 원칙: 객체 지향 설계의 5가지 핵심 원칙

SOLID 원칙은 소프트웨어 설계에서 유지보수성확장성을 높이기 위해 적용되는 객체 지향 프로그래밍의 핵심 원칙입니다. 이 원칙들은 소프트웨어 시스템을 더 유연하게 만들고, 코드의 품질을 향상시키는 데 도움을 줍니다. SOLID는 다음 다섯 가지 원칙의 첫 글자를 따서 만든 약어입니다:

  • 단일 책임 원칙 (Single Responsibility Principle, SRP)
  • 개방-폐쇄 원칙 (Open/Closed Principle, OCP)
  • 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
  • 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  • 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

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

단일 책임 원칙은 클래스는 하나의 책임만을 가져야 하며, 단 하나의 변화 이유만 가져야 한다는 원칙입니다.
즉, 한 클래스는 오직 하나의 기능만을 담당해야 합니다. 여러 책임을 가진 클래스는 유지보수가 어렵고, 다른 이유로 인해 변경될 가능성이 높기 때문에 문제가 발생할 수 있습니다.

예시:

public class Report
{
    public void GenerateReport()
    {
        // 보고서 생성 로직
    }

    public void PrintReport()
    {
        // 보고서 출력 로직
    }
}

위 예시는 단일 책임 원칙을 위반한 코드입니다. 보고서 생성과 출력을 한 클래스에서 모두 처리하고 있기 때문에 변화 이유가 두 가지입니다. 이를 단일 책임 원칙에 맞게 리팩터링하면 아래와 같습니다.

public class ReportGenerator
{
    public void GenerateReport()
    {
        // 보고서 생성 로직
    }
}

public class ReportPrinter
{
    public void PrintReport()
    {
        // 보고서 출력 로직
    }
}

이제 보고서 생성과 출력이 각각 다른 클래스에서 관리되므로, 서로 다른 변경 이유에 대응할 수 있습니다.


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

개방-폐쇄 원칙은 소프트웨어 구성 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다는 원칙입니다.
즉, 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다는 의미입니다.

예시:

public class Rectangle
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double CalculateArea()
    {
        return Width * Height;
    }
}

위 예시는 만약 새로운 도형(예: 원, 삼각형)을 추가해야 한다면, CalculateArea() 메서드를 계속 수정해야 하므로 개방-폐쇄 원칙을 위반한 코드입니다.

이를 해결하려면 Shape라는 추상 클래스인터페이스를 도입해 확장할 수 있도록 합니다.

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

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public override double CalculateArea()
    {
        return Width * Height;
    }
}

public class Circle : Shape
{
    public double Radius { get; set; }

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

이제 새로운 도형을 추가할 때 기존 코드를 수정하지 않고 Shape 클래스를 상속받아 확장할 수 있습니다.


3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

리스코프 치환 원칙은 서브클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 원칙입니다. 즉, 상위 타입 객체를 하위 타입 객체로 치환해도 프로그램이 문제없이 동작해야 한다는 것입니다.

예시:

public class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("새가 날고 있습니다.");
    }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("타조는 날 수 없습니다.");
    }
}

위 코드는 타조가 날 수 없으므로 Fly 메서드에서 예외를 발생시키는데, 이는 리스코프 치환 원칙을 위반한 것입니다. 부모 클래스의 동작이 서브클래스에서 유지되지 않기 때문입니다. 이를 해결하려면 다음과 같이 설계를 수정해야 합니다.

public class Bird
{
    // 공통 동작
}

public class FlyingBird : Bird
{
    public void Fly()
    {
        Console.WriteLine("새가 날고 있습니다.");
    }
}

public class Ostrich : Bird
{
    // 타조는 날 수 없음
}

이제 FlyingBird 클래스만 Fly 메서드를 가집니다.
타조와 같은 날지 않는 새는 Bird 클래스를 상속받지만 Fly 기능을 가지지 않으므로 치환 원칙을 지킵니다.


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

인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 인터페이스에 의존하지 않도록 해야 한다는 원칙입니다.
즉, 작고 특정한 기능을 가진 인터페이스 여러 개로 분리해야 한다는 것입니다.

예시:

public interface IWorker
{
    void Work();
    void Eat();
}

위 인터페이스는 작업자(Worker)와 로봇(Robot) 모두에 사용될 수 있지만, 로봇은 Eat 메서드를 필요로 하지 않기 때문에 인터페이스 분리 원칙을 위반한 것입니다.

이를 해결하기 위해서는 각 기능에 맞는 인터페이스를 분리해야 합니다.

public interface IWorker
{
    void Work();
}

public interface IHumanWorker : IWorker
{
    void Eat();
}

이제 Worker와 Robot은 각각 자신에게 맞는 인터페이스만을 구현하게 되어 원칙을 준수합니다.


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

의존성 역전 원칙상위 모듈이 하위 모듈에 의존하지 않고, 추상화된 인터페이스에 의존해야 한다는 원칙입니다.
이를 통해 상위 모듈과 하위 모듈 간의 결합도를 낮추고, 코드의 유연성과 재사용성을 높일 수 있습니다.

예시:

csharp
 
public class LightBulb
{
    public void TurnOn()
    {
        Console.WriteLine("전구를 켭니다.");
    }
}

public class Switch
{
    private LightBulb _lightBulb;

    public Switch(LightBulb lightBulb)
    {
        _lightBulb = lightBulb;
    }

    public void TurnOn()
    {
        _lightBulb.TurnOn();
    }
}

위 코드는 Switch가 LightBulb에 의존하고 있어 의존성 역전 원칙을 위반한 코드입니다.
이를 해결하려면 추상화된 인터페이스를 사용해 의존성을 역전시켜야 합니다.

public interface ISwitchable
{
    void TurnOn();
}

public class LightBulb : ISwitchable
{
    public void TurnOn()
    {
        Console.WriteLine("전구를 켭니다.");
    }
}

public class Switch
{
    private ISwitchable _device;

    public Switch(ISwitchable device)
    {
        _device = device;
    }

    public void TurnOn()
    {
        _device.TurnOn();
    }
}

이제 Switch는 ISwitchable 인터페이스에 의존하며, 전구 외에도 다른 장치가 ISwitchable을 구현하면 손쉽게 교체할 수 있습니다.


 

SOLID 원칙은 객체 지향 설계의 기본 원칙으로, 소프트웨어 시스템을 더 유연하고 확장 가능하게 만들어 줍니다.
각 원칙을 준수하면 코드의 유지보수성을 높이고, 코드 간의 결합도를 줄이며 재사용성을 극대화할 수 있습니다.