저번 시간에는 SOLID 5가지 원칙의 정의와 그 첫번째, 단일 책임 원칙에 대해 배웠습니다.

이번 시간에는 두번째 개방 폐쇄 원칙에 대해 함께 알아봅시다.

🚪 개방 폐쇄 원칙

개방 폐쇄 원칙은 Open/closed priciple로, SOLID 원칙 중 O에 해당합니다. 이 원칙은 클래스가 확장에 열려 있어야 하지만 수정에는 닫혀 있어야 한다는 의미를 가지고 있습니다.

SOLID 개발자 Robert에 따르면, 개방 폐쇄 원칙의 '확장에 열려있다'는 것은 프로그램의 기존 기능을 확장할 수 있다는 것이고 '수정에 닫혀있다'는 것은 한 번 작성한 코드를 바꾸지 않아도 된다는 것입니다. 이 말은 즉, 어떤 클래스의 코드를 수정하지 않아도 기존 기능을 확장할 수 있어야 한다는 것입니다.

시작에 앞서, 아래 코드를 천천히 읽어보시길 바랍니다.

class AppleKeyboard:
    """애플 키보드 클래스"""

    def __init__(self):
        """키보드 인풋과 터치바 인풋"""
        self.keyboard_input = ""

    def set_keyboard_input(self, input):
        """키보드 인풋 저장 메소드"""
        self.keyboard_input = input

    def send_keyboard_input(self):
        """키보드 인풋 전송 메소드"""
        return self.keyboard_input

class KeyboardManager:
    def __init__(self):
        """키보드 관리 클래스"""
        self.keyboard = None

    def connect_to_keyboard(self, keyboard):
        """키보드 교체 메소드"""
        self.keyboard = keyboard

    def get_keyboard_input(self):
        """유저가 키보드로 입력한 내용을 받아오는 메소드"""
        return self.keyboard.send_keyboard_input()

AppleKeyboard 클래스는 애플의 키보드를 나타냅니다. 이 클래스는 인스턴스 변수로 키보드 인풋과 터치바 인풋을 가지고 있고 사용자 입력을 받습니다. 입력 받은 내용은 set_keyboard_input 메소드를 통해 저장되고 저장된 내용의 전송은 send_keyboard_input 메소드가 처리합니다.

KeyboardManager 클래스는 키보드를 관리합니다. 인스턴스 변수로는 keyboard를 갖습니다. 이 말은 AppleKeyboard 같은 클래스의 인스턴스를 가지고 작업을 한다는 걸 의미합니다. connect_to_keyboard 메소드는 키보드를 가져오는 역할을 맡고 get_keyboard_input은 사용자가 입력한 내용을 받아오는 역할을 하는 메소드입니다.

두 클래스를 사용해보겠습니다.

keyboard_manager = KeyboardManager()
apple_keyboard = AppleKeyboard()

keyboard_mananger.connect_to_keyboard(apple_keyboard)

apple_keyboard.set_keyboard_input("안녕!")
print(keyboard_manager.get_keyboard_input())

각 클래스의 인스턴스를 생성하고 키보드 매니저 클래스와 애플 키보드를 연결해주었습니다. 다음으로 사용자가 '안녕'이라는 메세지를 입력했고 키보드 매니저 클래스로 입력된 값을 가져왔습니다.

실행해보면 성공적으로 메시지를 출력합니다.

안녕!

이번에는 삼성 키보드 클래스를 만들어보겠습니다.

class SamsungKeyboard:
    """삼성 키보드 클래스"""
    
    def __init__(self):
        """키보드 인풋"""
        self.user_input = ""
        
    def save_user_input(self, input):
        """키보드 인풋 저장 메소드"""
        self.user_input = input
        
    def give_user_input(self):
        """키보드 인풋 전송 메소드"""
        return self.user_input

이 클래스의 인스턴스 변수는 사용자의 입력을 받는 'user_input'입니다. save_user_input 메소드를 통해 사용자의 입력 내용을 저장하고 give_user_input 메소드를 통해 입력 내용을 전송합니다.

삼성 키보드 내용에 맞춰 사용 코드를 변경해봅시다.

keyboard_manager = KeyboardManager()
samsung_keyboard = SamsungKeyboard()

keyboard_mananger.connect_to_keyboard(samsung_keyboard)

samsung_keyboard.save_user_input("안녕!")
print(keyboard_manager.get_keyboard_input())

실행해보면, 에러가 납니다!

AttributeError: 'SamsungKeyboard' object has no attribute 'send_keyboard_input'

