본문 바로가기
👩🏻‍💻Computer Science/clean-code

Chapter4. SOLID 원칙(ISP, 인터페이스 분리 원칙)

by devhertz 2026. 7. 26.

 

인터페이스 분리 원칙(ISP)

  • 정의
    • 인터페이스 분리 원칙(Interface Segregation Principle, ISP)“클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안 된다” 는 원칙
    • 하나의 큰(fat) 인터페이스를 여러 개의 작고 응집력 높은 인터페이스로 분리하고
    • 클래스는 필요한 인터페이스만 구현하도록 강제하는 것.
  • 객체지향에서 인터페이스의 본질
    • 인터페이스 = 객체가 외부에 노출하는 메서드의 집합(메시지 프로토콜)
    • 클래스는 “구현”, 인터페이스는 “행동의 추상화”
    • 클라이언트는 인터페이스를 통해 객체와 상호작용함.
  • Python에서의 인터페이스 정의 방식
    • 예전: 덕 타이핑(Duck Typing)
      • “어떤 새가 오리처럼 걷고 오리처럼 꽥꽥 소리를 내면 그것은 오리다.”
      • 타입 이름, docstring, 속성 이름에 상관없이 실제로 가지고 있는 메서드가 객체의 정체성을 결정함.
    • 현재: 추상 기본 클래스(Abstract Base Class, ABC) 가 도입(Python 3(PEP-3119)부터)
      • 파생 클래스가 구현해야 할 일부분을 공통된 기본 동작으로 구현하거나 인터페이스로 정의하는 것
      • 특정 중요 메서드가 실제로 재정의 되었는지 확인이 필요할 때 유용, isinstance()와 같은 메서드의 기능을 재정의하거나 확장하는 메커니즘으로도 작동함.

c.f.) ABCregister() 메서드를 사용하면 기존 기본 클래스에 파생 클래스를 추가 할 수 있다. 이렇게 추가된 파생 클래스를 가상 서브 클래스(virtual subclass)라고 한다. (ex. 오리와 같이 걷고, 오리와 같이 소리를 내고, 자신을 오리라고 말할 수 있는 새로운 유형의 오리를 등록하는 것)

 

구분 설명 특징
Duck Typing 메서드 존재 여부로 인터페이스 판단 런타임 동적 검사
ABC + @abstractmethod 명시적으로 구현해야 할 메서드를 강제 인스턴스화 시점에 검사
Virtual Subclass ABC.register()로 기존 클래스를 계층에 등록 상속 없이 인터페이스 만족
  • @abstractmethod: 기본 클래스에서 이 데코레이터로 마킹한 메서드는 파생 클래스에서 모두 구현해야만 인스턴스화가 가능함.
    • @abstractmethod가 붙은 메서드가 구현되지 않으면 __new__ 단계에서 TypeError를 발생시킴
  • 가상 서브클래스(Virtual Subclass): abc 모듈의 register() 메서드를 사용하면 기존 기본 클래스에 파생 클래스를 계층 구조의 일부로 등록할 수 있음.
    • Virtual subclass는 __subclasscheck__ / __instancecheck__를 오버라이드하여 isinstance(), issubclass()의 동작을 확장한다.

재사용성을 높이기 위해 가능한 작은 단위로 인터페이스를 분리한다면 인터페이스를 구현하려는 각 클래스가 매우 명확한 동작과 책임을 지니기 때문에 응집력이 높아진다.

너무 많은 일을 하는 인터페이스

나쁜 예: 모든 기능을 한 인터페이스에 몰아넣음
class EventParser:          # 그림 4.7
    def from_xml(self): ...
    def from_json(self): ...

 

문제점

  • XML만 필요한 클래스도 JSON 메서드를 강제로 구현해야 함
  • 결합력(Coupling) 증가, 유연성 감소
  • 클라이언트가 사용하지 않는 메서드에 의존 → ISP 위반
  • SRP도 함께 위반 (변경 이유가 두 가지 이상이 됨)

인터페이스는 작을수록 좋다

  • 위의 예시에서 각각 하나의 메서드를 가진 두 개의 다른 인터페이스로 분리하는 것이 좋다.
  • 이렇게 하면 XMLEventParser에서 파생된 클래스는 from_xml() 메서드만을 구현하면 되고, 마찬가지로 JSONEventParser에서 파생된 클래스는 from_json() 메서드만을 구현하면 됨.

장점

  • 각 인터페이스의 응집력(Cohesion) 극대화
  • 필요한 기능만 구현 → 클래스 책임이 명확해짐
  • 다중 상속을 통해 필요한 조합을 자유롭게 구성 가능
  • 새로운 파서(예: YAML)를 추가할 때 기존 코드 수정 불필요 (OCP와 연계)

그림 4.8 구현코드)

from abc import ABCMeta, abstractmethod

class XMLEventParser(metaclass=ABCMeta):
    @abstractmethod
    def from_xml(self, xml_data: str):
        """XML 형태의 데이터를 파싱"""

class JSONEventParser(metaclass=ABCMeta):
    @abstractmethod
    def from_json(self, json_data: str):
        """JSON 형태의 데이터를 파싱"""

class EventParser(XMLEventParser, JSONEventParser):
    """XML과 JSON 형태를 모두 지원"""
    def from_xml(self, xml_data: str):
        pass

    def from_json(self, json_data: str):
        pass
  • 주의: 인터페이스에서 정의한 추상 메서드는 구체 클래스에서 반드시 구현해야 한다는 점. 추상메서드를 구현하지 않으면 런타임 오류가 발생함.
  • SRP와 유사하지만, ISP는 '행동의 추상화(인터페이스)'에 초점을 맞추고 있음.
  • ISP를 준수하지 않아 별개의 기능이 결합된 거대한 인터페이스를 만들게 되면, 이를 상속받은 구체 클래스 역시 변경해야 할 이유가 두 가지 이상이 됨. 즉, ISP 위반은 필연적으로 SRP 위반으로 이어짐.

인터페이스는 얼마나 작아야 할까?

  • "작은 인터페이스"라는 말을 무조건 '딱 한 가지 메서드만 있어야 한다’는 뜻으로 오해해서는 안 됨. 응집력 관점에서 하나의 클래스에 하나 이상의 메서드가 적절하게 속해 있을 수 있음.
    • 예외 사례 (컨텍스트 관리자): 컨텍스트 관리자를 추상화한 클래스는 __enter____exit__ 두 가지 메서드를 반드시 함께 제공해야 함. 이 둘을 다른 클래스로 분리하면 유효한 컨텍스트 관리자가 될 수 없으며, 전혀 쓸모없는 코드가 됨.
  • 즉, 서로 완전히 관련된 로직이라면 여러 개의 메서드라도 하나의 인터페이스로 디자인하는 것이 좋음.

 

댓글