설계/설계 원칙 / / 2025. 5. 7. 15:58

SOLID의 ISP : 역할 중심 설계를 도와주는 인터페이스 분리 원칙

1. 구조를 망치는 인터페이스

정교하게 설계된 애플리케이션도, 시간이 지나고 요구사항이 누적되면 점차 구조적인 균열이 드러난다. 특히 문서에는 '공통 인터페이스'라 명시되어 있지만, 실제 구현을 맡은 개발자는 "이 메서드, 왜 내가 구현해야 하지?"라고 생각하게 된다. 이 질문은 단순한 불만이 아니다. 이는 인터페이스의 책임과 역할 경계가 무너지고 있다는 신호다. 그 원인은 범용 인터페이스에 있다.

 

처음에는 기능 통합과 재사용성 증대를 위해 하나의 인터페이스로 묶는 것이 타당해 보였다. 그러나 시간이 흐르면서 각기 다른 책임이 뒤섞이기 시작하고, 구현체는 자신과 무관한 기능까지 포함한 인터페이스를 강제로 구현하게 된다. 결과적으로 '모두를 위한 인터페이스'는 '아무도 적합하지 않은 구조'로 변질된다. 이로 인해 발생하는 문제는 두 가지다.

 

  • 불필요한 구현 : 필요하지 않은 기능도 구현해야 하므로, 빈 메서드나 예외 처리, 책임 회피 코드가 늘어난다.
  • 변화의 확산 : 인터페이스 변경이 관련 없는 클래스까지 영향을 미쳐 유지보수가 어려워진다.

 

이러한 구조는 설계의 유연성을 떨어뜨리고, 개발자에게 반복적으로 '이 구조, 정말 괜찮은 건가?'라는 의문을 던지게 만든다. 건강하지 않은 구조는 결국 생산성과 안정성 모두에 악영향을 끼친다.

 

< 하늘에 닿는 탑을 쌓으려는 야심으로 시작했지만 진행될 수록 비틀리고 무너지기 시작하는 피터 브뤼헐의 바벨탑 >

 

  • 역할에 맞는 인터페이스란 무엇인가?
  • 구현체가 불필요한 기능에 의존하지 않도록 하려면 어떻게 해야 하는가?

 

해당 질문에 해답을 제시하는 원칙이 인터페이스 분리 원칙(ISP, Interface Segregation Principle)이다. 

 

2. ISP란 무엇인가? 정의 및 현실 비유

인터페이스 분리 원칙(ISP, Interface Segregation Principle)은 다음과 같이 정의된다.

 

"클라이언트는 자신이 사용하지 않는 메서드에 의존해서는 안 된다."

 

이 문장은 단순히 문법적 정합성이나 코드 양의 문제를 말하는 것이 아니다. 이는 '역할 중심 설계'라는 객체 지향의 핵심 철학을 담고 있다. 각 객체는 자신이 수행할 수 있는 책임에만 집중해야 하며, 그 외의 기능에는 독립적이어야 한다. ISP는 이 철학을 코드와 구조 설계 차원에서 실현할 수 있도록 돕는 원칙이다.

 

ISP에 대한 이해를 돕기 위해 실생활의 비유를 들어본다.

 

한식, 중식, 양식을 모두 제공하는 대형 음식점에 갔다고 상상해 보자. 자리에 앉으니 종업원이 통합 주문서를 건넨다. 그 안에는 세 가지 국적의 요리 이름이 빼곡히 적혀 있다. 나는 단지 햄버거 하나를 주문하고 싶은데, 그걸 찾기 위해 온갖 낯선 요리 이름들 사이를 뒤적이며 체크박스를 찾아야 한다. 주문 과정은 번거롭고, 실수도 쉽게 발생한다.

 

< 과도한 선택지 앞에서 필요한 정보를 찾는 클라이언트의 고민 >

 

이런 상황은 범용 인터페이스와 매우 유사하다. 클라이언트가 필요로 하는 기능은 하나인데, 범용 인터페이스는 다양한 역할의 기능을 모두 포함하고 있어, 개발자는 필요 없는 메서드까지 인지하고 다뤄야 한다. 이는 단순한 불편을 넘어, 구현 오류와 유지보수 부담 등 구조적인 문제로 이어진다.

 