위 문구를 해석하면 삼성 키보드가 send_keyboard_input이라는 메소드가 없다고 합니다. 이 메소드는 키보드 매니저 클래스에서 찾아볼 수 있는데요. 다시 그 출처를 찾으면 이 부분은 애플 키보드 클래스로부터 온 메소드입니다. 따라서, 삼성 키보드에는 이 메소드가 없는 것이죠.

키보드 매니저 클래스로 삼성 키보드를 가져오고 싶으면 get_keyboard_input 메소드의 self.keyboard.send_keyboard_input() 부분을 변경해야 합니다. isinstance 함수를 사용해서 말이죠.

if isinstance(self.keyboard, AppleKeyboard):
    return self.keyboard.send_keyboard_input()
elif isinstance(self.keyboard, SamsungKeyboard):
    return keyboard.give_user_input()

실행하면 성공적으로 잘 출력됩니다.

그런데 이렇게 새로운 키보드가 등장할 때마다 isinstance 함수를 사용하다보면 코드가 복잡해지겠죠? 이는 기존 기능을 확장하더라도 코드를 수정하지는 말아야 한다는 개방 폐쇄 원칙에도 위반됩니다. 따라서, 기존의 KeyboardManager 클래스의 코드는 그대로 두면서 새로운 회사의 키보드를 추가할 수 있어야 합니다.

🚪 개방 폐쇄 원칙 적용

그럼 위 코드를 개방 폐쇄 원칙에 맞게 수정해봅시다. 어떻게 하면 될까요? 여러 종류의 키보드 클래스를 보면 떠오르는 개념이 있지 않나요? 네, 맞습니다! 다형성을 활용하면 개방 폐쇄 원칙에 맞게 수정이 가능합니다.

먼저 모든 키보드의 추상화된 공통점을 가진 추상 클래스 Keyboard를 만들어보겠습니다.

from abc import ABC, abstractmethod

class Keyboard(ABC):
    """키보드 클래스"""
    @abstractmethod
    def save_input(self, content: str) -> None:
        """키보드 인풋 저장 메소드"""
        pass
        
    @abstractmethod
    def send_input(self) -> str:
        """키보드 인풋 전송 메소드"""
        pass

추상 클래스 Keyboard에는 키보드의 입력 값을 저장하는 추상 메소드 save_input과 키보드 입력 값을 전송하는 추상 메소드 send_input이 있습니다. 이제 모든 회사의 키보드 클래스는 Keyboard 클래스를 상속 받습니다. 그리고 추상 메소드 save_input과 send_input을 각자의 특성에 맞게 오버라이딩 하게 되죠.

위 내용에 맞게 코드를 수정해 보겠습니다.

class AppleKeyboard(Keyboard):
    """애플 키보드 클래스"""

    def __init__(self):
        """키보드 인풋과 터치바 인풋"""
        self.keyboard_input = ""

    def save_input(self, input):
        """키보드 인풋 저장 메소드"""
        self.keyboard_input = input

    def send_input(self):
        """키보드 인풋 전송 메소드"""
        return self.keyboard_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:
    def __init__(self):
        """키보드 관리 클래스"""
        self.keyboard = None

    def connect_to_keyboard(self, keyboard):
        """키보드 교체 메소드"""
        self.keyboard = keyboard

    def get_keyboard_input(self):
        """유저가 키보드로 입력한 내용을 받아오는 메소드"""
        return self.keyboard.send_input()

애플 클래스와 삼성 클래스의 메소드를 추상 메소드로 통일시켰고 KeyboardManager 클래스는 마지막 메소드의 코드만 수정했습니다.

사용을 위한 코드도 수정한 뒤, 실행해보겠습니다.

keyboard_manager = KeyboardManager()

apple_keyboard = AppleKeyboard()
samsung_keyboard = SamsungKeyboard()

keyboard_mananger.connect_to_keyboard(apple_keyboard)
apple_keyboard.save_input("안녕!")
print(keyboard_manager.get_keyboard_input())

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

잘 실행되네요.

수정된 KeyboardManager 클래스는 개방 폐쇄 원칙을 잘 지키고 있다고 할 수 잇는데요. 먼저, 이 클래스는 확장에 열려 있습니다. 추상 클래스 Keyboard를 상속받기만 하면 언제든지 새로운 키보드를 연결해서 사용할 수 있으니까요. 다음으로 이 클래스는 수정에 닫혀 있습니다. 마찬가지로 Keyboard를 상속받기만 하면 기존 코드를 수정할 필요가 전혀 없으니까요.

