💡 적용 대상이 ElementType.Type인 마커 애너테이션을 작성하고 있다면, 마커 인터페이스와 비교해보라
📌 마커 인터페이스 vs 마커 어노테이션
interface MarkerInterface {
// 아무런 메서드가 없음
}
public class MyClass implements MarkerInterface {
// 표식을 나타내기 위해 MarkerInterface를 구현
}
- 아무런 메서드나 상수를 가지지 않는 인터페이스
- 클래스가 해당 인터페이스를 구현하는 것으로 표식을 나타냄
- 주로 인터페이스 구현을 통해 특정 동작이나 기능 활성화 용도로 사용됨
- ex. Serializerable 인터페이스를 구현한 클래스의 인스턴스는 ObjectOutputStream을 통해 쓸(write) 수 있다고, 즉 직렬화할 수 있다고 알려준다.
- 상속을 통해 확장 가능하며, 인터페이스 계층 구조를 통해 다양한 표식 가능
@interface MarkerAnnotation {
// 애너테이션 요소가 없음
}
public class MyClass {
@MarkerAnnotation
public void myMethod() {
// 표식을 나타내기 위해 MarkerAnnotation을 사용
}
}
- 컴파일러에게 특정 요소에 대한 정보를 제공하는 역할
- @ 기호를 사용하며, 메타 데이터로 사용
- 애너테이션은 요소, 타입, 메서드 등 다양한 프로그램 요소에 적용 가능
- 주로 런타임에 애너테이션 정보를 읽어와 동작을 변경하거나 처리
- 애너테이션을 정의하는 것만으로 사용할 수 있으며, 상속이나 구현을 통한 확장 불가능
- ex. @Override 어노테이션은 메서드가 상위 클래스나 인터페이스의 메서드를 오버라이드 하고 있음을 나타낸다.
📌 마커 인터페이스의 장점
마커 어노테이션에 비해 구식이라는 평을 받지만, 2가지 면에서 마커 인터페이스가 나은 점이 있다.
1️⃣ 마커 인터페이스를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있다.
- 마커 인터페이스와 마커 어노테이션 모두 클래스가 어떤 속성(Type)을 가진다는 표시를 할 수 있다.
- 마커 인터페이스는 컴파일 타임, 마커 어노테이션은 런타임에서 오류를 검출할 수 있다.
- 마커 인터페이스는 ObjectOutputStream.wirteObject() 인수로 Serializerable을 받도록 한다.
- 마커 어노테이션은 Object를 받도록 설계되어, 런타임에서야 문제 확인이 가능하다.
public class Main {
static void newWriteObject(Serializable object, String path) {
File file = new File(path);
try (ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream(file))) {
oops.writeObject(object);
} catch (IOException e) {
System.out.println("Runtime Error");
}
}
}
public class Main {
static class Person implements Serializable { // Serializerable 상속
...
}
static class Student {
...
}
public static void main(String[] args) throws IOException {
String path = "hello.txt";
newWriteObject(new Person("홍길동", 20, "서울시"), path);
newWriteObject(new Student("홍길동", 20, 20123456), path); // 컴파일 에러
}
}
반면, Serializable의 writeObject()는 다음과 같이 구현되어 있다.
private void writeObject(Object obj, boolean unshared)
throws IOException {
...
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
...
2️⃣ 적용 대상을 더 정밀하게 지정할 수 있다.
- 마커 어노테이션은 @Retention(RetentionPolicy.TYPE)으로 클래스, 인터페이스, enum 어노테이션에만 달 수 있는 마커를 만들 수 있다.
- 마커 인터페이스는 이보다 정밀한 제한을 거는데 용이하다.
예를 들어, Likelion 인터페이스를 구현한 클래스에만 EffectiveJava라는 마커를 달고 싶은 경우를 가정하자.
Likelion 인터페이스를 EffectiveJava가 확장하고, EffectiveJava를 해당 클래스가 구현하면 된다.
그렇게 되면 EffectiveJava라는 마커 인터페이스를 사용하면서, Likelion 인터페이스의 하위 타입이 성립한다.
📌 마커 어노테이션의 장점
마커 인터페이스보다 훨씬 거대한 어노테이션 시스템 지원을 받고 있다. (더 설명이 필요해?)
한 마디로 정리하면 이렇게 쓸 수 있다.
"타입을 정의할 게 아니라면 인터페이스를 사용하지 말라"
📌 무엇을 사용해야 할까?
- 마킹하려는 곳이 클래스, 인터페이스가 아니라면 애너테이션
- 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있다면 인터페이스를 쓰는 게 좋다.
- 그럼에도 애너테이션을 활발히 활용하는 프레임워크에서 사용한다면 애너테이션을 사용하는 편이 좋을 것이다.