결국 ISP는 단순히 인터페이스를 작게 나누자는 접근이 아니다. 정확한 역할 분리를 통해 각 객체가 자신에게 필요한 기능만을 책임지도록 유도하는 설계의 실천 원칙이다. 그 목적은 단순화가 아니라, 변화에 강하고 책임이 명확한 구조를 만드는 데 있다.

 

3. ISP 적용 전후 예제 코드

앞서 "클라이언트는 자신이 사용하지 않는 기능에 의존해서는 안 된다."라는 ISP의 정의에 대해 살펴보았다. 이번 장에서는 이 원칙이 실제 코드에서 어떻게 적용되는지를 살펴본다. 예제는 프린터 장치의 인터페이스 설계다. 프린터는 사무 환경에서 흔히 접할 수 있는 장비이며, 모델에 따라 지원하는 기능이 다르다. 예를 들어 어떤 프린터는 인쇄만 가능하고, 어떤 프린터는 복사나 스캔, 팩스 기능까지 포함되어 있다.

 

이처럼 기능이 다양함에도 불구하고, 모든 프린터를 하나의 인터페이스로 추상화하려는 시도가 있다고 가정하여 예시를 작성한다. ISP를 고려하지 않은 구조를 리팩토링하는 과정을 통해 ISP가 단지 인터페이스를 작게 나누자는 기계적인 지침이 아니라, 실제로 불필요한 구현을 제거하고, 시스템을 변화에 강하게 만드는 설계 전략임을 확인하는 계기가 될 수 있을 것이다.

 

3-1. 초보 개발자의 범용 인터페이스가 만드는 문제들

초보 개발자는 다양한 프린터 기기를 다루는 시스템을 설계하게 되었다. 그는 "모든 프린터는 기본적으로 같은 종류의 장비이므로 하나의 공통 인터페이스로 처리하면 유연할 것"이라고 판단했다. 초보 개발자가 생각한 목표는 다음과 같다.

 

  • 모든 프린터 모델을 하나의 인터페이스로 추상화하면 코드의 일관성을 유지할 수 있다.
  • 기능을 미리 정의해두면, 확장에 대비한 구조가 될 것이다.
  • 추상화는 클수록 유연하다는 생각에 따라 범용 인터페이스가 바람직할 것이다.

 

그 결과 다음과 같은 인터페이스가 만들어졌다.

interface MultiFunctionPrinter {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
}

 

초보 개발자는 이후 프린터 모델을 이 인터페이스에 맞춰 구현하기 시작했다. 그중 하나는 기본형 흑백 프린터였고, 이 장비는 오직 인쇄 기능만 제공했다. 하지만 MultiFunctionPrinter 인터페이스를 구현하려면, 사용하지 않는 기능까지 반드시 포함해야 했다. 다음은 구현 예시이다.

class BasicPrinter implements MultiFunctionPrinter {

    @Override
    public void print(Document doc) {
        System.out.println("문서를 출력합니다.");
    }

    @Override
    public void scan(Document doc) {
        throw new UnsupportedOperationException("이 모델은 스캔을 지원하지 않습니다.");
    }

    @Override
    public void fax(Document doc) {
        throw new UnsupportedOperationException("이 모델은 팩스를 지원하지 않습니다.");
    }
}

 

해당 구현에서 볼 수 있듯이, 이 프린터는 실제로는 print( ) 메서드만 필요하지만, 인터페이스를 따르기 위해 scan( )과 fax( ) 메서드도 강제로 구현해야 한다. 이로 인해 다음과 같은 문제가 발생한다.

 

  • ⚠️ 불필요한 구현 : 지원하지 않는 기능에 대해 예외를 던지는 코드를 매번 작성해야 함.
  • ⚠️ 의미 없는 예외 처리 : 코드상에서는 해당 기능이 존재하는 것처럼 보이지만, 실제 실행 시 예외가 발생하는 모순적 상태.
  • ⚠️ 구조의 오해 유발 : 인터페이스만 보면 이 프린터가 모든 기능을 지원하는 것처럼 보이므로, 클라이언트 측에서 잘못된 의존이 생김.
  • ⚠️ 유지보수 부담 증가 : 인터페이스 변경 시, 기능이 없는 장비까지 영향을 받아 불필요한 수정이 발생.

 

