Programming/Java

[Effective Java] item7 - 다 쓴 객체 참조를 해제하라.

VSFe 2021. 2. 1. 21:07
다 쓴 객체 참조를 해제하라!

때는 2018년... 다른 학과였던 나는 자료구조 수업을 들었다.

사실 시험을 잘봐서 그렇지, 과제는 엉망이었다. 7번 중 0점이 2개... 눈물났다. 그땐 "아... 코딩에 재능이 없나?" 라고 생각하고 그랬었다. (지금은 재능의 문제가 아니라 노력으로 충분히 커버할 수 있는 부분이라고 생각한다!)

TMI는 집어 치우고, 그때 작성했던 Stack 자료구조의 코드를 읽어보자.

class Stack {
	private String [] data;
	private final int size;
	private int top;
	
	public Stack(int s) {
		data = new String[s];
		size = s;
		top = -1;
	}
	
	public boolean isEmpty() { return top == -1; }
	public boolean isFull() { return top == (size - 1); }
	
	public void push(String x) {
		if (!isFull()) data[++top] = x;
	}
	
	public String pop() {
		if (isEmpty()) return ""; else return data[top--];
	}
}

음... 뭐 평범한 학생의 코드다. 사실 자료구조를 처음 배우는 입장에선 이정도면 충분하다. 그렇지만 이 코드에는 큰 문제가 있는데, 바로 다 쓴 객체이다. "어? 자바는 GC 언어 아니에요?" 라고 생각할 수 있지만, 가비지 컬렉터는 다 쓴 해당 객체를 버리지 않는다. 객체의 다 쓴 참조 (Obsolete Reference)를 여전히 갖고 있기 때문이다. 스택의 크기가 어떻게 되던, 내가 가리키고 있는 점 바깥을 쓸데없이 가지고 있다.

가비지 컬렉션 원리는 꽤 복잡한데, 이런 상황에선 객체 참조 하나를 살려뒀다가 관련된 객체 여러개를 쓸데없이 살릴 수도 있는 상황이기에 꽤나 신경쓸게 많다.

그럼 어떻게 할까? 그냥 필요 없어진 객체의 레퍼런스를 null로 지정하면 된다.

public String pop() {
	if (isEmpty()) return ""; 
	String result = data[top];
	data[top--] = null;
	else return result;
}

이렇게 바꿔주면 된다. 사실 이 자료구조는 isEmpty() 일 경우 Exception을 발생 시키는게 맞겠지만, 뭐 어쩐담. 꼬꼬마 시절 코드이니 봐주길 바란다...

다만 일일히 null로 만드는 것도 참 곤란하다. 프로그램 코드를 1000줄 가량 짰는데 20줄이 ~~~ = null; 이면 좀 그렇지 않을까?

음... 그럼 어떨때 null로 지정해야 할까? 위의 스택을 다시 한 번 보자. 스택 클래스는 자기 메모리를 직접 관리하기 때문이다. 스택 클래스는 객체 자체를 담는 것이 아닌, 객체 참조를 담는 배열을 만들어서 관리하기 때문에 이런 부분에서 신경 써야 한다.

캐시를 사용한다면, 이 친구도 메모리 누수의 용의자라고 볼 수 있다. 가끔 객체 참조를 캐시에 넣고 까먹고 있다 객체를 다 쓰고도 캐시를 유지하고 있으면 답이 없어진다. 만약 캐시 외부에서 키를 참조하는 동안만 (값이 아니라 키!!!) 캐시가 필요하다면 그냥 WeakHashMap을 사용하면 된다. 이건 뭐냐고? 잠시만 뒤에서 확인하도록 하자.

사실 캐시를 사용하게 되면 유효 기간을 정확히 파악하기 어렵다. 그래서 일반적으로는 사용하지 않는 엔트리를 가끔씩이라도 청소해줘야 한다. 백그라운드 스레드를 활용하거나, 캐시에 엔트리를 추가하는 과정에서 정리하는 절차를 함께 실행하도록 해주면 될 것이다. LinkedHashMap을 사용하면 removeEldestEntry라는 메소드를 사용해 후자를 구현한다.

콜백이나 리스너도 나름 책임이 있다. 콜백을 등록만 하고 해지하지 않으면 계~속 쌓여갈 것이다. 이를 방지하기 위해선 콜백을 Weak Reference로 등록하면 되는데, 이는 WeakHashMap에 등록함으로써 가능하다.

 

 

책의 내용을 그대로 적었는데, Weak Reference가 뭘까?

 

일반적으로 우리는 객체를 생성할 떄 new()를 쓰는데, 이런 방식으로 생성하는 객체는 Strong Reference라고 한다. GC는 이러한 객체의 참조가 없어질 때 까지 객체를 없애지 않는데, Weak Reference의 경우 GC가 발생하면 무조건 수고된다. 이런 참조는 짧은 주기에 자주 사용되는 객체를 캐시할 때 자주 사용한다.

class BigSizeClass {
    private int[] arr = new int[25000];
}

public class ReferenceTests {
    private List<WeakReference<BigSizeClass>> weaks = new LinkedList<>();
    private List<BigSizeClass> strongs = new LinkedList<>();

    public void weakReferenceTest() {
        try {
            while(true) {
                weaks.add(new WeakReference<BigSizeClass>(new BigSizeClass()));
            }
        } catch (OutOfMemoryError e) {
            System.out.println("Out of Memory");
        }
    }

    public void strongReferenceTest() {
        try {
            while(true) {
                strongs.add(new BigSizeClass());
            }
        } catch (OutOfMemoryError e) {
            System.out.println("Out of Memory");
        }
    }

    public static void main(String[] args) {
        System.out.println("Tests Start");

        ReferenceTests test = new ReferenceTests();
        //test.strongReferenceTest();
        test.weakReferenceTest();

        System.out.println("Tests End");
    }
}

이런 테스트를 만들어서 돌려보면, Strong의 경우 문제가 발생하고 프로그램이 종료되지만, Weak의 경우 프로그램이 종료되지 않고 계~속 도는 것을 확인할 수 있다.

즉, 위에서 계속 언급한 Weak Reference를 활용하면 콜백이나 캐시를 활용하고 빠르게 정리할 수 있어서 메모리 누수를 확실하게 방지할 수 있다. 다만 생각지도 못하게 정리될 수 있으니 본인의 상황에 맞게 관리하도록 하자.

weakHashMap이 뭔지는 이제 대충 감이 올 것이다. HashMap은 한번 데이터가 들어오면 절대 없어지지 않는데, 이걸 캐시로 사용하면 적체되다 메모리 누수가 발생하기 쉽다. weakHashMap을 사용하면 잘 참조되지 않는 데이터는 gc가 발생할 때 그냥 지워버리니, 캐시로 사용하고자 하면 매우 유용하다.