지난 시간에는 SOLID 세번째, 리스코프 치환 원칙에 대해 배웠습니다.

이번 시간에는 SOLID 네번째이자 I에 해당하는 인터페이스 분리 원칙에 대해 함께 알아봅시다.

👫 인터페이스 분리 원칙

인터페이스는 추상 클래스 중에서 추상 메소드만 있고 일반 메소드는 없는 것을 말합니다. 이는 Python에서는 없는 개념입니다.

인터페이스 분리 원칙(Interface segregation principle)의 정의는 클래스가 사용하지 않을 메소드에 의존할 것을 강요하면 안된다입니다. 여기서 의존이라는 말은 메소드를 가진다는 것으로 보면 됩니다. 다시 말해, 클래스가 나중에 사용하지도 않을 메소드를 가지도록 강제하지 말라는 뜻입니다.

추상 클래스를 상속 받으면 자식 클래스는 추상 메소드들을 반드시 오버라이딩 해야 한다고 배웠었죠? 이때, 추상 메소드를 반드시 오버라이딩 해야 하는 것이 바로 메소드를 가지도록 강제하는 것입니다.

이제부터는 추상 클래스를 인터페이스라고 부르겠습니다. 예시를 한 번 볼까요?

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

IMessage라는 인터페이스가 있습니다. 이 인터페이스 안에는 여러 추상 메소드들이 있는데요. 먼저, content는 메세지 내용을 나타내는 추상 getter 메소드이구요. edit_content는 메세지 내용을 수정하는 추상 메소드, send는 작성한 메세지를 전송하는 추상 메소드입니다.

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(f"{self.owner_email}에서 {destination}로 이메일 전송!\n내용: {self.content}")
        return True

다음으로 IMessage를 상속받는 Email 클래스가 있습니다. 이 클래스는 이메일을 보낸 내용과 보낸 사람의 주소를 인스턴스 변수로 가지고 있습니다. 메소드를 보면 IMessage의 메소드들을 잘 오버라이딩 해서 가지고 있네요.

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(f"{destination}로 문자 메시지 전송!\n내용: {self._content}")

그 아래로 TextMessage라는 클래스가 있습니다. 마찬가지로 IMassage를 상속받고 그 메소드들을 잘 오버라이딩 해서 가지고 있습니다.

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)

마지막으로 TextReader 클래스는 texts라는 인스턴스 변수를 가지고 있고 add_text에서 리스트의 요소를 추가해주는데 파라미터의 타입 힌팅을 보면 IMessage라고 되어 있습니다. 이는 이 메세지가 IMessage 인터페이스를 상속받은 클래스의 인스턴스여야 한다는 뜻입니다. read_all_texts는 인스턴스 안에 있는 모든 텍스트 내용을 출력해주는 메소드입니다.

그럼 이 클래스들을 한 번 사용해보도록 하겠습니다.

