Computer Science

SOLID란? 객체지향 설계 5원칙(Python)

emhaki 2023. 1. 11. 12:07
728x90
반응형
SMALL

✅ SOLID란?

SOLID란 로버트 마틴이 2000년대 초에 명명한 객체 지향 프로그래밍의 다섯 가지 기본 원칙을 마이클 페더스가 원칙의 앞 글자를 따서 다시 SOLID라는 이름으로 소개했다.

SOLID의 5대 원칙은 다음과 같다.

 

  1. 단일 책임 원칙(Single responsibility principle) : SRP
  2. 개방 폐쇄 원칙(Open/closed principle) : OCP
  3. 리스코프 치환 원칙(Liskov substitution principle) : LSP
  4. 인터페이스 분리 원칙(Interface segregation principle) : ISP
  5. 의존관계 역전 원칙(Dependency inversion principle) : DIP

✅ 1.SRP (Single Responsibility) 단일 책임 원칙

모든 클래스는 각각 하나의 기능만을 가진다는 의미. 해당 클래스가 제공하는 모든 서비스는 단 하나의 책임을 수행하는데 집중되어야 한다는 원칙.

 

SRP 원칙을 적용하면 다른 클래스들이 서로 영향을 미치는 연쇄작용을 줄일 수 있고, 책임을 적절하게 분배함으로써 코드의 가독성 향상, 유지보수 용이라는 이점까지 누릴 수 있다.

 

예) A 메소드는 A 메소드의 결과를 기반으로 B메소드를 호출, B메소드의 결과를 기반으로 C메소드를 호출 => A 메소드가 수정된다면 B, C 모두 바꿔야할 수 있으므로 비효율적

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

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

# good Case
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

🔎 쉽게 이해하기

class Car에는 다양한 기능(heater, seat등)들이 있다. 하지만 class Car안에서 내부 기능들을 계속해서 추가할 경우 클래스 자체가 무거워질 뿐 아니라 복잡해진다. 따라서 각각의 기능들을 쪼개 다른 클래스 객체에 의존하게 하여 Car의 기능들을 추가해준다. 

✅ 2. OCP (Open Close Principle) 개방-폐쇄 원칙

SOLID의 O에 해당하는 원칙으로 소프트웨어의 모든 구성요소(클래스, 모듈, 함수)는 확장에는 열려있고, 변경에는 닫혀있어야 한다는 원칙

 

요구사항의 변경이나 추가사항이 발생하더라도, 기존 구성요소는 수정이 일어나지 말아야하며 쉽게 확장이 가능하여 재사용할 수 있어야 한다는 뜻이다. 이렇게 하지 않으면 객체지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 모두 잃어버리는 셈이고, OOP를 사용하는 의미가 사라지게 된다. OPC는 객체지향의 장점을 극대화하는 아주 중요한 원리.

# 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)

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

# good Case
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)

🔎 쉽게 이해하기

OCP는 기존의 코드를 변경하지 않고도 기능을 추가할 수 있어야 한다. 왼쪽과 같이 Animal 클래스에 각 동물들과 울음소리들을 추가하게 되면, 동물을 추가할 때마다 Animal 코드를 계속해서 수정해야 한다.

 

오른쪽처럼 Animal이라는 클래스에 상속받는 것으로 강아지와 고양이를 구현해준다. 만약에 기러기를 추가해야 된다면 기러기 클래스를 상속받아서 사용하면 된다. 이렇게 하면 기존의 코드를 수정하지 않고도 기능을 추가할 수 있게 된다. 

ex) a = 강아지() / a.울기()

✅ 3. LSP (Liskov Substitution) 리스코프 치환 원칙

SOLID의 L에 해당하는 원칙으로 부모 크래스를 가리키는 포인터에 해당 클래스를 상속하는 자식 클래스를 할당하더라도 모든 기능이 정상적으로 작동해야 하며 자식 클래스의 상세 내부를 부모 클래스는 알 필요가 없다는 뜻. 한마디로 부모 클래스를 상속한 자식 클래스는 부모 클래스의 역할을 정확히 해내야한다는 뜻

 

