프로그래밍공부(Programming Study)

SOLID 원칙과 파이썬 예제

Chaany 2024. 9. 8.
728x90

 

1. SOLID 원칙이란?

SOLID 원칙은 로버트 C. 마틴(Robert C. Martin)이 소프트웨어 개발에서 객체 지향 설계 원칙을 효율적으로 적용하기 위해 제시한 다섯 가지의 핵심 원칙입니다. 이 원칙들은 코드의 가독성, 유지보수성, 재사용성을 높여줍니다. 각 원칙은 특정 문제를 해결하고 시스템의 복잡성을 줄이기 위해 고안되었습니다.

SOLID는 다음과 같은 다섯 가지 원칙으로 이루어져 있습니다:

  1. S - 단일 책임 원칙 (Single Responsibility Principle, SRP)
  2. O - 개방-폐쇄 원칙 (Open-Closed Principle, OCP)
  3. L - 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
  4. I - 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)
  5. D - 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

2. SOLID 원칙의 특징

2.1. 단일 책임 원칙 (SRP)

단일 책임 원칙은 클래스나 모듈이 오직 하나의 책임만 가져야 한다는 원칙입니다. 즉, 하나의 클래스는 오직 하나의 기능만을 수행해야 하며, 이 기능에 대한 변경 사항만 해당 클래스에 영향을 미쳐야 합니다.

특징:

  • 각 클래스가 독립적으로 변화할 수 있도록 설계합니다.
  • 클래스가 여러 책임을 지게 되면 유지보수가 어려워집니다.

파이썬 코드 예시:

class User:
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

class EmailSender:
    def send_email(self, email: str, message: str):
        print(f"Sending email to {email}: {message}")

# 사용자 정보 관리와 이메일 전송을 각각의 클래스에 분리하여 단일 책임을 준수합니다.
user = User("John Doe", "john@example.com")
email_sender = EmailSender()
email_sender.send_email(user.email, "Welcome to SOLID principles!")

설명:
User 클래스는 사용자 정보만을 담당하고, EmailSender 클래스는 이메일 전송만을 책임집니다. 이처럼 두 클래스는 각각의 책임을 갖도록 설계되어 서로 간섭 없이 변경될 수 있습니다.


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

개방-폐쇄 원칙확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 원칙입니다. 즉, 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있도록 설계해야 합니다.

특징:

  • 코드를 수정하는 것보다 확장하는 것이 더 안전합니다.
  • 새로운 요구사항이 생겼을 때 기존 코드를 건드리지 않도록 해야 합니다.

파이썬 코드 예시:

from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def pay(self, amount: float):
        pass

class CreditCardPayment(PaymentMethod):
    def pay(self, amount: float):
        print(f"Paying {amount} using credit card")

class PaypalPayment(PaymentMethod):
    def pay(self, amount: float):
        print(f"Paying {amount} using PayPal")

# 새로운 결제 방식이 필요하면 기존 코드를 수정하지 않고 확장할 수 있습니다.
def process_payment(payment_method: PaymentMethod, amount: float):
    payment_method.pay(amount)

# CreditCard와 Paypal 둘 다 적용 가능
payment = CreditCardPayment()
process_payment(payment, 100.0)

payment = PaypalPayment()
process_payment(payment, 200.0)

설명:
위 코드에서는 PaymentMethod라는 추상 클래스를 이용하여 결제 방식에 대한 인터페이스를 정의합니다. 이후 CreditCardPaymentPaypalPayment와 같은 구체적인 클래스들이 이를 구현합니다. 새로운 결제 방법을 추가할 때 기존 코드를 수정하지 않고 클래스를 확장함으로써 OCP를 준수할 수 있습니다.


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

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체할 수 있어야 한다는 원칙입니다. 즉, 부모 클래스의 기능을 수행하는 곳에 자식 클래스를 사용해도 문제가 없어야 합니다.

특징:

  • 상속 구조에서 자식 클래스는 부모 클래스의 계약을 완전히 이행해야 합니다.
  • 다형성을 안전하게 사용할 수 있게 해줍니다.

파이썬 코드 예시:

class Bird:
    def fly(self):
        print("Bird is flying")

class Penguin(Bird):
    def fly(self):
        raise NotImplementedError("Penguins can't fly")

# 리스코프 치환 원칙을 위반하는 예시
def make_bird_fly(bird: Bird):
    bird.fly()

penguin = Penguin()
make_bird_fly(penguin)  # 의도한대로 작동하지 않음

설명:
여기서 Penguin 클래스는 Bird 클래스를 상속받지만, Penguin은 날지 못하는 새입니다. 이런 구조는 LSP를 위반합니다. Penguin 클래스가 부모 클래스 Bird의 기능을 완전히 대체하지 못하기 때문입니다. 이런 경우에는 상속 대신 인터페이스나 다른 설계 패턴을 사용하는 것이 좋습니다.


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

