SW공학

[SW공학] SOLID 원칙 | 객체 지향 설계 5대 원칙

revolutionarylife 2024. 10. 30. 14:13
반응형

📚[SW공학] SOLID 원칙 | 객체 지향 설계 5대 원칙

목차

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

개요

소프트웨어 설계는 코드를 더 쉽고 효율적으로 유지 관리할 수 있게 만드는 핵심 요소입니다. 이를 위해 우리는 SOLID 원칙을 따르는데, 이 다섯 가지 원칙은 객체 지향 프로그래밍(OOP)의 기반이 되는 설계 지침으로, 코드의 유연성확장성을 높여줍니다.

이번 글에서는 SOLID 각 원칙의 핵심 개념과 이를 적용하는 방법을 알아보겠습니다.


SOLID 원칙이란?

SOLID 원칙은 객체 지향 설계에서 유연하고 유지보수하기 쉬운 코드를 작성하는 데 도움을 주는 다섯 가지 기본 원칙을 말합니다. 이 원칙은 소프트웨어 아키텍처에서 코드의 중복을 줄이고 결합도를 낮추며, 각 클래스와 모듈을 독립적으로 설계하는 데 초점을 둡니다. SOLID 원칙의 유래와 중요성에 대해 간단히 살펴봅니다.


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

 

  • : 각 클래스는 하나의 책임만 가져야 한다는 원칙.
  • 중요성: 단일 책임 원칙은 코드가 변화할 이유를 단순화하여 유지보수를 쉽게 만들어줍니다.
  • 예제: 코드에서 단일 책임 원칙을 적용하여 클래스의 역할을 명확하게 구분하는 방법을 예시로 설명합니다.

예제

하나의 클래스는 하나의 책임만 가져야 합니다. 아래 Invoice 클래스는 청구서를 생성하는 역할만 수행하고, 프린트와 저장 기능은 다른 클래스로 분리합니다. 이 예시에서는 InvoicePrinterInvoiceRepository프린트저장 책임을 각각 분리해 수행합니다.

# SRP 위반 예시
class Invoice:
    def __init__(self, amount):
        self.amount = amount

    def print_invoice(self):
        print(f"Invoice amount: {self.amount}")

    def save_to_db(self):
        # 데이터베이스에 저장하는 로직
        pass


# SRP를 준수한 코드
class Invoice:
    def __init__(self, amount):
        self.amount = amount

class InvoicePrinter:
    def print_invoice(self, invoice):
        print(f"Invoice amount: {invoice.amount}")

class InvoiceRepository:
    def save_to_db(self, invoice):
        # 데이터베이스에 저장하는 로직
        pass

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

  • 개념: 클래스는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 한다는 원칙.
  • 중요성: 기존 코드를 수정하지 않고 기능을 확장할 수 있어야 하며, 이로 인해 코드 안정성을 유지할 수 있습니다.
  • 예제: OCP를 적용한 설계 예제를 통해 코드 변경 없이 기능을 추가하는 방법을 보여줍니다.

예제

클래스는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 합니다.

할인을 위한 클래스를 추가하는 대신 기존 클래스(Discount)를 수정하지 않고 새로운 할인 클래스를 추가해 확장합니다.

Discount 클래스는 확장 가능하게 설계하여, 새로운 할인 유형이 필요할 때 별도의 클래스만 추가하면 됩니다.

# OCP 위반 예시
class Discount:
    def apply_discount(self, amount, discount_type):
        if discount_type == "percentage":
            return amount * 0.9
        elif discount_type == "fixed":
            return amount - 10

# OCP를 준수한 코드
from abc import ABC, abstractmethod

class Discount(ABC):		
    @abstractmethod
    def apply_discount(self, amount):
        pass

class PercentageDiscount(Discount):
    def apply_discount(self, amount):
        return amount * 0.9

class FixedDiscount(Discount):
    def apply_discount(self, amount):
        return amount - 10

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

  • 개념: 서브 클래스는 언제나 기반 클래스의 역할을 대체할 수 있어야 한다는 원칙.
  • 중요성: 코드에서 상속을 사용할 때 기능의 일관성을 유지하도록 돕습니다.
  • 예제: LSP를 위반한 사례와 이를 개선한 코드 구조를 제시하여 올바른 상속 구조를 설명합니다.

예제

서브 클래스는 언제나 기반 클래스의 역할을 대체할 수 있어야 합니다.

