견고한 객체지향 프로그래밍

파이썬의 견고한 객체지향 프로그래밍 강의를 듣고 정리한 내용입니다.
문제가 될 시 삭제하겠습니다.

견고한 객체지향 프로그래밍

1. 단일 책임 원칙

01. 견고한 객체 지향 프로그래밍을 위한 SOLID 원칙

  1. 단일 책임 원칙 (Single Responsibility Principle)
  2. 개방 폐쇄 원칙 (Open-Closed Principle)
  3. 리스코프 치환원칙 (Liskov Substitution Principle)
  4. 인터페이스 분리 원칙 (Interface Segregation Principle)
  5. 의존 관계 역전 원칙 (Dependency Inversion Principle)
  • 객체 지향 프로그래밍으로 만드는 프로그램의 크기가 커질수록 SOLID 원칙을 잘 지키는 것이 중요하다.
  • 아무리 코드가 많은 프로그래밍이라도 SOLID 원칙을 잘 지키면서 작성한다면 코드의 복잡성을 최소화하고, 코드를 유지보수 하기 쉬운 상태로 유지할 수 있다.
  • 작고 간단한 프로그램을 만드는데 원칙을 모두 지키는 것은 시간 낭비일 수도 있다.
  • 각 원칙을 적용했을 때 얻는 이점을 깨닫고 코드에서 개선해야할 점, 개선하지 않으면 생길 문제들을 파악해보자

02. 단일 책임 원칙(Single Responsibility Principle)과 God Object

  • 모든 클래스는 한 가지의 책임만을 갖고, 클래스 안에 정의 되어 있는 모든 기능은, 이 하나의 책임을 수행하는데 집중되어 있어야 한다.
  • 하나의 클래스로 너무 많은 일을 하지 말고, 딱 한 가지 책임만 수행하라는 뜻
  • 단일 책임 원칙은 같이 수정해야될 것 들은 묶고, 따로 수정해야될 것들은 분리하는 것
  • 책임 하나하나가 작은 크기의 클래스에 나눠있었다면

03. 단일책임 원칙 적용

  • 각각의 클래스가 각각의 역할을 하도록 적용
class Ship:
    def __init__(self, fuel, fuel_per_hour, supplies, num_crew):
        self.fuel_tank = FuelTank(fuel)
        self.crew_manager = CrewManager(num_crew)
        self.supply_hold = SupplyHold(supplies, self.crew_manager)
        self.engine = Engine(self.fuel_tank, fuel_per_hour) 

05. 단일 책임 원칙 적용3

  • 코드를 작성할 떄, 한 클래스가 너무 많은 책임을 갖고 있는 건 아닌지 의심 , 클래스 안에 분리할 수 있는 변수와 메소드가 많다면 의심해서 단일 책임 원칙을 신경쓰기
  • 프로그램의 크기가 커질 수록 코드가 수정하기가 어려워 짐

2. 개방 폐쇄 원칙

  • 클래스는 확장에 열려 있어야 하며, 수정에는 닫혀있어야 한다.
  • 확장에 열려 있다는 건 프로그램의 기존 기능을 확장할 수 있다는 것이고, 수정에 닫혀 있다는 건 한 번 작성한 코드를 바꾸지 않아도 되는 것
  • 어떤 클래스의 코드를 수정하지 않아도 기존 기능을 확장할 수 있어야 된다
  • 추상 클래스라는 약속이 있기 때문에 여러 개발자가 동시에 개발을 해도 됨
    • 개방 폐쇄원칙을 지키면 더 쉽게 협력하고 더 편하게 수정할 수 있음
from abc import ABC, abstractmethod

class Keyboard(ABC): 
    @abstractmethod
    def save_input(self, content: str) -> None:
        pass

    @abstractmethod
    def send_input(self) -> str:
        pass

class AppleKeyboard(Keyboard):
    def __init__(self):
        self.keyboard_input = ""

    def save_input(self, input):
        self.user_input = input

    def send_input(self):
        return self.keyboard_input

    def give_user_input(self):
        return self.user_input

class SamsungKeyboard(Keyboard):
    def __init__(self):
        self.user_input = ""

    def save_input(self, input):
        self.user_input = input

    def send_input(self):
        return self.user_input

class KeyboardManager: # 확장에 열려있음. 추상 클래스 Keyboard를 상속받는 Keyboard 클래스 이면 사용할 수 있기 때문
    def __init__(self):  # 수정에는 닫혀있음. 추상 클래스 Keyboard를 상속받는 Keyboard 클래스 이면 KeyboardManager를 수정할 필요가 없기 때문에
        self.keyboard = None

    def connect_to_keyboard(self, keyboard):
        self.keyboard = keyboard

    def get_keyboard_input(self):
        return self.keyboard.send_input()

keyboard_manager = KeyboardManager()

apple_keyboard = AppleKeyboard()
samsung_keyboard = SamsubgKeyboard()

keyboard_manager.connect_to_keyboard(apple_keyboard)
apple_keyboard.save_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())

keyboard_manager.connect_to_keyboard(samsung_keyboard)
samsung_keyboard.save_input("안녕하세요")
print(keyboard_manager.get_keyboard_input())

