728x90
반응형
SMALL

자바는 두가지 객체 소멸자를 제공한다. 그 중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있다. 아예 사용하지 않아야 한다. 그래서 자바9부터는 아예 Deprecated됐다. 또 다른 하나로는 cleaner가 있다. 이 cleanerfinalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고 역시나 일반적으로 불필요하다. 

 

자바 언어 명세에서 finalizer, cleaner는 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 그러니까 제때 실행되어야 하는 작업은 저 두가지로 절대 할 수 없다.

 

그럼 자바에서 적절하게 객체를 소멸하는 방법은 뭐가 있을까? 

→ AutoCloseable을 구현한 객체를 try-with-resource 구문으로 처리하는 것이다.

(클라이언트에서 인스턴스를 다 쓰고 나면 AutoCloseable이 구현해야 하는 메서드인 close 메서드를 호출해도 되지만, 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resource 구문을 사용해야 한다)

 

AutoCloseable을 구현한 객체를 try-with-resource 구문으로 처리할 때, 구체적으로 알아두면 좋을 내용이 있다.

→ 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다. 다시 말해, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지는 것이다.

 

그럼 finalizerDeprecated 됐다고 치면, cleaner는 여전히 사용가능하게 되어 있는데 도대체 어디에 쓰는 물건인고? 싶을것이다.

적절한 쓰임새가 한가지 정도 있다고 보는데, 자원의 소유자가 close 메서드를 호출하지 않는 것에 대한 안전망 역할이다. 즉, 사용자가 다 쓰고 close 메서드를 호출하지 않거나 try-with-resource 구문도 사용하지 않는 그런 예. 그렇다고 cleaner가 즉시 호출되리라는 보장은 없지만 아예 자원 회수를 안하는 것 보다는 나으니 말이다. 

 

자바 라이브러리도 이와 유사한 클래스가 있다. FileInputStream, FileOutputStream 등등이 말이다.

 

그래서 가장 좋은 방법은 cleaner를 사용한 클래스를 AutoCloseable로 구현하고 사용할 땐 try-with-resource 구문을 꼭 사용하는데 까먹고 사용하지 않은 경우 cleaner에게 제발 실행만이라도 되도록 기도하는 것이다. 

 

다음은 cleaner만 사용한 예시이다. 어떻게 사용하는지는 알아야 하니까.

import java.lang.ref.Cleaner;

class Resource {
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    public Resource() {
        // 리소스 초기화
        System.out.println("리소스가 할당되었습니다.");

        // 리소스가 수거될 때 수행할 작업 정의
        this.cleanable = cleaner.register(this, () -> {
            System.out.println("리소스가 해제되었습니다.");
        });
    }

    public void close() {
        cleanable.clean();  // 명시적으로 클리닝 작업 실행 가능
    }
}

public class CleanerExample {
    public static void main(String[] args) {
        Resource resource = new Resource();
        resource.close();  // 명시적으로 리소스를 해제
    }
}

 

근데 완전하지 않으니 AutoCloseable까지 구현해보자. try-with-resource 구문을 사용해서 반드시 객체를 소멸시키게 만들 수 있도록 말이다.

 

Resource (AutoCloseable + Cleaner)

package items.item8;

import java.lang.ref.Cleaner;

public class Resource implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    public Resource() {
        // 리소스 초기화
        System.out.println("리소스가 할당되었습니다.");

        // 리소스가 수거될 때 수행할 작업 정의
        this.cleanable = cleaner.register(this, () -> {
            System.out.println("리소스가 해제되었습니다.");
        });
    }

    public void close() {
        cleanable.clean();  // 명시적으로 클리닝 작업 실행 가능
    }
}
  • AutoCloseableCleaner를 같이 사용한다. Cleaner만으로는 확신을 가질 수 없다. 그래서 AutoCloseableclose 메서드안에 Cleaner를 사용해 자원을 정리하게 한다.
package items.item8;

