[Java] 인터페이스를 사용하자!
자바는 다중 구현을 위해 인터페이스, 추상 클래스 기능을 제공한다.
그러면 어느 것을 사용하는게 더 좋을지 한번 고민해보자!
🤗 인터페이스의 장점
❗ 자유로운 추가
자바는 단일 상속이 조건이다.
이러한 관점에서 추상 클래스 방식은 새로운 타입을 정의하는 데 커다란 제약이다.
예를 들어 개발 도구는 컴퓨터, 안드로이드 개발자에게는 삼성폰, IOS 개발자에게는 아이폰 등으로 나눌 수 있다.
그렇다면 'Computer ⊂ DevelopmentTool'로 표현할 수 있다.
이번에는 게임기를 생각해보자.
게임기에는 컴퓨터, 스위치, 닌텐도, 플스 등등 여러가지가 있다.
그렇다면 'Computer ⊂ GameMachine'로 볼 수도 있겠다.
그런데 위에서 말했다시피 자바는 단일 상속이 조건이라 아래와 같은 표현이 불가능하다.
class Computer extends DevlopmentTool, GameMachine { ... }
반면 인터페이스는 다른 인터페이스가 존재 하더라도 새롭게 추가할 수 있다.
class Computer implements DevlopmentTool, GameMachine { ... }
❗ 계층구조가 없는 타입 프레임워크 생성
타입을 계층적으로 정의하면 수많은 개념을 구조적으로 잘 표현할 수 있다.
하지만 현실에는 이를 구분하기 어려운 경우가 많다.
예를 들어 가수(Singer)와 작곡가(Songwriter)가 있다고 하자.
가수는 노래를, 작곡가는 작곡을 한다고 정의한다.
public interface Singer {
AudioClip sing(Song s);
}
public interface Songwriter {
Song compose(int chartPosition);
}
하지만 가수가 작곡하는 경우도 있지 않은가?
클래스는 Singer, Songwirter 인터페이스를 둘 다 implements할 수 있다.
또는 아래와 같은 새로운 타입 프레임워크를 구현할 수 있다.
public interface SingerSongwirter extends Singer, Songwriter {
AudioClip strum();
void actSensitive();
}
❗ 래퍼 클래스 관용구와의 조합
래퍼 클래스와 인터페이스 조합은 안전하고 강력하다.
타입을 매개변수로 받아 래퍼 클래스로 유연하게 대처할 수 있다.
추상 클래스로 저장된 타입에 기능을 추가하려면 상속을 해야한다.
이는 래퍼 클래스보다 활용도가 떨어지고 깨지기 쉽다.
👉 이에 대해서는 Effective Java - item 18을 참고하자.
관련 글: https://yeonyeon.tistory.com/206
❗ 믹스인 정의에 안성맞춤
믹스인; mixin: 클래스가 구현할 수 있는 타입
👉 믹스인을 구현한 클래스에 원래의 주된 타입 외에도 특정 선택적 행위를 제공한다고 선언함
example - Comparable
👉 자신을 구현한 클래스의 인스턴스들끼리 순서를 정할 수 있다고 선언
😣 인터페이스 단점
- 인스턴스 필드 불가
- private 정적 메서드 외의 public이 아닌 정적 멤버 불가
😊 디폴트 메서드
- 인터페이스 메서드 중 구현 방법이 명백하다면 디폴트 메서드를 이용하자. (Java 8부터 이용 가능)
- @implSpec을 붙여 문서화할 것 (Effective Java - item 19)
제약 사항
- Objects의 equals와 hashCode 같은 메서드는 디폴트로 제공 X
- 직접 생성하지 않은 인터페이스에는 디폴트 메서드 추가 X
😆 골격 구현 클래스
- 인터페이스와 추상 클래스의 장점을 모두 취하는 방법
- 인터페이스: 타입 정의 (+필요 시 디폴트 메서드 추가)
- 골격 구현 클래스: 나머지 메서드 정의
- 👉 템플릿 메서드 패턴
이름을 보통 Abstract~로 짓는다.
ex: 인터페이스 이름: List, 골격 구현 클래스 이름: AbstractList
🔻 골격 구현 작성 방법
- 다른 메서드 구현에 사용되는 기반 메서드 선정
- 1을 사용해 직접 구현 가능한 메서드들을 디폴트 메서드로 제공
- 남아있는 메서드가 있다면 이 인터페이스를 구현하는 골격 구현 클래스를 생성해 남은 메서드들 작성
public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
// Map.Entry의 기반 메서드: getKey(), getValue()
// setValue()도 선택적으로 기반 메서드
@Override
public V setValue(V value) {
throw new UnsupportedOperationException();
}
// Object 관련 메서드
// -> default 메서드로 제공 X
// Map.Entry.equals의 일반 규약 정의
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (!(obj instanceof Map.Entry)) return false;
Map.Entry<?, ?> e = (Map.Entry) obj;
return Objects.equals(e.getKey(), getKey())
&& Objects.equals(e.getValue(), getValue());
}
// ...
}
example - intArrayAsList
// List 구현체를 반환하는 정적 팩토리 메소드
// List: 인터페이스. Integer라는 타입을 정의
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
// AbstractList: 골격 구현체. int[]를 List<Integer>로 변환
// Java 8 이하인 경우 AbstractList<Integer>으로 수정
return new AbstractList<>() {
@Override
public Integer get(int i) {
return a[i]; // 오토박싱(Effective Java - item 6)
}
@Override
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val; // 오토언박싱
return oldVal; // 오토박싱
}
@Override
public int size() {
return a.length;
}
};
}
- List 구현체를 반환하는 정적 팩토리 메서드
- int[]를 List<Integer> 형태로 보여주는 어댑터이기도 함
시뮬레이트한 다중 상속; simulated multiple inheritance
골격 구현 클래스를 우회적으로 이용하는 것도 가능하다.
- 인터페이스를 구현한 클래스에서 골격 구현을 확장한 private 내부 클래스를 정의
- 각 메서드 호출을 내부 클래스의 인스턴스에게 전달
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
public void clear() {
s.clear();
}
// ...
}
단순 구현; Simple Implementation
- 골격 구현의 작은 변종으로 동작하는 가장 단순한 구현을 의미
- 골격 구현 같이 상속을 위해 인터페이스 구현
- != 추상 클래스
example - AbstractMap.SimpleEntry
// Entry interface의 단순 구현
// SimpleEntry를 참조가 살아있는 동안 Map의 내용이 변경되지 않음을 보여주고
// extra map 조회를 피하기 위해 entry의 값을 캐싱할 수도 있음
public static class SimpleEntry<K,V> implements Entry<K,V>, java.io.Serializable {
private static final long serialVersionUID = -8499721149061103585L;
private final K key;
private V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() {...}
public V getValue() {...}
public V setValue(V value) {...}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return eq(key, e.getKey()) && eq(value, e.getValue());
}
public int hashCode() {...}
public String toString() {...}
}
📔 글을 마치며...
여태까지 상속 하지마!!! 인터페이스 써!!! 라는 뉘앙스의 글을 많이 봐왔다.
공감 가지 않았다. 사실 지금도 딱히 100% 공감하지는 않는 중이다.
캡슐화를 해친다는 입장에서 자주 쓰면 안좋기야 하겠다만은 추상 클래스도 필요한 경우가 분명 존재한다.
상황에 따라 잘 판단해보자.
여러 상황을 겪으며 코딩하다 보면 잘 판단할 때가 오겠지 :)
참고
- Effective Java / 조슈아 블로크 / 4장 - item 20
- https://medium.com/@logishudson0218/effective-java-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%99%80-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-06-f97175b5eeef
- https://docs.oracle.com/en/middleware/fusion-middleware/coherence/12.2.1.4/java-reference/com/tangosol/util/InvocableMapHelper.SimpleEntry.html