초보 개발자는 이후 다양한 프린터를 구현하면서 자신의 설계가 점점 불편하고 확장에 취약하다는 사실을 깨닫게 된다. 그리고 이 구조가 ISP를 위반한 설계임을 알게 되면서, 인터페이스를 기능별로 분리해야 할 필요성을 느끼게 된다.

 

3-2. 능숙한 개발자의 ISP 적용 예시

초보 개발자의 구조를 검토한 능숙한 개발자는 다음과 같은 문제를 짚어냈다.

 

  • 하나의 인터페이스가 서로 다른 기능(인쇄, 스캔, 팩스)을 강제로 묶고 있다.
  • 그로 인해, 일부 기능만 필요한 클래스도 불필요한 책임을 지게 된다.
  • 인터페이스 변경 시, 모든 구현체가 영향을 받아 유지보수가 어려워진다.

 

이러한 문제를 해결하기 위해 능숙한 개발자는 ISP의 원칙에 따라 기능을 역할별로 명확히 분리하기로 했다. 그 결과 다음과 같은 단일 역할 인터페이스들이 정의된다.

interface Printer {
    void print(Document doc);
}

interface Scanner {
    void scan(Document doc);
}

interface Fax {
    void fax(Document doc);
}

 

이제 프린터 모델은 자신이 지원하는 기능에 해당하는 인터페이스만 구현하면 된다. 예를 들어, 기본형 흑백 프린터는 다음과 같이 단일 Printer 인터페이스만 구현한다.

class BasicPrinter implements Printer {
    @Override
    public void print(Document doc) {
        System.out.println("문서를 출력합니다.");
    }
}

 

반면, 고급형 복합기 모델은 여러 기능을 조합하여 다음과 같이 구현할 수 있다.

class AdvancedMultiFunctionPrinter implements Printer, Scanner, Fax {
    @Override
    public void print(Document doc) {
        System.out.println("문서를 고속으로 출력합니다.");
    }

    @Override
    public void scan(Document doc) {
        System.out.println("문서를 고해상도로 스캔합니다.");
    }

    @Override
    public void fax(Document doc) {
        System.out.println("문서를 팩스로 전송합니다.");
    }
}

 

초보 개발자의 구조는 범용성을 추구했지만, 결과적으로 확장성과 명확성을 잃었다. 반면 능숙한 개발자는 역할 중심으로 인터페이스를 분리함으로써, ISP 원칙을 실천했고, 실제 시스템의 구조를 유연하고 명료하게 만드는 데 성공했다.

 

4. ISP 적용의 효과

ISP는 시스템이 변화에 유연하게 대응하고, 각 객체가 자신에게 적절한 책임만을 갖도록 만드는 데 있다. 앞선 프린터 예제에서처럼 ISP를 잘 적용하면 다음과 같은 장점을 얻을 수 있다.

 

  • 책임이 명확해진다.
ISP를 적용하면 각 클래스는 자신이 필요한 기능에 대해서만 인터페이스를 통해 의존하게 된다. 예를 들어, 기본 흑백 프린터는 Printer 인터페이스만 구현하고 있다면, 이 클래스가 오직 출력 기능만을 수행한다는 사실은 코드만 보아도 분명하다.

이는 설계 문서를 읽지 않아도 객체의 역할을 쉽게 유추할 수 있도록 해주며, 팀 내 커뮤니케이션을 단순화하고 실수를 줄인다. 역할과 책임이 명확히 분리되면, 구조 또한 더 단순하고 예측 가능해진다.

 

  • 불필요한 구현이 제거된다.
모든 기능을 포함한 범용 인터페이스는 기능을 지원하지 않는 클래스까지도 메서드를 억지로 구현하게 만든다. 이는 불필요한 예외 처리나, 의미 없는 빈 구현을 양산하는 원인이 된다.

ISP는 이러한 구현 강제를 구조적으로 차단한다. 클래스는 오직 자신에게 필요한 인터페이스만 선택적으로 구현하면 되므로, 코드의 명료성과 품질이 향상된다. 이는 곧 유지보수 비용의 절감으로 이어진다.

 

  • 테스트가 간결해진다.
역할별로 분리된 인터페이스는 테스트 단위 또한 자연스럽게 나뉜다. 예를 들어 Printer만 테스트하고 싶다면, 인쇄 기능만 제공하는 클래스에 대해 집중적으로 검증할 수 있다.

