[Python] SOLID 원칙

금지수·2022년 4월 27일
0

Python

목록 보기
1/4

1. S: 단일 책임 원칙 (Single Responsibility Principle)

단일 책임 원칙은 하나의 클래스는 하나의 책임을 져야한다는 원칙이다. 두 개 이상의 책임을 가지게 된다면 결합(Couple)이 발생한다. 명확한 의미를 같게끔 더 작게 클래스를 분리해준다. 특정 기능을 캡슐화하여 나머지 클래스에 영향을 끼치지 않는다.

class Human:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass

    def save(self, human: Human):
        pass

위 코드를 보면 하나의 클래스에서

  1. Human properties 관리
  2. Human database 관리

두 가지의 책임을 가지고 있다. 이는 코드의 응집력을 저하시키며, 유지보수에도 용이하지 않다.
하나의 클래스에 너무 많은 역할을 주지 말고 각각의 클래스로 책임을 분산시켜보자.

class Human:
    def __init__(self, name: str):
        self.name = name
    
    def get_name(self) -> str:
        pass
    
    
class HumanDB:
	def get_human(self, id) -> Human:
        pass
    
    def save(self, human: Human):
        pass

다음과 같이 코드를 수정하면 하나의 클래스가 하나의 책임을 가지며, 수정시에도 책임에 맞는 클래스를 찾아서 수정하면 된다.

2. O: 개방/폐쇄의 원칙 (The open/Close Principle)

개방/폐쇄 원칙은 모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙이다. 새로운 기능을 추가하다가 기존코드를 수정하였더만 해당 원칙을 위반하였다는 점이다.

class Human:
    def __init__(self, name):
        self.name = name

    def get_name(self):
        pass
        
humans = [
    Human('철수'),
    Human('영희')
]

def make_hobby(humans: list):
    for human in humans:
        if human.name == '철수':
            print('노래를 잘한다')
        elif human.name == '영희':
            print('춤을 잘춘다')

make_hobby(humans)

Human으로 보면 사람은 이름을 얻게 되고, 취미를 가질 수 있다. 모든 사람의 취미가 같을 순 없고, 철수와 영희 이외에도 다른 사람이 추가 된다고 하면 make_hobby 메소드를 수정해야하고, 내용이 방대해지면 소스 코드의 길이도 무한정 늘어난다.

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

class Human:
    def __init__(self, name):
        self.name = name

    def get_name(self):
        pass
    
    def make_hobby(self):
        pass
        
class 철수(Human):
    def make_hobby(self):
        print('노래를 잘한다')
    
class 영희(Human):
    def make_hobby(self):
        print('춤을 잘춘다')


def make_hobby(humans: list):
    for human in humans:
        human.make_hobby()

make_hobby(humans)

사람이 추가 되어진다고 하더라도, make_hobby의 내용을 수정하지 않아도 된다. 새롭게 추가되는 사람의 클래스만 생성해주면 오류없이 진행된다.

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

3. L: 리스코프 치환 원칙 (Liskov Substitution Principle)

리스코프 치환 원칙은 어떤 하위 타입을 사용해도 실행에 따른 결과는 같아야 한다는 원칙이다. 자식 클래스는 부모 클래스를 대체 할 수 있어야 한다. 이 원칙의 핵심은 자식클래스가 부모 클래스를 에러 없이 대체 할 수 있다는 것을 의미한다.

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

가장 위반을 많이 하는 경우는 바로 자식 클래스가 오버라이딩을 할 때입니다.
아래의 코드를 보고 이해를 해보도록 합시다.

class Product:
    percentage = 0.1

    def __init__(self, name, price):
        self.name = name
        self._price = price

    def discount(self):
        """상품의 가격을 할인하는 메소드"""
        self._price = self.price - self.price * (1 - self.percentage)

	def buy(self):
    	pass
        
    @property
    def price(self):
        return self._price
        
        
class Bag(Product):
	def buy(self):
    	print("가방을 삽니다.")
        
class Book(Product):
	def discount(self, add_percentage):
        """오버라이딩"""
        self._price = self.price - self.price * (1 - self.percentage + add_percentage)
	
    def buy(self):
    	print("책을 삽니다.")

위의 클래스는 상품을 정의해두는 클래스이며, 할인 행사가 있어 상품에 대해 가격을 할인 하는 메소드를 가지고 있다. 상품들을 정의하고 할인된 가격을 구하고자 합니다.

proudcts_list = [
	Bag('가방',10000),
    Book('책',10000)
]

for product in proudcts_list:
	product.discount()

TypeError: discount() missing 1 required positional argument: 'add_percentage'

문제는 자식 클래스인 Book입니다. Book에서는 설정된 할인보다 조금더 주기 위해 discount 메소드에 add_percentage 파라미터를 추가했기 때문인데요. 이는 리스코프 치환 원칙에 위반됩니다. 받는 파라미터의 타입이 다르거나 return 해주는 값의 타입이 다르더라도 원칙에 위반됩니다.

원칙을 위반하지 않으려면 두가지 조건을 만족해야 합니다.

형식적 측면
자식 클래스가 오버라이딩하는 변수와 메소드가 부모 클래스에 있는 형식과 일치해야 한다
1.변수: 타입
2.메소드: 파라미터와 리턴값의 타입 및 개수

내용적 측면
자식 클래스가 부모 클래스의 메소드에 담긴 의도 즉, 행동 규약을 위반하지 않는다
위반해도 실행에 에러가 나진 않는다.
그러나, 의도한 결과가 나오지 않는다.

