[Python] Python Clean code - SOLID 원칙

Sean park·2021년 5월 5일
1
post-thumbnail

이 글은 책 '파이썬 클린코드'를 읽고 일부를 정리한 내용입니다.
Python 코드를 작성할때 도움이 되기를 바랍니다.

4장 SOLID 원칙

1. S: 단일 책임 원칙

단일 책임 원칙은 하나의 클래스는 하나의 책임을 져야한다는 원칙이다. 하나의 클래스를 수정 할 이유는 하나뿐이며, 다른 이유로 클래스를 수정해야 한다면 추상화가 잘못되어 하나의 클래스에 너무 많은 책임이 있다는 것을 뜻한다.

class StudentScoreAndCourseManager:
    def __init__(self):
        scores = {}
        courses = {}
        
    def get_score(self, student_name, course):
        pass
    
    def get_courses(self, student_name):
        pass

위 코드를 보면 하나의 클래스에서 학생의 성적과 과목을 모두 관리하고 있다. 이는 코드의 응집력을 저하시키며, 유지보수에도 용이하지 않다.

하나의 클래스에 너무 많은 역할을 주지 말고 각각의 클래스로 책임을 분산시켜보자.

class ScoreManager:
    def __init__(self):
        scores = {}
        
    def get_score(self, student_name, course):
        pass
    
    
class CourseManager:
    def __init__(self):
        courses = {}
    
    def get_courses(self, student_name):
        pass

다음과 같이 코드를 수정하면 하나의 클래스가 하나의 책임을 가지며, 각 클래스는 서로 어떠한 영향도 주지 않는다.

2. O: 개방/폐쇄의 원칙

개방/폐쇄 원칙은 모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙이다. 다시 말해, 확장 가능하여 새로운 기능을 추가하기 좋게 개방되어 있어야 하며, 새로 추가한 기능 때문에 기존 코드가 수정되지는 않도록 폐쇄적이어야 한다는 의미이다.

# Bad Case
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        
class Circle:
    def __init__(self, radius):
        self.radius = radius

class AreaCalculator:
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.width * shape.height
        return total

>>> shapes = [Rectangle(2, 3), Rectangle(1, 6)]
>>> calculator = AreaCalculator(shapes)

처음에는 Ractangle 클래스와 넓이의 합을 구해주는 AreaCalculator 클래스 두 가지 클래스만 있었다고 가정해보자. 언뜻 보면 문제가 없어보이지만 Circle이라는 클래스가 추가되면 AreaCalculator 클래스도 같이 수정해야 올바른 결과를 얻을 수 있다. 만약 다른 도형 클래스가 더 추가된다면 AreaCalculator 클래스는 점점 누더기가 될 것이다.

그렇다면 다음 코드를 살펴보자.

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

    def area(self):
        return self.width * self.height
    
class Circle:
    def __init__(self, radius):
        self.radius = radius
        
    def area(self):
        return 3.14 * self.radius ** 2
    
class AreaCalculator:
    def __init__(self, shapes):
        self.shapes = shapes

    def total_area(self):
        total = 0
        for shape in self.shapes:
            total += shape.area()
        return total

>>> shapes = [Rectangle(1, 6), Rectangle(2, 3), Circle(5), Circle(7)]
>>> calculator = AreaCalculator(shapes)

위 코드는 각 도형 클래스에서 넓이를 구하는 메서드를 구현 함으로서 코드의 확장성이 증가하고 AreaCalculator 클래스가 모든 도형의 넓이의 합을 구할 수 있게 되었다.

위와 같이 새로운 코드가 추가되어도 기존의 코드가 수정되지 않도록 유의하며 코드를 작성해야 한다.

3. L: 리스코프 치환 원칙

리스코프 치환 원칙은 어떤 하위 타입을 사용해도 실행에 따른 결과는 같아야 한다는 원칙이다. 조금 더 설명하면 S가 T의 하위 타입이라면 프로그램을 변경하지 않고 T타입의 객체를 S타입의 객체로 치환 가능해야 한다는 의미다.