ISP를 적용하여 인터페이스를 역할 단위로 분리하면, 각각의 테스트가 어떤 기능을 검증하는지 명확하게 구분할 수 있다. 이로 인해 테스트 코드에서 if문이나 switch문과 같은 조건 분기를 줄일 수 있어, 각 테스트는 더 단순하고 직관적으로 작성된다. 각 테스트가 하나의 목적만을 갖게 되므로, 의도 또한 분명해진다.

이렇게 작성된 테스트는 코드의 변경이 발생했을 때도 기존 기능이 깨졌는지를 확인할 수 있는 회귀 테스트로서의 역할을 충실히 하며, 리팩토링 과정에서도 기능이 잘 유지되고 있는지 자동으로 검증할 수 있으므로 시스템의 안정성을 확보하는데 크게 기여한다.

 

  • 유지보수 시 연관 없는 것에 영향을 미치지 않는다.
실제 시스템은 시간이 지남에 따라 기능이 추가되거나 변경되는 일이 빈번하다. 범용 인터페이스를 사용할 경우, 그중 하나의 메서드가 변경되면 모든 구현체가 검토 대상이 된다.

반면 ISP를 적용하면, 기능별 인터페이스가 분리되어 있으므로 변경의 파급 범위가 줄어든다. 예를 들어 Fax 인터페이스에 새로운 옵션이 추가되더라도, 이를 구현하지 않는 클래스는 전혀 영향을 받지 않는다. 이는 개방-폐쇄 원칙(OCP, Open-Closed Principle)과도 긴밀하게 연결되는 구조적 이점이다.

 

5. 언제 ISP를 적용해야 하는가?

ISP는 역할 중심 설계를 가능하게 해주는 원칙이지만 모든 상황에 무조건 적용되는 만능 해결책은 아니다. 실제 프로젝트에서 ISP를 잘못 적용하거나 역할을 과도하게 분리할 경우, 다음과 같은 문제에 직면할 수 있다.

 

  • ❗️ 인터페이스가 지나치게 세분화되어 관리가 어려워진다.
  • ❗️ 하나의 흐름이 인위적으로 분리되어 응집력이 떨어진다.
  • ❗️ 인터페이스의 과도한 분리가 오히려 추상화 수준을 떨어뜨릴 수 있다.

 

결국 ISP의 본질은 불필요한 의존을 제거하고, 역할에 맞는 책임만 갖게 만드는 데 있다. 그러므로 중요한 것은 '언제 나눌 것인가', '어디까지 나눌 것인가'에 대한 설계적 판단 기준을 갖는 일이다.

 

다음은 ISP를 적용할지 여부는 맥락에 따라 다르지만, 다음과 같은 상황에서는 적용을 고려해야 한다.

 

  • 구현 시 일부 메서드가 빈 구현이거나 예외를 던지고 있는가?
해당 질문에 "네"라고 답변하고 있다면, 이는 해당 기능이 필요하지 않음에도 불구하고 인터페이스 때문에 구현이 강제되고 있다는 신호다. 이 경우 인터페이스를 분리하여 각 구현체가 자신의 역할에 해당하는 기능만 포함되도록 구조를 바꾸는 것이 바람직하다.

 

  • 인터페이스를 공유하는 클래스들이 실제로 같은 역할을 수행하고 있는가?
인터페이스를 설계할 때 흔히 할수 있는 실수가, 겉보기에는 유사한 객체들을 하나의 인터페이스로 묶는 것이다. 예를 들어 어떤 클래스들이 모두 '프린터'라는 이름을 갖고 있거나, 외형적으로 비슷한 기능을 가진 것처럼 보인다고 해서, 반드시 같은 역할을 수행한다고 볼 수는 없다.

인터페이스는 비슷해 보이는 클래스들을 묶기 위한 수단이 아니다. 오히려, 실제로 같은 책임을 수행하고 있는 객체들끼리만 공유되어야 하는 계약이다. 따라서 해당 질문이 명확하게 "네"라고 답해지지 않는다면, 그 인터페이스 역시 분리되어야 한다.

 

  • 변경이 일어날 때 관련 없는 구현체에 영향을 주는가?
