아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라. - ksw6169/effective-java GitHub Wiki
마커 인터페이스(marker interface)
아무 메소드도 담고 있지 않으면서 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스라 한다. 마커 인터페이스의 대표적인 예로는 Serializable
인터페이스가 있는데, 이 인터페이스는 자신을 구현한 클래스의 인스턴스는 ObjectOutputStream
을 통해 write 할 수 있다고, 즉 직렬화(serialization)할 수 있다고 알려준다.
public class User {
private long id;
private String name;
public User(long id, String name) {
this.id = id;
this.name = name;
}
}
만약 위의 User 클래스처럼 Serializable
을 구현하지 않을 시에는 직렬화 시 NotSerializableException
이 발생한다.
public static void main(String[] args) {
User user = new User(1L, "corgi");
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
}
} catch (Exception e) { // NotSerializableException 발생
e.printStackTrace();
}
}
public class ObjectOutputStream
extends OutputStream implements ObjectOutput, ObjectStreamConstants {
...
private void writeObject0(Object obj, boolean unshared) throws IOException {
...
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
// NotSerializableException 발생
if (extendedDebugInfo) {
throw new NotSerializableException(cl.getName() + "\n" + debugInfoStack.toString());
}
} else {
throw new NotSerializableException(cl.getName());
}
...
}
}
마커 인터페이스가 마커 어노테이션보다 좋은 점
마커 어노테이션이 등장하면서 마커 인터페이스는 구식이 되었다는 얘기가 있지만 이는 사실이 아니다. 마커 인터페이스는 두 가지 면에서 마커 인터페이스보다 낫다.
1. 마커 인터페이스는 인스턴스를 구분하는 타입의 용도로 사용할 수 있다.
마커 인터페이스는 타입의 용도로 사용할 수 있는 반면에 마커 어노테이션은 이 용도로 사용할 수 없다. 또한 마커 어노테이션을 사용했다면 런타임에야 발견할 오류를 컴파일 타임에 잡을 수 있다.
예를 들어 ObjectOutputStream.writeObject()
메소드는 인수로 받은 객체가 Serializable을 구현했을 것이라 가정한다. 그런데 이 메소드는 Serializable
이 아닌 Object
객체를 받도록 설계되었다. 즉, 직렬화할 수 없는 객체를 넘겨도 런타임에야 문제를 확인할 수 있게 되는 것이다. 마커 인터페이스를 사용하는 주요 이유가 컴파일타임 오류 검출인데, 그 이점을 살리지 못한 것이다.
2. 적용 대상을 더 정밀하게 지정할 수 있다.
적용 대상(@Target
) 을 ElementType.TYPE
으로 선언한 어노테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 어노테이션)에 달 수 있다. 부착할 수 있는 타입을 더 세밀하게 제한하지는 못한다는 뜻이다. 만약 마커를 인터페이스로 정의했다면 마킹하고 싶은 클래스에만 그 인터페이스를 구현하게 하면 되므로 적용 대상을 더 정밀하게 지정할 수 있는 이점이 생긴다.
반대로 마커 어노테이션이 마커 어노테이션보다 좋은 점
거대한 어노테이션 시스템의 지원을 받을 수 있다.
어노테이션을 적극 활용하는 프레임워크에서는 마커 어노테이션을 쓰는 쪽이 일관성을 지키는 데 유리하다.
마커는 언제 사용하는 게 좋을까?
마커 어노테이션을 써야 하는 상황
클래스, 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역변수 등)에 마킹해야 할 때 사용한다.클래스와 인터페이스만이 인터페이스를 구현하거나 확장할 수 있기 때문이다.
마커 인터페이스를 써야 하는 상황
마킹된 객체를 매개변수로 받는 메소드를 작성할 일이 있다고 판단될 경우 사용한다. 이 경우 마커 인터페이스를 해당 메소드의 매개변수 타입으로 사용하여 컴파일 타임에 오류를 잡아낼 수 있게 된다.
마커 어노테이션 vs 마커 인터페이스
마커 인터페이스를 메소드의 매개변수 타입으로 사용하는 일이 절대 없다고 확신하는 경우 마커 어노테이션을 사용하는 것이 더 나은 선택이다. 추가로 어노테이션을 주로 사용하는 프레임워크에서 사용하려는 마커라면 마커 어노테이션을 사용하는 편이 좋을 것이다.
정리
- 새로 추가하는 메소드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자.
- 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나 어노테이션을 적극 활용하는 프레임워크의 일부로 마커를 사용하고자 한다면 마커 어노테이션이 올바른 선택이다.
참고 자료
- Effective Java 3/E