1. 인터페이스란?
1.1 처음보는 전자레인지로 음식을 데우는 방법
위 사진은 구글에 `전자레인지`라고 검색했을 때 나오는 사진들 중 아무 사진이나 가져온 것이다.
이 전자렌지로 어제 먹고 남은 치킨을 데워달라.
할 수 있겠는가?
쉽다. 문을 열고, 치킨을 넣은 다음 문을 닫고,
아래에 시간으로 추측되는 숫자들이 적힌 다이얼을 돌리고 기다리면 따뜻하고 눅눅한 치킨이 나오게 된다.
우리는 오늘 처음 보는 이 전자레인지로 차디찬 닭고리를 데웠다.
내가 전자레인지라고 알려주지 않아도 데울 수 있었을 것이다.
어떤 원리로 음식이 데워지는가? 이에 대한 지식이 있는 사람이라면 대답할 수도 있다. 마이크로파를 이용해 데웠다.
그럼 마이크로파를 쬐면 왜 데워지는가? 마이크로파가 음식물에 있는 물 분자에 흡수되면, 물 분자의 회전 운동이 빨라지게 되어 뜨겁게 달아오르고, 시간이 지나면 에너지가 확산되어 전체가 뜨거워진다.
- 출처 : 경희대학교 과학, 알고싶다(101)
혹시 당신이 음식을 데우는 과정에서, 이 지식들을 직접 응용했나?
혹시 이 지식들 중 하나라도 모른다면, 전자레인지로 음식을 데울 수 없나?
아니다.
우리는 단지 문을 열고, 음식을 넣고, 문을 닫고, 다이얼을 돌렸다.
핑크색 덤벨을 드는 것 보다 간단한 몇 가지 동작만으로도 위에 구구절절 적은 원리대로 전자레인지가 음식을 데워 주었다.
우리가 알아야 할 것은 단지 A라는 행동을 하면 B라는 결과가 주어진다는 것이다.
전자레인지의 버튼과 다이얼을 "인터페이스"라고 부른다.
사람과 사람, 장치와 장치, 시스템과 시스템 그리고 사람과 장치.. 등 서로 다른 두 시스템, 장치 등의 사이에서 정보나 신호를 주고 받는 접점이나 경계면을 인터페이스라고 부른다.
(프로그래밍을 처음 배웠을 때 위 설명이 너무 어려웠다)
우리는 돌출된 손잡이나, 다이얼로 간단하게 전자레인지에게 음식을 데우라는 신호를 보낼 수 있었다.
직관적이고 유저 친화적으로 잘 만든 인터페이스 덕분에,
우리는 아무런 설명 없이 덩그러니 전자레인지가 놓인 방에 먹다 남은 치킨과 함께 놓여진 방에 갇혀도, 닭을 데울 수 있다.
내부 동작 원리를 전혀 몰라도 말이다!!
예시로 든 사람과 장치를 이어주는 인터페이스를 User Interface 즉, 우리가 자주 사용하는 단어인 UI가 이것이다.
API 또한 Application Programming Interface의 약자이다.
어떤 프로그램과 상호작용할 수 있는 Interface를 의미한다.
지금 부터 다룰 자바의 Interface 또한 같은 기능을 한다.
어떤 기능을 이용하기 위한 버튼과 다이얼을 제공해준다.
그 내부 동작을 몰라도 A를 주면 B를 준다는 일종의 약속이며 specification이다.
사용하는 쪽은 단지 희미한 기대만 품으면 된다.
"이 다이얼을 돌리면 따뜻한 치킨을 주겠군?"
이는 곧 변경에 대한 유연함을 가져다준다.
이러한 인터페이스를 활용하면 좀 더 범용적이고 유연한 객체지향적인 코드를 작성할 수 있다.
이제부터 자바의 인터페이스를 알아보자!
2. 자바 interface
지금까지 인터페이스랑 무엇인지에 대해 알아 보았다.
Java의 Interface 개념 또한 앞서 언급한 인터페이스와 같다.
인터페이스엔 메서드들이 있다.
이 메서드를 호출하는 쪽에선 내가 A를 주면, B를 돌려줄 것이라는 희미한 기대만을 품고 호출한다.
호출 되는 쪽에선 어떻게 해오던지 상관 없다. 그냥 B 혹은 B로 다룰 수 있는 무언가를 돌려주기만 하면 된다.
위와 같이 class를 정의할 때와 비슷하게 만든다. 다만 `class`라는 키워드가 들어갈 자리에 `interface`라는 키워드를 적으면 된다.
사진을 보면 메서드의 내부 동작 구현부가 없이 선언부만 달랑 있는 getNameById라는 메서드가 하나 있다.
인터페이스는 이런 메서드를 제공해 주는데, 호출하는 쪽에서는
"Long 형식으로 된 Member의 id를 주면, Name을 주겠구나!!" 정도의 기대만 가지고 이 메서드를 호출한다.
내부적으로는 어떻게 id를 통해 Name을 가져오는지 몰라도 된다.
몰라도 된다..
"몰라도 된다"는 규칙을 만들기 위해 자바는 아예 "모르게 한다"
인터페이스를 보면 구현하는 몸통이 없는데, 메서드의 구현은 인터페이스를 "구현"한 클래스에서 진행한다.
`implements`라는 키워드를 통해 구현할 인터페이스를 지정하면,
강제로 interface에서 제공하는 메서드들의 몸체를 구현해야 한다. -
구현하지 않으면 컴파일 에러가 발생한다! - 구현을 강제한다
Java에서는 이런 식으로 "누가 어떻게 수행해서 결과를 가져오는지"를 모르게 한다.
나는 단지 치킨을 주문하기만 하면 된다.
어떤 요리사이던, 어떤 레시피이던, 어떤 닭을 쓰던 그냥 "치킨"을 나에게 주기를 기대할 뿐이다.
이렇게 하는 장점은 뭘까? 자유로워진다!
덕분에 치킨집에선 맘대로 요리사를 바꿀 수 있고, 맘대로 레시피를 바꿀 수 있고, 맘대로 치킨을 바꿀 수 있다.
이는 곧 프로그램의 유연함을 뜻한다.
프로그램을 만드는 사람은 언제든 편하게 특정 기능의 구현을 바꿀 수 있게 되는 것이다.
인터페이스만 잘 만들고, 인터페이스 메서드의 규약 - id를 넣으면 Name을 준다. - 만 잘 지킬 수 있다면 내부적으론 유연하게 변화를 줄 수가 있다.
두 객체간 직접적인 관계를 간접적인 관계로 만들어 준다. 구현체의 변화가 호출하는 쪽까지 미치는 영향이 크게 줄어드는 것이다.
이는 나중에 언급할 "의존성 주입"과 함께 엄청난 시너지를 일으킨다.
일단은 이런 자바 인터페이스에 대해 좀 더 자세히 알아보자.
2.1 인터페이스는 타입을 정의하는 용도로만 써라. (+ mixin)
자바의 클래스는 이름 그대로 "분류"와 관련된 추상화 개념이다.
플라톤은 세상의 존재하는 모든 것들은 어떤 이데아를 본뜬 것이라고 했다.
내가 타이핑을 하고 있는 COX 사의 키보드 모델 "COX CK87 BLACK"은 어떤 본질적이고 추상적인 "키보드"라는 물건을 본뜬 프랙티스일 뿐이다.
이런 이론을 제자인 아리스토텔레스가 "classification"이란 개념으로 정립했고, 이것이 우리가 아는 "클래스"의 유래가 되었다.
그만큼 상위 클래스와 이를 extends한 하위 클래스의 관계는 is a kind of 관계를 만족한다.
개, 고양이, 향유고래를 자바 코드로 나타낸다면, 포유류 클래스를 상속 받을 것이다.
그리고 포유류 클래스는 동물 클래스를 상속 받을 것이다.
이를 말로 표현하면
개는 포유류의 한 분류이다.
고양이는 포유류의 한 분류이다.
포유류는 동물의 한 분류이다.
향유고래는 동물의 한 분류이다.
전부 가능한 표현이지 않는가?
영어로 표현하자면 is a kind of 관계라고 할 수 있다.
상위 클래스와 하위 클래스는 is a kind of 관계이다.
"COX CK87 BLACK" is a kind of Keyboard
그렇다면 인터페이스와 인터페이스를 구현한 클래스는 어떤 관계일까?
바로 is able to 관계라고 할 수 있다.
클래스가 어떤 인터페이스를 구현한다는 것은, 그 클래스의 인스턴스로 "무엇을 할 수 있다"라는 것을 클라이언트에 알려줄 수 있게 된다. 그리고 오로지 이 용도로만 써야 한다. (이펙티브 자바 아이템 22)
위의 두 인터페이스는 java에서 제공해주는 인터페이스 "Runnable"과 "Comparable"이다.
인터페이스는 메서드의 구현을 강제하는데, Runnable은 run이라는 메서드를, Comparable은 compareTo라는 메서드를 강제로 구현해야 한다.
그럼 강제로 구현됐으니, 이 클래스는 각각 run과 comapreTo라는 "기능"이 생긴 것이나 마찬가지다.
따라서, 클라이언트는 어떤 A라는 기능을 하는 클래스가 구현한 인터페이스 "Runnable", "Comparable"을 보고 "이 클래스는 Run 가능하고, Compare 가능하구나!"라고 알 수 있게 되는 것이다.
이렇게 어떤 A라는 기능을 하는 타입에 기능을 "혼합" - mixed in 했으므로, 이런 인터페이스를 mixin 인터페이스라고 부른다.
MemberRepository라는 인터페이스가 있고, 이를 구현하는 MemberRepositoryImpl이라는 클래스가 있을 때,
아래와 같이 MemberRespotiroy라는 인터페이스 참조 변수가 MemberRepositoryImpl 인스턴스를 가리키게 해보자.
(적합한 인터페이스가 있다면, 객체는 인터페이스로 참조해야 한다 - 이펙티브 자바 아이템 64)
이 경우 우리는 MemberRepositoryImpl 의 내부 구현을 몰라도
MemberRepository 인터페이스만 보고도 "이 참조 변수는 Id를 주면 이름을 줄 수 있구나"라고 알 수 있다.
이 점을 이용하면 인터페이스로 로직을 구현하고, 런타임에서 동적으로 인스턴스만 갈아끼면서 내가 원하는 동작을 수행하도록 할 수 있다.
2.2 기본 맴버 규칙
자바 인터페이스의 멤버 변수와 메서드는 기본적으로 아래와 같은 규칙을 갖는다.
1. 모든 맴버 변수는 public static final 이다.
2. 모든 메서드는 public abstract 이다.
우리는 아래와 같이 인터페이스를 사용했지만,
사실은 아래와 같이
필드의 경우 `public static final`을
메서드의 경우 public abstract를 숨기고 있었다.
(인텔리제이는 적으나 마나 영향을 주지 않는 요소를 회색으로 표시한다.)
그래서 필드를 외부에서 호출 가능하고, 값을 초기화 하지 않는 경우 컴파일 에러가 발생한다.
2.3 기본 맴버 규칙 예외와주의할 점 - default, static, private 메서드
java 8에 도입된 default 메서드와 static method, 그리고 java 9의 private 메서드는 기본 맴버 규칙을 따르지 않는다.
더 정확히는 따르지 않아도 되게 해준다!
interface의 default 메서드는 java 8에 도입된 기능으로 몸체를 가질 수 있다.
abstract가 아니다!
어떤 메서드의 구현 방법이 너무 명백하다면, default 메서드를 제공해줄 수 있다.
default 메서드를 사용하는 입장에서, 맴버 변수가 다 public 상수이거나, private 메서드를 선언할 수 없다는 점은 메서드를 구현하는데 불편함을 줄 수 있다. 이런 불편을 해소하기 위해 java 9에서 private 메서드가 추가되었다. 이제 인터페이스 안에서도 private 메서드를 구현 가능하다.
첫 메서드와 같이 private 메서드를 사용할 수 있다.
단, private 메서드는 body가 필요한데, 용도상 인터페이스 내에서 쓰려고 선언하는 것인데, body가 없을 거면 사실 필요도 없기 때문인 것 같다.
default method 사용시 주의할 점!!
default method를 사용할 때 주의해야 할 점이 있다.
이러한 default 메서드는 단순히 구현 편의를 제공할 뿐만 아니라,
해당 인터페이스를 구현한 모든 클래스에 메서드를 "끼워 넣을 수 있다"
인터페이스에서 기존에 없던 default 메서드를 추가했다고 생각해보자.
인터페이스를 구현한 클래스들은 손 놓고 가만히만 있어도 새로운 메서드를 사용할 수 있게 된다.
자바 8 부터는 구현 클래스도 모르는 새로운 식구가 우리집에 숨어 들어올 수 있게 된 것이다.
이는 자바 8에 새로 도입된 람다를 활용하기 위해서라고 한다.
물론 자바 라이브러리에서 만든 만큼 잘 만들었지만,
"모든" 상황에서 이전의 "모든" 구현들과 문제를 일으키지 않을 것이라고 확신할 수 있는가?
예를 들어 자바 8 이전에 구현된 아파치 커먼즈 라이브러리의 SynchronizedCollection은 클라이언트가 제공한 객체로 락을 건다. 모든 메서드에서 주어진 락 객체로 동기화를 진행한 다음, 내부 컬렉션 객체에 기능을 위임하는 래퍼 클래스인데, 자바 8에서 Collection 인터페이스에 추가된 removeIf를 바로 구현하고 있지는 않다.
어떤 클래스를 이용중인 클래스들은 이런 새로운 메서드의 등장에 바로바로 대응하지 못 할테고, 대응하는 동안 문제가 발생할 수도 있다.
같은 이유로 Object의 equals와 hashCode를 default로 안 된다.
이펙티브 자바 아이템 20에서 짧게 이유는 언급하지 않는 문제인데, 아래 아티클을 참고해보자.
equals나 hashCode는 두 객체가 같은 객체인지 확인하기 위해 쓰인다.
단순히 같은지 비교할 때나, Set, Map 같은 유일 Key자료구조에서 "다름"을 확인하기 위해 쓰인다.
만약 어떤 인터페이스에 내 마음대로 equals나 hashCode를 default method로 구현한다면 어떤 일이 생길까?
내 인터페이스를 구현해서 사용하던 사람들은 기본적인 Object의 equals나 hashCode의 동작을 기대하면서
다양한 로직을 짤 수 있다. 혹은 이미 그렇게 작성했다.
더는 설명하지 않아도 될 것이다.
우리가 default method로 위의 메서드들을 구현하는 순간 마음대로 "대체" 하게 된다.
이후 사용자는 그냥 가만히 있는데, 코드가 원하는대로 동작하지 않는다.
이런 논리적 오류는 당연히 찾아내기 쉽지 않고, 많은 문제로 이어질 수 있다.
그래서 디폴트 메서드를 작성할 때는 인터페이스를 구현하거나 상속하는 다른 인터페이스들을 위해 문서화를 해주는 것이 중요하다! (이펙티브 자바 Item 21)
그리고 Object의 equals와 hashCode를 default로 구현하면 안 된다. (이펙티브 자바 아이템 20)
가장 중요한건 웬만하면 진짜 디폴트 메서드의 추가가 필요한지 고민해 보는 것이 되겠다.
java 8 interface static method
static 메서드는 인스턴스와 독립적이기 때문에 사실 인터페이스에 추가되지 못할 이유는 없었다.
추가하지 않은 이유는 자바의 학습을 좀 더 쉽게 하기 위해서라고 한다.
위에서 언급했듯이 자바 인터페이스는 기본적으로 abstract 메서드만을 갖는데, 이 규칙을 지키기 위해서 구현이 꼭 필요한 static method를 허용하지 않았다고 한다.
그래서 등장한 것이 Collections라고 한다.
원래는 인터페이스에는 abstract 메서드만이 가능하기 때문에,
아래와 같이 Collection을 위한 static 메서드들은 Collections에 구현했다고 한다.
자바 8 부터는 static 메서드의 선언이 가능해졌고, body를 가질 수 있다 (필요하다)
2.4 상속과 구현
자바의 클래스는 C++의 클래스와 달리 다이아몬드 상속 문제로 인해 단일 상속만을 지원한다.
위 그림과 같이 여러 클래스를 extends 할 수 없다.
한 클래스는 여러 인터페이스를 구현할 수 있다!
인터페이스가 꼭 이 문제를 해결하기 위해 도입된 것이라는 말이 있지만 그건 오해다.
그리고 한 인터페이스는 여러 인터페이스를 확장(상속) 할 수 있다.
2.5 Object 메서드는 interface가 상속하는가 구현체가 상속하는가?
예시로 사용한 GreedyInterface의 구현체인 GreedyInterfaceImpl 클래스를 구현했다.
그리고 아래와 같이 참조변수 greedyInterface는 Object 클래스의 메서드를 호출할 수 있다.
모든 클래스의 최상위 클래스는 Object이다.
그럼 클래스인 GreedyInterfaceImpl 덕분에 메서드의 사용이 가능한걸까?
아니다, 이는 최상위 인터페이스인 GreedyInterface가 암묵적으로 Object의 public 메서드를 선언하기 때문이다.
위 글을 보면, 직접적인 superinterface가 없는 인터페이스 즉, 최상위 인터페이스는 Object에서 선언된 public 인스턴스 메서드를 암묵적으로 선언한다고 되어 있다.
그리고 Object에서 final로 선언된 메서드를 인터페이스가 선언하면 컴파일 에러가 발생한다고 적혀 있다.
Object에서 final로 선언되지 않은 hashCode나 equals는 문제 없지만,
나머지 메서드들은 위와 같이 에러가 발생한다.
2.6 당신이 "어떻게" 해오던 관심 없다 - 의존성 주입
인터페이스는 구현에 관계 없이 프로그램을 작성할 수 있게 해주었다.
기대만 충족한다면, 어떤 구현이 와도 상관 없이 구현할 수 있게 되었기 때문이다.
어떤 객체가 다른 객체를 "사용"하는 것을 "의존한다"고 표현한다 (정말 짧게 요약)
인터페이스를 사용한다면 구체적인 구현체가 없어도, 구현체가 어떻게 동작하는지를 몰라도 다른 객체를 사용할 수 있다.
이런 느슨한 의존은 엄청난 유연성을 가져다 주는데,
일단 프로그램을 작성하고 나중에 의존성을 "주입" 받을 수 있다.
[순수 자바 의존성 주입 예시 코드와 설명 넣기]
2.6.1 프레임 워크와 의존성 주입
한국에서 가장 유명한 프레임워크 "스프링"은 이런 의존성 주입을 아주 간단하게 동적으로 할 수 있도록 도와준다.
스프링에서 생성자를 통해서 혹은 @Autowired를 통해 의존성을 주입 받는 것이 바로 이 의존성 주입이다.
주입 받는 예시를 한번 보자.
위 코드는 스프링을 이용한 의존성 예시의 한 예이다.
빈으로 관리중인 OpenLectureService가 있고, private final 필드인 OpenLectureCacheRepository 인터페이스가 있다. RequiredArgsConstructor는 초기화가 필요한 필드에 대한 생성자를 만들어 주는데, 이 생성자를 통해 스프링이 스프링 빈으로 관리중인 OpenLectureCacheRepository의 구현체를 주입해준다.
그러니까, 저 인터페이스의 구현체를 직접 주입해준다는 것이다. 예를 들어 위 인터페이스에 대한 아래와 같은 구현체가 있고, 스프링 빈으로 관리중이라면, 저 구현체로 인터페이스 필드를 채워준다는 것이다.
스프링이 어딘가에서, OpenLectureService에 대한 빈을 생성할 때 OpenLectureRedisCacheRepository 객체를 미리 하나 만들어둔 다음, 주입해준다는 것이다.
따라서, OpenLectureService 입장에선 OpenLectureCachRepository의 구체적인 구현체를 직접적으로 알 필요가 없다! 인터페이스만을 알면 스프링을 통해 주입 받을 수 있다.
이런 의존성 주입을 통해 의존성을 역전할 수 있다.
2.6.2 의존성 "역전"의 의미?
이러한 상황을 왜 의존성 "역전"이라고 부를까?
의존하거나, 상속-구현 하는 경우 이렇게 화살표로 표현할 수 있다.
만약 의존성 주입이 없었다면, OpenLectureService는 구현체를 필드로 직접 가지고 있었을 것이다.
아무리 인터페이스 필드로 가지고 있어도,
`OpenLectureRepository openLectureRepository = OpenLectureRedisCacheRepository.INSTANCE` 와 같은 형태로 어떻게든 구현체를 알고 있어야 했을 것이다.
이런 상황에서 인터페이스를 의존하게 해보자. 그러면 의존 화살표가 아래와 같이 바뀐다.
Service는 구체적인 구현체가 어떻게 생겼는지, 누구인지 알지 못한다.
단지 프레임 워크가 넣어줄 뿐이다.
가운데에 추상화된 인터페이스가 끼이면서 이렇게 화살표를 반대로 돌려버릴 수가 있다.
OpenLectureService는 OpenLectureCacheRepository만을 알고
OpenLectureRedisCacheRepository 또한 OpenLectureCacheRepository가 어떻게 구성 되어 있는지 만을 알면 된다.
이렇게 의존성의 방향을 반대로 만드는 것을 "의존성 역전"이라고 부른다.
이런 의존성의 역전은 두 계층을 분리하여 사용할 수 있게 돕고,
이는 계층간의 연결을 약하게 만들 수 있다!
(더 자세한 내용 다른 글에서.. - 디미터 법칙과 객체의 자율성, 결합도 응집도)
이것이 인터페이스를 아주 멋지게 활용하는 방식이다.
인터페이스는 의존성 주입과 역전을 위한 아주 아주 아주 유용한 도구가 된다.
3 . 자바 클래스 vs 인터페이스
위에서 자바의 클래스와 인터페이스의 개념적 차이를 알아보았다.
이제 코드를 구현하면서 고려해야 하는 차이에 대해 알아보자.
3.1 추상 클래스 vs 인터페이스
추상클래스와 인터페이스는 꽤나 비슷해 보인다.
특히 Java 8에 추가된 인터페이스 default method 덕분에, 맘만 먹으면 거의 비슷한 역할을 수행하도록 만들 수 있다.
이 때문에 둘 중 어떤 것을 사용해야 할지 헷갈린다.
이럴때 중요한게 바로 "용도"이다.
용도를 정확하게 알면, 언제 어떤걸 사용할지가 명확해진다.
3.1.1 추상적 개념의 추상화! - Blueprint
일단 추상클래스는 말 그대로 "클래스"이다.
어떤 개념을 추상화하고, 상위 개념과 하위 개념을 나누고,
A라는 것이 B의 한 종류임을 나타내기 위한 것 - is a kind of - 이 클래스이다.
만약 이 역할을 하는 "무언가"를 만들 것이라면 인터페이스 대신 추상 클래스를 이용하면 된다.
추상 클래스는 추상 메서드가 하나라도 있는 클래스를 추상 클래스라고 부른다.
이런 추상 클래스는 일종의 "설계도"로써 "추상적 개념의 추상화"에 매우 유용하다.
예를 들어보자.
포유류 클래스가 있고, 이를 상속받은 동물 클래스들이 있다.
예를 들어 포유류인 향유고래와 고양이는 자손을 만들때 "새끼를 낳는다"
조류 클래스의 하위 클래스인 독수리와 비둘기는 "알을 낳는다"
그렇다면 이들의 공통 조상 클래스인 "동물 클래스"는 자손을 어떻게 만드는가?
모른다;
동물이라는 개념 자체는 자손을 만드는 방식이 딱 정해져 있지 않고 다양한다.
밥은 어떻게 먹는가? 울음은 어떻게 내는가?
"개념"은 행동을 정의할 수 없고, 행동할 수도 없다.
즉, 어떤 행동을 한다는 것을 정할 수는 있지만 "어떻게"할지는 바로 정할 수가 없고,
개념 자체의 행동 가능한 개체를 만들 수도 없다.
즉
1. 인스턴스가 만들어 져서는 안됨
2. 메서드가 호출되면 안됨 (행동 불가능)
3. 하위 클래스에서 오버로딩을 강제해야함 (자손을 "어떻게" 만들지 무조건 정해야 한다)
4. 상위 클래스의 객체 참조 변수로 만든 인스턴스가 해당 메서드를 호출할 수 있어야 함 (동물 참조변수가 가리키는 향유고래는 자식을 낳을 수가 있음)
이런식으로 어떤 추상적인 "개념"을 추상화 할 때 유용한 것이 바로 추상 클래스이다.
추상클래스는 일종의 설계도로써 추상 메서드를 이용해 위와 같은 4가지 조건을 충족시켜준다.
이런 용도로 사용할 경우 추상 클래스를 사용해야 하는 것이다.
아닌 경우, 타입을 정의하는 용도 혹은 어떤 행동이 가능하다는 것을 정의할 용도로 사용할 때는 인터페이스로 만들어야 한다. java 8dml default method의 등장으로 인해 헷갈릴 때는 용도를 정확히 따져보자.
3.1.2 추상 클래스 보다 인터페이스를 우선해야 하는 이유
바로 앞에서 용도의 차이와 관계의 차이에 대해 알아봤다.
앞에선 용도의 차이에 집중했고, 이번엔 관계 차이에 집중하자.
추상클래스의 하위 클래스는 반드시 추상 클래스의 하위 클래스가 된다.
그리고 인터페이스의 구현체들은 다른 어떤 클래스를 상속했던 간에 구현한 인터페이스와 같은 타입으로 취급된다. (인터페이스의 규약만 잘 지킨다면)
추상클래스 대신 인터페이스를 우선하면 얻게되는 장점을 알아보자. (이펙티브 자바 아이템 20)
1, 기존 클래스에 새로운 인터페이스를 추가하기 쉽다.
2. 믹스인 정의에 안성맞춤이다.
3. 계층구조 없는 타입을 만들 때 편리하다.
추상 클래스는 하위 모든 개념들의 상위 개념으로써 존재해야 한다. 따라서, 어떤 클래스 위에 새로운 추상 클래스를 넣게 된다면, 기존의 클래스를 상속 받는 모든 클래스들의 공통 조상이 되어 주어야 한다. 그리고 모든 자손은 그 영향을 그대로 받게 된다. 이는 말만 들어도 너무 어려운 일임을 알 수 있다. 때로는 적절하지 못한 영향을 미칠 수 있다.
반면, 새로운 인터페이스를 추가하기는 너무나도 쉽다.
단지 새로운 기능을 섞고 싶은 클래스를 찾아가, 해당 인터페이스를 구현하기만 하면 된다.
너무나도 간단하며, 영향이 작다.
이런 mixin을 추상 클래스로 구현 할때는 일이 복잡해진다. 클래스는 단일 상속만이 가능하기 떄문이다.
또한 계층을 엄격하게 구분하기 어려운 개념의 표현이 용이해진다.
예를 들어 가수 클래스와 작곡가 클래스가 있다고 생각해보자.
에드 시런을 코드로 작성할 것인데, 에드 시런은 조금 불쾌할 것이다.
저는 노래도 하고 작곡도 제가 하는데요?
이런 상황을 클래스로 표현하려면 머리가 아파진다.
클래스는 단일 상속만이 가능하기 때문이다.
하지만, 인터페이스로 표현하면 너무 간단하다.
에드시런 클래스가 두 인터페이스를 구현하기만 해도 된다.
혹은 두 인터페이스를 확장한 새로운 인터페이스를 만들면 그만이다.
이런 인터페이스를 사용하는 이점들과 용도, 관계를 잘 생각하면 추상 클래스와 인터페이스 중 어떤 것을 고를지 더욱 명확해질 것이다.
3.2 extends class method vs default method
만약 상위 클래스에서 정의한 메서드와 default method가 겹치면 어떻게 될까?
-> 상위 클래스의 메서드가 상속된다. 그리고 default method는 무시된다.
위 그림과 같이 이름이 똑같은 메서드 두 개를 선언한 다음
출력문을 다르게 설정 해주었다!
그림과 같이 클래스 최고가 호출 되었다!
그렇다면 interface끼리 default method의 이름이 겹치는 경우 어떻게 처리 될까?
-> 개정의를 강요한다
이렇게 default method의 이름이 겹치는 인터페이스 3개를 정의해 보았다
구현을 강요한다!
3.3 클래스는 풍성하게, 인터페이스는 작게
객체지향적으로 클래스와 인터페이스를 작성할 때는 LSP와 ISP를 준수해야 한다.
상위 클래스는 풍성할 수록 좋고, 인터페이스는 작을 수록 좋다는 이야기와 관련된 원칙들이다.
단일 책임을 갖는 다는 원칙 하에, 상위 클래스는 풍성할 수록 좋다.
LSP는 리스코프 치환 원칙으로 "서브 타입은 언제나 자신의 base type으로 교체할 수 있어야 한다"는 원칙이다.
좀 더 구체적인 지침을 말 하자면,
1. 하위 클래스 is a kind of 상위 클래스이여야 하고
2. 구현 클래스 is able to 인터페이스여야 한다.
하위 클래스를 작성하거나, 인터페이스를 클래스에서 구현할 때 이 두가지 지침을 지켜야 한다.
2번은 어렵지 않으나, 1번이 어려울 수 있다.
내가 추가중인 기능이 하위 클래스에 모두 적용되어도 개념적으로, 문제가 없는가?
내가 어떤 기능을 추가하더라도, 상위 개념으로 표현할 수 있는가?
말만 들으면 당연하지만, 클래스 상속 개념을 확장이 아닌, 정말 가계도상 상속으로 이해하고 있다면 논리적으로 흠이 생긴다.
예를 들어 딸은 아버지의 자식이다. 아버지 참조 변수로 딸 인스턴스를 가리키는 상황은 자연스러운가?
이는 클래스의 상속을 is a kind of 관계가 아닌 계층도 형식으로 상속을 이해했을 때 발생한다.
그러니까 하위 클래스 인스턴스는 상위 클래스로 선언한 객체 참조 변수에 대입해서 사용하는데 문제가 없어야 한다.
하위 클래스가 공통적으로 가지는 특성들을 가진 풍성한 상위 클래스를 짜려고 노력해야 한다. (물론 단일 책임 하에)
반대로 인터페이스는 작을 수록 좋다.
인터페이스를 작성할 때는 ISP - 인터페이스 분리 원칙을 지켜야 하는데, 인터페이스가 최소 메서드만을 제공해야 한다는 원칙이다.
SRP와 비슷한 개념으로, 인터페이스는 자기 이름 및 역할에 맞는 최소한의 기능만 가지고 있어야 더 사용성이 높다.
정말 극단적인 예를 들면, "식당을 관리할 수 있다"라는 인터페이스가 있다고 가정해보자.
식당을 관리할 때는 가게도 청소해야 하고, 홀도 봐야하고, 재고도 관리해야 하는 등 여러 업무가 가능해야 한다.
이 경우 위 인터페이스를 구현하는 클래스는 위 역할들을 모두 할 줄 알아야한다.
식당이 커져 분업을 해야한다면, 위 인터페이스만으로는 무리이다.
차라리 "청소할 수 있음", "손님 관리가 가능함", "재고 관리할 수 있음" 등으로 나뉘어 있다면,
각각을 직원 한명 한명에게 구현해 줄 때도 용이하고, 위 3가지 역할 중 2가지만 할 수 있는 사람, 3가지 전부 할 수 있는 사람 모두를 표현하기가 너무 용이하다.
그래서 상위 클래스를 작성할 때와 달리 인터페이스는 자신의 역할에 맞는 최소한의 기능만을 갖는 것이 유리하다고 할 수 있다.
기본적으로 SRP를 지키면서, 클래스와 인터페이스를 작성할 LSP와 ISP를 고려하며 작성해야 한다.
4. 인터페이스의 품질을 높히는 방법
어떤 인터페이스가 좋은 인터페이스일까?
인터페이스의 품질을 높히는 방법들을 구체적인 코드 작성 지침들과 함께 만나보자.
지금 이 글이 너무 길어져 다른 글에 적겠다. -> [ 디미터 법칙과 인터페이스 품질 높히기 ]
Reference
- 자바의 정석 <남궁성>
- 스프링 입문을 위한 자바 객체지향의 원리와 이해 <김종민>
- 이펙티브 자바 - 아이템 18 ~ 22, 64 <조슈아 블로크>
- 오브젝트 <조영호>
- Oracle - Chapter 9. Interfaces
'🌱 Java & Spring 🌱' 카테고리의 다른 글
코드로 이해하는 Red-Black Tree의 연산과 Java TreeMap에서의 구현 (0) | 2024.04.09 |
---|---|
자바의 Type에 대해 (0) | 2023.06.15 |
Lambda & Stream의 도입 배경과 원리, 최적화 전략! 알고 쓰자!!! (4) | 2023.06.02 |
바이트 코드를 JVM에 싸서 드셔보세요 (0) | 2023.05.24 |
Bucket4J 사용하는 법 자세히 알랴드림 (0) | 2023.03.20 |