그리고 이때, KeyboardManager 클래스는 여러 회사의 키보드와 연결이 가능한데요. 따라서, 이 클래스의 변수 keyboard는 다형성을 가진다고 볼 수 있습니다.

이런 식으로 추상 클래스 혹은 다형성을 활용하여 코드를 작성하는 것이 isinstance 함수를 사용하여 코드를 작성하는 것보다 더 바람직합니다. 그 이유는 다형성 코드와 연계되는 개방 폐쇄 원칙이 개발 편의성과 코드의 유지보수성을 보장하기 때문입니다.

KeyboardManager를 만드는 개발자 A와 여러 키보드를 제작하는 회사들의 개발자들은 추상 클래스 Keyboard 단 하나를 기준으로 각자의 개발을 진행하면 됩니다. 전자는 Keyboard 추상 클래스를 상속받는 클래스의 인스턴스를 사용하도록 개발하면 되고 후자는 Keyboard 추상 클래스를 상속받도록 개발하면 됩니다.

즉, Keyboard라는 공통된 기준이 있기 때문에 두 측면의 개발자들은 동시에 개발이 가능하고 나중에 새로운 종류의 키보드가 생기더라도 제조사들만 키보드를 새로 생산하면 되고 키보드 매니저 프로그램을 만드는 개발자 A는 더 이상 기존의 프로그램을 수정할 필요가 없습니다.

만약 isinstance를 활용한다면 개발자 A는 제조 회사들의 키보드가 모두 출시될 때까지 기다려야 합니다. 그에 맞는 isinstance 함수를 계속해서 추가해줘야 하니까요. 뿐만 아니라 나중에 새로운 종류의 키보드가 나타나면 제조 회사는 그 키보드에 대한 새로운 코드를 만들어야 하고 A도 또 다시 isinstance 함수를 추가해줘야 합니다.

두 케이스를 비교하니 개방 폐쇄 원칙이 필요한 이유가 확 와닿죠?

🚪 개방 폐쇄 원칙 정리

지금까지 배운 내용을 정리해보겠습니다.

개방 폐쇄 원칙의 뜻은 '클래스는 확장에 열려있어야 하며, 수정에는 닫혀있어야 한다'입니다. 그러니까 기존 클래스의 코드를 수정하지 않고도 기능을 확장할 수 있어야 한다는 뜻입니다.

이를 구현할 수 있는 방법은 추상 클래스의 활용입니다. 먼저, 개방 폐쇄 원칙을 적용했던 KeyboardManager 클래스는 내부에서 사용할 수 있는 인스턴스를 Keyboard라는 추상 클래스(자식 클래스)의 인스턴스로 제한했습니다. 다음으로 다양한 종류의 키보드 클래스는 Keyboard 추상 클래스를 상속 받도록 했습니다.

이렇게 추상 클래스로 일종의 제약을 두면 KeyboardManager 클래스는 코드를 수정하지 않고도(수정에는 닫힘) 기능의 확장(확장에는 열림)이 가능하게 됩니다.

따라서, KeyboardManger 클래스에는 Keyboard 추상 클래스의 자식 클래스의 인스턴스만 들어갈 수 있다는 제한이 생겨 그 인스턴스가 Keyboard 추상 클래스의 메소드를 오버라이딩 해서 가지고 있을 것이라고 믿고 사용할 수 있게 됩니다.

만약 이런 제한이 없다면 KeyboardManager 클래스에 어떤 클래스의 인스턴스가 들어올지 알 수가 없습니다. 이는 새로운 종류의 키보드가 생길 때마다 그에 맞게 클래스를 수정해줘야 한다는 뜻인데요. 매번 isinstance 함수로 체크하며 그 클래스가 가지는 고유의 메소드를 호출해야 하기 때문입니다. 그만큼 유지보수도 힘들어지겠죠?

이것이 개방 폐쇄 원칙을 지켜야 하는 이유입니다.


이번 시간에는 SOLID의 두 번째, 개방 폐쇄 원칙에 대해 배웠습니다. 기존 코드를 수정하지 않고 기능만 확장시킨다는 아이디어가 참 참신한 것 같습니다.

다음 시간에는 계속해서 SOLID의 세 번째, 리스코프 치환 원칙에 대해 함께 알아봅시다.

* 이 자료는 CODEIT의 '객체 지향 프로그래밍' 강의를 기반으로 작성되었습니다.
profile
There's Only One Thing To Do: Learn All We Can

0개의 댓글