SOLID 원칙과 인터페이스 분리 원칙(ISP) | Today I Learned

hoya·2022년 10월 17일
0

Today I Learned

목록 보기
10/11
post-thumbnail

피드백은 언제나 환영합니다 :)


🤔 Interface?

인터페이스(interface)는 클래스들이 구현해야 하는 동작을 지정하는데 사용되는 추상 자료형이다. - Wikipedia

Interface 를 더 쉽게 이야기하면 구현된 것이 거의 없는, 밑그림만 있는 기본 설계도라고 이야기할 수 있다.

Interface일반 메소드나 멤버 변수를 가질 수 없고, 오로지 추상 메소드와 상수만을 가질 수 있다.

그렇다면 Interface 를 사용하는 이유는 무엇일까?

한 회사에서 각 브랜드의 스마트폰과 관련된 기능을 구현하려고 한다.

개발자 A에게는 갤럭시의 기능을, 개발자 B에게는 아이폰의 기능을 구현하라고 지시했을 때, Interface 를 사용하지 않으면 어떻게 될까?

class 스마트폰_갤럭시() {
    fun 전화() {
		// TODO : 갤럭시 전화
    }

    fun 문자() {
		// TODO : 갤럭시 문자
    }
}

class 아이폰() {
    fun 통화() {
		// TODO : 아이폰 전화
    }

    fun 메신저() {
		// TODO : 아이폰 문자        
    }
}

위의 코드를 보면 알겠지만, 갤럭시와 아이폰 모두 동일한 기능을 구현했음에도 메소드의 이름이 다른 것을 확인할 수 있다. 즉, 표준화가 진행되지 않은 것이다.

이러한 상황에서 Interface 를 사용해 표준화를 진행하면 어떻게 될까?

interface 스마트폰 {
    fun 전화(전화_상대: String)
    fun 문자(문자_상대: String)
}

class 갤럭시 : 스마트폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 갤럭시 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 갤럭시 문자    
    }
}

class 아이폰 : 스마트폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 아이폰 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 아이폰 문자
    }
}

fun main() {
    val 갤럭시: 스마트폰 = 갤럭시()
    val 아이폰: 스마트폰 = 아이폰()

    갤럭시.전화("어머니")
    아이폰.전화("아버지")
}

각자 다른 클래스에서 비슷한 기능을 구현할 때 메소드명이 고정된 것을 확인할 수 있다.

이렇게 되면, 나중에 새로운 브랜드의 스마트폰 기능을 구현하더라도 클래스 설계가 이전에 비해 훨씬 더 쉬워질 것이다.

이 외에도, Interface다중 상속을 지원하고 서로 관계없는 클래스끼리 관계를 맺어준다는 장점이 있다.


✂️ 인터페이스 분리 원칙(Interface Segregation Principle)

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

Interface 에 대해 간략하게 알아 보았으니, ISP 에 대해서 알아보도록 하자.

위의 코드 구현을 통해 이제 표준화는 완성이 된 상태이다.

이제 고도화를 위해 스마트폰 Interface 에 여러 메소드를 추가해보자.

interface 스마트폰 {
    fun 전화(전화_상대: String)
    fun 문자(문자_상대: String)
    fun 삼성뮤직()
    fun 삼성페이()
}

이런 식으로 삼성뮤직, 삼성페이 메소드를 추가했다. 위 코드를 보면 위화감이 느껴지질 것이다.

이유가 뭘까?

class 갤럭시 : 스마트폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 갤럭시 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 갤럭시 문자
    }

    override fun 삼성뮤직() {
        // TODO : 삼성 뮤직
    }

    override fun 삼성페이() {
        // TODO : 삼성페이
    }
}

class 아이폰 : 스마트폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 아이폰 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 아이폰 문자
    }

    override fun 삼성뮤직() {
        // TODO : ?
    }

    override fun 삼성페이() {
        // TODO : ?
    }
}

삼성뮤직, 삼성페이 메소드는 갤럭시 클래스에서 정상적으로 기능 구현이 가능하다.

그러나 아이폰 클래스에선 존재하지 않는 기능임에도 불구하고 강제로 구현해야 하는 상황이 발생한다.

만약 각 메소드에 return 값이 있다고 가정했을 때, return 타입을 변경한다면 갤럭시 클래스 뿐 아니라 해당 기능을 전혀 사용하지 않는 아이폰 클래스도 변경이 일어나야하므로 필요하지 않은 리팩터링 비용이 발생하게 된다.

이런 상황에서는 아래와 같이 분리를 더욱 명확하게 하여 각자 기능에 맞게 Interface 를 구현해야 한다.

interface 스마트폰 {
    fun 전화(전화_상대: String)
    fun 문자(문자_상대: String)
}

interface 삼성폰 {
    fun 삼성뮤직()
    fun 삼성페이()
}

interface 애플폰 {
    fun 애플뮤직()
    fun 애플페이()
}

class 갤럭시 : 스마트폰, 삼성폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 갤럭시 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 갤럭시 문자
    }

    override fun 삼성뮤직() {
        // TODO : 삼성 뮤직
    }

    override fun 삼성페이() {
        // TODO : 삼성페이
    }
}

class 아이폰 : 스마트폰, 애플폰 {
    override fun 전화(전화_상대: String) {
        // TODO : 아이폰 전화
    }

    override fun 문자(문자_상대: String) {
        // TODO : 아이폰 문자
    }

    override fun 애플뮤직() {
        // TODO : 애플 뮤직
    }

    override fun 애플페이() {
        // TODO : 애플페이
    }
}

각 클래스에서는 필요한 Interface 를 다중 상속받아 클래스가 필요한 기능만 사용하는 것을 확인할 수 있다.

이렇게 Interface 를 구체적이고 작은 단위로 분리시키면 클라이언트 입장에서는 꼭 필요한 메소드만 사용하게 되어 클래스의 코드량도 적어지고, 확장성도 높아진다는 장점이 있다.

상속은 확장에 매우 용이하지만, 그만큼 클래스의 양이 많아져 프로젝트의 용량이 비대해질 가능성이 높으므로 ISP 와 같은 원칙들을 고려하며 신중하게 사용해야 한다.


✍️ TL;DR

  • Interface동일한 목적 하에 동일한 기능을 수행하게끔 강제하기 위해 사용한다.
  • Interface다중 상속을 지원한다.
  • 클라이언트는 자신이 사용하지 않는 메소드에 의존해선 안된다.
  • ISP 를 준수하면 유지보수에 용이해진다.

참고 및 출처

Wikipedia
인터페이스 분리 원칙 (ISP: Interface Segregation Principle)

profile
즐겁게 하자 🤭

0개의 댓글