본문 바로가기
프로그래밍/Python 관련 정보

[Pythonic Programming] SOLID원칙

by 물박사의 저장공간 2024. 10. 25.

1. Single Responsibility Principle(단일 책임 원칙) : Class와 Method는 하나의 역할만 하도록 한다. 

open/closed principle : 확장에는 열려있어야 하지만 수정에는 닫혀있어야 한다. 

이 원칙은 클래스를 재사용하기 쉽고, 변경이 발생할 때 다른 코드에 미치는 영향을 최소화하려데 목적이 있습니다. 

 

이 원칙을 지키지 못한 예시와 잘 지킨 예시를 비교해보면서 생각해볼까요?

먼저 잘 지키지 못한 예시입니다. 

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

    def save_user_data(self):
        # 사용자 데이터를 저장하는 로직
        print(f"Saving user {self.name}")

    def send_email(self, message):
        # 이메일을 전송하는 로직
        print(f"Sending email to {self.email}: {message}")

# 사용 예
user = User("Alice", "alice@example.com")
user.save_user_data()
user.send_email("Welcome!")

 

User 클래스는 사용자 데이터 관리와 이메일 전송이라는 두 가지 책임을 가지고 있습니다. 만약 이메일 전송 방식이 바뀐다면 User 클래스를 수정해야 합니다. 그래서 이것을 아래와 같이 바꿔주면

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

    def save_user_data(self):
        # 사용자 데이터를 저장하는 로직
        print(f"Saving user {self.name}")

class EmailService:
    @staticmethod
    def send_email(email, message):
        # 이메일을 전송하는 로직
        print(f"Sending email to {email}: {message}")

# 사용 예
user = User("Alice", "alice@example.com")
user.save_user_data()

# EmailService 클래스를 이용해 이메일 전송
EmailService.send_email(user.email, "Welcome!")

 

이제는 이메일 전송방식이 바뀌더라도 EmailService 클래스만 바꿔주면 됩니다. 

 

2. Liskov’s Substitution Principle (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  # 정사각형은 너비와 높이가 같아야 함

# 사용 예시
def print_area(rect):
    rect.set_width(4)
    rect.set_height(5)
    print(rect.area())

rect = Rectangle(2, 3)
square = Square(2, 3)

print_area(rect)  # 출력: 20 (사각형은 가로 4, 세로 5로 계산됨)
print_area(square)  # 출력: 25 (정사각형은 5x5로 계산됨, 예상과 다른 결과)

 

위의 코드는 LSP를 위반한 예시라고 볼 수 있겠죠. 좀 더 바람직하게 코드를 수정하기 위해서 '추상클래스'를 도입해봅시다.

from abc import ABC, abstractmethod

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

# 사용 예시
def print_area(shape):
    print(shape.area())

rect = Rectangle(4, 5)
square = Square(5)

print_area(rect)  # 출력: 20
print_area(square)  # 출력: 25

 

3. Interface segregation principle (인터페이스 분리 원칙) : 하나의 큰 인터페이스를 여러 개의 작은 인터페이스로 나누어, 필요한 기능만을 제공하는 인터페이스로 만들어야 한다.

from abc import ABC, abstractmethod

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

    @abstractmethod
    def eat(self):
        pass

class Employee(Worker):
    def work(self):
        print("Employee is working")

    def eat(self):
        print("Employee is eating")

class Robot(Worker):
    def work(self):
        print("Robot is working")

    def eat(self):
        raise NotImplementedError("Robot doesn't eat")

 

Robot class는 Worker 인터페이스의 eat method를 구현할 필요가 없으나, 큰 인터페이스를 상속함으로써 eat method를 강제로 구현해야 합니다. 이는 Robot이 사용하지 않는 기능에 의존하게 되는 문제를 만듭니다. 이런 불필요하고도 위험한 의존관계가 생기지 않으려면 아래와 같이바꿔주면 되겠죠

from abc import ABC, abstractmethod

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

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

class Employee(Workable, Eatable):
    def work(self):
        print("Employee is working")

    def eat(self):
        print("Employee is eating")

class Robot(Workable):
    def work(self):
        print("Robot is working")

 

4. Dependency inversion principle (의존성 역전 원칙) : 고수준 모듈이 저수준 모듈에 의존하지 않고, 둘 다 추상화(인터페이스 또는 추상 클래스)에 의존하도록 해야 한다. 

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

class Notification:
    def __init__(self):
        # Notification 클래스가 EmailService에 직접 의존하고 있음
        self.email_service = EmailService()

    def notify(self, message):
        self.email_service.send_email(message)

# 사용 예시
notification = Notification()
notification.notify("Hello World!")

 

위의 코드를 볼까요? EmailService가 Notification 클래스 내부에 구체적으로 사용되고 있으며, Notification이 특정 구현체인 EmailService에 의존하고 있습니다. 이렇게되면 Notification 클래스는 EmailService와 강하게 결합되어 있어, EmailService 대신 다른 서비스를 사용하려면 Notification을 수정해야 합니다(고수준 모듈인 Notification이 저수준 모듈인 EmailService의 구체적인 구현에 의존하고 있습니다). 그러니까 아래와 같이 다시 고쳐볼 수 있겠습니다. 

from abc import ABC, abstractmethod

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

class EmailService(MessageService):
    def send(self, message):
        print(f"Sending email: {message}")

class SMSService(MessageService):
    def send(self, message):
        print(f"Sending SMS: {message}")

class Notification:
    def __init__(self, service: MessageService):
        # Notification은 이제 추상화된 인터페이스에 의존함
        self.service = service

    def notify(self, message):
        self.service.send(message)

# 사용 예시
email_service = EmailService()
notification = Notification(email_service)
notification.notify("Hello World!")  # 이메일 전송

sms_service = SMSService()
notification = Notification(sms_service)
notification.notify("Hello World!")  # SMS 전송

 

Notification 클래스는 이제 구체적인 EmailService나 SMSService 대신, MessageService라는 인터페이스에 의존하고 있습니다. 이를 통해 메시지 전송 방식이 바뀌어도 Notification 코드를 수정할 필요가 없습니다.

 

자, 오늘은 pythonic programming의 일환으로 SOLID원칙을 살펴보았습니다. 사실 저도 이 원칙을 잘 지켜가면서 코딩하고 있다고 보기는 어렵지만 의식적으로 노력하고 있습니다. 처음에 이런 원칙을 접했을 때 굉장히 신선한 충격이었거든요. 저는 코딩이라는 것이 거의 딱딱 정해진 답이있는 자유도가 굉장히 떨어지는 작업이라고 생각했었던 것 같습니다. 그런데 pythonic programming을 공부하면서 프로그래밍도 일종의 "글쓰기"구나 "논리적으로" 프로그래밍하는 것이 이래서 중요하구나 하는 것을 깨달았던 것 같습니다. 여러분도 코딩 습관에 도움이 되는 포스팅이었길 바랍니다.