본문 바로가기
IT 공부/객체지향 설계 공부

인터페이스 분리 원칙(ISP) 제대로 이해하기

by exdus3156 2023. 12. 11.

인터페이스 분리 원칙(Interface Segregation Principle)은 처음 내가 SOLID 원칙을 공부했을 때 가장 납득하기 힘들었던 원칙이었다.

대부분의 교과서와 강의가 SOLID 원칙을 변경의 용이성 관점에서만 설명하기 때문에, 객체지향이 지향하는 관점과 사고방식에 대해 몰랐던 당시의 나로선 ISP를 100% 납득할 수 없었다.

"클래스가 있는데 그대로 쓰지, 왜 굳이 인터페이스를 또 만들어야 하는거지? 굳이 나누는 것이 의미가 있나?" 라고 생각했다. 물론 인터페이스를 나누는 것이 유용해 보이기는 했지만, 딱 그 뿐이었다. 유용성 이상으로 ISP를 지켜야 하는 이유를 몰랐다. "의존하지 않는 메소드가 있으면 그냥 안 쓰면 그만 아닌가?" 라고 생각했었다.

유용하다는 것은 달리 말하면 그것을 지키지 않았을 때 조금 불편해지는 정도로 끝난다는 뜻이다. 그러나 ISP는 '원칙'이다. 원칙이란 무엇인가? 그것을 지키지 않으면 논리적으로 더 이상 타당하지 않다는 뜻이다. 무언가 더 큰 관점에서 ISP를 지켜야 하는 논리적이고 타당한 이유가 분명 있을거라 생각했다. 물론 그 이유에 대한 실마리를 얻은 것은 한참 나중의 일이었다.

 

1. 인터페이스 나누기가 아니라 관계를 봐야 한다.

ISP는 말 그대로 인터페이스(Interface)를 나누라는(Segregate) 원칙(Principle)이다. 문제는 ISP라는 이름은 다른 원칙에 비해 WHAT이나 WHY가 아니라 HOW를 설명하고 있기 때문에 자칫 잘못하면,

"그래, 나누면 좋지 뭐.... 깔끔하고... 인터페이스가 많으면 복잡해보이고... 안 쓰는 기능이 잡다하게 있으면 좀 불편해보이긴 하네..."

수준에서 받아들일 위험이 있다. (내가 그랬다.) 

이것을 피하기 위해서는 애초에 왜 인터페이스가 필요한 것인지 자문해야 한다.

인터페이스는 의존성 역전(DI) 등, 다방면으로 활용될 수 있으나 본질적으로는 한 객체가 다른 객체에게 보내는 메시지의 규격을 의미한다. 인터페이스는 클라이언트 객체가 보내는 메시지이며, 런타임에 특정 수신 객체가 특정 메소드로 책임을 수행하기를 요청하는 것이다.

따라서 인터페이스는 객체간 협력 관계에서 클라이언트 객체가 수신 객체에게 원하는 책임과 역할이 무엇인지 암시한다.

물론 구체적인 수신 객체의 타입은 알 수 없다. 인터페이스나 추상클래스처럼 추상화된 구현 문법을 사용하는 경우 수신 객체와 메소드는 런타임에 결정된다. 하지만 수신 객체에게 원하는 책임과 역할은 분명해진다. 인터페이스는 두 객체가 서로 어떻게 협력하고 싶은 것인지 알려주기 때문이다. 즉, 인터페이스는 두 객체 사이의 협력과 역할을 알려준다.

ISP는 그냥 보기 좋게 나누라는 것이 아니다. ISP는 그저 작은 규모로 인터페이스를 쪼개라는 기계적인 조언이 아니라, 클라이언트 객체와 수신 객체 사이의 협력을 고려해 적절한 역할을 할당하라는 뜻이다.

소프트웨어의 객체는 여러 클라이언트에게 사용될 수 있다. 이때 퍼블릭 인터페이스를 두 클라이언트에게 제공할 수 있다는 이유로 그대로 재사용한다면 어떻게 될까? 두 클라이언트 입장에서는 서로 자신과 관련이 없는 협력 관계에서 사용될 인터페이스도 식별할 수 있게 된다. 이는 곧 자신과 관련 없는 역할로 상대를 인지한다는 뜻이다.

상관없는 인터페이스가 있다면 그저 사용하지 않으면 괜찮은 것 아니냐고 되묻는 것은, 객체지향의 협력 관계에 대한 고려가 없다는 증거다.

클라이언트 입장에서 인터페이스가 중요한 이유는 단순히 기능을 요청할 수 있어서가 아니다. 클라이언트와 수신 객체 사이를 역할과 협력이라는 관계로 이어주기 때문에 중요한 것이다. 그런데 자신과 관련이 없는 객체의 인터페이스와도 연결될 수 있다는 말은, 달리 말하면 해당 협력이 잘못 묘사되고 있다는 증거다.

이에 대한 좋은 예시로, <코드 컴플리트> 6장의 2-1-3. 클래스가 구현하고 있는 역할이 무엇인지 모르는 경우의 예시에 잘 나와있다. (링크)

