인터페이스 분리 원칙
1. 개요
1. 개요
인터페이스 분리 원칙은 객체 지향 프로그래밍과 소프트웨어 설계에서 중요한 SOLID 원칙 중 하나이다. 이 원칙은 '클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다'는 핵심 아이디어를 담고 있다. 다시 말해, 하나의 크고 범용적인 인터페이스보다는 여러 개의 구체적이고 클라이언트 중심적인 인터페이스로 분리하는 것이 바람직하다는 설계 지침을 제시한다.
이 원칙은 로버트 C. 마틴에 의해 1996년 논문에서 처음 소개되었다[1]. 그 목적은 소프트웨어 구성 요소 사이의 불필요한 결합도를 낮추고, 시스템의 유연성과 유지보수성을 높이는 데 있다. 클라이언트는 오직 필요한 기능에만 접근할 수 있어야 하며, 사용하지 않는 기능의 변경으로 인한 영향을 받지 않도록 보호받아야 한다.
인터페이스 분리 원칙을 적용하면, 시스템은 더 작고 응집력 있는 모듈들로 구성된다. 이는 코드의 재사용성을 촉진하고, 단위 테스트를 용이하게 하며, 전체적인 아키텍처의 안정성을 강화한다. 결과적으로 이 원칙은 소프트웨어가 변화에 더 잘 적응하도록 돕는 기반을 마련한다.
2. 원칙의 정의와 핵심 개념
2. 원칙의 정의와 핵심 개념
인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 객체 지향 설계 원칙이다. 이 원칙은 로버트 C. 마틴(Robert C. Martin)이 제안한 SOLID 원칙 중 'I'에 해당한다.
ISP의 핵심은 '두꺼운' 또는 '뚱뚱한' 인터페이스의 문제점을 해결하는 데 있다. 여러 클라이언트가 다양한 기능을 필요로 할 때, 모든 메서드를 하나의 거대한 인터페이스에 모아두면 특정 클라이언트는 자신이 전혀 사용하지 않는 메서드들에도 의존하게 된다. 이는 시스템에 불필요한 결합을 초래하며, 사용하지 않는 메서드의 변경이 클라이언트에 영향을 미칠 수 있는 취약성을 만든다.
따라서 인터페이스 분리 원칙은 더 작고 응집된 인터페이스들로 분리할 것을 권장한다. 각 인터페이스는 특정 클라이언트나 클라이언트 그룹의 요구에 초점을 맞추어야 한다. 이를 통해 클라이언트는 오직 필요한 기능에만 의존하게 되고, 시스템의 모듈성과 명확성이 향상된다. 결과적으로 인터페이스는 클라이언트의 요구에 따라 분리되며, 이는 단일 책임 원칙(SRP)을 인터페이스 수준에서 적용한 것으로 볼 수 있다.
2.1. ISP의 공식적 정의
2.1. ISP의 공식적 정의
인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 객체 지향 프로그래밍의 SOLID 원칙 중 하나로, 로버트 C. 마틴(Robert C. Martin)에 의해 공식화되었다. 이 원칙의 핵심 정의는 "클라이언트가 자신이 사용하지 않는 메서드에 의존하도록 강제되어서는 안 된다"이다.
보다 구체적으로, ISP는 하나의 크고 범용적인 인터페이스보다는 여러 개의 구체적이고 클라이언트 중심적인 인터페이스를 설계할 것을 권장한다. 이는 소프트웨어 모듈이나 클래스가 오직 자신에게 필요한 메서드들만을 알고 의존하도록 하여, 불필요한 결합을 방지하는 것을 목표로 한다. 공식적인 정의는 시스템의 추상화 계층을 설계할 때 적용되는 중요한 지침을 제공한다.
이 원칙을 위반하는 대표적인 사례는 '두꺼운' 또는 '뚱뚱한' 인터페이스(Fat Interface)이다. 예를 들어, Printer, Scanner, Fax 기능이 모두 결합된 MultiFunctionDevice 인터페이스가 있다면, 단순히 인쇄만 필요한 클라이언트는 스캔이나 팩스 기능에 대한 의존성까지 강제로 가지게 된다. ISP는 이러한 인터페이스를 Printer, Scanner, Fax와 같이 각 기능별로 분리된 인터페이스로 나눌 것을 요구한다.
용어 | 설명 |
|---|---|
클라이언트 | 인터페이스를 사용하는 모듈, 클래스 또는 객체. |
의존성 | 클라이언트가 특정 인터페이스의 메서드를 사용하거나 참조함으로써 생기는 관계. |
인터페이스 분리 | 하나의 범용 인터페이스를 여러 개의 클라이언트 중심 인터페이스로 나누는 설계 활동. |
결국 ISP의 정의는 설계의 '분리'와 '특화'에 초점을 맞춘다. 이는 단순히 인터페이스의 물리적인 크기를 줄이는 것이 아니라, 클라이언트의 관점에서 응집력 있는 책임 단위로 인터페이스를 구성함으로써, 더 나은 모듈성과 유연성을 달성하는 것을 의미한다.
2.2. 클라이언트와 불필요한 의존성
2.2. 클라이언트와 불필요한 의존성
클라이언트는 자신이 실제로 사용하는 메서드에만 의존해야 합니다. 만약 클라이언트가 사용하지 않는 메서드를 포함하는 인터페이스에 의존하게 되면, 이는 불필요한 의존성을 형성하게 됩니다. 이 의존성은 시스템을 취약하게 만드는 주요 원인 중 하나입니다.
예를 들어, 프린터, 스캐너, 팩스 기능이 모두 결합된 하나의 MultiFunctionDevice 인터페이스가 있다고 가정해 보겠습니다. 이 인터페이스는 print(), scan(), fax() 메서드를 모두 포함합니다. 만약 오직 인쇄 기능만 필요한 PrintClient 클래스가 이 거대한 인터페이스를 구현하거나 의존한다면, scan()과 fax() 메서드의 구현에 대한 변경이 발생했을 때도 PrintClient는 영향을 받을 수 있습니다. 이는 클라이언트가 전혀 사용하지 않는 기능의 변경에 의해 불필요하게 재컴파일되거나, 테스트를 다시 해야 하는 상황을 초래합니다.
이러한 불필요한 의존성은 소프트웨어의 결합도를 높이고, 모듈성을 저해합니다. 결과적으로 시스템의 한 부분을 수정할 때 예상치 못한 다른 부분에서 오류가 발생할 가능성이 증가합니다. 인터페이스 분리 원칙은 클라이언트별로 세분화된 인터페이스를 제공함으로써, 이러한 불필요한 의존성을 제거하고 각 모듈이 독립적으로 진화할 수 있는 기반을 마련합니다.
2.3. '두꺼운' 인터페이스의 문제점
2.3. '두꺼운' 인터페이스의 문제점
인터페이스가 지나치게 많은 메서드를 포함하고 있을 때, 이를 '두꺼운' 또는 '비대한' 인터페이스라고 부른다. 이러한 인터페이스는 여러 클라이언트의 요구를 하나의 계약에 무분별하게 통합한 결과이다. 핵심 문제는 인터페이스를 사용하는 클라이언트가 자신이 실제로 필요하지 않은 메서드들에 대해 강제적으로 의존하게 된다는 점이다. 이는 결합도를 불필요하게 높이고, 시스템의 유연성을 심각하게 저해한다.
구체적인 문제점으로는, 인터페이스에 변경이 발생할 때 그 영향을 받지 않아야 할 클라이언트까지도 재컴파일하거나 재배포해야 하는 상황이 발생한다. 예를 들어, 프린터, 스캐너, 팩스 기능이 모두 포함된 IMultifunctionDevice 인터페이스가 있다고 가정하자. 오직 인쇄 기능만 필요한 클라이언트는 스캔이나 팩스 관련 메서드의 시그니처 변경에도 영향을 받게 된다. 이는 변경의 파급 효과를 확대시키고, 시스템을 취약하게 만든다.
또한, 두꺼운 인터페이스는 구현체에게 불필요한 부담을 준다. 인터페이스를 구현하는 클래스는 모든 메서드에 대한 구현(또는 더미 구현)을 제공해야 한다. 이는 단일 책임 원칙을 위반하여 클래스가 여러 개의 변경 이유를 가지게 만든다. 결과적으로 해당 클래스의 유지보수 비용이 증가하고, 오류가 발생할 가능성이 높아진다.
마지막으로, 이러한 인터페이스는 코드의 가독성과 이해도를 떨어뜨린다. 개발자는 특정 클라이언트의 관점에서 필요한 메서드만을 빠르게 식별하기 어려워진다. 불필요한 의존성은 테스트를 어렵게 만들며, 모의 객체를 생성하는 과정도 복잡해진다. 따라서 인터페이스 분리 원칙은 이러한 '두꺼운' 인터페이스의 문제점을 해결하기 위해 더 작고 집중된 역할의 인터페이스들로 분리할 것을 권장한다.
3. 원칙의 동기와 필요성
3. 원칙의 동기와 필요성
인터페이스 분리 원칙의 주요 동기는 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 하여 시스템의 유연성과 견고성을 높이는 데 있다. 이 원칙이 없으면, 하나의 '두꺼운' 또는 '뚱뚱한' 인터페이스는 여러 클라이언트에게 불필요한 의존성을 강제하게 된다. 예를 들어, 프린터, 팩스, 스캐너 기능이 모두 결합된 다기능 장치 인터페이스를 생각해 볼 수 있다. 단순히 인쇄만 필요한 클라이언트는 팩스나 스캔 메서드의 존재를 알게 되며, 이 메서드들의 변경이 해당 클라이언트의 재컴파일이나 재배포를 유발할 수 있다[2]. 이는 시스템을 불필요하게 취약하게 만든다.
이 원칙의 필요성은 단일 책임 원칙과 깊은 연관성을 가진다. 단일 책임 원칙이 클래스 수준에서 하나의 변경 이유만을 가지도록 하는 것이라면, 인터페이스 분리 원칙은 인터페이스 수준에서 동일한 철학을 적용한다. 하나의 인터페이스가 여러 역할을 수행하도록 설계되면, 그 인터페이스를 구현하는 클래스는 사용하지 않는 메서드에 대해 의미 없는 구현(예: 예외를 던지거나 빈 메서드)을 제공해야 할 수 있다. 이는 코드를 오염시키고, 리팩토링과 이해를 어렵게 만든다.
결과적으로, 인터페이스 분리 원칙을 적용하면 유지보수성과 재사용성이 크게 향상된다. 세분화된 인터페이스는 각각 더 명확한 목적을 가지므로, 클라이언트는 정확히 필요한 기능에만 결합된다. 이는 의존성의 범위를 최소화하여, 한 부분의 변경이 시스템의 다른 불필요한 부분으로 전파되는 것을 방지한다. 또한, 작고 응집력 높은 인터페이스는 새로운 컨텍스트에서 재사용하기가 훨씬 쉬워진다.
3.1. 단일 책임 원칙(SRP)과의 관계
3.1. 단일 책임 원칙(SRP)과의 관계
단일 책임 원칙(SRP)은 하나의 클래스나 모듈이 변경되어야 하는 이유는 단 하나여야 한다는 원칙이다. 이 원칙은 주로 구체적인 클래스의 설계에 초점을 맞춘다. 반면 인터페이스 분리 원칙(ISP)은 "클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다"고 명시하며, 인터페이스 설계의 문제를 다룬다.
두 원칙은 서로 다른 차원에서 유사한 목표를 공유한다. SRP가 클래스의 책임을 분리하여 변경의 영향을 국소화하려 한다면, ISP는 인터페이스를 통해 클라이언트에 대한 의존성을 세분화한다. 결과적으로 ISP는 SRP를 인터페이스 수준에서 적용한 형태로 볼 수 있다. 하나의 '두꺼운' 인터페이스는 여러 클라이언트를 위한 서로 다른 책임을 포함하게 되며, 이는 SRP 위반을 초래한다.
다음 표는 두 원칙의 관계를 요약한다.
관점 | 단일 책임 원칙 (SRP) | 인터페이스 분리 원칙 (ISP) |
|---|---|---|
주요 적용 대상 | 클래스, 모듈 | 인터페이스 |
핵심 목표 | 변경 이유의 단일화 | 클라이언트별 의존성 최소화 |
문제 상황 | 하나의 클래스가 여러 이유로 변경됨 | 클라이언트가 사용하지 않는 메서드에 강제로 의존함 |
해결 전략 | 책임에 따라 클래스를 분리함 | 인터페이스를 클라이언트의 필요에 따라 분리함 |
따라서 ISP는 SRP를 준수하는 설계를 촉진하는 실질적인 도구 역할을 한다. SRP를 위반하는 모놀리식 클래스를 여러 클라이언트가 사용할 때, 그 클래스에 대한 인터페이스 역시 모놀리식이 될 가능성이 높다. ISP에 따라 이 인터페이스를 분리하면, 자연스럽게 각 인터페이스는 더 좁고 명확한 책임을 가지게 되며, 이는 궁극적으로 SRP를 지키는 더 작은 구현 클래스들의 등장으로 이어진다[3].
3.2. 인터페이스 오염과 시스템 취약성
3.2. 인터페이스 오염과 시스템 취약성
인터페이스 오염은 하나의 인터페이스가 서로 관련이 없는 여러 클라이언트를 위해 과도하게 많은 메서드를 포함하게 되는 현상을 말한다. 이는 특정 클라이언트가 자신이 실제로 사용하지 않는 메서드들에 대해 의존성을 가지게 만든다. 결과적으로, 한 클라이언트의 요구사항 변경이 인터페이스를 수정하게 만들고, 이는 다른 클라이언트들에게도 영향을 미칠 수 있다. 이렇게 불필요한 의존성이 생기면 시스템의 한 부분이 변경될 때 예상치 못한 다른 부분에서 오류가 발생할 위험이 크게 증가한다.
이러한 오염은 시스템의 취약성을 직접적으로 높인다. '두꺼운' 또는 '뚱뚱한' 인터페이스는 본질적으로 높은 결합도를 유발한다. 클라이언트는 자신이 필요로 하지 않는 기능의 구현 세부사항에 묶이게 되고, 이는 리팩토링을 어렵게 만든다. 또한, 인터페이스가 변경될 때마다 모든 의존 클라이언트를 다시 컴파일하거나 배포해야 하는 상황이 발생할 수 있다. 이는 소프트웨어의 진화와 확장을 방해하는 주요 장애물이 된다.
시스템 취약성은 단순한 컴파일 오류를 넘어 런타임 동작에도 영향을 미친다. 예를 들어, 분리되지 않은 인터페이스를 구현하는 구상 클래스는 일부 메서드에 대해 의미 없는 구현(예: null을 반환하거나 예외를 던지는 방식)을 제공할 수밖에 없다. 이는 리스코프 치환 원칙을 위반할 가능성을 높이고, 사용자에게 혼란을 줄 뿐만 아니라 버그를 유발할 수 있는 안전하지 않은 코드를 양산한다.
문제점 | 설명 | 결과 |
|---|---|---|
불필요한 의존성 | 클라이언트가 사용하지 않는 메서드에 강제로 의존하게 됨. | 변경의 영향 범위가 넓어지고, 재사용성이 떨어짐. |
변경의 파급 효과 | 한 클라이언트를 위한 변경이 다른 무관한 클라이언트에 영향을 미침. | 시스템이 취약해지고 유지보수 비용이 증가함. |
안전하지 않은 구현 | 관련 없는 메서드를 구현하기 위해 빈 구현이나 예외 발생 코드를 작성함. | 런타임 오류 가능성이 증가하고, 계약의 명확성이 훼손됨. |
따라서 인터페이스 분리 원칙은 인터페이스 오염을 방지함으로써, 각 모듈이 최소한의 필수 계약에만 의존하도록 보장한다. 이는 시스템의 각 구성 요소를 보다 격리된 상태로 유지시켜, 변경에 강하고 이해하기 쉬운 아키텍처를 구축하는 데 기여한다.
3.3. 유지보수성과 재사용성 향상
3.3. 유지보수성과 재사용성 향상
인터페이스 분리 원칙을 준수하면 시스템의 유지보수성이 크게 향상된다. 클라이언트가 자신이 실제로 사용하지 않는 메서드에 의존하지 않게 되므로, 해당하지 않는 인터페이스의 변경이 시스템 전반에 미치는 영향이 제한된다. 예를 들어, 하나의 거대한 인터페이스를 수정할 경우, 이를 구현하는 모든 클래스와 이를 사용하는 모든 클라이언트를 검토하고 수정해야 할 수 있다. 그러나 인터페이스가 세분화되어 있다면, 변경이 필요한 특정 기능과 직접 관련된 인터페이스만 수정하면 되고, 그 변경의 영향은 해당 인터페이스에 의존하는 클라이언트로만 국한된다. 이는 코드 변경의 부작용을 최소화하고 시스템의 안정성을 높인다.
또한, 이 원칙은 코드 재사용성을 증진시키는 핵심 메커니즘이 된다. 작고 응집된 인터페이스는 특정한 역할이나 기능에 집중하므로, 새로운 컨텍스트에서 해당 기능만 필요할 때 쉽게 재사용될 수 있다. 반면, 사용하지 않는 메서드로 가득 찬 '두꺼운' 인터페이스는 필요 이상의 의존성을 강제하여 재사용을 어렵게 만든다. 개발자는 필요한 기능만 제공하는 경량의 인터페이스를 선택하여 구현함으로써, 불필요한 코드 구현 부담 없이 컴포넌트를 다양한 곳에 활용할 수 있다.
아래 표는 인터페이스 분리 원칙 적용 전후의 유지보수성 및 재사용성 특성을 비교한다.
특성 | 분리 적용 전 (두꺼운 인터페이스) | 분리 적용 후 (세분화된 인터페이스) |
|---|---|---|
변경 영향도 | 인터페이스 한 부분의 변경이 모든 클라이언트에 영향을 미칠 수 있음 | 변경 영향이 해당 인터페이스를 사용하는 특정 클라이언트로 제한됨 |
기능 재사용 난이도 | 불필요한 메서드 구현이 강제되어 재사용이 어려움 | 필요한 기능만 담은 인터페이스를 쉽게 재사용 가능 |
클라이언트 의존성 | 사용하지 않는 메서드에 대한 불필요한 의존성 존재 | 실제 사용하는 메서드에만 명시적으로 의존 |
결과적으로, 인터페이스 분리 원칙은 소프트웨어 컴포넌트를 더욱 모듈화하고 결합도를 낮춤으로써, 장기적인 시스템 진화와 기능 확장을 용이하게 한다. 이는 요구사항 변경에 민첩하게 대응할 수 있는 유연한 아키텍처의 기반을 마련한다.
4. 원칙 적용 방법과 예시
4. 원칙 적용 방법과 예시
인터페이스 분할은 하나의 거대한 인터페이스를 클라이언트가 실제로 필요로 하는 기능별로 더 작고 응집된 인터페이스들로 나누는 과정이다. 핵심 전략은 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 하는 것이다. 예를 들어, 복합기 장치를 위한 MultiFunctionDevice 인터페이스에 print(), scan(), fax() 메서드가 모두 선언되어 있다면, 오직 인쇄 기능만 필요한 클라이언트는 스캔과 팩스 메서드에도 불필요하게 의존하게 된다. 이를 해결하기 위해 인터페이스를 Printer, Scanner, FaxMachine으로 분리하면, 클라이언트는 정확히 필요한 기능에만 의존할 수 있다.
클래스 기반 언어와 인터페이스 기반 언어에서의 적용 방식은 다르다. 자바, C#, TypeScript와 같이 인터페이스를 명시적으로 지원하는 언어에서는 언어의 기본 기능을 사용하여 쉽게 분리를 구현한다. 반면, C++와 같은 다중 상속을 지원하는 클래스 기반 언어에서는 순수 가상 함수를 가진 추상 클래스를 인터페이스처럼 사용하여 원칙을 적용한다. 파이썬과 같은 덕 타이핑 언어에서는 암시적 인터페이스에 의존하므로, 클라이언트가 실제로 호출하는 메서드들만을 제공하는 작은 객체를 설계함으로써 원칙의 정신을 따를 수 있다.
다음은 인터페이스 분리 원칙을 적용하기 전과 후의 간단한 코드 예시이다.
Before (적용 전):
```java
interface Worker {
void work();
void eat();
}
class HumanWorker implements Worker {
public void work() { /* 일을 한다 */ }
public void eat() { /* 점심을 먹는다 */ }
}
class RobotWorker implements Worker {
public void work() { /* 일을 한다 */ }
public void eat() {
// 로봇은 먹지 않지만, 인터페이스 구현을 위해 빈 메서드를 강제해야 함
throw new UnsupportedOperationException("Robot doesn't eat");
}
}
```
After (적용 후):
```java
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class HumanWorker implements Workable, Eatable {
public void work() { /* 일을 한다 */ }
public void eat() { /* 점심을 먹는다 */ }
}
class RobotWorker implements Workable {
public void work() { /* 일을 한다 */ }
// eat() 메서드를 구현할 필요가 전혀 없음
}
```
적용 후에는 RobotWorker 클래스가 자신과 무관한 eat 메서드를 구현하거나 더미 코드를 작성할 필요가 사라진다. 각 클라이언트(Human 관리 모듈, Robot 관리 모듈)는 이제 자신이 관심 있는 인터페이스(Workable, Eatable)에만 의존하게 되어 시스템의 결합도가 낮아지고 유연성이 향상된다.
4.1. 인터페이스 분할 전략
4.1. 인터페이스 분할 전략
인터페이스 분할은 단순히 큰 인터페이스를 여러 개로 쪼개는 것이 아니라, 클라이언트의 실제 사용 관점에 따라 응집된 단위로 재구성하는 과정이다. 효과적인 분할을 위한 주요 전략으로는 클라이언트 기반 분리, 역할 기반 분리, 그리고 기능적 응집도에 따른 분리가 있다.
클라이언트 기반 분리는 이 원칙의 핵심 동기에서 직접 비롯된다. 특정 클라이언트가 사용하지 않는 메서드에 의존하지 않도록, 각 클라이언트 유형별로 전용 인터페이스를 제공한다. 예를 들어, 보고서를 생성하는 클라이언트와 데이터를 저장하는 클라이언트가 하나의 두꺼운 인터페이스를 공유한다면, 이를 ReportGenerator 인터페이스와 DataPersister 인터페이스로 분리한다. 역할 기반 분리는 객체가 수행하는 다양한 역할을 명확히 구분한다. 하나의 클래스가 Printer, Scanner, FaxMachine 역할을 모두 수행한다면, 세 가지 별도의 인터페이스를 구현하도록 설계하여, 팩스 기능이 없는 클라이언트는 FaxMachine 인터페이스에 의존하지 않게 한다.
분할 시 고려해야 할 구체적인 지침은 다음과 같다.
전략 | 설명 | 고려 사항 |
|---|---|---|
사용 시나리오 분석 | 인터페이스 메서드들이 함께 호출되는 패턴을 조사한다. | 항상 함께 사용되는 메서드 그룹은 하나의 인터페이스로 유지한다. |
의존성 방향 제어 | 고수준 모듈이 저수준 모듈의 구체적인 메서드에 의존하지 않게 한다. | 의존성 역전 원칙과 결합하여 추상화에만 의존하도록 한다. |
새로운 클라이언트 예측 | 현재는 하나의 클라이언트뿐이라도, 향후 다른 요구를 가진 클라이언트가 생길 수 있다. | 확장성을 고려하여 적절한 수준의 분할을 미리 설계한다. |
분할의 궁극적인 목표는 시스템의 결합도를 낮추는 것이므로, 인터페이스가 지나치게 많아져 관리 복잡성이 증가하는 '인터페이스 폭발' 현상을 주의해야 한다. 실용적인 접근법은 초기에는 비교적 넓은 인터페이스로 시작하여, 클라이언트의 요구가 분화되고 변경이 빈번해지는 지점에서 점진적으로 인터페이스를 분리하는 것이다. 이는 단일 책임 원칙과도 깊이 연관되어, 각 인터페이스가 하나의 명확한 책임이나 역할을 갖도록 보장한다.
4.2. 클래스 기반 vs 인터페이스 기반 언어 적용
4.2. 클래스 기반 vs 인터페이스 기반 언어 적용
인터페이스 분리 원칙은 객체 지향 프로그래밍에서 사용되는 설계 원칙이지만, 언어의 특성에 따라 적용 방식에 차이가 발생한다. 주로 인터페이스를 명시적으로 지원하는 언어와 그렇지 않은 클래스 기반 언어에서의 접근법이 다르다.
인터페이스를 일급 요소로 지원하는 언어(예: Java, C#, TypeScript)에서는 ISP를 적용하는 것이 직관적이다. 이러한 언어들은 interface나 protocol 같은 키워드를 통해 명시적인 계약을 정의할 수 있다. 개발자는 하나의 큰 인터페이스를 여러 개의 작고 집중적인 인터페이스로 분리하여, 클라이언트가 정말 필요한 메서드에만 의존하도록 설계한다. 예를 들어, Printer, Scanner, Fax 기능이 결합된 MultiFunctionDevice 인터페이스를, 각 기능별로 분리된 세 개의 인터페이스로 나눌 수 있다. 클라이언트 클래스는 Printer 인터페이스만 구현하는 객체를 사용하여, scan이나 fax 메서드에 대한 의존성을 제거한다.
반면, 인터페이스를 문법적으로 지원하지 않는 클래스 기반 언어(예: C++ [4], Python)에서는 다른 방식으로 원칙을 구현한다. 이러한 언어에서는 추상 기본 클래스나 믹스인, 또는 덕 타이핑 개념을 활용한다. C++에서는 순수 가상 함수만으로 구성된 추상 클래스를 인터페이스처럼 사용한다. Python과 같은 동적 타입 언어에서는 명시적인 인터페이스 선언이 없으므로, 특정 메서드 집합을 제공하는 객체는 암시적으로 그 '인터페이스'를 구현한 것으로 간주한다[5]. 따라서 ISP는 클래스의 공개 메서드 집합을 최소한으로 유지하고, 클라이언트별로 필요한 메서드만 노출하는 방식으로 설계에 반영된다. 이는 상속 계층을 세분화하거나, 조합을 통한 기능 위임으로 달성된다.
언어 유형 | 주요 구현 메커니즘 | ISP 적용 특징 |
|---|---|---|
인터페이스 기반 언어 (Java, C#) | 명시적 | 인터페이스 분리가 문법적 수준에서 명확하게 지원된다. |
클래스 기반 언어 (C++) | 순수 가상 함수를 가진 추상 기본 클래스 | 인터페이스는 추상 클래스로 흉내 내며, 다중 상속을 통해 조합할 수 있다. |
동적 타입 언어 (Python) | 암시적 프로토콜 (덕 타이핑), 추상 기본 클래스(ABC) | 객체의 행동(메서드 집합)이 인터페이스를 정의하므로, 메서드 노출 최소화에 초점을 맞춘다. |
결론적으로, 언어의 패러다임이 적용 방식을 규정하지만, 핵심 목표는 동일하다. 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 설계하여 결합도를 낮추고 유연성을 높이는 것이다.
4.3. 실제 코드 예시 (Before/After)
4.3. 실제 코드 예시 (Before/After)
인터페이스 분리 원칙을 위반하는 전형적인 예는 하나의 거대한 인터페이스가 여러 클라이언트의 요구를 모두 포함하는 경우이다. 예를 들어, 프린터, 스캐너, 팩스 기능을 모두 갖춘 복합기 장치를 모델링한다고 가정해 보자.
Before (ISP 위반 사례)
다음은 모든 기능을 하나의 인터페이스에 몰아넣은 설계이다.
```java
interface MultiFunctionDevice {
void print(Document d);
void scan(Document d);
void fax(Document d);
}
class OldFashionedPrinter implements MultiFunctionDevice {
public void print(Document d) {
// 실제 프린트 로직
}
public void scan(Document d) {
throw new UnsupportedOperationException(); // 지원하지 않는 기능
}
public void fax(Document d) {
throw new UnsupportedOperationException(); // 지원하지 않는 기능
}
}
class Photocopier implements MultiFunctionDevice {
public void print(Document d) {
// 프린트 로직
}
public void scan(Document d) {
// 스캔 로직
}
public void fax(Document d) {
throw new UnsupportedOperationException(); // 지원하지 않는 기능
}
}
```
OldFashionedPrinter 클래스는 프린트 기능만 필요하지만, scan과 fax 메서드를 구현해야 하므로 사용하지 않는 메서드에 대해 예외를 던지게 된다. 이는 클라이언트가 사용하지도 않는 메서드에 의존하게 만들어 결합도를 높이고, 코드를 취약하게 만든다.
After (ISP 적용 사례)
인터페이스 분리 원칙을 적용하여 기능별로 세분화된 인터페이스를 정의한다.
```java
interface Printer {
void print(Document d);
}
interface Scanner {
void scan(Document d);
}
interface Fax {
void fax(Document d);
}
// 기능별 인터페이스를 조합하여 복합기 구현
interface MultiFunctionMachine extends Printer, Scanner, Fax {
// 추가적인 복합기 고유 기능 가능
}
class OldFashionedPrinter implements Printer {
public void print(Document d) {
// 실제 프린트 로직
}
}
class Photocopier implements Printer, Scanner {
public void print(Document d) {
// 프린트 로직
}
public void scan(Document d) {
// 스캔 로직
}
}
class HighEndDevice implements MultiFunctionMachine {
public void print(Document d) { /* 구현 */ }
public void scan(Document d) { /* 구현 */ }
public void fax(Document d) { /* 구현 */ }
}
```
이제 각 클래스는 자신이 실제로 사용하는 기능에 해당하는 인터페이스만 구현하면 된다. 클라이언트(예: PrintClient 클래스)도 Printer 인터페이스에만 의존하여 오직 프린트 기능을 사용할 수 있다. 이로 인해 불필요한 의존성이 제거되고, 시스템의 모듈성과 재사용성이 향상된다.
적용 전 문제점 | 적용 후 개선점 |
|---|---|
사용하지 않는 메서드 강제 구현 | 필요한 인터페이스만 선택 구현 |
불필요한 의존성으로 인한 결합도 증가 | 명확한 계약으로 결합도 감소 |
인터페이스 변경이 모든 클라이언트에 영향 | 인터페이스 변경 영향 범위 최소화 |
테스트 시 불필요한 모킹(Mocking) 필요 | 관심사에 집중한 단순한 테스트 가능 |
이 예시는 단일 책임 원칙이 클래스 수준의 응집도를 다룬다면, 인터페이스 분리 원칙은 인터페이스 수준에서 클라이언트의 관점을 고려하여 의존성을 관리한다는 점을 보여준다.
5. 다른 SOLID 원칙과의 관계
5. 다른 SOLID 원칙과의 관계
단일 책임 원칙과 깊은 연관성을 가진다. ISP는 인터페이스 수준에서 SRP를 적용한 것으로 볼 수 있다. SRP가 "하나의 클래스는 하나의 이유로만 변경되어야 한다"는 원칙이라면, ISP는 "하나의 인터페이스는 하나의 클라이언트 그룹을 위해 존재해야 한다"는 원칙이다. 즉, 인터페이스도 하나의 명확한 책임만을 가져야 한다는 점에서 두 원칙의 정신은 일치한다.
리스코프 치환 원칙과는 적용 대상과 목적에서 차이가 있다. LSP는 상속 계층 구조에서 하위 타입이 상위 타입을 완전히 대체할 수 있어야 함을 강조하는 반면, ISP는 인터페이스 자체의 설계 품질과 클라이언트에 대한 의존성의 정확성을 다룬다. 그러나 두 원칙 모두 시스템의 신뢰성과 확장성을 높이기 위해 결합도를 낮추는 데 기여한다는 공통점을 가진다.
의존성 역전 원칙의 실현을 위한 구체적인 수단으로 작동한다. DIP는 "추상화에 의존해야 하며, 구체화에 의존해서는 안 된다"고 명시한다. ISP는 이 추상화(인터페이스)가 지나치게 크고 범용적이어서는 안 되며, 클라이언트의 실제 필요에 맞게 세분화되어야 함을 제시한다. 잘 분리된 세밀한 인터페이스는 DIP를 통해 유연한 의존성 주입을 가능하게 하는 기반이 된다.
SOLID 원칙들은 상호 보완적으로 작용하여 전반적인 객체 지향 설계의 견고함을 향상시킨다. ISP는 특히 SRP와 DIP 사이의 간극을 메우는 역할을 하며, 이들 원칙이 함께 적용될 때 시스템은 변경에 더욱 강하고 모듈화가 잘 이루어진 구조를 갖게 된다.
5.1. 단일 책임 원칙(SRP)과의 연계
5.1. 단일 책임 원칙(SRP)과의 연계
단일 책임 원칙(SRP)과 인터페이스 분리 원칙(ISP)은 서로 밀접하게 연관되어 있으며, 종종 동일한 문제를 클래스와 인터페이스라는 서로 다른 관점에서 바라본 것으로 설명된다. SRP는 하나의 클래스가 변경되어야 하는 이유는 단 하나여야 한다고 명시한다. 즉, 하나의 클래스는 하나의 책임만 가져야 한다. ISP는 이 개념을 인터페이스 수준으로 확장한다. 하나의 '두꺼운' 인터페이스는 여러 클라이언트의 요구를 모두 포함함으로써 사실상 여러 책임을 지게 되며, 이는 SRP 위반과 유사한 상황을 초래한다.
구체적으로, SRP를 위반한 클래스는 여러 이유로 변경될 수 있어 모듈성이 떨어지고 취약해진다. 마찬가지로, ISP를 위반한 인터페이스는 서로 관련 없는 메서드들을 하나로 묶어, 그 인터페이스를 구현하는 클래스가 사용하지도 않는 메서드에 대한 의존성을 강제한다. 이는 해당 클래스가 여러 클라이언트의 변경에 영향을 받게 만든다. 따라서 ISP는 SRP가 클래스의 '책임'에 초점을 맞춘다면, 인터페이스의 '클라이언트'에 초점을 맞춘 원칙이라 볼 수 있다. 인터페이스를 클라이언트별로 분리하는 것은 결국 인터페이스가 하나의 클라이언트 집단에 대한 하나의 책임만 가지도록 하는 것이다.
두 원칙의 적용은 상호 보완적이다. 잘 설계된 시스템에서는 SRP에 따라 책임이 분리된 클래스들이, ISP에 따라 클라이언트의 필요에 맞게 세분화된 인터페이스를 통해 협력한다. 예를 들어, 하나의 복합기 클래스가 인쇄, 스캔, 팩스 기능을 모두 담당한다면 이는 SRP 위반이다. 이를 SRP에 따라 프린터, 스캐너, 팩스기 클래스로 분리하고, ISP에 따라 Printable, Scannable, Faxable 인터페이스를 제공하면, 클라이언트는 정확히 필요한 기능에만 의존할 수 있다. 이처럼 SRP와 ISP는 결합도를 낮추고 응집도를 높여 유연하고 견고한 설계를 달성하기 위한 공동의 목표를 지닌다.
5.2. 리스코프 치환 원칙(LSP)과의 비교
5.2. 리스코프 치환 원칙(LSP)과의 비교
리스코프 치환 원칙(LSP)과 인터페이스 분리 원칙(ISP)은 모두 객체 지향 설계의 SOLID 원칙을 구성하며, 특히 하위 타입의 안정성과 클라이언트의 의존성을 관리한다는 점에서 깊은 연관성을 가진다. 두 원칙은 서로 다른 문제를 해결하지만, 궁극적으로 시스템의 견고성과 유연성을 높이는 데 기여한다.
LSP는 상속 계층 구조에서 하위 클래스가 상위 클래스의 계약을 완전히 준수해야 함을 강조한다. 즉, 클라이언트가 상위 타입을 사용하는 코드를 변경하지 않고도 하위 타입으로 안전하게 교체할 수 있어야 한다. 반면 ISP는 하나의 '두꺼운' 인터페이스가 여러 클라이언트에게 불필요한 메서드에 대한 의존성을 강요함으로써 발생하는 문제를 해결한다. ISP를 위반하면, 클라이언트는 사용하지도 않는 메서드의 변경에 영향을 받게 되어 LSP의 전제 조건인 '안전한 치환'을 저해할 수 있다.
구체적으로 비교하면, LSP는 '행위적 하위 타입화'에 초점을 맞춰 타입의 치환 가능성을 보장하는 반면, ISP는 인터페이스의 '구조적 최소화'에 초점을 맞춰 클라이언트별로 최적화된 의존성을 제공한다. 다음 표는 두 원칙의 주요 차이점을 정리한다.
비교 요소 | 리스코프 치환 원칙 (LSP) | 인터페이스 분리 원칙 (ISP) |
|---|---|---|
주요 관심사 | 상속 계층에서의 치환 가능성과 행위 일관성 | 인터페이스의 세분화와 클라이언트별 의존성 최소화 |
위반 시 나타나는 문제 | 하위 클래스가 상위 클래스의 기대 행위를 깨트려 예상치 못한 오동작 발생 | 클라이언트가 사용하지 않는 메서드 변경에 영향을 받아 시스템이 취약해짐 |
해결 전략 | 역할에 따른 인터페이스 분리 및 클라이언트 특화 인터페이스 제공 |
결론적으로, LSP는 타입 계층의 수직적 안정성을 담당한다면, ISP는 클라이언트와 제공자 사이의 수평적 결합도를 관리한다고 볼 수 있다. 잘 설계된 시스템에서는 ISP를 통해 클라이언트가 정말 필요한 메서드에만 의존하게 만들고, LSP를 통해 그 인터페이스를 구현한 모든 구체 클래스가 기대된 대로 동작하도록 보장한다. 따라서 두 원칙은 협력하여 더욱 모듈화되고 변경에 강한 설계를 가능하게 한다.
5.3. 의존성 역전 원칙(DIP)에서의 역할
5.3. 의존성 역전 원칙(DIP)에서의 역할
의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙이다. 구체적으로, DIP는 세부 사항이 추상화에 의존하도록 함으로써 시스템의 결합도를 낮추고 유연성을 높이는 것을 목표로 한다. 이때, 인터페이스 분리 원칙(ISP)은 DIP가 성공적으로 적용되기 위한 필수적인 전제 조건 역할을 한다.
DIP의 핵심인 '추상화에 의존하라'는 지침을 실현할 때, 그 추상화의 형태가 바로 인터페이스이다. 만약 이 인터페이스가 지나치게 크고 두꺼워서 여러 클라이언트의 관심사를 한데 묶어놓았다면, DIP를 적용하더라도 문제가 발생한다. 고수준 모듈은 여전히 자신이 사용하지 않는 저수준 모듈의 메서드에 대한 의존성을 강제로 가지게 되기 때문이다. ISP는 이러한 문제를 해결하여, DIP가 의도하는 진정한 추상화 의존과 결합도 감소를 가능하게 한다.
실제로 DIP를 적용한 설계에서 ISP는 다음과 같은 구체적인 역할을 수행한다. DIP에 따라 추상화된 인터페이스를, 클라이언트의 특정 역할에 맞게 더 세분화하여 분리한다. 이로 인해 클라이언트는 정말로 필요한 연산에만 의존할 수 있게 되고, 시스템에 새로운 저수준 모듈을 추가하거나 기존 모듈을 변경할 때 발생하는 영향의 범위가 최소화된다. 결과적으로 DIP가 제공하는 구조적 유연성은 ISP에 의해 그 실용성과 견고함이 보강된다.
원칙 | 주요 초점 | ISP의 역할 |
|---|---|---|
의존성 역전 원칙 (DIP) | 의존성 방향 (추상화 → 구체화) | DIP가 정의한 추상화 인터페이스의 '질'을 보장 |
인터페이스 분리 원칙 (ISP) | 인터페이스의 크기와 응집도 | 클라이언트가 필요로 하는 최소한의 의존성만을 제공하여 DIP의 효과 극대화 |
요약하면, ISP는 DIP가 의도하는 바를 실현하는 데 있어 중요한 실천 도구이다. DIP가 '무엇에 의존해야 하는가'에 대한 방향을 제시한다면, ISP는 '그 의존 대상이 어떻게 구성되어야 하는가'에 대한 구체적인 방법을 제공한다. 따라서 두 원칙은 함께 적용될 때 비로소 느슨한 결합과 높은 유연성을 갖춘 설계를 달성할 수 있다.
6. 장점과 이점
6. 장점과 이점
인터페이스 분리 원칙을 적용하면 시스템 설계에 몇 가지 명확한 이점을 가져온다. 가장 큰 장점은 결합도가 감소한다는 점이다. 클라이언트는 자신이 실제로 사용하는 메서드에만 의존하게 되어, 사용하지 않는 다른 메서드의 변경으로부터 영향을 받지 않는다. 이는 시스템의 한 부분을 수정할 때 예상치 못한 다른 부분의 오류를 최소화하며, 변경의 영향을 국소화하는 데 도움을 준다.
시스템의 유연성과 재사용성이 크게 향상된다. 세분화된 인터페이스는 다양한 클라이언트의 요구에 맞게 더 쉽게 조합되고 구현될 수 있다. 예를 들어, 하나의 거대한 인터페이스를 구현하는 대신, 필요한 몇 개의 작은 인터페이스만 선택적으로 구현함으로써 클래스의 목적이 더 명확해지고 불필요한 코드를 제거할 수 있다. 이는 새로운 기능 추가나 기존 기능 수정을 더 쉽게 만든다.
테스트 용이성도 중요한 이점 중 하나이다. 클라이언트를 테스트할 때, 실제로 의존하는 메서드만을 가진 간단한 목 객체나 스텁을 쉽게 생성할 수 있다. 거대한 인터페이스를 완전히 구현하거나 모의해야 하는 부담이 줄어들어 단위 테스트 작성이 훨씬 간편해진다. 결과적으로 코드의 품질과 안정성을 높이는 데 기여한다.
마지막으로, 이 원칙은 코드의 가독성과 유지보수성을 개선한다. 인터페이스가 명확하고 구체적인 책임을 가짐으로써, 개발자가 해당 모듈의 용도를 이해하기 쉬워진다. 시스템의 복잡성이 관리 가능한 수준으로 분산되며, 장기적인 프로젝트 생명 주기에서 변경 비용을 낮추는 효과를 가져온다.
6.1. 결합도 감소
6.1. 결합도 감소
인터페이스 분리 원칙을 적용하면 클라이언트는 자신이 실제로 사용하는 메서드에만 의존하게 된다. 이는 클라이언트와 서비스 제공자 사이의 결합도를 현저히 낮춘다. 클라이언트가 필요하지 않은 기능에 대한 지식이나 의존성을 갖지 않기 때문에, 서비스 제공자의 내부 구현이 변경되더라도 영향을 받는 범위가 최소화된다.
낮은 결합도는 시스템의 모듈성을 강화한다. 각 인터페이스는 더 좁고 구체적인 책임을 가지므로, 해당 인터페이스를 구현하는 클래스의 변경 사유가 명확해진다. 하나의 모듈을 수정할 때 다른 모듈을 변경해야 할 가능성이 줄어들어, 시스템의 한 부분을 독립적으로 이해하고 수정하기가 쉬워진다.
결합도 감소의 직접적인 효과는 다음과 같은 표로 정리할 수 있다.
높은 결합도의 상황 (ISP 위반) | 낮은 결합도의 상황 (ISP 준수) |
|---|---|
클라이언트가 사용하지 않는 메서드 변경 시에도 재컴파일/재배포 필요 | 사용하는 메서드만 변경될 경우에만 영향 받음 |
한 인터페이스의 변경이 다수의 클라이언트에 파급 효과를 줌 | 변경의 영향이 특정 클라이언트 집단으로 국한됨 |
시스템 구성 요소 간 의존 관계가 복잡하고 이해하기 어려움 | 의존 관계가 명확하고 단순해짐 |
이러한 구조는 대규모 협업 개발 환경이나 장기적인 유지보수에서 특히 유리하다. 개발자는 자신이 담당하는 모듈과 직접적으로 연관된 인터페이스만 신경 쓰면 되며, 시스템의 다른 부분에 대한 깊은 이해 없이도 작업을 진행할 수 있다. 결과적으로 코드의 안정성이 높아지고 변경에 대한 두려움이 줄어든다.
6.2. 시스템 유연성 증가
6.2. 시스템 유연성 증가
인터페이스 분리 원칙을 적용하면 시스템의 결합도가 낮아지고, 이는 시스템의 유연성을 크게 향상시킨다. 클라이언트가 자신이 실제로 사용하는 메서드에만 의존하게 되므로, 시스템의 한 부분을 변경하거나 확장할 때 다른 부분에 미치는 영향을 최소화할 수 있다. 이는 새로운 기능 추가나 기존 기능 수정 시 발생하는 파급 효과를 줄여준다.
시스템의 구성 요소들이 더 작고 응집력 있는 인터페이스를 통해 상호작용하게 되면, 개별 컴포넌트를 독립적으로 교체하거나 업그레이드하기가 훨씬 쉬워진다. 예를 들어, 특정 클라이언트의 요구사항이 변경되어 새로운 동작이 필요할 때, 기존의 '두꺼운' 인터페이스를 전체적으로 수정하지 않고도 해당 클라이언트 전용의 새로운 인터페이스를 만들고 구현체를 제공할 수 있다. 이는 개방-폐쇄 원칙을 실현하는 데에도 기여한다.
유연성 증가는 다양한 환경과 요구사항에 시스템을 적응시키는 능력을 의미한다. ISP를 준수한 설계는 다음과 같은 이점을 제공한다.
유연성 측면 | 설명 |
|---|---|
기능 확장 | 새로운 클라이언트 유형이 등장해도 기존 인터페이스를 변경하지 않고 새로운 인터페이스를 추가하여 대응할 수 있다. |
기술 스택 변경 | 특정 구현 기술(예: 데이터베이스, 외부 서비스)을 교체할 때, 해당 기술에 의존하는 인터페이스만 새로 구현하면 된다. |
테스트 및 모킹 | 작은 인터페이스는 테스트 목적의 모의 객체를 만들기 쉽고, 다양한 시나리오를 구성하는 데 유리하다. |
결론적으로, 인터페이스 분리 원칙은 시스템을 단단하게 결합된 하나의 덩어리가 아닌, 느슨하게 연결된 모듈들의 집합으로 만든다. 이는 비즈니스 요구사항의 변화나 기술적 진화에 대해 시스템이 더 민첩하고 탄력적으로 대응할 수 있는 기반을 마련해준다.
6.3. 테스트 용이성 향상
6.3. 테스트 용이성 향상
인터페이스 분리 원칙을 준수하면 단위 테스트 작성이 훨씬 용이해진다. 클라이언트가 자신이 실제로 사용하는 메서드들만 의존하도록 인터페이스를 분리하면, 테스트를 위한 목 객체나 스텁을 생성하고 구성하는 작업이 간소화된다. 특정 기능만 테스트하려는 경우, 관련 없는 메서드들을 구현할 필요 없이 해당 기능에 집중된 작은 인터페이스의 가짜 구현체를 쉽게 만들 수 있다.
예를 들어, 데이터를 저장하고 로그를 기록하는 두 가지 책임을 가진 '두꺼운' 인터페이스가 있다고 가정하자. 저장 기능만 테스트하려면, 로그 기록과 관련된 모든 메서드에 대한 더미 구현을 테스트 더블에 제공해야 하며, 이는 불필요한 상용구 코드를 양산한다. ISP에 따라 인터페이스를 DataPersister와 Logger로 분리하면, 저장 기능 테스트 시 DataPersister 인터페이스의 목 객체만 정확히 준비하면 된다. 이는 테스트의 의도를 명확히 하고, 테스트 코드의 복잡성을 낮춘다.
또한, 작고 응집된 인터페이스는 테스트의 격리성을 높인다. 한 인터페이스의 변경이 여러 클라이언트에 영향을 미칠 가능성이 줄어들기 때문에, 변경으로 인한 테스트 실패의 범위가 제한된다. 이는 테스트를 더 안정적으로 만들고, 리팩토링에 대한 자신감을 부여한다. 결과적으로 ISP는 시스템의 테스트 커버리지를 높이고 유지보수 비용을 낮추는 데 기여한다.
7. 주의사항과 오용 사례
7. 주의사항과 오용 사례
인터페이스 분리 원칙을 지나치게 엄격하게 적용하면 인터페이스 폭발 문제가 발생할 수 있다. 이는 하나의 거대한 인터페이스를 너무 많은 작은 인터페이스로 분리함으로써, 시스템에 수많은 인터페이스가 생겨 관리가 어려워지는 현상을 가리킨다. 클라이언트가 여러 협력 객체와 상호작용해야 할 경우, 오히려 복잡성이 증가하고 코드 가독성이 떨어질 수 있다. 따라서 원칙의 이점을 얻기 위해서는 실용적인 판단이 필요하며, 항상 현재와 예상 가능한 미래의 요구사항을 함께 고려해야 한다.
적용 시 고려해야 할 핵심 요소는 클라이언트의 실제 사용 패턴이다. 서로 다른 클라이언트 그룹이 인터페이스의 서로 다른 메서드 조합을 사용한다면 분할의 강력한 신호가 된다. 반면, 대부분의 클라이언트가 인터페이스의 거의 모든 메서드를 필요로 한다면, 분할은 불필요한 오버헤드를 초래할 뿐이다. 또한, 변경의 빈도와 이유도 중요한 기준이 된다. 함께 변경되는 이유가 같은 메서드들은 동일한 인터페이스에 속하는 것이 바람직하다.
실용성과 원칙 사이의 균형을 유지하는 것이 중요하다. 이상적인 설계를 추구하다 보면 지나치게 세분화된 인터페이스가 만들어질 수 있으며, 이는 프로젝트 초기 단계에서는 예측하기 어렵다. 때로는 명확한 사용 패턴이 나타날 때까지 인터페이스 분할을 미루는 것이 더 나은 전략이 될 수 있다. 최종 목표는 클라이언트가 자신이 실제로 사용하지 않는 메서드에 의존하지 않도록 하면서도, 시스템 전체의 복잡도를 관리 가능한 수준으로 유지하는 것이다.
7.1. 과도한 분할로 인한 인터페이스 폭발
7.1. 과도한 분할로 인한 인터페이스 폭발
인터페이스 분리 원칙을 지나치게 엄격하게 적용하면, 시스템에 수많은 작은 인터페이스가 생성되는 인터페이스 폭발 문제가 발생할 수 있다. 이는 각 인터페이스가 단 하나의 메서드만을 포함하는 극단적인 상황까지 이어질 수 있으며, 결과적으로 클라이언트가 필요한 기능을 사용하기 위해 여러 인터페이스를 동시에 구현하거나 의존해야 하는 복잡한 상황을 초래한다. 이러한 과도한 분할은 오히려 코드베이스를 이해하고 관리하기 어렵게 만들며, 설계의 실용성을 떨어뜨린다.
인터페이스 폭발은 특히 클래스 기반 언어에서 두드러진다. 예를 들어, 하나의 클래스가 여러 세분화된 인터페이스를 모두 구현해야 할 경우, 클래스 선언부가 불필요하게 길어지고 구현 자체는 단순하지만 서명 관리가 번거로워진다. 이는 원칙을 적용하는 본래 목적인 단순성과 명확성을 해치는 결과를 낳는다. 따라서 설계자는 인터페이스를 분리할 때 기능적 응집도를 기준으로 합리적인 크기로 그룹화하는 전략이 필요하다.
적용 시 고려해야 할 핵심 요소는 실용성과 원칙 사이의 균형이다. 인터페이스 분리의 궁극적 목표는 클라이언트가 사용하지 않는 메서드에 의존하지 않게 하는 것이지, 인터페이스의 개수를 최소화하거나 최대화하는 것이 아니다. 다음과 같은 요소를 고려하여 분할 수준을 결정하는 것이 바람직하다.
고려 요소 | 설명 | 과도한 분할 시 발생 가능한 문제 |
|---|---|---|
클라이언트의 다양성 | 인터페이스를 사용하는 클라이언트 유형이 몇 가지인가? | 유사한 클라이언트 그룹을 위해 중복된 인터페이스가 다수 생성될 수 있다. |
변경의 빈도와 범위 | 인터페이스 내 메서드들이 함께 변경되는가? | 변경 주기가 같은 메서드들이 서로 다른 인터페이스로 흩어지면 변경 관리가 복잡해진다. |
의존성 관리 | 클라이언트가 정말로 불필요한 메서드를 의존하게 되는가? | 의존성 주입이나 어댑터 패턴 등으로 해결 가능한 문제까지 인터페이스 분할로 접근할 수 있다. |
결론적으로, 인터페이스 분리 원칙은 맹목적으로 적용하기보다는 시스템의 복잡도, 팀의 협업 방식, 그리고 장기적인 유지보수 비용을 종합적으로 평가하여 적용해야 한다. 작고 명확한 인터페이스를 지향하되, 그것이 프로젝트 전반의 설계 복잡성을 급격히 증가시키지 않도록 주의해야 한다.
7.2. 실용성과 원칙 사이의 균형
7.2. 실용성과 원칙 사이의 균형
인터페이스 분리 원칙을 적용할 때는 원칙의 순수성보다 실용적인 요구사항을 함께 고려하는 것이 중요하다. 지나치게 많은 작은 인터페이스를 생성하면 오히려 시스템의 복잡도를 증가시키고 이해하기 어려운 설계를 초래할 수 있다. 이 현상을 인터페이스 폭발이라고 부르며, 관리해야 할 의존성이 기하급수적으로 늘어나는 문제점이 있다.
적용 시에는 클라이언트의 실제 사용 패턴과 변경 가능성을 신중히 평가해야 한다. 여러 클라이언트가 인터페이스의 동일한 메서드 하위 집합을 공유한다면, 그룹을 유지하는 것이 더 합리적일 수 있다. 또한, 변경 주기가 동일한 메서드들은 하나의 인터페이스에 묶는 것이 변경 관리를 용이하게 한다. 원칙의 궁극적 목표는 유연하고 견고한 설계이므로, 무의미한 분할보다는 의미 있는 경계를 식별하는 데 초점을 맞춰야 한다.
다음은 균형을 맞출 때 고려할 수 있는 요소를 정리한 표다.
고려 요소 | 설명 | 균형 예시 |
|---|---|---|
클라이언트 다양성 | 인터페이스를 사용하는 클라이언트의 종류와 요구사항이 얼마나 다른가 | 클라이언트 그룹별로 명확히 다른 기능을 사용하면 분리, 대부분의 기능이 중복되면 통합 |
변경 빈도 | 인터페이스 내 메서드들의 변경 이유와 시점이 일치하는가 | 함께 변경되는 메서드는 같은 인터페이스에 유지, 독립적으로 변경되는 메서드는 분리 |
시스템 규모와 수명 | 프로젝트의 규모와 예상 수명 주기 | 장기적이고 대규모 프로젝트는 원칙을 엄격히 적용, 소규모 프로토타입은 실용적 접근 |
결론적으로, 인터페이스 분리 원칙은 맹목적으로 따르기보다는 설계 의사결정의 지침으로 활용해야 한다. 개발자는 '클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않아야 한다'는 본질을 유지하면서, 과도한 설계 복잡도를 초래하지 않는 선에서 적절한 수준의 인터페이스 결합도를 유지해야 한다.
7.3. 적용 시 고려해야 할 요소
7.3. 적용 시 고려해야 할 요소
인터페이스 분할 시 가장 먼저 고려해야 할 요소는 도메인 모델과 비즈니스 로직의 경계이다. 인터페이스는 응집력 있는 기능 집합을 나타내야 하며, 이는 실제 업무 흐름이나 개념적 단위와 일치하는 것이 이상적이다. 단순히 메서드 개수를 줄이기 위해 임의로 분할하는 것은 오히려 설계를 복잡하게 만들 수 있다.
두 번째 요소는 클라이언트의 다양성과 변화 주기이다. 서로 다른 클라이언트가 동일한 인터페이스의 서로 다른 부분만 사용한다면 분할의 강력한 신호가 된다. 반면, 대부분의 클라이언트가 인터페이스의 모든 메서드를 함께 사용한다면, 그것은 하나의 응집된 인터페이스로 남아 있어야 할 가능성이 높다. 변화의 이유와 시기가 다른 메서드들을 묶어두는 것은 미래의 불필요한 재컴파일과 재배포를 초래한다.
고려 요소 | 설명 | 주의점 |
|---|---|---|
도메인 응집성 | 인터페이스가 하나의 명확한 추상화 수준과 책임을 가져야 함 | 기술적 편의보다는 비즈니스 개념에 기반해 분할 |
클라이언트 프로필 | 인터페이스를 사용하는 클라이언트의 유형과 사용 패턴 | 클라이언트가 실제로 필요로 하는 메서드만 노출 |
변화율 | 인터페이스 내 메서드들이 함께 변경되는 빈도 | 변화 이유가 다른 메서드는 분리 후보 |
구현 복잡성 | 분할된 인터페이스들을 구현하는 구상 클래스의 부담 | 인터페이스 폭발로 인한 구현체가 다중 상속/구현에 시달리지 않도록 |
마지막으로, 언어와 플랫폼의 제약을 고려해야 한다. 자바나 C#과 같이 명시적 인터페이스를 지원하는 언어에서는 원칙 적용이 직관적이지만, 루비나 파이썬과 같은 덕 타이핑 언어에서는 암시적 인터페이스와 프로토콜을 통해 다른 방식으로 접근해야 한다. 또한, 과도한 분할은 '인터페이스 폭발' 현상을 일으켜, 시스템을 이해하고 네비게이션하기 어렵게 만들 수 있다. 따라서 원칙의 이점과 실용적인 유지보수 비용 사이의 균형을 맞추는 것이 중요하다.
8. 실무 적용 사례
8. 실무 적용 사례
실무에서 인터페이스 분리 원칙은 시스템의 복잡성을 관리하고 장기적인 유지보수성을 보장하는 핵심 설계 도구로 활용된다. 특히 대규모 모놀리식 아키텍처에서는 특정 모듈의 변경이 다른 모듈에 미치는 영향을 최소화하기 위해 인터페이스를 클라이언트의 역할에 맞게 세분화한다. 예를 들어, 사용자 관리 시스템에서 '관리자 전용 기능'과 '일반 사용자 기능'이 하나의 거대한 인터페이스에 묶여 있다면, 일반 사용자 클라이언트는 사용하지도 않는 관리자 메서드에 의존하게 되어 시스템이 취약해진다. ISP를 적용하여 역할별로 인터페이스를 분리하면, 모듈 간 결합도가 낮아지고 변경에 더욱 견고하게 대응할 수 있다.
마이크로서비스 환경에서 ISP는 서비스 간 API 설계의 지침이 된다. 각 마이크로서비스는 특정한 비즈니스 능력을 제공하며, 이 서비스를 소비하는 클라이언트(다른 서비스 또는 프론트엔드)는 오직 자신이 필요로 하는 연산과 데이터만을 정의한 인터페이스에 의존해야 한다. 하나의 포괄적인 서비스 계약을 여러 클라이언트가 강제로 공유하면, 한 클라이언트의 요구사항 변경이 불필요하게 다른 모든 클라이언트의 업데이트를 유발할 수 있다. 따라서 서비스는 클라이언트의 유형에 따라 세분화된 API를 제공하거나, GraphQL과 같은 기술을 활용하여 클라이언트가 정확히 필요한 데이터 필드만 요청할 수 있도록 설계한다.
라이브러리 및 공용 API 설계에서도 ISP는 매우 중요하다. 라이브러리 제공자는 다양한 사용자 시나리오를 고려하여 하나의 'God Interface'를 제공하는 대신, 작고 응집된 기능 단위로 인터페이스를 구성한다. 이렇게 하면 라이브러리 사용자는 자신의 애플리케이션 컨텍스트에 맞는 인터페이스만 구현하거나 의존할 수 있어 학습 부담과 통합 비용이 줄어든다. 또한, 미래에 새로운 기능이 추가될 때 기존 인터페이스를 수정하기보다 새로운 인터페이스를 도입하는 방식으로 확장이 용이해지며, 하위 호환성을 훼손하지 않고 시스템을 진화시킬 수 있다.
8.1. 대규모 시스템 아키텍처 설계
8.1. 대규모 시스템 아키텍처 설계
대규모 시스템 아키텍처 설계에서 인터페이스 분리 원칙은 시스템을 구성하는 모듈 간의 경계를 명확히 정의하고 결합도를 관리하는 핵심 도구로 작용한다. 복잡한 비즈니스 로직과 수많은 컴포넌트로 이루어진 시스템에서는 특정 모듈이 전체 시스템의 거대한 인터페이스에 의존하게 되면, 한 부분의 변경이 예상치 못한 광범위한 영향을 미칠 수 있다. ISP를 적용하여 각 모듈이 실제로 필요한 기능만을 정의한 세분화된 인터페이스에 의존하도록 강제하면, 시스템의 독립적인 부분들이 보다 안정적으로 진화할 수 있는 기반을 마련한다.
이 원칙은 특히 계층형 아키텍처나 마이크로서비스 아키텍처에서 두드러지게 적용된다. 예를 들어, 데이터 접근 계층에 하나의 거대한 IRepository 인터페이스가 존재하고, 여기에 Create, Read, Update, Delete 외에도 다양한 검색 및 보고서 생성 메서드가 포함되어 있다고 가정해 보자. 주문 관리 모듈과 사용자 분석 모듈은 각기 다른 메서드 집합만 필요로 할 것이다. ISP에 따라 인터페이스를 ICommandRepository(쓰기 작업)와 IQueryRepository(읽기 작업)로 분리하거나, 더 나아가 IOrderQuery와 IUserReportQuery 같은 도메인 특화 인터페이스로 분할하면, 각 서비스나 모듈은 자신의 책임에 맞는 최소한의 계약에만 의존하게 되어 변경의 영향을 최소화할 수 있다.
적용 영역 | ISP 적용 전 문제점 | ISP 적용 후 개선점 |
|---|---|---|
API 게이트웨이 | 모든 다운스트림 서비스의 통합 인터페이스에 의존하여 특정 서비스 변경 시 게이트웨이 전체가 불안정해짐 | 각 클라이언트 유형(예: 모바일, 웹) 또는 비즈니스 능력별로 세분화된 API 인터페이스를 구성하여 변경을 격리함 |
공유 라이브러리 | 다양한 팀에서 사용하는 라이브러리가 하나의 '두꺼운' 인터페이스를 제공하여 불필요한 의존성과 컴파일 영향 발생 | 핵심 기능별로 인터페이스를 분리(예: |
이벤트 드리븐 시스템 | 모든 이벤트 핸들러가 하나의 메시지 버스 인터페이스에 묶여 있어 특정 이벤트 형식 변경이 전체 핸들러에 영향을 줌 | 이벤트 유형별로 핸들러 인터페이스( |
결론적으로, 대규모 설계에서 ISP는 단순한 클래스 설계 원칙을 넘어 아키텍처적 경계를 설정하는 원리로 확장된다. 이를 통해 시스템은 보다 느슨하게 결합되고, 개별 컴포넌트의 개발, 배포, 확장이 독립적으로 이루어질 수 있으며, 결과적으로 시스템 전체의 복잡성을 관리 가능한 수준으로 유지하는 데 기여한다.
8.2. 마이크로서비스 환경에서의 적용
8.2. 마이크로서비스 환경에서의 적용
마이크로서비스 아키텍처에서 각 서비스는 독립적으로 배포되고 진화하는 자율성을 가진 구성 요소입니다. 인터페이스 분리 원칙은 이러한 자율성을 보장하고 서비스 간 결합도를 낮추는 데 핵심적인 역할을 합니다. 각 마이크로서비스가 제공하는 API는 해당 서비스의 핵심 기능에 집중한 세분화된 인터페이스 집합으로 설계되어야 합니다.
이를 적용하지 않을 경우 발생하는 문제는 명확합니다. 하나의 거대하고 포괄적인 API 계약을 여러 클라이언트 서비스가 공유하면, 한 클라이언트의 요구사항 변경이 API 수정을 유발하고 이는 다른 무관한 클라이언트 서비스까지 재배포를 강제할 수 있습니다[6]. ISP는 서비스별 또는 클라이언트 역할별로 특화된 인터페이스(또는 API 엔드포인트)를 분리할 것을 권장합니다. 예를 들어, 주문 서비스는 '주문 생성', '주문 조회', '주문 관리'와 같은 별도의 리소스나 작업 단위로 API를 분할하여, 결제 서비스는 생성 API만, 관리자 서비스는 관리 API만 의존하도록 구성할 수 있습니다.
실제 적용은 다음과 같은 전략을 통해 이뤄집니다.
적용 전략 | 설명 | 기대 효과 |
|---|---|---|
BFF(Backend For Frontend) 패턴 | 웹, 모바일, 관리자 콘솔 등 클라이언트 유형별로 최적화된 API 게이트웨이를 두는 방식입니다. 각 BFF는 필요한 마이크로서비스의 특정 인터페이스만 호출합니다. | 클라이언트 요구사항 변화가 다른 클라이언트나 하위 서비스에 미치는 영향을 최소화합니다. |
이벤트 기반 통신 | 서비스가 포괄적인 API 호출 대신 특정 도메인 이벤트를 발행하고, 관심 있는 서비스만 해당 이벤트를 구독하는 방식입니다. | 발행 서비스는 구독자의 존재를 알 필요가 없어 인터페이스 의존성이 완화됩니다. |
API 버저닝 전략 | 세분화된 인터페이스는 개별적으로 버전 관리가 용이합니다. 오래된 인터페이스를 폐기하는 과정에서 다른 기능을 사용하는 클라이언트를 방해하지 않습니다. | 서비스의 점진적 진화와 기술 부채 관리가 용이해집니다. |
결과적으로 ISP는 마이크로서비스 환경에서 변경의 영향을 국소화하고, 서비스의 독립적인 배포 주기를 가능하게 하여 시스템 전체의 민첩성을 높이는 기반을 제공합니다. 이는 곧 더 빠른 기능 배포와 안정적인 시스템 운영으로 이어집니다.
8.3. 라이브러리 및 API 설계
8.3. 라이브러리 및 API 설계
라이브러리 설계에서 인터페이스 분리 원칙은 클라이언트가 필요로 하는 기능만을 노출하는 세분화된 API를 제공하는 데 핵심이 된다. 라이브러리 제공자는 하나의 거대한 인터페이스 대신, 논리적으로 구분된 기능별 인터페이스(예: DataReader, DataWriter, DataValidator)를 정의한다. 이를 통해 라이브러리 사용자는 자신의 애플리케이션에 필요한 인터페이스에만 의존하게 되어, 불필요한 종속성과 컴파일 의존성을 제거할 수 있다. 결과적으로 라이브러리의 특정 모듈이 변경되더라도, 해당 모듈의 인터페이스를 사용하지 않는 클라이언트는 재컴파일하거나 재배포할 필요가 없어진다.
공용 API 설계 시 이 원칙을 적용하면 API의 안정성과 유지보수성이 크게 향상된다. 클라이언트가 사용하지 않는 메서드에 대한 의존성을 강제하지 않음으로써, 향후 API를 진화시키거나 새로운 버전을 출시할 때 발생할 수 있는 호환성 문제를 최소화할 수 있다. 예를 들어, 파일 처리 라이브러리에서 saveToCloud 메서드를 새로 추가해야 할 경우, 기존의 거대한 FileOperations 인터페이스에 추가하면 모든 사용자가 영향을 받지만, CloudStorage라는 별도의 인터페이스를 만들어 제공하면 해당 기능이 필요한 클라이언트만 선택적으로 사용할 수 있다.
설계 방식 | 문제점 | ISP 적용 후 개선점 |
|---|---|---|
하나의 포괄적 인터페이스 (예: | 보고서를 출력만 하는 클라이언트도 데이터베이스 연결 메서드( | 인터페이스를 |
모든 기능을 포함하는 SDK 패키지 | 간단한 기능만 필요한 경량 애플리케이션이 불필요하게 무거운 의존성을 가지게 됨 | 기능별로 모듈화된 패키지(예: |
이러한 접근 방식은 특히 대규모 팀이나 오픈 소스 생태계에서 협업할 때 효과적이다. 각 팀이나 외부 개발자는 전체 시스템의 복잡성을 알 필요 없이, 자신의 책임 영역에 해당하는 명확하고 작은 인터페이스에만 집중하여 개발할 수 있다. 최종적으로는 더욱 견고하고, 결합도가 낮으며, 확장 가능한 라이브러리와 API 설계로 이어진다.