인터페이스 분리 원칙은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙입니다. 인터페이스는 가능한 한 구체적이고 작은 단위로 나누어야 합니다.

특징:

  • 인터페이스를 작게 유지하여 불필요한 기능을 강제하지 않도록 합니다.
  • 클라이언트는 필요한 기능만 사용할 수 있습니다.

파이썬 코드 예시:

from abc import ABC, abstractmethod

class Printer(ABC):
    @abstractmethod
    def print(self, document: str):
        pass

class Scanner(ABC):
    @abstractmethod
    def scan(self, document: str):
        pass

class MultiFunctionPrinter(Printer, Scanner):
    def print(self, document: str):
        print(f"Printing document: {document}")

    def scan(self, document: str):
        print(f"Scanning document: {document}")

# 프린터와 스캐너 기능을 별도의 인터페이스로 분리하여 필요할 때만 사용 가능하게 만듦
printer = MultiFunctionPrinter()
printer.print("SOLID Principles")
printer.scan("SOLID Principles")

설명:
여기서는 PrinterScanner 인터페이스를 분리하여 필요한 기능만 구현할 수 있도록 설계되었습니다. 이렇게 하면 사용자는 자신이 필요한 인터페이스만 선택하여 의존할 수 있습니다.


2.5. 의존성 역전 원칙 (DIP)

의존성 역전 원칙은 고수준 모듈이 저수준 모듈에 의존해서는 안 되고, 둘 다 추상화에 의존해야 한다는 원칙입니다. 구체적인 클래스보다는 인터페이스나 추상 클래스에 의존함으로써 유연한 설계를 가능하게 합니다.

특징:

  • 모듈 간의 결합도를 낮춥니다.
  • 확장성 있는 설계를 가능하게 합니다.

파이썬 코드 예시:

from abc import ABC, abstractmethod

class NotificationSender(ABC):
    @abstractmethod
    def send(self, message: str):
        pass

class EmailSender(NotificationSender):
    def send(self, message: str):
        print(f"Sending email: {message}")

class SmsSender(NotificationSender):
    def send(self, message: str):
        print(f"Sending SMS: {message}")

class NotificationService:
    def __init__(self, sender: NotificationSender):
        self.sender = sender

    def notify(self, message: str):
        self.sender.send(message)

# 이메일과 SMS 전송 방식에 관계없이 유연한 알림 시스템 구현
email_service = NotificationService(EmailSender())
sms_service = NotificationService(SmsSender())

email_service.notify("You've got mail!")
sms_service.notify("You've got a message!")

설명:
위 코드에서는 NotificationService가 구체적인 이메일 전송 방식이나 SMS 전송 방식에 의존하지 않고 NotificationSender라는 추상 클래스에 의존하도록 설계되었습니다. 이를 통해 이메일 또는 SMS 전송 방식이 바뀌더라도 NotificationService는 그대로 유지될 수 있습니다.


3. SOLID 원칙의 장단점

장점:

  • 유지보수성 향상: 코드를 모듈화하고 책임을 분리함으로써 유지보수가 쉬워집니다.
  • 확장성 강화: 기존 코드를 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다.
  • 가독성 증가: 명확한 책임 분리로 코드를 이해하기가 쉬워집니다.
  • 재사용성 증가: 독립적인 컴포넌트들은 다른 프로젝트에서도 재사용할 수 있습니다.

단점:

  • 설계 복잡성 증가: 처음부터 SOLID 원칙을 엄격하게 적용하면 설계가 과도하게 복잡해질 수 있습니다.
  • 학습 곡선: SOLID 원칙을 완전히 이해하고 적용하기 위해서는 많은 학습이 필요합니다.

4. 구체적인 사례

SOLID 원칙을 실제 프로젝트에서 활용하면 코드의 유지보수성과 확장성이 크게 향상됩니다. 예를 들어, 데브옵스 엔지니어로서 CI/CD 파이프라인을 구축할 때에도 SOLID 원칙을 적용하여 각 모듈(빌드, 테스트, 배포 등)을 명확히 분리하고, 새로운 배포 전략을 도입할 때 기존 코드를 수정하지 않고 확장할 수 있습니다.


결론

SOLID 원칙은 소프트웨어 설계에서 필수적으로 알아야 할 중요한 개념입니다. 이 원칙들을 올바르게 이해하고 적용하면, 코드의 유지보수성과 확장성이 크게 향상되며, 복잡한 시스템에서도 유연한 설계를 유지할 수 있습니다. 주니어 데브옵스 엔지니어로서 SOLID 원칙을 깊이 이해하고 활용한다면 더욱 뛰어난 엔지니어로 성장할 수 있을 것입니다.

728x90

댓글