아이템 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