** 리스코프 치환 원칙에 관한 오류의 대부분은 Mypy 혹은 Pylint와 같은 툴로 문제를 검사할 수 있다.

# Bad Case
class Event:
	def meets_condition(self, event_data: dict) -> bool:
		return False

class LoginEvent(Event):
	def meets_condition(self, event_data: list) -> bool:
		return bool(event_data)

위 코드에 대해 Mypy를 실행하면 오류 메시지가 표시된다. Event 클래스와 LoginEvent 클래스의 인자 타입이 다르기 때문이다. LoginEvent 클래스는 Event 클래스를 상속 받았기 때문에 다른 곳에서 사용된 Event 클래스는 LoginEvent 클래스로 치환될 수 있어야 하며 오류 또한 발생해서는 안된다.

하지만 Mypy과 같은 툴은 함수의 사전조건을 검사해주지만, 인자(dict, list) 내부의 변수의 타입에 대한 오류는 검사해주지 못하니 이점은 사용자가 유의해서 코드를 작성해야 한다.

4. I: 인터페이스 분리 원칙

인터페이스 분리 원칙은 여러개의 메서드를 가진 인터페이스가 있다면 매우 정확하고 구체적인 구분에 따라 더 적은 수의 메서드를 가진 여러개의 메서드로 분할하는 것이 좋다는 원칙이다.

하나의 인터페이스에서 서로 관련이 없는 메서드가 두 개 있다면, 각각의 인터페이스로 분리하는 것이 좋다.

하지만 무조건 작은 단위의 인터페이스로 분리해야 한다는 것은 아니다. 컨텍스트 관리자는 enterexit메서드 두 가지가 함께 있을때 유효하기 때문에 이러한 인터페이스는 같이 묶어두어도 좋다.

5. D: 의존성 역전 원칙

의존성 역전 원칙은 상위 클래스는 하위 클래스 구현에 의존해서는 안되며, 하위 클래스는 상위 클래스의 인터페이스에 의존하도록 해야한다는 원칙이다.

예를 들어보자, A와 B 두 객체가 있을때, A는 B의 인스턴스를 사용한다. 만약 코드가 B에 크게 의존한다면 B 코드가 변경되면 원래의 코드는 쉽게 깨지게 될 것이다. 이럴때 의존성을 역전시켜 B를 A에 적응하도록 코드를 작성해야 한다.

# Bad Case
class MainClass:
    def __init__(self):
        subInstance = SubClass()
        subInstance.method_a()

class SubClass:
    def __init__(self):
        pass

    def method_a(self):
        pass

위의 코드를 살펴보면 MainClass는 SubClass를 의존하고 있다. 하지만 위의 경우 SubClass가 변경 될 경우 MainClass의 코드도 수정되어야 한다. SubClass의 method_a가 method_b로 수정되었다고 가정하면, MainClass에서도 동일하게 코드를 수정해야 한다는 의미다. 이러한 경우 코드의 오류가 발생하기 쉽고, 파급 효과를 증가하게 만든다.

class MainClass:
    def __init__(self, subInstance):
        self.setSubMethod(subInstance)
    
    def setSubMethod(self, subInstance):
        self.subMethod = subInstance
        
    def excuteMethod(self):
        self.subMethod.method()

class SubClassA:
    def __init__(self):
        pass

    def method(self):
        pass

class SubClassB:
    def __init__(self):
        pass

    def method(self):
        pass

반면 위와 같이 메소드명을 일반화 하여 코드를 작성하면 SubClassA와 SubClassB의 method의 내용이 변경되어도 MainClass는 수정하지 않아도 된다.


여기까지 책 '파이썬 클린코드' 4장 SOLID 원칙에 대한 내용에 대해 정리해 보았습니다.
개인적으로 Clean code를 작성하기 위해 중요하고 기본이 되는 원칙이라고 생각합니다. 실제로 적용하기는 쉽지 않지만 Clean하고 편-안 한 코드를 작성하기 위해 노력하겠습니다.
profile
제 코드가 세상에 보탬이 되면 좋겠습니다.

0개의 댓글