Python/파이썬클린코드

[파이썬 클린코드] Chapter4 : SOLID원칙-1

공부하는 sum 2023. 3. 16. 22:41
728x90

http://www.yes24.com/Product/Goods/114667254

파이썬 클린코드 2nd edition 을 읽고 정리한 내용입니다.


0. SOLID란?

  • S : 단일 책임 원칙
  • O : 개방 / 폐쇄의 원칙
  • L : 리스코프 치환 원칙
  • I : 인터페이스 분리 원칙
  • D : 의존성 역전 원칙

1. 단일 책임 원칙

Single Resposibility Principle

클래스와 같은 소프트웨어 컴포넌트는 단 하나의 책임만을 져야 한다는 원칙. 도메인의 문제가 변경되는 경우에만 변경이 필요하고, 다른 이유로 변경이 필요하다면 그것은 추가적인 추상화가 필요함을 의미하게 된다. 

1-1. 너무 많은 책임을 가진 클래스

독립적인 동작을 하는 클래스를 하나의 인터페이스에 정의하는 경우에는 유지 보수를 어렵게 하고, 오류가 발생할 확률을 높게 한다. 

1-2. 책임 분산

모든 메서드를 다른 클래스로 분리해서 각 클래스마다 단일 책임을 가지게 한다면, 다른 하나가 수정이 필요하게 되어도 나머지에는 영향을 미치지 않게 된다. 이렇게 되면 유지보수가 쉽고, 재사용이 쉽다. 단, 클래스가 하나의 메서드만 가져야 한다는 뜻은 아님.

1-3. 예시

class Student:
    def __init__(self, name, age, grade):
        self.name = name
        self.age = age
        self.grade = grade

    def get_info(self):
        print(f"Name: {self.name}")
        print(f"Age: {self.age}")
        print(f"Grade: {self.grade}")

    def save_to_database(self):
        # 데이터베이스에 저장하는 코드

 

학생 정보를 저장하는 클래스에

  1. 학생의 정보를 저장하면서(get_info)
  2. 학생의 정보를 출력하는(save_to_database)

메소드를 두 가지 가지고 있다면, 이것은 단일 책임 원칙을 위반하는 것이 된다. 이렇게 하나의 크래스가 여러 책임을 가지게 되면, 유지 보수가 어렵고 복잡해지는 문제가 발생할 수 있다. 그렇기 때문에 단일 책임 원칙에 맞게 학생 정보 저장과 출력하는 클래스를 분리해야 한다. 

class Student:
    def __init__(self, name, id, major):
        self.name = name
        self.id = id
        self.major = major

class StudentPrinter:
    def __init__(self, student):
        self.student = student

    def print_info(self):
        print(f"Name: {self.student.name}")
        print(f"ID: {self.student.id}")
        print(f"Major: {self.student.major}")

그렇게 되면 학생의 정보를 출력하는 클래스인 StudentPrinter 클래스를 새로 정의해서 해당 메소드를 분리할 수 있다. 이렇게 되면 Student클래스는 학생의 정보만 저장하고, StudentPrinter 클래스는 정보를 출력하는 책임만 가질 수 있게 되어 각 클래스의 역할이 명확해져 코드의 유지보수성과 클래스의 재사용성을 높일 수 있게 된다. 

그렇다면, 클래스가 단일 책임을 가지고 있는지 아닌지 어떻게 알 수 있을까? 

가장 간단한 방법은 클래스의 이름으로 확인할 수 있다고 한다. 클래스의 이름이 어떤 기능을 수행하는지 명확하게 알려줄 때, 해당 클래스는 단일 책임 원칙을 따르고 있을 가능성이 높다고 한다. 

클래스 내부의 메서드와 속성이 어떤 기능을 수행하는지 파악함으로서 알 수도 있다. 클래스가 한 가지 기능을 수행하는 경우에는 그 클래스 내에는 하나의 기능만을 하는 메서드, 속성만 있어야 한다. 만약 클래스에 여러 기능을 수행하는 메서드와 속성이 있으면 해당 원칙을 위반하고 있을 가능성이 높다.

마지막으로, 클래스 변경 시 어떤 부분이 변경되어야 하는지 고려해 볼 수도 있다. 클래스가 단일 책임을 따르는 경우에는 해당 기능과 관련한 부분만 수정하면 되지만 그렇지 않은 경우에는 여러곳을 손봐야 할 수 있다.  

클래스에는 그럼 하나의 메서드만 가져야 하는건 아닐까?

단일 책임 원칙은 위해서도 말했듯이, 한 클래스에 하나의 책임을 가져야 한다는 원칙이지 클래스당 하나의 메서드만을 만들어야 한다는 의미는 아니다. 클래스가 가지는 책임이 다른 클래스와 구분이 되어야 하고, 이를 위해서는 하나 이상의 메서드를 가질 수 있다. 단, 이런 경우에는 작성된 메서드들은 해당 클래스의 책임에 맞게 작성되어야한다는 점은 명심해야 한다. 


2. 개방 / 폐쇄 원칙

Open / Close Priciple

