(Java/Kotlin) SOLID - ISP 인터페이스 분리 원칙 완전 정리

개요


1. 정의

클라이언트는 자신이 사용하지 않는 메서드에 의존하도록 강제받아서는 안 된다.
— Robert C. Martin

비대한 인터페이스 하나 → 작고 구체적인 인터페이스 여럿으로 분리

2. 왜 필요한가

비대한 인터페이스 변경 → 해당 인터페이스를 구현하는 모든 클래스에 영향

3. 위반 사례 — Before

❌ 모든 기능을 하나의 인터페이스에 몰아넣은 경우

interface Worker {
    fun work()
    fun eat()
    fun sleep()
    fun attendMeeting()
    fun writeReport()
}
// 일반 직원 — 모두 사용
class Employee : Worker {
    override fun work()          = println("업무 처리")
    override fun eat()           = println("식사")
    override fun sleep()         = println("수면")
    override fun attendMeeting() = println("회의 참석")
    override fun writeReport()   = println("보고서 작성")
}

// 로봇 — eat, sleep, attendMeeting이 필요 없음
class Robot : Worker {
    override fun work()          = println("로봇 작업 수행")
    override fun eat()           { /* 로봇은 먹지 않음 */ }           // ❌ 빈 구현
    override fun sleep()         { /* 로봇은 자지 않음 */ }           // ❌ 빈 구현
    override fun attendMeeting() { /* 로봇은 회의에 참석 안 함 */ }   // ❌ 빈 구현
    override fun writeReport()   = println("보고서 자동 생성")
}

// 인턴 — writeReport, attendMeeting 권한 없음
class Intern : Worker {
    override fun work()          = println("보조 업무")
    override fun eat()           = println("식사")
    override fun sleep()         = println("수면")
    override fun attendMeeting() { throw UnsupportedOperationException("인턴은 회의 불가") }  // ❌
    override fun writeReport()   { throw UnsupportedOperationException("인턴은 보고서 불가") } // ❌
}

문제점:


4. 개선 — After

✅ 역할 단위로 인터페이스 분리

interface Workable {
    fun work()
}

interface Eatable {
    fun eat()
}

interface Sleepable {
    fun sleep()
}

interface MeetingAttendable {
    fun attendMeeting()
}

interface Reportable {
    fun writeReport()
}
// 일반 직원 — 필요한 인터페이스를 모두 구현
class Employee : Workable, Eatable, Sleepable, MeetingAttendable, Reportable {
    override fun work()          = println("업무 처리")
    override fun eat()           = println("식사")
    override fun sleep()         = println("수면")
    override fun attendMeeting() = println("회의 참석")
    override fun writeReport()   = println("보고서 작성")
}

// 로봇 — 필요한 것만 구현
class Robot : Workable, Reportable {
    override fun work()        = println("로봇 작업 수행")
    override fun writeReport() = println("보고서 자동 생성")
}

// 인턴 — 필요한 것만 구현
class Intern : Workable, Eatable, Sleepable {
    override fun work()  = println("보조 업무")
    override fun eat()   = println("식사")
    override fun sleep() = println("수면")
}
// 업무만 처리하는 함수 — Workable만 받으면 됨
fun assignWork(worker: Workable) {
    worker.work()
}

// 보고서 수집 — Reportable만 받으면 됨
fun collectReports(reporter: Reportable) {
    reporter.writeReport()
}

assignWork(Employee())  // 업무 처리 ✅
assignWork(Robot())     // 로봇 작업 수행 ✅
assignWork(Intern())    // 보조 업무 ✅

collectReports(Employee())  // 보고서 작성 ✅
collectReports(Robot())     // 보고서 자동 생성 ✅
// collectReports(Intern())  — 컴파일 에러로 방지 ✅

개선 효과:


5. 인터페이스 조합 — 여러 역할을 묶을 때

분리한 인터페이스가 항상 함께 쓰인다면 타입 별칭이나 결합 인터페이스 로 묶을 수 있습니다.