여기서는 Rectangle 클래스와 Square 클래스가 상위 클래스 역할을 잘 수행하도록 합니다. 

# LSP 위반 예시
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_width(self, width):
        self.width = width

    def set_height(self, height):
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Rectangle):
    def set_width(self, width):
        self.width = width
        self.height = width

    def set_height(self, height):
        self.width = height
        self.height = height

여기서는 Square가 Rectangle을 대체할 수 없으므로 LSP를 위반합니다.

# LSP를 준수한 코드:
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side * self.side

이 구조에서는 각각의 도형 클래스가 Shape라는 추상 클래스(혹은 인터페이스)를 구현하도록 하여, Rectangle과 Square가 각각 독립적으로 area 메서드를 구현할 수 있게 합니다. 이제 Rectangle을 Square로 치환해도 정상적으로 동작합니다.


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

  • 개념: 특정 클라이언트를 위한 인터페이스를 여러 개로 분리하는 것이 좋다는 원칙.
  • 중요성: 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 하여, 인터페이스 의존성을 줄일 수 있습니다.
  • 예제: ISP를 적용해 인터페이스를 분리하고 사용성 높은 클래스를 설계하는 방법을 보여줍니다.

예제

특정 클라이언트를 위한 인터페이스를 여러 개로 분리합니다. 모든 기능을 포함한 인터페이스 대신 필요한 기능만 구현하도록 합니다.

# ISP 위반 예시
class Worker:
    def work(self):
        pass

    def eat(self):
        pass

class Robot(Worker):
    def work(self):
        pass

    def eat(self):
        raise NotImplementedError("로봇은 먹지 않습니다")

Worker 인터페이스에 work와 eat가 모두 포함되어 있으면, Robot과 같은 클래스는 불필요한 eat 메서드를 구현해야 하므로 ISP를 위반합니다.

로봇은 먹지 않으므로 eat 메서드를 사용할 필요가 없습니다.

class Workable(ABC):
    @abstractmethod
    def work(self):
        pass

class Eatable(ABC):
    @abstractmethod
    def eat(self):
        pass

class Human(Workable, Eatable):
    def work(self):
        print("일하는 중")

    def eat(self):
        print("식사하는 중")

class Robot(Workable):
    def work(self):
        print("일하는 중")

Workable과 Eatable 인터페이스를 각각 분리하여 Human은 둘 다 구현하고, Robot은 Workable만 구현하도록 했습니다.

이제 각 클래스는 필요한 기능만을 구현합니다.


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

  • 개념: 고수준 모듈은 저수준 모듈에 의존해서는 안 된다는 원칙.
  • 중요성: 모듈 간 결합도를 줄이고, 코드의 유연성을 높이는 데 중점을 둡니다.
  • 예제: DIP를 적용하여 구체적인 구현이 아닌 인터페이스에 의존하게 설계하는 방법을 예제로 설명합니다.

예제

고수준 모듈은 저수준 모듈에 의존하지 않고, 인터페이스에 의존하도록 설계합니다.  

# DIP 위반 예시
class LightBulb:
    def turn_on(self):
        print("전구 켜짐")

class Switch:
    def __init__(self, bulb):
        self.bulb = bulb

    def operate(self):
        self.bulb.turn_on()

Switch가 LightBulb 구체 클래스에 의존하고 있으므로 DIP를 위반합니다.

# DIP를 준수한 코드
class Switchable(ABC):
    @abstractmethod
    def turn_on(self):
        pass

class LightBulb(Switchable):
    def turn_on(self):
        print("전구 켜짐")

class Switch:
    def __init__(self, device: Switchable):
        self.device = device

    def operate(self):
        self.device.turn_on()

Switch는 이제 Switchable 인터페이스에 의존하여 LightBulb가 아닌 다른 장치도 교체할 수 있습니다.


마무리

SOLID 원칙은 객체 지향 설계를 통해 코드를 더 견고하게 만듭니다.

각 원칙은 개별적으로도 유용하지만, 함께 적용할 때 그 효과는 극대화됩니다.

SOLID 원칙을 이해하고 실제 프로젝트에 적용한다면, 코드의 가독성뿐만 아니라 유지보수성, 재사용성이 크게 향상될 것입니다.

앞으로의 개발에서 SOLID 원칙을 참고하여 더 나은 소프트웨어를 설계해 보세요!🌟⭐

반응형