ISP — 무엇인가, 개발에서의 인터페이스 분리 원칙

저자: IT Sectr 게시일: 2026-05-12 읽는 시간: 9 분

ISP(Interface Segregation Principle)는 SOLID의 네 번째 원칙으로, 클라이언트는 사용하지 않는 메서드에 의존해서는 안 된다고 규정합니다. 이 원칙은 로버트 마틴이 객체 지향 시스템을 위한 인터페이스 설계의 맥락에서 공식화했습니다. Clean Architecture (2017)에 설명된 바와 같이, 인터페이스 분리 원칙은 하나의 범용 인터페이스 대신 좁게 특화된 인터페이스를 생성하여 결합도를 낮추고 변경을 용이하게 합니다.

핵심 사항

  • ISP — 인터페이스 분리 원칙, SOLID의 네 번째
  • 클라이언트는 호출하지 않는 메서드에 의존해서는 안 된다
  • Fat Interface는 일부 클라이언트와 관련 없는 메서드를 포함한다
  • 인터페이스 분할은 결합도를 낮추고 코드 재사용성을 높인다
  • ISP는 SRP 및 인터페이스 수준의 단일 책임과 밀접하게 관련된다

ISP(Interface Segregation Principle)란?

ISP(Interface Segregation Principle)는 모든 클라이언트가 사용하지 않는 메서드가 포함된 “뚱뚱한” 인터페이스 생성을 금지하는 인터페이스 분리 원칙입니다. 수많은 메서드가 있는 하나의 인터페이스 대신, 각각의 클라이언트 그룹을 위한 여러 개의 작은 인터페이스가 설계됩니다.

이 원칙은 로버트 마틴이 “인터페이스 오염” 문제에 대한 해결책으로 도입했습니다. 클래스가 공통 인터페이스에 선언되어 있다는 이유만으로 필요하지 않은 메서드를 구현해야 하는 경우입니다. 정적 타입 언어에서는 이로 인해 빈 구현이나 예외 발생이 발생하며, 이는 ISP 위반의 직접적인 징후입니다.

ISP와 SRP는 서로 보완합니다. SRP는 클래스의 책임에 관한 것이고, ISP는 인터페이스 계약에 관한 것입니다. SRP는 “하나의 클래스 — 변경할 이유 하나”라고 말하고, ISP는 “하나의 인터페이스 — 하나의 클라이언트 시나리오”라고 말합니다. 함께 시스템의 각 요소가 명확한 경계를 가진 모듈식 아키텍처를 형성합니다.

Fat Interface와 그 결과

Fat Interface — 특정 클라이언트가 필요로 하는 것보다 더 많은 메서드를 포함하는 인터페이스입니다. 예를 들어 work, eat, sleep 메서드가 있는 Worker 인터페이스가 있습니다. 로봇 작업자는 eat과 sleep을 구현해서는 안 되지만 강제로 구현해야 합니다. 해결책은 Workable, Eatable, Sleepable로 분할하는 것입니다. 각 클라이언트는 정확히 필요한 것을 얻습니다.

모바일 개발에서 Fat Interface는 델리게이트 프로토콜과 DataSource에서 발견됩니다. 하나의 프로토콜이 두 가지 다른 시나리오(편집 + 표시)를 위한 메서드를 포함할 수 있지만, 특정 화면은 그중 하나만 사용합니다.

인터페이스 분리 원칙의 작동 방식

ISP 구현은 각 인터페이스의 클라이언트를 분석하는 것부터 시작됩니다. 두 클라이언트가 하나의 인터페이스에서 서로 다른 메서드 세트를 사용하는 경우, 인터페이스를 분할해야 합니다. 각 새 인터페이스는 하나의 시나리오 내에서 함께 호출되는 메서드를 그룹화합니다.

분할 메커니즘: 원래 인터페이스는 여러 개의 좁은 인터페이스로 나뉘며, 각각은 공통 부분(있는 경우)을 상속합니다. 클라이언트는 일반 인터페이스 대신 필요한 좁은 인터페이스에 의존하도록 전환합니다. 원래 인터페이스를 구현했던 클래스는 이제 실제로 필요한 좁은 인터페이스만 구현합니다.

중요한 설명: 분할의 정도는 클라이언트의 수와 시나리오에 의해 결정됩니다. ISP는 최대 분할(각각 하나의 메서드를 가진 마이크로 인터페이스)을 요구하지 않습니다. 이는 과도한 복잡성을 초래할 것입니다. 목표는 클라이언트의 불필요한 메서드 의존성을 제거하는 것이지, 각 인터페이스의 크기를 최소화하는 것이 아닙니다.

ISP 위반 징후

ISP 위반의 주요 징후로는 빈 메서드(더미 구현)로 인터페이스를 구현하는 클래스, 구현에서 UnsupportedOperationException 발생, 일부 클라이언트가 사용하지 않는 많은 수의 매개변수나 반환 타입, 그리고 일부 클라이언트에만 영향을 미치는 빈번한 인터페이스 변경이 있습니다.

