[Java] 힙 펄루션 (Heap pollution)

아두치·2021년 3월 26일
7

🚀 힙 펄루션 (Heap pollution)

펄루션(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 되면 곧바로 예외를 발생시키기 때문에 아까와 같은 난처한 상황은 일어나지 않을 것이다.

profile
HAVE YOU TRIED IT?

3개의 댓글

comment-user-thumbnail
2021년 7월 7일

야나보현이 너 어디갔서

답글 달기
comment-user-thumbnail
2022년 3월 27일

안녕하세요 Heap Pollution 관련한 포스팅을 하려고 하는데, 너무 좋은 글이라 내용을 발췌해서 작성해보려고 합니다. 출처 남기고 사용해도 될까요?

1개의 답글