모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙. 확장에는 열려있어 새로운 요구 사항이 있을 경우 추가가 가능해야 하지만, 기존 코드는 수정하지 않아야 한다는 뜻이다. 

2-1. 확장성을 가진 이벤트 시스템으로 리팩토링

개방/폐쇄 원칙을 따르는 디자인을 하려면 추상화를 해야 한다. 추상화란, 잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 의미한다. 그렇게 되면, 새로운 이벤트가 추가되더라도 기존의 코드는 수정하지 않으면서 새로운 코드를 추가하며 새 이벤트를 처리할 수 있게 된다. 

이 원칙은 다형성의 효과적인 사용과 관련이 되어 있는데, 다형성을 따르는 형태의 계약을 만들고 모델을 쉽게 확장할 수 있는 일반적인 구조로 디자인을 하는 것이다. 이렇게 하면 유지보수를 쉽게 하여 변경이 코드 전체에 영향을 미치는 것을 방지한다. 

즉, 확장에 대해서는 열려있지만 수정에 대해서는 닫혀있어야 한다는 원칙으로 소프트웨어 엔터티가 자신의 행동을 변경하지 않으면서 새로운 동작을 추가할 수 있어야 한다는 것을 의미한다. 이를 위해서는 다형성을 이용해서 확장이 가능한 구조로 만들어야 한다. 

2-2. 예시

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def make_sound(self):
        pass

class Cat(Animal):
    def make_sound(self):
        return "Meow"

class Dog(Animal):
    def make_sound(self):
        return "Woof"

class AnimalSound:
    def make_sound(self, animal: Animal) -> str:
        return animal.make_sound()

동물을 나타내는 Animal이라는 클래스가 있고, 이 클래스는 make_sound라는 메서드를 가지고 있다. 
그리고 이 Animal 클래스를 상속받아서 Dog과 Cat 클래스를 만들어서 각 울음 소리를 구현했다.

이렇게 울음소리를 구현하는 과정에서 Animal 클래스는 수정하지 않고, Animal 클래스의 인스턴스를 받아서 make_sound메서드를 호출해서 구현했다.

이 때 make_sound에는 다양한 형태가 올 수 있으므로 다형성을 활용한 구현이라고 할 수 있으며, 새로운 동물이 와도 Animal클래스를 상속받기만 하면 되고, AnimalSound는 따로 수정하지 않아도 되므로 개방/폐쇄 원칙을 따른 코드라고 할 수 있다. 이렇게 하면 코드 수정이나 유지보수가 더욱 쉬워지고, 유연성과 확장성이 높아진다.

참고1)

AnimalSound 클래스는 Animal 클래스에 의존하지 않고 Animal 클래스의 인스턴스를 받아 make_sound 메서드를 호출하여 다형성을 활용한 구현을 했다. 

클래스에 의존한다는 것은 해당 클래스를 사용하는 코드가 클래스의 내부 구현에 직접적으로 의존하고 있다는 것을 의미하며, 래스가 변경될 때 해당 클래스를 사용하는 코드도 함께 변경될 가능성이 높다는 것을 뜻한다. 그렇기 때문에 클래스 간의 의존성을 최소화하고, 인터페이스에 의존하도록하는 것이 좋다. 

<인터페이스에 의존하도록 하는 방법>
1. 인터페이스를 정의
2. 클래스에서 인터페이스를 구현
3. 클래스는 인터페이스를 사용

참고2)

Dog과 Cat클래스는 Animal클래스를 상속받았고, 각각의 클래스에서 make_sound메서드를 오버라이딩해서 구현했다. 

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def eat_food(self, food):
        pass

class Cat(Animal):
    def eat_food(self, food):
        print(f"The cat is eating {food}.")

class Dog(Animal):
    def eat_food(self, food):
        print(f"The dog is eating {food}.")

위의 예제와는 조금 다르지만, 이렇게 추상 클래스를 구현해서 표현을 할수도 있다.

이 경우 ABC는 Abstract Base Class의 약자로 추상 클래스를 만들 때 사용되며, Animal이 추상클래스라는 것을 명시하기 위해서 ABC를 상속받아야 한다. 

또한 추상 클래스는 하나 이상의 추상 메서드를 가지고 있어야 하므로 @abstractmethod 데코레이터를 붙인 eat_food메서드를 구현한다.이렇게 만들어진 추상 메서드는 하위 클래스에서 반드시 구현해야하며, 그렇지 않으면 에러가 발생한다.

만약 @abstractmethod 데코레이터를 사용하지 않고 추상 메서드를 정의하면 구현이 없는 메서드가 되는데, 파이썬에서는 에러가 발생하지는 않는다. 하지만 데코레이터를 사용하여 추상메서드임을 명시하여 하위 클래스에서 오버라이드함을 알리고, 추후 코드 유지보수나 개발 시 가독성을 높일 수 있다.


좋은 책인 건 알겠는데 예시가 너무 복잡해서 오히려 이해를 방해한다고 생각했다. 마침 ChatGPT가 유행이길래 예시 코드 작성해달라고 하면서 이것저것 물어봤더니 오히려 이해가 잘 되더라(!)
작성한 예시 코드들은 ChatGPT에게 받아낸(?) 것입니다.

728x90