Android 개발에서 ISP 위반의 전형적인 예는 클릭, 길게 클릭, 스와이프를 위한 메서드를 포함하는 OnItemClickListener 인터페이스입니다. 특정 화면이 클릭만 사용하는 경우, 나머지 메서드는 비어 있습니다. 해결책은 OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener로 분할하는 것입니다.

iOS 개발에서 ISP 위반은 UIKit 델리게이트에서 나타납니다. 하나의 프로토콜에 다양한 컴포넌트 상태를 위한 메서드가 포함됩니다. UITableViewDelegate에는 표시, 선택, 편집, 스와이프 동작을 위한 메서드가 포함됩니다. 개발자는 종종 수많은 빈 메서드로 전체 프로토콜을 구현합니다. 책임 그룹별로 여러 프로토콜로 분할하면 문제가 해결됩니다.

사용하지 않는 메서드에 대한 의존성

문제는 단지 코드의 미학에 관한 것이 아닙니다. 인터페이스가 변경될 때(새 메서드가 추가될 때), 모든 구현 클래스를 업데이트해야 합니다 — 새 메서드가 필요하지 않은 클래스도 포함됩니다. 수많은 화면이 있는 모바일 개발에서는 이로 인해 연쇄적인 변경이 발생합니다. ISP는 각 클라이언트를 해당 클라이언트와 관련 없는 변경으로부터 격리합니다.

암시적 ISP 위반은 구성 매개변수를 통해 발생합니다. 메서드가 많은 필드를 가진 객체를 수용하고 클라이언트가 그중 2-3개만 사용하는 경우, 이는 분할 신호입니다. 대안: 최소 매개변수 세트를 가진 여러 개의 특화된 메서드.

Android 개발에서 모든 애플리케이션 설정을 읽고 쓰기 위해 단일 SharedPreferencesManager를 사용할 때 ISP가 위반됩니다. 테마만 읽으면 되는 Fragment가 다양한 데이터 타입을 위한 수많은 메서드를 가진 전역 관리자에 의존하게 됩니다. ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider로 분할 — 구성 서비스 수준에서 ISP를 적용하는 것입니다. 각 제공자는 해당 클라이언트가 필요로 하는 메서드만 정확히 포함합니다.

모바일 애플리케이션에서의 ISP 예시

데이터 작업을 위한 인터페이스를 사용한 Android 예시를 살펴보겠습니다. ISP 위반 — 모든 CRUD 작업을 위한 하나의 인터페이스이지만, 모든 클라이언트가 모든 작업을 필요로 하지는 않습니다.

kotlin
// ISP 위반: Fat Interface
interface UserRepository {
    fun getAll(): List<User>
    fun getById(id: Int): User
    fun save(user: User)
    fun delete(id: Int)
}

// ISP 적용 후: 좁은 인터페이스
interface UserReader {
    fun getAll(): List<User>
    fun getById(id: Int): User
}

interface UserWriter {
    fun save(user: User)
    fun delete(id: Int)
}

// ReadOnlyViewModel은 쓰기 메서드에 의존하지 않음
class ReadOnlyViewModel(
    private val reader: UserReader
)

미디어 작업을 위한 프로토콜 분리를 사용한 iOS 예시:

swift
// ISP 위반: 모든 미디어 작업에 하나의 프로토콜
protocol MediaService {
    func play(url: URL)
    func pause()
    func stop()
    func upload(data: Data) async -> URL
    func download(url: URL) async -> Data
}

// ISP 후: 책임별 프로토콜로 분리
protocol MediaPlayer {
    func play(url: URL)
    func pause()
    func stop()
}

protocol MediaTransfer {
    func upload(data: Data) async -> URL
    func download(url: URL) async -> Data
}

// PlayerViewModel은 다운로드 메서드에 의존하지 않음
class PlayerViewModel {
    private let player: MediaPlayer
}

실용적 결론: ISP는 인터페이스의 관련 없는 부분의 변경으로부터 클라이언트를 보호합니다. UserRepository를 UserReader와 UserWriter로 분할하면 save의 변경이 ReadOnlyViewModel에 영향을 미치지 않으며, 그 반대도 마찬가지입니다. 각 클라이언트는 사용하지 않는 기능으로부터 격리되며 시스템의 다른 부분이 수정되어도 변경이 필요하지 않습니다.

ISP와 SRP — 자연스러운 쌍입니다. SRP는 클래스가 변경할 이유를 하나만 가져야 한다고 정의합니다. ISP는 동일한 논리를 인터페이스에 적용합니다: 인터페이스는 하나의 클라이언트 시나리오를 제공해야 합니다. 클래스는 여러 개의 좁은 인터페이스(각각 하나의 책임에 해당)를 구현할 수 있으며, 이는 여러 책임을 가진 하나의 Fat Interface보다 깔끔합니다.