LSP 원칙을 지키지 않으면 OCP(개방 폐쇄 원칙)을 위반하게 되는 것. 따라서 상속 관계를 잘 정의하여 LSP 원칙이 위배되지 않도록 설계해야 한다. 

# 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)

🔎 쉽게 이해하기

LSP 부모클래스의 로보트를 Ai로보트와 휴먼로보트가 상속을 받았을 때에도 모든 기능이 정상적으로 작동해야 된다는 원칙이다. 예를 들어서 부모 클래스의 로보트 무게 규격을 30kg로 되어있고, 무게 규격을 넘어가게 되면 정상작동이 되지 않는다고 치자. 그런데 Ai 로보트에 기능을 추가하다보니 무게가 30kg를 초과 되게 되어 정상 작동이 되지 않는다면 부모 클래스를 상속 받지 않는 것이 좋다.

✅ 4. ISP (Interface Segregation) 인터페이스 분리 원칙

자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다는 원칙. 하나의 큰 인터페이스를 상속 받기 보다는 인터페이스를 구체적이고 작은 단위들로 분리시켜 꼭 필요한 인터페이스만 상속하자는 의미. SRP가 클래스의 단일책임을 강조했다면 ISP는 인터페이스의 단일책임을 강조.

 

각 클라이언트가 필요로 하는 인터페이스들을 분리함으로써, 클라이언트가 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 만들어야 하는 것이 핵심.

🔎 쉽게 이해하기

ISP는 어떤 클래스가 하나의 인터페이스를 구현을 하고 있을 때 너무 큰 범위의 인터페이스를 사용하지 않고, 작고 구체적인 인터페이스를 여러개로 나누어 사용하라는 뜻이다. 

 

위와 같이 강아지 클래스에서 동물이라는 인터페이스를 상속받고, 동물에는 짖기와 걷기라는 인터페이스가 있다고 치자. 그러면 사람이라는 클래스를 새롭게 만들어 걷기라는 인터페이스만 사용하는데에 어려움을 겪게 된다. 따라서 각 인터페이스를 분리함으로써 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않도록 만들어야 한다.

✅ 5. DIP (Dependency Inversion) 의존 역전 원칙 

상위 모듈은 하위 모듈에 의존해서는 안되고, 둘 다 추상화에 의존해야 하며, 추상화는 구체적인 것에 의존하면 안된다. 즉 의존 관계를 맺을 때, 변하기 쉬운 것(구체적인 것) 보다는 변하기 어려운 것(추상적인 것)에 의존해야 한다.

# 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의 코드도 수정되어야 한다. 이러한 경우 코드의 오류가 발생하기 쉽고, 파급 효과를 증가하게 만든다. 

 

반면에 아래와 같이 메소드명을 일반화 하여 코드를 작성하면 SubClassA와 SubClassB의 method의 내용이 변경되어도 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

🔎 쉽게 이해하기

DIP는 의존 관계를 맺을 때 구체적인 것보다는 추상적인 것에 의존해야 한다는 원칙이다. 토끼가 만약에 Carrot이라는 먹이를 먹다가 사과로 변경을 해야한다고 생각해보자. 근데 마침 당근과 사과가 Vagetable이라는 상위 클래스를 상속받고 있다면 Carrot이나 Apple같은 하위개념을 사용하지말고 Vagetable이라는 상위개념으로 의존을 해야한다는 의미이다.

 

참고

https://jaeyeong951.medium.com/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-5%EC%9B%90%EC%B9%99-solid-ac7d4d660f4d

https://velog.io/@haero_kim/SOLID-%EC%9B%90%EC%B9%99-%EC%96%B4%EB%A0%B5%EC%A7%80-%EC%95%8A%EB%8B%A4

https://velog.io/@hytenic/Python-Python-Clean-code-SOLID-%EC%9B%90%EC%B9%99

728x90
반응형