티스토리 뷰
추상 클래스와 인터페이스의 공통점과 차이점에 대해서는 넘어가고 바로 본론으로 들어가도록 하자. 우선 JAVA 8 부터 인터페이스에서 Default Method 를 제공하기 때문에 추상 클래스와 같이 인스턴스 메서드를 구현한 형태로 제공할 수 있다. 기존에 "인터페이스에는 추상 메서드로만 구성되어야 한다" 에서 벗어나 완성된 메서드를 제공할 수 있다는 것이다.
둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다는 점이다. 즉, 반드시 상속을 통해서만 추상 클래스 사용할 수 있다. 이 자체만으로 자바의 다중상속으로 인해 큰 제약이 생기게 된다.
이외에도 인터페이스는 믹스인 정의에 안성맞춤이고, 기존 클래스에도 손쉽게 구현할 수 있는 등 다양한 장점을 가지고 있다. 또한, 인터페이스를 이용하면 게층구조가 없는 타입 프레임워크를 만들 수 있다.
인터페이스와 추상골격 클래스 구현
인터페이스를 구성함에 있어 괜찮은 방법이 소개되어 있어 포스팅에 담으려 한다.
인터페이스의 메서드 중 구현 방법이 명백한 것이 있다면, 그 구현을 디폴트 메서드로 제공해 인터페이스를 구현하려는 프로그래머들의 일감을 덜어줄 수 있다. 어차피 누가 구현하든 동일한 구현이 나올 것이라면 미리 만들어주는 것이다.
추상골격 구현 클래스를 함께 제공하는 식으로 인터페이스와 추상 클래스의 장점은 모두 취하는 방법도 있다. 인터페이스를 통해서 타입을 정의하고, 필요하다면 디폴트 메서드를 함께 정의한다. 그리고 추상골격 클래스를 통해 인터페이스에서 정의단 나머지 메서드를까지 구현한다.
(디폴트 메서드로 구현할 수 있는 것들은 모두 디폴트 메서드로 구현해주는 것이 좋다)
public abstract class AbstractList<T> implements List<T> {
// TODO : List<T> 인터페이스의 추상메서드중 공통된 골격을 구현
}
이렇게 해두면 아래와 같이 추상골격 클래스(AbstractList) 를 확장하는 것만으로 인터페이스를 구현할 수 있다.
(물론 추상골격 구현 클래스에서조차 구현하지 않은 메서드들에 대해서는 구현을 해줘야 한다)
static List<Integer> intArrayAsList(int[] array) {
Objects.requireNonNull(a);
return new AbstractList<>() {
@Override
public Integer get(int i) {
return array[i];
}
@Override
public Integer set(int i, Integer val) {
int oldVal = array[i];
array[i] = val;
return oldVal;
}
@Override
public int size() {
return array.length();
}
}
}
하지만 이러한 골격구현 클래스를 사용할 수 없거나 상속받을 수 없는 상황(다중상속 문제)에서 아래와 같이 Delegator 패턴을 통해서 추상클래스 상속에 대한 단점을 극복할 수 있다.
(Delegator 패턴은 Helper Object 를 두어서 해당 객체에게 어떠한 일을 위임한 후 포워딩하는 패턴을 말한다)
public class MyList<T> implements List<T> {
private class AbstractListDelegator<T> extends AbstractList<T> {
@Override
// TODO : 추상골격 클래스에서도 구현해주지 못한 메서드들에 대해서 Overriding
}
private AbstractList<T> delegator = new AbstractListDelegator<>();
@Override
public Integer get(int i) {
return this.delegator.get(i);
}
@Override
public Integer set(int i, Integer val) {
retrun this.delegator.set(i, val);
}
...
}
구현 클래스에 추상골격 구현 클래스를 상속한 private 이너 클래스를 만들고, 해당 이너 클래스에 대한 객체를 인스턴스 변수(Helper Object)로써 생성한다. 그리고 그 인스턴스 변수를 통해서 구현하려 했던 인터페이스의 메서드들을 포워딩하는 것이다.
책에서는 이러한 방법을 시뮬레이트한 다중 상속이라고 한다.
사실 이러한 구현 방식은 "[아이템 18] 상속보다는 컴포지션을 사용하라" 에서 나왔었다. Delegation(위임) 과 Composition(구성) 은 굉장히 유사한 것처럼 보인다. 조금 알아본 바로 Delegation 은 Composition 하고 있는 인스턴스 변수를 통해서 포워딩하는 것까지의 과정을 일컫는 것 같다. 즉, Delegation 을 위해서는 Composition 이 필요하다!
자세한 내용은 아래 블로그를 참고하자!
https://ch4njun.tistory.com/247
https://blog.kotlin-academy.com/programmer-dictionary-delegation-vs-composition-3025d9e8ae3d
여기서 이러한 의문이 들 수 있다. 추상골격 구현 클래스에서 구현해주려는 클래스들을 그냥 디폴트 메서드로 다 구현해놓으면 되는거 아니야..? 하지만, equals, hashCode 와 같은 Object 클래스의 메서드는 디폴트 메서드로 제공하면 안되기 때문에 이러한 메서드를 기본적으로 제공하기 위해서는 추상골격 구현 클래스가 필요하다.
그리고 가능한 한 인터페이스의 디폴트 메서드를 통해서 정의해주는 것이 좋다.
뜬금 없이 디폴트 메서드의 사용법에 대해서 하나 던져보려고 한다. 사실 마찬가지로 Effective JAVA 에서 나왔지만 별도로 포스팅하지 않을 것이기 때문에 여기에 남긴다.
기본적으로 JAVA 8 이전에는 이미 만들어진 인터페이스를 다양한 클래스에서 구현하고 있는 상황에서 새로운 메서드를 인터페이스에 삽입하는 것이 불가능했다.
하지만 JAVA 8 이후에는 디폴트 메서드를 통해서 이러한 삽입이 가능해졌다.
디폴트 메서드를 재정의하고 있는 클래스들은 자신의 방법대로 사용하고, 그렇지 못한(미처 삽입을 알지 못하거나 의도적으로 재정의하지 않은) 클래스들은 자연스럽게 삽입된 디폴트 메서드를 사용하게 된다.
하지만 이런 디폴트 메서드 삽입은 일방적이기 때문에 반드시 문제가되는 부분이 생긴다.
(궁금하면 [아이템 21] 을 참고하자)
'개발 > Effective JAVA' 카테고리의 다른 글
[아이템 18] 상속보다는 컴포지션을 사용하라 (0) | 2021.07.15 |
---|---|
[아이템 17] 변경 가능성을 최소화하라 (0) | 2021.07.13 |
[아이템 15] 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2021.07.13 |
[아이템10~13] 모든 객체의 공통 메서드 재정의 (0) | 2021.06.27 |
[아이템9] try-finally보다는 try-with-resources를 사용하라 (0) | 2021.06.22 |