[JAVA] Type Erasure의 함정

Ju Pyo Hong
asuraiv
Published in
6 min readApr 15, 2018

필자의 회사는 많은 조직이 서비스 성능 향상을 도모하기 위해 ‘memcached’ 를 사용한다. (물론 ‘redis’와 같은 다른 솔루션을 사용하는 팀도 상당수 있긴 하다) 그리고 이를 스프링 프레임워크로 된 자바 어플리케이션과 연동하기 위해 AOP 기반의 ‘SSM(Simple Spring Memcached)’ 을 사용하고 있다.

함정

JAVA Generic의 Type Erasure가 타이틀인데 생뚱맞은 ‘memcached’ 이야기를 하는 이유는, 이 ‘memcached’를 사용하다가 Type Erasure의 함정에 빠졌기 때문이다.

가령 ‘SSM’을 이용한 아래와 같은 코드를 업무에서 작성했다고 해보자.

@ReadThroughSingleCache(namespace = "VERSION_1", expiration = 432000)
public List<Foo> readSomeList(@ParameterValueKeyProvider Long id) {
// someList를 가져오는 작업
return someList;
}

public void doSomething(Long id) {

List<Foo> someFooList = readSomeList(id);

for(Foo foo : someFooList) {
// foo dto를 이용한 어떤 작업
}
}

위의 코드는 ‘id’ 를 이용해 캐쉬에서 ‘Foo’ 타입의 List를 가져온 뒤, for loop를 수행하는 코드이다. 실무에서 자주 볼수 있는 형식의 코드라고 할 수 있겠다.

그러나 소프트웨어 개발에서 코드베이스의 변경은 필연적이다. 위의 코드에서 ‘readSomeList’의 반환타입이 ‘List<Bar>’로 변경 되었다고 하자. 이 경우는 List가 취급하는 데이터 타입 자체가 바뀐 것이다. 따라서 새로운 namespace에서 caching을 해야 하므로 ‘@ReadThroughSingleCache’ 의 ‘namespace’ 프로퍼티의 값도 반드시 갱신(예를 들면 ‘VERSION_2’와 같은 식으로) 한 뒤 배포를 해야 한다.

하지만 필자의 실수로 namespace를 업데이트하지 않았고, 테스트 서버에 배포된 코드는 아래와 같았다.

@ReadThroughSingleCache(namespace = "VERSION_1", expiration = 432000)
public List<Bar> readSomeList(@ParameterValueKeyProvider Long id) {
// someList를 가져오는 작업
return someList;
}

public void doSomething(Long id) {

List<Bar> someBarList = readSomeList(id);

for(Bar bar : someBarList) {
// foo dto를 이용한 어떤 작업
}
}

반환 타입이 ‘List<Bar>’ 로 변경 되었지만 SSM 어노테이션의 namespace 프로퍼티 값은 여전히 ‘VERSION_1’ 이다.

당연히 해당 코드를 사용하는 기능에서 아래와 같은 Exception이 발생했다.

java.lang.ClassCastException: Foo cannot be cast to Bar

namespace를 업데이트 하지 않은채 기존의 caching된 ‘Foo’ 타입의 List를 가가져와 ‘Bar’ 타입의 List에 바인딩 하려 했을테니, Casting Exception이 발생했다는 것은 단번에 이해 할 수 있었다.

그런데 예외 발생 위치가 조금 이상했다. 바로 for each 구문을 시작할때 즉, “for(Bar bar : someBarList)” 부분 에서 발생한 것이다. 처음 해당 예외 메시지를 보았을때는, 캐쉬에서 가져온 ‘Foo’ 타입 List를 ‘Bar’ 타입 List에 바인딩 하는 코드 즉, “List<Bar> someBarList = readSomeList(id)” 에서 예외가 발생 했으리라 예상했지만 실제 발생 위치는 예상을 빗나갔다.

다시 정리하자면, 캐쉬에서 잘못 가져온 ‘Foo’ 타입의 List는 ‘Bar’ 타입으로 선언한 ‘someBarList’ 지역참조 List에 안전하게(?) 바인딩 되었고, 그 다음 for each 구문에서 해당 List에서 꺼낸 ‘Foo’ 타입의 객체를 ‘Bar’ 타입 참조에 바인딩 할때 Casting Exception이 발생 한 것이다.

IDE의 기능을 이용해 Debugging을 해보니,
역시나 희한한 결과를 볼 수 있었다.

‘Bar’ 타입의 List에 ‘Foo’ 타입의 객체가 저장 되어있다.

원인

이처럼 전혀 다른 제네릭 타입의 List가 바인딩 될 수 있었던 것은 다름아닌 Java Generic의 특징이라고 할 수 있는 ‘Type Erasure’ 때문 이었다.

아주 간단하게 설명하자면, Java의 Generic을 사용한 아래와 같은 코드는,

List<Bar> someBarList = readSomeList(id);

컴파일이 되는 순간 아래와 같이 변경된다.

List someBarList = readSomeList(id);

이것은 메서드 Signature에도 동일하게적용된다. Generic 구문이 지워져 버린 List는 List<?>와 같으며 이것은 결과적으로 List<? extends Object>를 의미한다. 따라서 어떠한 타입을 담고 있는 List 객체라도 예외 발생 없이 바인딩 되었던 것이다. 아래 Link 에 자세한 설명이 있다.

여담으로, 임백준씨 저서인 ‘폴리글랏 프로그래밍' 에서 저자는 이러한 Type Erasure를 예로 들면서 Java의 한계에 대해 설명하고 있다. (예를들어 위와 같은 특성때문에 Runtime에 Type Checking을 하기 힘들다는 식이다. 이 때문에 개발자가 실수 하기 쉽게 된다거나, 찾기 힘든 버그 발생 따위로 시스템의 불안정성이 증가할 수 있을 것 같긴하다.)

일단 필연적으로 Casting 예외가 발생할 수 밖에 없었던 코드이고, 금방 수정할 수 있었던 부분이라 실무에서 큰 문제가 되진 않았다.

하지만 책에서만 봤었던 ‘Type Erasure’로 인해 예상하지 못한 시점에서 예외가 발생 했기에, 그리고 이것을 통해 다시한번 Java에 대해 고찰 할 수 있었기에 필자에게 유의미한 경험 이었고, 이렇게 글로 정리하게 되었다.

--

--