3. 리스코프 치환 원칙 이해하기

01. 리스코프 치환 원칙 이해하기

  • 부모 클래스이 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 때 코드가 원래 의도대로 작동해야 한다.
  • 자식 클래스의 인스턴스는 부모 클래스의 행동규약을 어기면 안된다.
  • 자식 클래스가 오버라이딩을 잘못하는 경우
    1. 자식 클래스가 부모 클래스의 변수의 타입을 바꾸거나 메소드의 파라미터 또는 리턴값의 타입 or 갯수를 바꾸는 경우
    2. 자식 클래스가 부모 클래스의 의도와 다르게 메소드를 오버라이딩 하는 경우
class Employee:
    """직원 클래스"""
    company_name = "코드잇 버거"
    raise_percentage = 1.03

    def __init__(self, name, wage):
        self.name = name
        self._wage = wage

    def raise_pay(self):
        """직원 시급을 인상하는 메소드"""
        self._wage *= self.raise_percentage

    @property
    def wage(self):
        return self._wage

    def __str__(self):
        """직원 정보를 문자열로 리턴하는 메소드"""
        return Employee.company_name + " 직원: " + self.name

class Cashier(Employee):
    """리스코프 치환 원칙을 지키지 않는 계산대 직원 클래스"""
    burger_price = 4000

    def __init__(self, name, wage, number_sold=0):
        super().__init__(name, wage)
        self.number_sold = number_sold

    def raise_pay(self, raise_amount): # 부모클래스에서는 self, 외에 파라미터를 받지 않았는데, 파라미터를 추가로 받았으므로 리스코프 치환원칙을 어긴 것임
        """직원 시급을 인상하는 메소드"""
        self.wage += self.raise_amount

    @property
    def wage(self): # 부모클래스에서는 숫자를 return 하는데, 자식클래스가 str을 리턴하므로 리스코프 치환원칙을 어긴 것임
        return "시급 정보를 알려줄 수 없습니다"

03. 행동 규약을 어기는 자식 클래스

  • 부모 클래스의 인스턴스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용했을 떄 코드가 원래 의도대로 작동해야 한다.
    • 부모클래스의 형식적인 측면(파라미터, 리턴값의 타입 및 개수)과 내용적인 측면(메소드에 담긴 의도) 의 규약을 위반하지 않아야 함
  • 리스코프 치환 원칙은 개발자들끼리 협력할 때 중요!

4. 인터페이스 분리 원칙

01. 인터페이스 분리 원칙

  • 인터페이스란 : 추상 클래스 중에서 추상 메소드만 있고 일반 메소드는 없는 것
  • 인터페이스 분리 원칙 : 자식 클래스가 사용하지 않을 메소드를 부모 메소드에 의존할 것을 강요하면 안 된다.
  • 지나치게 많은 추상 메소드를 가진 거대한 인터페이스 하나를, 관련된 추상 메소드들만 모여있도록 작은 크기의 인터페이스로 분리하라는 원칙 이렇게 해야 하는 이유는 지나치게 큰 인터페이스는 그걸 상속하는 클래스가 자신에게 필요하지도 않은 메소드를 굳이 오버라이딩하도록 만들기 때문
from abc import ABC, abstractmethod

class IMessage(ABC):
    @property
    @abstractmethod
    def content(self):
        """추상 getter 메소드"""
        pass

    @abstractmethod
    def edit_content(self, new_content: str) -> None:
        """작성한 메시지를 수정하는 메소드"""
        pass

    @abstractmethod
    def send(self, destination: str) -> bool:
        """작성한 메시지를 전송하는 메소드"""
        pass

class Email(IMessage):
    def __init__(self, content, owner_email):
        """이메일은 그 내용과 보낸 사람의 이메일 주소를 인스턴스 변수로 가짐"""
        self._content = content
        self.owner_email = owner_email

    @property
    def content(self):
        """_content 변수 getter 메소드"""
        return self._content

    def edit_content(self, new_content):
        """이메일 내용 수정 메소드"""
        self._content = self.owner_email + "님의 메일\n" + new_content

    def send(self, destination):
        """이메일 전송 메소드"""
        print("{}에서 {}로 이메일 전송!\n내용: {}").format(self.owner_email, destination, self._content)
        return True

class TextMessage(IMessage):
    def __init__(self, content):
        """문자 메시지는 그 내용을 인스턴스 변수로 가짐"""
        self._content = content

    @property
    def content(self):
        """_content 변수 getter 메소드"""
        return self._content

    def edit_content(self, new_content):
        """문자 메시지 내용 수정 메소드"""
        self._content = new_content

    def send(self, destination):
        """문자 메시지 전송 메소드"""
        print("{}로 문자 메시지 전송!\n내용: {}").format(destination, self._content)

class TextReader:
    """인스턴스의 텍스트 내용을 읽어주는 클래스"""
    def __init__(self):
        self.texts = []

    def add_text(self, text: IMessage):
        """인스턴스 추가 메소드, 파라미터는 IMessage 인터페이스를 상속받을 것"""
        self.texts.append(text)

    def read_all_texts(self):
        """인스턴스 안에 있는 모든 텍스트 내용 출력"""
        for text in self.texts:
            print(text.content)

