직렬화할 때 사용하는 방법 중에 Serializable과 Parcelable 두 개가 있다.
그 중에서 Serializable을 공부하던 중 이것이 마커 인터페이스라고 부르는 것을 보았다.
그래서 생긴 의문은 도대체 마커 인터페이스는 무엇인가

마커 인터페이스

자바의 마커 인터페이스는 일반적인 인터페이스와 동일하지만 사실상 아무 메소드도 선언하지 않은 인터페이스를 말한다. 예를 들면 아래와 같다.

1
2
3
public interface SomethingObject{

}

얼핏 보기엔 난해한 코드이다. 인터페이스만 있고 메소드가 없으니 어디에다 쓸지도 난해하다. 자바로 코딩을 하다보면 저런 인터페이스가 종종 있다. 자바의 대표적인 마커 인터페이스로는 위에서 언급한 Serializable, Cloneable과 흔히 알지는 못하지만 Spring에서 event 리스너를 사용한다면 종종 보이는 EventListener라는 인터페이스도 있다.

뭔가 대단한 것처럼 보일수도 있지만 실질적으로는 간단하다. 대부분의 경우에는 단순한 타입 체크라고 할 수 있다. 자바의 대표적인 마커 인터페이스인 Serializable를 살펴보자.

1
2
3
public interface Serializable{

}

메소드가 한개도 선언되어 있지 않다. Serializable 인터페이스 같은 경우에는 직렬화를 할 수 있다는 뜻이다. 즉, 이 인터페이스를 구현하지 않은 클래스의 경우에는 직렬화를 하지 못한다.

1
2
3
4
5
6
7
8
9
10
11
public void serializableTest() throws IOException, ClassNotFoundException {
File f= new File("a.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(f));
objectOutputStream.writeObject(new SomeObject("wonwoo", "test@test.com"));
}

class SomeObject {
private String name;
private String email;
//생성자 및 기타 메서드 생략
}

위 코드를 실행하면 아래와 같은 에러가 발생한다.

1
java.io.NotSerializableException: me.wonwoo.SomeObject

이는 직렬화를 할 수 있는 Serializable을 구현하지 않았기 때문이다. 그렇다면 직렬화를 할 수 있도록 Serializable 인터페이스를 구현해보자.

1
2
3
4
class SomeObject implements Serializable {
private String name;
private String email;
}

간단하게 구현할 수 있다. 인터페이스의 메소드도 없으니 구현할 메소드도 필요 없다. 그냥 선언만 해주면 된다. 그럼 위에서 사용했던 writeObject() 메소드 안을 들여다 보자. writeObject() 메소드 안에는 writeObject0()가 존재한다. 이 메소드 맨 아래에 보면 다음과 같은 코드가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//... 

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 {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
//...

이 코드에는 if문이 꽤 있다. 보면 String은 Serializable을 구현했으니 되고 배열도 Serializable 할 수 있고 Enum도 Serializable를 구현했으니 되고 다음으로는 Serializable가 되어있는지 체크하는 부분이다. 만약 Serializable가 없다면 에러로 처리한다. 위에서 보았듯이 간단하게 Serializable가 선언되었는지 안되어 있는지 체크 정도만한다. 실질적으로 뭘 하는 건 아니다. 그래서 마커 인터페이스라고 부른다.

마커 인터페이스는 어노테이션으로도 대체 가능하다. 만약 @SomeAnnotation이라는 어노테이션이 있다면 아래와 같이 가져와서 체크하면 된다.

1
final SomeAnnotation someAnnotation = someObject.getClass().getAnnotation(SomeAnnotation.class);

그럼 마커 인터페이스와 마커 어노테이션의 차이를 살펴보자.
마커 인터페이스 같은 경우에는 컴파일 시점에 발견할 수 있다는 큰 장점이 있다. 그리고 또한 적용 범위를 좀 더 세밀하게 지정할 수 있다.

만약 어노테이션 자료형을 선언할 때 target에 ElementType.TYPE라고 지정해서 사용한다고 하면 ElementType.TYPE은 클래스 뿐만 아니라 인터페이스에도 적용 가능하다. 그런데 특정한 인터페이스를 구현한 클래스에만 적용할 수 있어야 하는 마커가 필요하다고 가정해보자.

마커 인터페이스를 쓴다면 그 특정 인터페이스를 상속하도록 선언만 하면 된다. 그럼 마커를 상속한 모든 자료형은 자동으로 그 특정 인터페이스의 하위 자료형이 된다.

그렇다면 마커 어노테이션의 장점은 뭘까? 마커 어노테이션은 유연하게 확장이 가능하다. 어노테이션을 만들어 사용한 뒤에도 계속적으로 더 많은 정보를 추가할 수 있는 것이 큰 장점이다.

예를 들어, 어떤 어노테이션을 만들고 배포를 한 뒤에 뭔가 더 정보를 추가하고 싶다면 새로 추가된 요소들에 대해 default 값을 갖게 하면 하위 호환성도 지킬 수 있으면 처음에는 마커 어노테이션으로 시작하여 쓰다가 나중에는 기능이 많은 어노테이션으로 진화 가능하다.

하지만 인터페이스 경우에는 메소드를 만드는 순간 하위 호환성이 깨지므로 마커 어노테이션처럼 지속적인 진화는 불가능하다.

마커 어노테이션과 마커 인터페이스 중 둘 중 어느게 낫다고 할 수 없다. 각각의 쓰임새가 다르기 때문이다. 위에서 언급했듯이 새로운 메소드가 없이 자료형을 정의하고 싶다면 마커 인터페이스를 이용해야 하고 클래스나 인터페이스 이외의 마커를 달아야 하고 앞으로도 더 많은 추가 정보가 있다고 생각하면 마커 어노테이션을 사용하면 된다.

참고