4. I: 인터페이스 분리 원칙 (The Interface Segregation Principle)

인터페이스 분리 원칙은 여러개의 메서드를 가진 인터페이스가 있다면 매우 정확하고 구체적인 구분에 따라 더 적은 수의 메서드를 가진 여러개의 메서드로 분할하는 것이 좋다는 원칙이다. 하나의 인터페이스에서 많은 메서드를 가지고 있다면 상속받는 곳에서 불필요한 메소드까지 오버라이드하여 구성해야 하기에 좋지 않은 코드가 된다.

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

import abc


class Excel(metaclass=abc.ABCMeta):
    
    @abc.abstractmethod
    def csv_download(self):
        """csv형식으로 엑셀 출력"""
        raise NotImplemented

    @abc.abstractmethod
    def xlsx_download(self):
        """xlsx형식으로 엑셀 출력"""
        raise NotImplemented


class 대용량파일다운(Excel):
    """대용량 파일을 다운 받을시에는 csv로만 출력한다"""

    def csv_download(self):
        print("csv 형식으로 엑셀 출력")

    def xlsx_download(self):
        """
        xlsx 다운로드는 시간이 오래걸리며 사용하지 않는다. 
        불필요하지만 구현하지 않으면 오류가 발생한다.
        """
        pass

class 소규모파일다운(Excel):
    """소규모 파일을 다운 받을시에는 xlsx로만 출력한다"""

    def csv_download(self):
    	"""
         csv 다운로드는 불필요하지만 구현하지 않으면 오류가 발생한다.
        """
        pass

    def xlsx_download(self):
        print("xlsx 형식으로 엑셀 출력")

Excel 클래스가 다운로드를 받는 2개의 이상의 책임을 가지기도 하여, 1번의 SRP를 위반하기도 하는 내용이다. 추상클래스를 사용하기 위해서는 사용하지 않는 메소드들도 표기를 해야하므로
좋지 않는 코드라고 할 수 있다.

인터페이스의 분리 원칙을 지키기 위해서는 아래와 같이 코드를 수정해야한다.

import abc


class Excel(metaclass=abc.ABCMeta):
    
    @abc.abstractmethod
    def download(self):
        """다운로드 기능을 가진다."""
        raise NotImplemented


class 대용량파일다운(Excel):
    """대용량 파일을 다운 받을시에는 csv로만 출력한다"""

    def download(self):
    	"""csv형식으로 출력 로직"""
        print("csv 형식으로 엑셀 출력")

class 소규모파일다운(Excel):
    """소규모 파일을 다운 받을시에는 xlsx로만 출력한다"""

    def download(self):
    	"""xlsx형식으로 출력 로직"""
        print("xlsx 형식으로 엑셀 출력")

인터페이스가 서로 관련성이 높은, 적절한 개수의 추상 메소드를 포함하게 될 때 역할입니다. 거대한 인터페이스 하나보다는 작은 역할 인터페이스가 여러 개 있는 것이 각 클래스의 기능을 쉽게 파악할 수 있고 주석을 달지 않아도 파악이 가능하고 유지보수가 쉬워집니다.

5. D: 의존성 역전 원칙 (Dependency Inversion Principle)

이 원칙의 정의는 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 되고 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다는 것입니다.

class Human:
    def __init__(self, name: str, clothes: Shirt):
        self.name = name
        self.clothes = clothes
	
    def clothes_status(self):
    	self.clothes.put_on()
    	
class Shirt():
    def put_on(self):
        print("셔츠는 위에서 밑으로 입는다.")

class Pants():
	def put_on(self):
    	print("바지는 밑에서 위로 입는다.")

위의 코드를 살펴보면 Human에서 clothes의 메소드는 Shirt를 의존하고 있다. Shirt의 put_on이 변경될 경우에는 Human 클래스의 내용도 수정해줘야하는 내용도 있다. 이와 비슷하게 OCP의 원칙도 위반하는 경우가 된다. 이처럼 SOLID는 긴밀한 관계를 갖습니다. DIP의 원칙을 위반 하지 않으려면 아래의 코드와 같이 수정해야 한다.

from ABC import *

class Clothes(metaclass=ABCMeta):
    @abstractmethod
    def put_on(self):
        pass  

class Human:
    def __init__(self, name: str, clothes):
        self.name = name
        self.clothes = clothes
	
    def clothes_status(self):
    	self.clothes.put_on()
    	
class Shirt(Clothes):
    def put_on(self):
        print("셔츠는 위에서 밑으로 입는다.")

class Pants(Clothes):
	def put_on(self):
    	print("바지는 밑에서 위로 입는다.")

추상 클래스는 상위 모듈과 하위 모듈 사이에 추상화 레이어를 만듬으로써 강한 결합을 깨어 느슨하게 만들어주는 역할을 하였습니다. 의존 관계 원칙을 다시 말하면 상위 모듈이 하위 모듈을 사용할 때 직접 인스턴스를 가져다 쓰지 말라는 뜻입니다. 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 되고 두 모듈 모두 추상화된 내용에 의존해야 한다는 것이었습니다.

이로써 5가지의 S.O.L.I.D 원칙을 정리 해보았습니다. 원칙을 이해하려고 Example 코드를 만들어 봤기 때문에 실제로 수행시 약간의 오류가 있을 수 도 있습니다. 원칙과 다른 내용으로 만들어졌다면 댓글이나 알려주시면 감사하겠습니다.

profile
언젠간 하겠지

0개의 댓글