펄루션(pollution)의 사전적 의미는 오염이다.
따라서 힙 펄루션은 JVM의 메모리 공간인 heap area 가 오염된 상태를 뜻한다.
즉, 어떠한 이유에서든 힙에 문제가 생기면 그것을 힙이 펄루션 되었다고 할 수 있다.
힙이 오염되는 대표적인 원인 하나를 살펴보도록 하자.
자바에서 제네릭은 타입의 안전성을 보장해주는 강력한 도구이다.
제네릭은 Java 5 에서 처음 도입될 때 약간의 논란이 있었다.
예를 들어서, 기존 ArrayList 클래스는 제네릭이 아니였기 때문에 내부적으로 저장되는 요소가 Object 타입이였지만 ArrayList 클래스를 제네릭으로 만들면서, 기존 ArrayList 와 제네릭 ArrayList 의 호환성을 고려해야하는 문제가 있었다.
수많은 라이브러리 메소드가 이미 제네릭이 없는 ArrayList 클래스를 인자로 받도록 정의되어 있었기 때문에 어쨌든 기존 사용자는 해당 메소드를 기존과 똑같이 사용할 수 있어야 했고 새로운 제네릭 클래스를 사용하는 사용자도 해당 메소드를 호출할 수 있어야 했다.
그래서 자바는 제네릭 클래스의 타입 파라미터를 컴파일이 끝난 시점에 제거하도록 만들었다.
타입 파라미터가 사라진 자리에는 Object 를 삽입해서 실행 시점에서는 기존의 클래스와 똑같은 클래스가 되도록 함으로써 하위 호환성을 해결한 것이다.
다음과 같은 코드가 있다고 가정하자.
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("String1");
arrayList.add("String2");
Object obj = arrayList;
이 코드는 정상적으로 컴파일 된다.
(이상할게 전혀 없는 코드이기 때문에 컴파일이 안되면 그게 이상한거긴 하지만..)
그리고 현재 arrayList 에는 문자열 두 개가 들어있는 상태이다.
그리고 다음 코드를 실행해보자.
ArrayList<Integer> arrayList2 = (ArrayList<Integer>)obj;
arrayList2.add(new Integer(100));
arrayList2.add(new Integer(200));
정상적으로 컴파일이 될까?
우리는 직관적으로 이건 잘못된 코드라는 것을 알아챌 수 있지만, 컴파일러는 그러지 못한다.
이 코드는 정상적으로 컴파일될 뿐만 아니라 실행에도 이상이 없다.
동일한 ArrayList 객체에 서로 상속관계도 아닌 상관없는 두 타입 객체가 들어가다니..
말도안된다.
하지만 하나하나 살펴보면 실행되는 이유가 나름 타당하다.
그럼 하나하나 살펴보자.
컴파일러가 컴파일을 진행하다가 타입 캐스팅 연산을 만났을 때 캐스팅 대상 객체를 캐스팅 할 수 있느냐 없느냐는 검사하지 않는다.
단지 캐스팅 했을때 대입되는 참조변수에 저장할 수 있느냐만 검사할 뿐이다.
그래서 대상 객체가 캐스팅할 수 없는 타입으로 캐스팅을 시도하면 컴파일 타임이 아니라 런타임에 익셉션이 발생하는 것이다.
따라서 위 코드에서
ArrayList<Integer> arrayList2 = (ArrayList<Integer>)obj;
부분은 컴파일러가 따로 체크를 하지 않고 가상 머신에게 맡기는 것이다.
위에서 설명했듯이 제네릭 타입 파라미터는 컴파일이 끝나면 컴파일러가 제거하고 그 자리에 Object 를 넣어준다.
즉, ArrayList<Integer>
이나 ArrayList<String>
이나 컴파일이 끝나면
ArrayList<Object>
과 똑같아진다는 뜻이다.
따라서
ArrayList<Integer> arrayList2 = (ArrayList<Integer>)obj;
이 코드는 지극히 타당한 코드가 되는 것이다.
또한 현재 arrayList2 가 참조하는 리스트는 내부적으로 저장하는 요소의 타입이 Object 이기 때문에
문자열을 저장하든, 래퍼 객체를 저장하든 뭐든 저장이 가능한 상태이다.
따라서,
ArrayList<Integer> arrayList2 = (ArrayList<Integer>)obj;
arrayList2.add(new Integer(100));
arrayList2.add(new Integer(200));
이 코드는 우리의 생각과 달리 문제없는 코드가 되는 것이다.
그리고 바로 이러한 상황을 힙 펄루션이라고 한다.
그렇다면 위와 같은 상황을 개발자가 인지하지 못하고 서비스를 제공했다고 가정한다면, 영원히 눈치채지 못할 수 있을까?
그렇지 않다.
모든 컬렉션은 저장하고 꺼내기 위해 사용한다.
위와 같은 상황에서는 컬렉션에 저장된 요소를 꺼낼 때 익셉션이 발생한다.
위에서 현재 arrayList2 에는 문자열 2개, Integer 2개가 저장되어 있는 상태이다.
String str = arrayList2.get(0);
이 코드는 문제없이 작동해야할 것 같지만, ClassCastException 이 발생한다.
컬렉션으로 요소를 꺼내올 때 컴파일러가 해당 객체의 제네릭 타입 파라미터로 캐스팅하는 문장을 자동으로 삽입해주기 때문이다.
즉, 위 코드는
String str = (Integer)arrayList2.get(0);
와 같다.
따라서 ClassCastException 예외가 발생한다.
그런데, 지금 이 에러의 원인이 과연 ClassCastException 예외가 맞을까?
근본적인 원인은 캐스팅 실패가 아니라 컬렉션에 잘못된 타입의 요소가 저장된 것이 문제다.
서비스 시작 이후 한참 뒤에 이 에러가 발생한다면 과연 제대로 된 원인을 찾을 수 있을까?
근본적인 해결법은 리스트를 만들 때 해당 리스트의 제네릭 타입 외 타입의 요소를 저장할 때
바로 예외를 발생시켜주는 감시자를 달아주는 방법이 있다.
Collections 클래스의 checkedList 메소드가 바로 그러한 감시자가 달린 리스트를 만들어서 반환해준다.
List<String> strings = Collections.checkedList(new ArrayList<>(), String.class);
첫번째 인자는 리스트 객체를, 두번째 인자는 타입 파라미터 클래스의 Class 객체를 전달해주면 된다.
이제 strings 참조변수로 요소를 add 할 때 String 클래스 또는 String 클래스의 하위 클래스의 객체가 아닌 객체가 add 되면 곧바로 예외를 발생시키기 때문에 아까와 같은 난처한 상황은 일어나지 않을 것이다.
야나보현이 너 어디갔서