[이펙티브 자바] 객체의 생성과 파괴 Item7 - 다 쓴 객체 참조를 해제하라
이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.
"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.
- 객체를 만들어야 할 때는 언제인가
- 객체를 만들지 말아야 할 때는 언제인가
- 올바른 객체 생성 방법은 무엇인가
- 객체의 불필요한 생성을 피하는 방법은 무엇인가
- 객체를 제 때에 파괴시키는 방법은 무엇인가
- 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가
위와 같은 맥락을 계속 기억하며 공부하자.
- Item1. 생성자 대신 정적 팩터리 메서드를 고려하라.
- Item2. 생성자에 매개변수가 많다면 빌더를 고려하라.
- Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
- Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라.
- Item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
- Item6. 불필요한 객체 생성을 피하라.
- Item7. 다 쓴 객체 참조를 해체하라.
- Item8. finalizer와 cleaner 사용을 피하라.
- Item9. try-finally 보다는 try-with-resources를 사용하라.
<"다 쓴 객체 참조를 해제하라">
누군가 자바의 가장 큰 특징이 뭐냐고 묻는다면 어떻게 답할까?
내가 배웠던 자바의 가장 큰 특징 중 하나는 바로 가비지 컬렉터가 있다는 것이다.
C나 C++은 가변 자료구조를 사용할 경우,
동적 메모리를 사용하기 때문에 사용자가 직접 그 관리까지 해줘야한다.
(이건 진짜 생각보다 매우 어려운 일이다.)
하지만 자바는 사용자가 직접 메모리 관리를 할 필요가 없다.
가비지 콜렉터라는 친구가 알아서 다 해주기 때문이다.
그렇지만 아무리 가비지 컬렉터가 메모리를 정리해준다고 하더라도,
메모리 관리에 아무런 신경을 쓰지 말라는 말은 절대 아니다.
지금부터 주로 메모리 누수를 일으키는 주범들을 알아본다.
# 메모리를 직접 관리하는 클래스
다음의 예제를 살펴보자.
public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { ensureCapacity(); elements[size++] = e; } public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; } /** * 원소를 위한 공간을 적어도 하나 이상 확보한다. * 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다. */ private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } }
아마 대부분 사람들에게 친숙한 코드일 것이다.
(코드가 안친숙해도 pop, push 만 봐도 알 것이다.)
아주 일반적인 자료구조 Stack을 구현한 코드이다.
코드를 봤을때 그렇게 큰 문제가 보이진 않고,
직접 코딩해서 테스트 해봐도 매끄럽게 잘 돌아간다.
하지만 위의 코드에는 메모리 누수를 발생시키는 부분이 있다.
<메모리 누수 발생 포인트>
- Stack에서 pop을 하게 되면, element 상의 포인터는 한칸 내려온다.
- 하지만 그 이전에 element에서 참조하고 있는 객체는 그대로 있다.
- 포인터만 한칸이 내려온 것이다.
- element에서 참조하고 있는 그 객체는 가비지컬렉터가 회수하지 못한다.
- 결국 메모리 누수가 계속 발생한다.
Stack에서 pop되어 더이상 사용되지 않는 객체라고는 하지만,
그것은 우리나 아는 얘기다.
java 입장에서는 참조되고 있는 객체이기 때문에 사용되고 있다고 생각한다.
그렇기 때문에 가비지컬렉터는 객체를 회수하지 못하게 된다.
여기까지면 다행이지만,
그 객체가 또 다른 여러 객체들을 참조한다면 그들 또한 회수를 못하게 되버린다.
메모리 누수가 무지막지하게 발생하는 것이다.
메모리 누수가 발생하는 프로그램이 오래도록 실행되면,
결국 전체적인 성능저하를 일으키므로 이는 간과할 수 없는 부분이다.
이를 고려하려 다음과 같이 간단하게 코드를 수정할 수 있다.
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 다 쓴 참조 해제 return result; }
위와 같이 객체를 pop 하고 나서 null 처리를 통해 참조를 해제해주면 된다.
이렇게 메모리 누수로 인한 프로그램의 성능 저하를 느껴본 개발자들은,
null 처리를 통해 다 쓴 객체 참조를 해제하는데 집중하기도 한다.
(성능 저하에 대한 이유를 찾는것 조차 한참을 걸렸을 것이다. 화날만도 하다.)
하지만 그럴 필요도 없고, 오히려 프로그램을 지저분하게 만들기 때문에 지양된다.
그렇다면 언제 null 처리가 필요할까?
일반적으로 Stack 처럼 자기 메모리를 직접 관리하는 클래스의 경우,
프로그래머는 메모리 누수를 신경써야 한다.
클래스가 스스로 메모리를 관리한다는 것은, 가비지컬렉터가 관여할 수가 없다는 것이다.
그렇기에 앞선 Stack의 예처럼 객체를 다 쓰고 나면,
null 처리를 통해 해당 객체는 더이상 쓰이지 않는다는 것을 알려줘야 한다.
# 캐시
캐시 또한 메모리 누수를 자주 일으키고는 한다.
객체 참조를 캐시에 넣고 그냥 까먹어서 계속 자리를 차지하고 있는 것이다.
이때 해결 방법은 여러가지가 있다.
- WeakHashMap 사용하기
만약 캐시 외부에서 키(Key)를 참조하는 동안만 객체가 살아 있도록 하고 싶다면,
WeakMapHash를 사용하는 것이 좋다.
이렇게 하면, 다 쓴 객체는 자동으로 캐시에서 제거된다.
다만, 이는 위와 같이 특정한 상황에서만 유용하다.
- LinkedHashMap 사용하기
보통 캐시를 만들 때, 캐시 객체의 유효 기간을 정확히 파악하는게 어렵다.
그래서 시간이 지날수록 객체의 가치를 떨어뜨리는 방식을 흔히 사용한다.
이럴 경우, 쓰지 않는 객체를 한번씩 청소해주는 작업이 필요하게 된다.
백그라운드 스레드를 활용할 수도 있지만,
캐시에 새 객체를 추가할 때 부수 작업을 추가하여 하는 방식도 있다.
LinkedHashMap의 경우, removeEldesEntry 메서드를 통해 객체를 정리해준다.
# 리스너 혹은 콜백
리스너 (Listener) 혹은 콜백 (Callback)이라고 불리는 것은,
클라이언트가 등록 및 사용하고 난 후에 명확히 해지하지 않는다면 계속해서 쌓여간다.
이럴 경우, 콜백을 약한 참조 (weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해갈 수 있다.
그 대표적인 예시가 바로 앞서 한번 말했던 WeakHashMap 이다.
지금까지 자바를 사용함에 있어 고려해야 할 메모리 누수범들에 대해 알아보았다.
필자의 코멘트를 마지막으로 글을 마친다.
<Item7 정리>
- 메모리 누수는 겉으로 잘 드러나지 않는다.
- 시스템에 수년간 잠복하는 경우도 있다.
- 웬만큼 발견하기 힘들기 때문에, 예방법을 미리 익혀두자.
Author And Source
이 문제에 관하여([이펙티브 자바] 객체의 생성과 파괴 Item7 - 다 쓴 객체 참조를 해제하라), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@holidenty/이펙티브-자바-객체의-생성과-파괴-Item7-다-쓴-객체-참조를-해제하라저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)