email = Email("안녕, 잘 지내?, "tataki26@email.com")
text_message = TextMessage("내일 시간 있어?")

text_reader = TextReader()

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

text_reader.read_all_texts()

우선, email과 text_message 인스턴스를 생성하고 두 메세지를 사용할 텍스트 리더기도 만들었습니다. 그 다음 이 텍스트 리더기에 이메일과 메세지를 추가해준 후 두 내용을 출력해줬습니다.

안녕, 잘 지내?
내일 시간 있어?

잘 출력됩니다.

그런데 이때, 어떤 사용자들이 포스트잇에 쓰는 메모를 나타낼 수 있는 클래스가 필요하다고 요청했습니다. 그래서 IMessage 인터페이스를 상속받는 새로운 클래스 Memo를 만들어야 하는데요.

class Memo(IMessage):
    def __init__(self, content, owner_email):
        """메모는 그 내용을 인스턴스 변수로 가짐"""
        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(f"{self.owner_email}에서 {destination}로 이메일 전송!\n내용: {self.content}")
        return True

이제 남은 것은 send 메소드입니다. 그런데 포스트잇 메모에도 전송 기능이 있나요? 문자나 이메일과 다르게 메모는 전송 기능이 없습니다. 그럼에도 TextReader에 추가 되기 위해서 IMessage 인터페이스를 상속 받은 상태인데요. 따라서, send 메소드를 오버라이딩 할 수밖에 없습니다.

이처럼 클래스가 사용하지도 않을 메소드를 강제로 가지고 있는 경우가 있습니다. 일단 send 메소드를 적당히 오버라이딩 해보겠습니다.

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

Memo 클래스를 사용하기 위한 코드를 작성하겠습니다.

memo = Memo("내일 2시에 약속 나갈 것")

text_reader.add_text(memo)

text_reader.read_all_texts

위 코드를 실행하면,

내일 2시까지 숙제 끝낼 것!

에러 없이 잘 동작합니다.

그런데 앞서 문제가 하나 있었죠? Memo 클래스가 IMessage 인터페이스를 상속 받으면서 사용하지도 않을 send 메소드를 억지로 오버라이딩 했었는데요. 이는 인터페이스 분리 원칙에 어긋나는 것입니다.

이 문제를 해결하려면 IMessage 인터페이스를 더 작은 인터페이스로 분리해야 합니다. 현재 IMessage처럼 너무 많은 메소드를 한번에 가지고 있는 인터페이스뚱뚱한 인터페이스라고 합니다. 이 뚱뚱한 인터페이스가 있으면 인터페이스 분리 원칙을 위반하기 쉽습니다.

👫 인터페이스 분리 원칙 적용

인터페이스 분리 원칙에 의해 더 작아진 인터페이스역할 인터페이스(role interface)라고 합니다. 이는 뚱뚱한 인터페이스의 여러 메소드를 그 역할에 맞게 나누었다는 뜻을 가지고 있습니다.

그럼 IMessage 인터페이스를 분리해볼까요? 우선, 메소드들의 역할을 나눠보겠습니다. content 메소드와 edit_content 메소드는 메세지 내용을 다룬다는 관련성이 있기 때문에 하나의 인터페이스로 묶고 나머지 send 메소드는 위 두 메소드와 성격이 다르니 다른 인터페이스로 묶겠습니다.

두 메소드들을 담을 IText 인터페이스를 만들겠습니다.

class IText(ABC):
    @property
    @abstractmethod
    def content(self):
        """추상 getter 메소드"""
        pass
        
    @abstractmethod
    def edit_content(self, new_content: str) -> None:
        """작성한 메세지를 수정하는 메소드"""
        pass

그리고 send를 담을 ISendable 인터페이스도 만들겠습니다.

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

그럼 이제 원래 인터페이스인 IMessage는 필요없게 되었으니 지워주면 됩니다. IMessage가 사라지면 상속 관계도 같이 사라지겠죠? 따라서, 자식 클래스로부터 IMessage를 지워주고 새롭게 정의된 인터페이스들을 상속 받게 하면 됩니다.

class Email(IText, Isendabel):
class TextMessage(IText, Isendabel):
class Memo(IText):

Memo 클래스에는 send 메소드가 필요 없으니 IText만을 상속 받게 하고 send 메소드를 지워주면 됩니다.

마지막으로 TextReader 클래스만 수정하면 되는데요. IText 인터페이스의 content 메소드만을 사용하고 있으니 타입 힌팅의 IMessage를 지워주고 IText를 넣어주기만 하면 됩니다.

이렇게 인터페이스를 분리하고 나니 Memo 클래스가 강제로 send 추상 메소드를 오버라이딩 할 필요가 없어졌습니다. 이처럼 코드를 작성할 때에는 인터페이스를 더 작게 구성할 수 있는지를 항상 고민해봐야 합니다. 추상 메소드를 나누는 기준이 있다면 인터페이스를 나눌 수 있다는 가능성이 있는 것입니다.

그렇지만 인터페이스 분리 원칙이 모든 인터페이스를 다 분리하라는 것은 아닙니다. 만약, 인터페이스가 추상 메소드 하나만 가질만큼 작아진다고 해봅시다. 그럼 자식 클래스가 너무 많은 부모 클래스를 상속 받아야 해서 문제가 생깁니다.

따라서, 인터페이스는 어떤 기능이나 역할을 중심으로 서로 관련 있는 추상 메소드들을 모으고 관련 없는 추상 메소드들은 분리하는 수준으로 작게 만들면 됩니다.

👫 인터페이스 분리 원칙 정리

인터페이스 분리 원칙은 지나치게 많은 추상 메소드를 가진 거대한 인터페이스 하나를, 관련된 추상 메소드들만 모여있도록 작은 크기의 인터페이스로 분리하라는 뜻입니다.

이렇게 해야 하는 이유는 지나치게 큰 인터페이스는 이를 상속받을 자식 클래스가 필요하지도 않은 메소드를 강제로 오버라이딩하도록 만들기 때문입니다.

인터페이스가 서로 관련성이 높은, 적절한 개수의 추상 메소드를 포함하게 될 때 역할 인터페이스라 불리는데요. 거대한 인터페이스 하나보다는 작은 역할 인터페이스가 여러 개 있는 것이 각 클래스가 본인의 역할에 맞는 인터페이스를 상속 받을 수 있게 하여 더 효율적입니다. 이렇게 하면 각 클래스의 기능을 쉽게 파악할 수 있다는 이점을 얻을 수도 있습니다.

인터페이스를 분리하는 기준은 상황에 따라 다릅니다. 중요한 것은 관련 있는 기능끼리 하나의 인터페이스에 모으되 지나치게 커지지 않도록 크기를 제한해야겠다는 생각을 가지고 인터페이스를 설계해야 한다는 점입니다.


이번 시간에는 SOLID 4번째, 인터페이스 분리 원칙에 대해 함께 알아보았습니다. 하나의 클래스가 많은 내용을 가지기보다는 기능에 따라 클래스를 나누어 상속 받게 하는 것이 더 효율적이라는 것을 배웠네요.

다음 시간에는 마지막 SOLID, 의존 관계 역전 원칙에 대해 알아봅시다.

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

0개의 댓글