class Memo(IMessage):
    def __init__(self, content):
        self._content = content

    @property
    def content(self):
        return self._content

    def edit_content(self, new_content):
        self._content = new_content

    def send(self, destination): # 필요 없는데 억지로 오버라이딩 함. 클래스가 사용하지 않을 메소드를 갖도록 강요하면 안된다. ( 인터페이스 분리원칙 위반 )
        print("메모는 아무 데도 보낼 수 없습니다!")
        return False

email = Email("안녕 잘지내니? 오랜만이다!", "young@codeit.kr")
text_message = TextMessage("내일 시간 가능? 한 1시쯤 만나자")
memo = Memo("내일 2시까지 숙제 끝낼 것!")

text_reader = TextReader()

text_reader.add_text(email)
text_reader.add_text(text_message)
text_reader.add_text(memo)

text_reader.read_all_texts()

02. 인터페이스 분리 원칙 적용

  • 인터페이스 분리 원칙을 위반하지 않는 방법
    • 인터페이스를 더 작게 쪼개고 분리할 수 있을지를 고민해야 함
  • 아래 예시에서 send메소드를 Memo 클래스가 사용하지 않기 때문에 Itext, Isendable 인터페이스로 분리해서 사용함

from abc import ABC, abstractmethod

class Itext(ABC):
    @property
    @abstractmethod
    def content(self):
        """추상 getter 메소드"""
        pass

    @abstractmethod
    def edit_content(self, new_content: str) -> None:
        """작성한 메시지를 수정하는 메소드"""
        pass

class ISendable(ABC):
    @abstractmethod
    def send(self, destination: str) -> bool:
        """작성한 메시지를 전송하는 메소드"""
        pass

class Email(Itext, ISendable):
    def __init__(self, content, owner_email):
        """이메일은 그 내용과 보낸 사람의 이메일 주소를 인스턴스 변수로 가짐"""
        self._content = content
        self.owner_email = owner_email

    @property
    def content(self):
        """_content 변수 getter 메소드"""
        return self._content

    def edit_content(self, new_content):
        """이메일 내용 수정 메소드"""
        self._content = self.owner_email + "님의 메일\n" + new_content

    def send(self, destination):
        """이메일 전송 메소드"""
        print("{}에서 {}로 이메일 전송!\n내용: {}").format(self.owner_email, destination, self._content)
        return True

class Memo(Itext):
    def __init__(self, content):
        self._content = content

    @property
    def content(self):
        return self._content

    def edit_content(self, new_content):
        self._content = new_content

    ~~def send(self, destination): 
        print("메모는 아무 데도 보낼 수 없습니다!")
        return False~~

class TextReader:
    """인스턴스의 텍스트 내용을 읽어주는 클래스"""
    def __init__(self):
        self.texts = []

    def add_text(self, text: IText):
        """인스턴스 추가 메소드, 파라미터는 IMessage 인터페이스를 상속받을 것"""
        self.texts.append(text)

    def read_all_texts(self):
        """인스턴스 안에 있는 모든 텍스트 내용 출력"""
        for text in self.texts:
            print(text.content)

5. 의존 관계 역전 원칙

01. 의존 관계 역전 원칙

  • 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 된다.
  • 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다.
  • 상위 모듈인 GameCharacter 클래스가 하위 모듈인 Sword 클래스의 구현 내용에 의존하고 있다.
    • Sword 클래스의 객체를 GameCharacter 클래스 함수에서 사용하고 있기 때문에, Sword 클래스의 함수가 변경되면 GameCharacter 클래스 함수명도 변경되어야 한다.

02. 의존 관계 역전 원칙 적용

  • 상위 모듈은 하위 모듈의 구현 내용에 의존하면 안 된다. 상위 모듈과 하위 모듈 모두 추상화된 내용에 의존해야 한다.
  • 의존 관계 역전 원칙은 상위 모듈이 하위 모듈을 사용할 때 직접 인스턴스를 가져다가 쓰지 말라는 뜻이다.
  • 인스턴스를 바로 가져다가 쓴다는 말은 하위 모듈의 구체적인 내용에 상위 모듈이 의존하게 되어 하위 모듈에 변화가 있을 때마다 상위 모듈의 코드를 자주 바꿔줘야 하기 때문입니다. 이에 대한 해결책은 추상 클래스로 상위 모듈과 하위 모듈 사이에 추상화 레이어를 만드는 것이다. 이렇게 되면
    1. 상위 모듈에는 추상 클래스의 자식 클래스의 인스턴스를 사용한다는 가정 하에 그 하위 모듈을 사용하는 코드를 작성해두면 되고,
    2. 하위 모듈은 추상 클래스의 추상 메소드들을 구현(오버라이딩)만 하면 된다.
    3. 그럼 상위 모듈은 새로운 하위 모듈이 생겨도 기존 코드를 수정하지 않고 새 하위 모듈을 자유롭게 가져다 쓸 수 있고 유지보수하기 편해진다.

댓글

Designed by JB FACTORY