인터페이스에 변경이 생겼을 때, 실제로 그 기능을 사용하지 않는 클래스까지 수정하거나 테스트해야  하는 경우가 있다면 이는 경계가 흐릿한 설계다. ISP는 이러한 변화의 전파를 줄이고, 변경을 필요한 대상에만 한정시킬 수 있게 해 준다.

 

  • 클래스가 너무 많은 역할을 수행하고 있지는 않은가?
하나의 클래스가 지나치게 다양한 기능을 담당하고 있다면, 이는 단일 책임 원칙(SRP, Single Responsibility Principle) 위반하고 있을 가능성이 높다. 그리고 SRP 위반은 결과적으로 ISP 위반으로도 이어지기 쉽다. 그 이유는 다음과 같다.

* SRP 위반이 발생하면 클래스 내부에 여러 책임이 뒤섞이게 되고,
* 각 책임을 수행하기 위해 여러 기능이 필요한 인터페이스를 참조하게 되며,
* 그 인터페이스는 결국 불필요하게 많은 메서드를 포함하게 된다.

이런 경우에는 단지 클래스를 나누는 것만으로 충분하지 않으며, 해당 클래스가 의존하고 있는 인터페이스 또한 역할 단위로 분리해 각 객체가 자신에게 필요한 기능에만 의존하도록 구조를 재설계해야 한다.

 

< 역할 분담으로 위기에 강한 늑대 무리처럼, ISP도 각 구성 요소의 명확한 역할로 전체 시스템의 신뢰성을 강화한다. >

 

결국, ISP를 적용할지 말지는 설계자의 판단 영역이다. 불필요한 의존을 없애고 객체가 본인의 역할에만 집중할 수 있도록 설계해야 한다. 역할을 나누는 것보다, 왜 나누는지를 물음으로써 ISP의 적용 여부를 판단하자.

 

6. 마무리 - 역할 중심으로 이끄는 설계 원칙 ISP

지금까지의 내용을 정리하면 다음과 같다.

 

인터페이스 분리 원칙(ISP, Interface Segregation Principle)은 OOP의 핵심 원칙 중 하나로, '클라이언트는 자신이 사용하지 않는 기능에 의존해서는 안 된다'는 철학을 바탕으로 한다. 이 원칙은 객체가 자신의 역할에 충실하도록 유도함으로써, 시스템이 변화에 유연하게 대응하고 구조적으로 안정성을 유지할 수 있도록 돕는다.

 

ISP에 대해 설명한 핵심 메시지는 다음과 같다.

 

  • 인터페이스는 기능이 아니라 역할을 기준으로 나누어야 한다.
  • 객체는 자신의 책임에 해당하는 기능에만 의존해야 하며, 그렇지 않은 의존은 설계의 실패로 이어진다.
  • ISP는 SRP, OCP 등 다른 원칙들과 함께 시스템의 견고함을 뒷받침하는 기반이 된다.

 

ISP를 올바르게 적용하면 객체의 책임이 명확해지고, 불필요한 의존이 제거되며, 테스트와 유지보수의 효율성이 높아지는 등 다양한 구조적 이점을 얻을 수 있다. 그러나 이를 지나치게 세분화하거나 모든 상황에 기계적으로 적용할 경우, 오히려 인터페이스가 과도하게 나뉘어 구조가 복잡해지고 응집력이 약화되는 부작용이 발생할 수 있다.

 

따라서, ISP의 적용 여부는 무조건적인 원칙이 아니라 설계적 판단의 대상으로 삼아야 하며, 다음과 같은 질문을 통해 그 필요성과 적합성을 점검해 보는 것이 바람직하다. 

 

  • 해당 클래스가 필요하지 않은 기능까지 구현하고 있는가?
  • 인터페이스를 공유하는 클래스들이 실제로 동일한 역할을 수행하고 있는가?
  • 인터페이스 변경 시, 관계없는 클래스까지 영향받고 있는가?
  • 클래스가 너무 많은 책임을 동시에 수행하고 있지는 않은가?

 

ISP는 객체가 자신이 실제로 수행해야 할 역할만을 수행하도록 유도하는 구조적 기준이다. 핵심은 불필요한 의존을 제거하고, 역할과 책임을 기준으로 구조를 해석할 수 있는 사고 도구로 활용하는 것에 있다. 이 원칙을 올바르게 이해하고 상황에 맞게 적용할 수 있다면, 코드는 더 명료해지고 시스템은 더욱 유연하고 변화에 강해질 수 있다.

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유