public class Main {
    public static void main(String[] args) {

        try (Resource r = new Resource()) {
            System.out.println("안녕?");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

실행결과

리소스가 할당되었습니다.
안녕?
리소스가 해제되었습니다.

Process finished with exit code 0
  • 완벽하게 close 메서드가 실행됐고 원하는대로 자원을 소멸시킨다.

 

그런데 만약, 이렇게 호출하면 어떻게 될까?

package items.item8;

public class Main {
    public static void main(String[] args) {

        Resource r = new Resource();
        System.out.println("안녕?");
    }
}

실행결과

리소스가 할당되었습니다.
안녕?

Process finished with exit code 0
  • Cleaner가 실행되지 않았다. 이렇다. cleaner는 믿을게 못된다. 그래서 안전망 역할 정도만을 기대하는 게 가장 좋다. 절대 AutoCloseable을 두고 Cleaner를 사용하려고 하지마라.

 

참고로, 다음과 같이 System.gc()를 호출해서는 정상적으로 리소스가 종료됐지만 이걸 따라하고 있는 당신도 그러리란 보장이 없다.

package items.item8;

public class Main {
    public static void main(String[] args) {

        Resource r = new Resource();
        System.out.println("안녕?");
        System.gc();
    }
}
728x90
반응형
LIST
728x90
반응형
SMALL

C, C++처럼 메모리를 직접 관리해야 하는 언어를 사용하다가 자바처럼 가비지 컬렉터를 갖춘 언어로 넘어오면 프로그래머의 삶이 훨씬 평안해진다. 다 쓴 객체를 알아서 회수해가니 말이다. 그래서 자칫 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데 절대 사실이 아니다. 

 

스택을 간단히 구현한 다음 코드를 보자.

Stack

package items.item7;

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack {
    private Object[] elements;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;
    
    public Stack() {
        elements = new Object[DEFAULT_CAPACITY];
    }
    
    public void push(Object element) {
        ensureCapacity();
        elements[size++] = element;
    }
    
    public Object pop() {
        if (size == 0) throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (size == elements.length) {
            elements = Arrays.copyOf(elements, size * 2);
        }
    }
}
  • 특별한 문제는 없어 보인다. 별의별 테스트를 다 해도 거뜬히 통과할 것이다. 하지만 꼭꼭 숨어 있는 문제가 있다. 이는 바로 '메모리 누수'이다. 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 드물게 OutOfMemoryError가 발생할 수도 있다.

어디서 문제가 생겼을까? 스택이 커졌다가 줄어들었을 때, 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 프로그램에서 그 객체들을 더 이상 사용하지 않는다고 하더라도 말이다. 이 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있기 때문이다.

 

가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그객체들이 참조하는 모든 객체 ...)를 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.

 

해법은 간단하다. 해당 참조를 다 썼을 때 null 처리(참조 해제)를 하면 된다. 각 원소의 참조가 더 이상 필요 없어지는 시점은 스택에서 꺼내질 때다. 

public Object pop() {
    if (size == 0) throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

 

다 쓴 참조를 null 처리하면 다른 이점도 따라온다. 만약, null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료할 것이다. (미리 null 처리하지 않았다면 아무 내색 없이 무언가 잘못된 일을 수행할 것이다)

 

그렇다고 모든 객체를 다 쓰자마자 일일이 null 처리하라는 말은 아니다. 오히려 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 변수의 범위를 최소가 되게 정의했다면 이 일은 자연스럽게 이뤄진다. 

 

예를 들면, elements 라는 배열을 사용하는 부분을 특정 메서드에서만 사용하도록 변경하여 메서드 호출 후 메서드가 종료되면 더 이상 elements를 참조하고 있는 곳이 없도록 한다던가 하는 방식으로 말이다. 

 

그게 아니라면, 이 경우에는 애시당초에 java.util.Stack, Deque를 사용하면 된다. 이미 내부적으로 잘 만들어져 있기 때문에 메모리 누수걱정을 할 필요가 없다. 그리고 사실 자바가 만들어놓은 Stack, Deque도 내부적으로 null처리를 한다. 

 

메모리 누수의 세 번째 주범은 바로 리스너 혹은 콜백이라 부르는 것이다. 클라이언트가 콜백, 리스너를 등록만 하고 명확히 해지하지 않는다면 뭔가 조치해주지 않는 한 콜백은 계속 쌓여간다. 이럴땐 리스너나 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어, WeakHashMap의 키로 저장하면 된다.

728x90
반응형
LIST
728x90
반응형
SMALL

"객체 생성은 비싸니 피해야 한다."가 아니다. 특히 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담이 되지도 않는다. 그러나, 불필요한게 있어서 좋을게 없다. 

 

가장 불필요한 객체 생성이 일어나는 시점은 오토박싱으로 예를 들 수 있다.

다음 코드를 보자.

package items.item6;

public class Main {
    public static void main(String[] args) {

        long start = System.currentTimeMillis();
        Long sum = 0L;
        for (long i = 0; i < Integer.MAX_VALUE; i++) {
            sum += i;
        }
        System.out.println("sum = " + sum);
        long end = System.currentTimeMillis();
        System.out.println("total time = " + (end - start) + "ms");
    }
}
  • 끔찍한 일이 일어나고 있다. 객체가 어디서 생성되는지 보이는가?
  • sumLong 박싱 타입인데, ilong 기본 타입이다. 이때 sum = sum + i를 이행하는 과정에서 i의 오토박싱이 일어난다. 즉, 이 루프를 돌면서 Long 인스턴스가 2의 31승개가 만들어진다.

이때, sum의 타입을 long으로만 바꿔주면 이 코드의 실행 속도는 2.2초에서 0.6초로 줄어든다.

그래서, 이 오토박싱도 불필요한 객체 생성이 될 수 있다. 

 

불필요한 객체 생성은 피하자.

 

 

728x90
반응형
LIST
728x90
반응형
SMALL

가끔 정적 메서드, 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다. 대표적인게 유틸클래스.

물론, 객체 지향적으로 사고하지 않는 이들이 종종 남용하는 방식이라 좋아보이진 않지만 유용할 때가 분명 있다.

 

근데, 내가 이 부분에서 빠뜨린 부분이 있었다.

나는 인스턴스화를 막기 위해 추상 클래스로 선언하면 아무 문제 없을 줄 알았는데, 그게 아니었다.

 

추상클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.

추상클래스로 만들어도, 하위 클래스를 만들어 인스턴스화하면 그만이다.

 

다음 코드를 보자. 

 

AbstractClass 

package items.item4;

public abstract class AbstractClass {

    public static void hello() {
        System.out.println("Hello Static Class");
    }
}

 

ChildClass

package items.item4;

public class ChildClass extends AbstractClass {

    public void childMethod() {
        System.out.println("child method");
    }
}
  • 이 하위클래스가 추상 클래스를 상속받았다.

 

Main

package items.item4;

public class Main {
    public static void main(String[] args) {
        ChildClass childClass = new ChildClass();
        childClass.hello();
    }
}
  • 하위클래스로 인스턴스를 생성하면, 결국 원하는만큼 인스턴스를 생성할 수가 있게 된다. 어차피 자식 인스턴스를 만들면 부모 인스턴스는 자연스럽게 생성자가 호출되니까 인스턴스도 계속해서 만들어질 수 있다.

결론

그래서, 이 인스턴스화를 막는 방법은 추상 클래스가 정답이 아니다. 인스턴스화를 막으려면 private 생성자를 만들면 된다.

명시적으로 생성자를 하나라도 만들면 컴파일러는 우리 대신 생성자를 만들지 않기 때문에 private 생성자만 존재하게 된다. 그리고 private 생성자만 있는 경우, 절대로 하위 클래스가 존재할 수 없다. 상속받는 클래스는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하는데 호출할 수 있는 생성자가 없으니 말이다. 

 

그래서, 자식 클래스를 만들 수도 없고 생성자를 통해 인스턴스를 만들수도 없는 private 생성자를 사용하자.

728x90
반응형
LIST

+ Recent posts