// 풀타임 직원의 역할을 묶은 결합 인터페이스
interface FullTimeEmployee : Workable, Eatable, Sleepable, MeetingAttendable, Reportable

class SeniorEngineer : FullTimeEmployee {
    override fun work()          = println("설계 및 개발")
    override fun eat()           = println("식사")
    override fun sleep()         = println("수면")
    override fun attendMeeting() = println("기술 회의 참석")
    override fun writeReport()   = println("기술 보고서 작성")
}

6. Android 실전 예제

❌ ISP 위반 — 비대한 Listener 인터페이스

interface VideoPlayerListener {
    fun onPlay()
    fun onPause()
    fun onStop()
    fun onBuffering()
    fun onError(error: Throwable)
    fun onFullScreenEntered()
    fun onFullScreenExited()
    fun onSubtitleChanged(subtitle: String)
    fun onQualityChanged(quality: Int)
}

// 재생/일시정지만 필요한 MiniPlayer — 나머지 7개를 빈 구현
class MiniPlayerView : VideoPlayerListener {
    override fun onPlay()    = println("미니 플레이어 재생")
    override fun onPause()   = println("미니 플레이어 일시정지")
    override fun onStop()    { }                     // ❌ 빈 구현
    override fun onBuffering() { }                   // ❌ 빈 구현
    override fun onError(error: Throwable) { }       // ❌ 빈 구현
    override fun onFullScreenEntered() { }           // ❌ 빈 구현
    override fun onFullScreenExited() { }            // ❌ 빈 구현
    override fun onSubtitleChanged(subtitle: String) { }  // ❌ 빈 구현
    override fun onQualityChanged(quality: Int) { }  // ❌ 빈 구현
}

✅ ISP 적용 — 역할 단위로 Listener 분리

interface PlaybackListener {
    fun onPlay()
    fun onPause()
    fun onStop()
}

interface BufferingListener {
    fun onBuffering()
    fun onError(error: Throwable)
}

interface FullScreenListener {
    fun onFullScreenEntered()
    fun onFullScreenExited()
}

interface SubtitleListener {
    fun onSubtitleChanged(subtitle: String)
}

interface QualityListener {
    fun onQualityChanged(quality: Int)
}
// 미니 플레이어 — 재생 제어만 필요
class MiniPlayerView : PlaybackListener {
    override fun onPlay()  = println("미니 플레이어 재생")
    override fun onPause() = println("미니 플레이어 일시정지")
    override fun onStop()  = println("미니 플레이어 정지")
}

// 풀 플레이어 — 모든 기능 필요
class FullPlayerView : PlaybackListener, BufferingListener, FullScreenListener,
                       SubtitleListener, QualityListener {
    override fun onPlay()                          = println("풀 플레이어 재생")
    override fun onPause()                         = println("풀 플레이어 일시정지")
    override fun onStop()                          = println("풀 플레이어 정지")
    override fun onBuffering()                     = println("버퍼링 표시")
    override fun onError(error: Throwable)         = println("오류: ${error.message}")
    override fun onFullScreenEntered()             = println("전체화면 진입")
    override fun onFullScreenExited()              = println("전체화면 종료")
    override fun onSubtitleChanged(subtitle: String) = println("자막: $subtitle")
    override fun onQualityChanged(quality: Int)    = println("화질: ${quality}p")
}
class VideoPlayer {
    private val playbackListeners = mutableListOf<PlaybackListener>()
    private val bufferingListeners = mutableListOf<BufferingListener>()

    fun addPlaybackListener(listener: PlaybackListener) {
        playbackListeners.add(listener)
    }

    fun addBufferingListener(listener: BufferingListener) {
        bufferingListeners.add(listener)
    }

    fun play() = playbackListeners.forEach { it.onPlay() }
    fun onBufferingStart() = bufferingListeners.forEach { it.onBuffering() }
}

// 사용
val player = VideoPlayer()
player.addPlaybackListener(MiniPlayerView())
player.addPlaybackListener(FullPlayerView())
player.addBufferingListener(FullPlayerView())  // MiniPlayerView는 버퍼링 리스너 불필요