즉, 기능을 제공하는데 치우치면 위와 같이 서로 다른 역할을 담당하고 있음에도 불구하고 일부 인터페이스를 제공해준다는 이유로 두 객체를 연결해버린다. 클라이언트 입장에서는 마치 서로 다른 시스템에서 온 역할을 담당하는 객체와 연결된 낯선 상황이다.

 

2. 변경을 제어하는데도 유리하다.

그렇다고 추상적인 역할 관계를 묘사하는 수준에서 ISP가 논해지는 것은 아니다. ISP는 변경과 관련이 있는 실무적인 차원에서도 유용한 원칙이다. (사실 많은 강의나 교과서가 소스코드 변경과 수정 관점에서 ISP를 설명한다.)

DI를 생각해보면, DI의 핵심은 소스 코드 의존성의 방향을 역전시켜 한 모듈이 다른 모듈로부터의 의존에서 벗어날 수 있게 한다는 점이다.

즉, 클라이언트 객체가 특정 구체 클래스와 결합되는 것이 아니라, 추상클래스나 인터페이스에 의존하게 되면 클라이언트 객체가 특정 메소드에 의존하지 않게 된다. 오히려 메소드를 제공할 책임이 있는 객체가 클라이언트가 제시한 인터페이스에 의존해야 한다.

따라서 인터페이스를 추상적이고 간략하게 디자인하면 해당 클라이언트는 잠재적인 변경으로부터 자유롭다. 왜냐하면 자신이 다른 객체에게 요청하는 역할에만 신경쓰기 때문에, 협력 관계가 달라지지 않는 이상 수정될 요인이 없으며, 다른 역할과 관련된 인터페이스는 완전히 무시할 수 있기 때문이다.

따라서 협력 관계가 변경되어 인터페이스 자체가 달라지지만 않는다면, 클라이언트 객체는 소스 코드를 변경할 이유가 없다. 설령 해당 인터페이스를 상속한 어떤 타입의 객체가 변경된다고 하더라도 자신과 관련된 역할에만 의존하고 있는 클라이언트 객체는 그러한 변화로 영향을 받지 않는다.

이것이 바로, "호출하지 않는 메소드에는 의존하지 말라"는 지침의 참뜻이다.

호출하지 않는 메소드에 의존한다는 것은 적절한 역할과 책임으로 연결되지 않았다는 증거이며, 또한 호출하지 않는 메소드가 있다는 것 자체가 해당 객체는 사실 다른 클라이언트와의 협력 관계에서 사용된다는 것을 의미한다. 즉, 해당 객체와 연결된 클라이언트 입장에서는 다른 클라이언트와의 협력에 의해 무언가 변동이 발생했음에도 불구하고 자신에게까지 영향을 끼친다는 것을 말한다.

예를 들어, 스프레드 시트를 다루는 어떤 객체가 있는데, 이 객체가 시트의 칸 색깔을 변경하는 기능이 있다고 하자. 이 기능 하나만을 사용하겠답시고 내 시스템에 그 객체를 가져와 그 어떤 변경도 없이 그대로 이식했다.

이때 만약 원래 시스템에서 협력의 구조가 달라졌다고 해보자. 그래서 클래스의 소스코드도 변경되었다. 내 시스템은 그저 해당 객체가 제공하는 색깔 변경 기능만을 사용하고 있음에도 불구하고 다시 컴파일해야 한다.

 

3. 복합기 비유

예를 들어, 복합기를 생각해보자. 

복합기는 객체이며 외부에 여러 인터페이스를 제공해준다. 한 번에 다양한 기능을 수행할 수 있다. 누군가에겐 프린터이고, 누군가에겐 팩스이고, 누군가에겐 스캐너이고, 누군가에겐 복사기다.

이때 어떤 직원의 복합기와 관련된 업무가 주로 문서를 다른 기관에 보내는 업무만 한다고 가정해보자. 과연 이 직원은 이 기계를 어떻게 이해해야 할까? 복합기일까? 아니면 팩스일까?

객체지향 관점에 따르면, 이 직원에게 복합기는 복합기가 아니라 그냥 팩스로 인식되어야 한다.

"다른 기능은 뭐, 몰라도 되니까!"라는 식의 가벼운 이유가 아니다. 이 직원이 복합기 기종의 변경으로 인해 영향을 받지 않으려면 ISP 원칙에 따라 복합기가 아니라 철저하게 팩스와 관련된 인터페이스만 익혀야 한다.

만약 복합기의 기종이 변경되었거나, 기능에 업데이트가 생겼거나, 아예 성능이 좋은 전문 팩스 기계를 들여온 경우면 어떻게 될까? 만약 팩스 기능만 쓰면서도 정작 복합기라는 구체적인 기계에 의존하고 있던 해당 직원은 새로운 기계를 다시 재인식해야 한다.

그러나 팩스 인터페이스만 익힌 직원은 새로운 기계가 오든, 성능이 업데이트되었든, 기종이 변경되었든 간에 팩스 사용 인터페이스가 변경되지 않았으므로 특별히 공부할 것이 없다. "이 기계, 예전에 했던 거랑 똑같아요" 라는 설명 한 번이면 충분할 것이다.