피드백은 언제나 환영합니다 :)
인터페이스(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
에 대해 간략하게 알아 보았으니, 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
와 같은 원칙들을 고려하며 신중하게 사용해야 한다.
Interface
는 동일한 목적 하에 동일한 기능을 수행하게끔 강제하기 위해 사용한다.Interface
는 다중 상속을 지원한다.ISP
를 준수하면 유지보수에 용이해진다.참고 및 출처
Wikipedia
인터페이스 분리 원칙 (ISP: Interface Segregation Principle)