7. RecyclerView Adapter 예제

❌ ISP 위반 — 하나의 인터페이스로 모든 이벤트를 처리

interface ItemEventListener {
    fun onItemClick(position: Int)
    fun onItemLongClick(position: Int)
    fun onItemSwipe(position: Int)
    fun onItemFavoriteToggle(position: Int)
    fun onItemShareClick(position: Int)
}

// 클릭만 필요한 간단한 화면 — 나머지 4개를 빈 구현
class SimpleAdapter(private val listener: ItemEventListener) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // ...
}

// 사용 측에서 빈 구현을 강제로 작성해야 함
SimpleAdapter(object : ItemEventListener {
    override fun onItemClick(position: Int) = openDetail(position)
    override fun onItemLongClick(position: Int) { }    // ❌ 필요 없음
    override fun onItemSwipe(position: Int) { }        // ❌ 필요 없음
    override fun onItemFavoriteToggle(position: Int) { } // ❌ 필요 없음
    override fun onItemShareClick(position: Int) { }   // ❌ 필요 없음
})

✅ ISP 적용 — 이벤트 단위로 분리

interface OnItemClickListener {
    fun onItemClick(position: Int)
}

interface OnItemLongClickListener {
    fun onItemLongClick(position: Int)
}

interface OnItemSwipeListener {
    fun onItemSwipe(position: Int)
}

interface OnItemFavoriteListener {
    fun onItemFavoriteToggle(position: Int)
}

interface OnItemShareListener {
    fun onItemShareClick(position: Int)
}
class SimpleAdapter(
    private val clickListener: OnItemClickListener? = null,
    private val longClickListener: OnItemLongClickListener? = null,
    private val swipeListener: OnItemSwipeListener? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    // ViewHolder bind 시 필요한 리스너만 연결
}

// 사용 — 필요한 것만 전달
SimpleAdapter(
    clickListener = OnItemClickListener { position -> openDetail(position) }
)

8. Kotlin fun interface (SAM) 활용

Kotlin의 fun interface를 활용하면 ISP를 더 간결하게 표현할 수 있습니다.

fun interface OnItemClickListener {
    fun onItemClick(position: Int)
}

fun interface OnItemLongClickListener {
    fun onItemLongClick(position: Int)
}

// 람다로 바로 전달 가능
val clickListener = OnItemClickListener { position -> openDetail(position) }
val longClickListener = OnItemLongClickListener { position -> showContextMenu(position) }

9. ISP vs 관련 원칙 비교

원칙 대상 핵심 질문
SRP 클래스 이 클래스가 변경되는 이유가 하나인가?
OCP 클래스 기존 코드를 수정하지 않고 확장할 수 있는가?
LSP 상속 자식이 부모를 완전히 대체할 수 있는가?
ISP 인터페이스 클라이언트가 필요 없는 메서드에 의존하지 않는가?

10. ISP 위반을 감지하는 신호

✔ 인터페이스를 구현하는데 빈 메서드({ })가 많다

✔ 인터페이스를 구현하는데 UnsupportedOperationException을 던지는 메서드가 있다

✔ 새 메서드를 인터페이스에 추가할 때 구현 클래스 대부분을 수정해야 한다

✔ 인터페이스 이름이 모호하다 (Manager, Handler, Helper — 너무 많은 책임을 암시)

✔ 인터페이스 메서드가 10개를 넘는다

11. 정리

항목 내용
정의 클라이언트는 사용하지 않는 메서드에 의존하면 안 된다
핵심 비대한 인터페이스를 역할 단위의 작은 인터페이스로 분리
위반 신호 빈 구현, UnsupportedOperationException, 비대한 인터페이스
해결 방법 역할별 인터페이스 분리, 결합 인터페이스로 조합
효과 변경 파급 범위 축소, 구현 클래스 단순화
Kotlin 활용 interface 분리, fun interface (SAM)

참고



Related Posts