ISP와 OCP도 관련되어 있습니다: 좁은 인터페이스는 확장하기 더 쉽습니다. 좁은 인터페이스에 새 메서드를 추가하면 해당 클라이언트에만 영향을 미칩니다. Fat Interface에 메서드를 추가하면 모든 클라이언트에 영향을 미치며, 클라이언트가 구현을 변경해야 하는 경우 잠재적으로 OCP를 위반합니다.

ISP와 DIP는 함께 작동합니다: DIP는 추상화에 대한 의존성을 요구합니다. ISP는 이러한 추상화를 좁고 집중적으로 만듭니다. 넓은 인터페이스에 대한 의존성은 여전히 추상화에 대한 의존성이지만, ISP 관점에서는 “나쁜” 추상화입니다. 네 가지 원칙(SRP, OCP, ISP, DIP)은 “모듈성 피라미드”를 형성합니다: SRP와 ISP는 경계를 정의하고, OCP와 DIP는 확장 및 결합 방식을 정의합니다.

컴포넌트 아키텍처에서 ISP 적용

모바일 프로젝트(모듈, 기능, 계층)의 컴포넌트 아키텍처는 공개 API 수준에서 ISP의 이점을 얻습니다. 각 모듈은 단일 공통 퍼사드 대신 소비자를 위한 좁은 인터페이스를 내보냅니다. 이를 통해 모듈의 기능 일부만 사용하는 소비자에게 영향을 주지 않고 모듈의 내부 구현을 변경할 수 있습니다.

Clean Architecture를 사용하는 Android 프로젝트에서 ISP는 UseCase에 적용됩니다: 각 UseCase는 단일 invoke 또는 execute 메서드를 가진 별도의 인터페이스입니다. 클라이언트(ViewModel)는 전체 리포지토리 대신 필요한 UseCase에만 의존합니다. 이는 의존성을 투명하고 테스트 가능하게 만듭니다.

자주 묻는 질문

ISP는 인터페이스의 과도한 증가를 초래하지 않나요?

네, 과도한 분할이 가능합니다. ISP는 메서드당 하나의 인터페이스를 요구하지 않습니다. 기준은: 인터페이스 메서드의 일부만 필요한 클라이언트가 있는가? 모든 클라이언트가 모든 메서드를 사용한다면 인터페이스를 분할할 필요가 없습니다. 최적의 분할 수준은 실제 사용 시나리오에 의해 결정됩니다.

ISP는 함수 매개변수에 어떻게 적용되나요?

매개변수 수준의 ISP란: 함수가 많은 필드를 가진 객체를 수용해서는 안 되며, 그 일부만 사용하는 경우입니다. 대신 필요한 데이터만 전달하거나 특화된 인터페이스(예: 완전한 User 대신 Renderable 인터페이스)를 사용해야 합니다.

ISP는 LSP와 어떻게 다른가요?

LSP는 올바른 상속과 행동적 하위 타입 호환성에 관한 것입니다. ISP는 인터페이스 설계에 관한 것입니다: 클라이언트는 사용하지 않는 메서드에 의존해서는 안 됩니다. LSP는 “하위 클래스를 기반 클래스 대신 사용할 수 있는가?”라는 질문에 답하고, ISP는 “클라이언트가 전체 인터페이스를 필요로 하는가?”라는 질문에 답합니다.

ISP는 테스트를 어떻게 단순화하나요?

좁은 인터페이스는 mock 객체 생성을 단순화합니다: 테스트는 하나 또는 두 개의 메서드로 mock을 생성하고, 수많은 메서드로 생성하지 않습니다. 인터페이스의 메서드가 적을수록 동작을 스터빙하기 쉽습니다. 이는 테스트 개발자의 인지 부하를 줄이고 mock 로직의 오류 가능성을 낮춥니다.

ISP에서 벗어나도 되는 경우는?

인터페이스가 안정적이고 모든 클라이언트가 모든 메서드를 사용하는 경우, 분할은 불필요합니다. 전형적인 예: Apple이 설계한 UIKit 프로토콜입니다. UIKit은 델리게이트의 완전한 구현을 기대하기 때문에 분할은 위험합니다. 이러한 경우 ISP 위반은 API 안정성에 의해 정당화됩니다.

요약

  • ISP(Interface Segregation Principle) — 인터페이스 분리 원칙, SOLID의 네 번째
  • 클라이언트는 사용하지 않는 메서드에 의존해서는 안 된다
  • Fat Interface는 클래스가 불필요한 메서드를 스터브나 예외로 구현하도록 강제한다
  • 인터페이스 분할은 결합도를 낮추고 클라이언트를 변경으로부터 격리한다
  • ISP + SRP는 모듈 경계를 형성한다: 하나의 책임 — 하나의 좁은 계약
  • Mock 테스트가 단순화된다: 좁은 인터페이스는 더 적은 스터브가 필요하다
  • 최적의 분할은 실제 클라이언트 시나리오에 의해 결정되며, 최대 분할이 아니다

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기