본문 바로가기
books/이펙티브 자바 3판

[이펙티브 자바 : 1장] 객체 생성과 파괴

by Moonsc 2020. 6. 27.
728x90

 

 

[아이템 1] 생성자 대신 정적 팩토리 메서드를 고려하라

 

public static Boolean valueOf(boolean b){
    return b ? Boolean.TRUE : Boolean.FALSE;
}

 

장점 하나 : 이름을 가질 수 있다.

생성자에 제공하는 인자가 반환하는 객체를 잘 설명하지 못할 경우에, 잘 만든 이름을 가진 정적 팩토리를 사용하는 것이 사용하기보다 더 쉽고 읽기 좋다. 그 예로 BigInteger(int, int, Random)과 BigInteger.probablePrime을 들고 있다.

또한, 생성자는 시그니처에 제약이 있다. 똑같은 타입을 파라미터로 받는 생성자 두 개를 만들 수 없으니까 그런 경우에도 정적 팩토리 메서드를 사용하는 것이 유리하다.

// 생성자를 사용하는 경우
public static void main(String[] args){
    Foo foo = new Foo(인자1);
}

// 팩토리 메소드를 사용하는 경우
public static void main(String[] args){
    Foo foo = Foo.인자는스트링이다(인자1);
}

 

장점 둘. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

public static final FOOOOOOO = new Foo();

public static Foo getFoo(){
    return FOOOOOOO;
}

 

이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 대표적인 예인 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다. 따라서 반복되는 객체 요청에 효과적이다.

장점 셋. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

public static void main(String[] args){
    //기존 생성자라면
    java.util.Collections collections = new Collections();
    collections.45개의API();
    
    //정적 팩토리 메소드라면
    Collections.45개의API();
}

 

자바 8 부터는 인터페이스에 public static 메서드를 추가할 수 있으니까, 굳이 Collections라는 클래스를 만들지 않고도 45개의 API를 사용할 수 있게 되었다. (자바 9부터는 인터페이스에 private static 메서드를 추가할 수 있다.)

장점 넷. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

public static final FOOOOOOO = new Foo();

public static Foo getFoo(bool){
    return bool ? FOOOOOOO : Bar;
}

public class Bar extends Foo{
    Foo의 하위 클래스
}

public static void main(String[] args){
    Foo foo = Foo.getFoo(false)
    //foo는 Bar다.
}

 

반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 이러한 객체 타입은 노출되지 않고 감춰져 있기 때문에 향후에 JDK 변화에 따라 새로운 타입을 만들거나 기존 타입이 사라지더라도 문제가 되지 않는다. 

장점 다섯. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

(장점 4번의 연장선이다..)

JDBC의 경우, 

DriverManager.registerDriver()가 프로바이더 등록 API. DriverManager.getConnection() 서비스 액세스 API.

그리고 Driver가 서비스 프로바이더 인터페이스 역할을 한다.

자바 6부터는 java.util.ServiceLoader라는 일반적인 용도의 서비스 프로바이더를 제공하지만, JDBC가 그 보다 이전에 만들어졌기 때문에 JDBC는 ServiceLoader를 사용하진 않는다.

이 때문에 자바 버전에 상관없이 이전의 코드들이 안정적으로 작동하게 만들 수 있다.

단점 하나. 상속을 하려면 public 이나 protected생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.

앞서 이야기한 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 이야기이다. 어찌 보면 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점일 수 있다.

단점 둘. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.

생성자는 javadoc 상단에 모아서 보여주지만 static 팩토리 메서드는 API 문서에서 특별히 다뤄주지 않는다. 따라서 클래스나 인터페이스 문서 상단에 팩토리 메서드에 대한 문서를 제공하는 것이 좋겠다.

 

[아이템 2] 생성자에 매개변수가 많다면 빌더를 고려하라

 

대안 하나. 점측정 생성자 패턴

public class NutritionFacts {
    private final int a
    private final int b 
    private final int c 
    private final int d 
    private final int e 
    private final int f 
    
    public NutritionFacts(int a, int b){
        this(a, b, 0);
    }
    
    public NutritionFacts(int a, int b, int c){
        this(a, b, c, 0);
    }
    
    .....

}

 

이 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출하면 된다.

점층적 생성자 패턴도  쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

대안 둘. 자바빈즈 패턴

public class NutritionFacts {

    // 매개변수들은 기본값으로 초기화한다.
    private int servingSise = -1; // 필수; 기본값 없음
    private int servings = -1;  // 필수; 기본값 없음
    private int calories = 0;  
    
    
    public NutritionFacts(){}
    //세터 메소드
    public void setServingSise(int val){
        this.servingSise = val;
    }
    public void setServings(int val){
        this.servings = val;    
    }
    public void setCalories(int val){
        this.calories = val;
    }
}

 

선택 매개변수가 많을 때 활용할 수 있는 대안이다. 매개변수가 없는 생성자로 객체를 만든 후, Setter 메서드를 호출하여 원하는 매개변수에 값을 설정하는 방식이다.

장점. 코드가 길어지기는 했지만 인스턴스를 만들기 쉽고 그 결과 더 읽기 쉬운 코드가 되었다.

단점 하나. 자바빈즈 패턴에서는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너 진다.

단점 둘. 클래스를 불변으로 만들 수 없다.

대안 셋. 빌더패턴

package com.company;

public class NutritionFacts {

    private final int servingSise;
    private final int servings;
    private final int calories;

    public static class Builder{
        // 필수 매개변수
        private final int servingSise;
        private final int servings;

        // 선택 매개변수
        private int calories = 0;

        public Builder(int servingSise, int servings){
            this.servingSise = servingSise;
            this.servings = servings;
        }

        public Builder calories(int val){
            calories = val;
            return this;
        }
        public NutritionFacts builder(){
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder){
        servingSise = builder.servingSise;
        servings = builder.servings;
        calories = builder.calories;
    }

    @Override
    public String toString() {
        return "NutritionFacts{" +
                "servingSise=" + servingSise +
                ", servings=" + servings +
                ", calories=" + calories +
                '}';
    }

    public static void main(String[] args){
        NutritionFacts cola = new Builder(240, 8)
                .calories(100).builder();

        System.out.println(cola.toString());
    }

}

 

장점. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바 빈즈보다 훨씬 안전하다.

단점 하나. 객체를 만들려면 그에 앞서 빌더부터 만들어야 한다. 

단점 둘. 성능에 민감한 상황에서는 문제가 될 수 있다.

단점 셋. 코드가 장황하다 값어치를 하려면 매개변수가 4개 이상은 되어야 한다. 하지만 대개의 API는 시간이 지날수록 매개변수가 많아지는 경향이 있으니 애초에 빌더로 시작하는 것이 좋다.

 

[아이템 3] private 생성자나 enum 타입으로 싱글톤임을 보증하라

 

오직 한 인스턴스를 만드는 클래스를 싱글톤이라 부른다. 보통 함수 같은 Stateless 객체 또는 본질적으로 유일한 시스템 컴포넌트를 그렇게 만든다.

싱글톤을 사용하는 클라이언트 코드는 테스트가 어렵다. 싱글톤이 인터페이스를 구현한 게 아니라면 Mock으로 대체할 수  없기 때문이다.

싱글톤을 만드는 방식은 보통 둘 중 하나이다. 두 방식 모두 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련한다. 

방식 하나. public static 멤버가 final 필드인 방식

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    
    public void leaveTheBuilding(){....}
}

 

private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한 번만 호출된다. public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다. 클라이언트는 손 쓸 방법이 없다. 

예외는 단 한 가지, 권한이 있는 클라이언트는 리플렉션 API인 AccesssibleOjbect.setAccesssible을 사용해 private 생성자를 호출할 수 있다. 이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지면 된다.

장점 하나.  해당 클래스가 싱글톤임이 API에 명백히 드러난다. public static 필드가 final이니 절대로 다른 객체를 참조할 수 없다.

장점 둘. 간결하다.

방식 둘. 정적 팩토리 메서드를 public static 멤버로 제공한다.

public class Elvis{
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    public static Elvis getInstance() {return INSTANCE; }
    
    public void elaveTheBuilding(){....}
}

 

Elvis.getInstance는 항상 같은 객체 참조를 반환하므로 제2의 Elvis 인스턴스란 결코 만들어지지 않는다.

장점 하나. 마음이 바뀌면 API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다는 점이다.

장점 둘. 정적 팩토리를 제네릭 싱글톤 팩토리로 만들 수 있다는 점이다.

장점 셋. 정적 팩토리의 메서드 참조를 공급자로 사용할 수 있다는 점이다. 

가령 Elvis::getInstance를 Supplier <Elvis>로 사용하는 식이다. 이러한 장점들이 필요하지 않다면 public 필드 방식인 방식 1번이 좋다.

두 방식의 싱글톤 클래스를 직렬화 하려면 단순히 Serializable을 구현하는 것만으로는 부족하다. 모든 인스턴스 필드를 일시적이라고 선언하고 readResolve 메서드를 제공해야 한다. 이렇게 하지 않으면 역직렬화할 때마다 가짜 Elvis 만들어진다.

//싱글톤임을 보장해주는 readResolve 메소드
private Object ReadResolve() {
    //진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTACE;
}

 

방식 셋. 원소가 하나인 열거 타입을 선언

public enum Elvis {
    INSTACE;
    
    public void leaveTheBuilding() {....}

}

 

public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

조금 부자연스러워 보일 수는 있으나 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글톤을 만드는 가장 좋은 방법이다.

단, 만들려는 싱글톤이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.(열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다.)

 

[아이템 4] 인스턴스화를 막으려거든 private 생성자를 사용하라.

 

static 메서드와 static 필드를 모아둔 클래스를 만든 경우 해당 클래스를 추상화(abstract)로 만들어도 인스턴스를 만드는 걸 막을 순 없다. 상속받아서 인스턴스를 만들 수 있기 때문에.

그리고 아무런 생성자를 만들지 않은 경우 컴파일러가 기본적으로 아무 인자가 없는 pubilc 생성자(디폴트 생성자)를 만들어 주기 때문에 그런 경우에도 인스턴스를 만들 수 있다.

명시적으로 private 생성자를 추가해야 한다.

public class UtilityClass {
    //기본 생성자가 만들어지느 것을 막는다(인스턴스화 방지용).
    private UtilityClass(){
        throw new AssertionError();
    }
    .....
}

 

AssetionError는 꼭 필요하진 않지만, 그렇게 하면 의도치 않게 생성자를 호출한 경우에 에러를 발생시킬 수 있고, private 생성자기 때문에 상속도 막을 수 있다.

생성자를 제공하지만 쓸 수 없기 때문에 직관에 어긋나는 점이 있는데, 그 때문에 위에 코드처럼 주석을 추가하는 것이 좋다.

부가적으로 상속도 막을 수 있다. 상속한 경우에 명시적이든 암묵적이든 상위 클래스의 생성자를 호출해야 하는데, 이 클래스의 생성자가 private이라 호출이 막혔기 때문에 상속을 할 수 없다.

 

[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

 

대부분의 클래스는 여러 리소스에 의존한다. 이 책에서는 SpellChecker와 Dictionary를 예로 들고 있다. 즉, SpellCheck가 Ditionary를 사용하고, 이를 의존하는 것을 리소스 또는 의존성이라고 부른다. 이때 SpellChecker를 다음과 같이 구현하는 경우가 있다.

부적절한 구현 하나. 정적 유틸 클래스 (아이템 4)

// 부적절한 static 유틸리티 사용 예 - 유연하지 않고 테스트 할 수 없다. 
public class SpellChecker { 
    private static final Lexicon dictionary = new KorDictionary(); 
    private SpellChecker() {
        // Noninstantiable 
        ....
    } 

    public static boolean isValid(String word) {
        throw new UnsupportedOperationException(); 
    } 
    public static List<String> suggestions(String typo) {
        throw new UnsupportedOperationException(); 
    } 
} 

interface Lexicon {} 
class KorDictionary implements Lexicon {}

 

부적절한 구현 둘. 싱글톤으로 구현하기 (아이템 3)

// 부적절한 싱글톤 사용 예 - 유연하지 않고 테스트 할 수 없다.
public class SpellChecker {

    private final Lexicon dictionary = new KoreanDicationry();

    private SpellChecker() {
        ....
    }

    public static final SpellChecker INSTANCE = new SpellChecker() {
        ....
    };

    public boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }


    public List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }

}

 

사전을 하나만 사용할꺼라면 위와 같은 구현도 만족스러울 수 있겠지만, 실제로는 각 언어의 맞춤법 검사기는 사용하는 사전이 각기 다르다. 또한 테스트 코드에서는 테스트용 사전을 사용하고 싶을 수도 있다.

어떤 클래스가 사용하는 리소스에 따라 행동을 달리 해야 하는 경우에는 static 유틸리티 클래스와 싱글톤을 사용하는 것은 부적절하다.

그런 요구 사항을 만족할 수 있는 간단한 패턴으로 생성자를 사용해서 새 인스턴스를 생성할 때 사용할 리소스를 넘겨주는 방법이 있다.

적절한 구현

public class SpellChecker {
    private final Lexicon dictionary;

    public SpellChecker(Lexicon dictionary) {
        this.dictionary = Objects.requireNonNull(dictionary);
    }

    public boolean isValid(String word) {
        throw new UnsupportedOperationException();
    }
    
    public List<String> suggestions(String typo) {
        throw new UnsupportedOperationException();
    }

}

class Lexicon {}

 

위와 같은 의존성 주입은 생성자, 정적 팩토리(아이템 1) 그리고 빌더(아이템 2)에도 적용할 수 있다.

이 패턴의 변종으로 리소스의 팩토리를 생성자에 전달하는 방법도 있다. 이 방법은 자바 8에 들어온 Supplier<T> 인터페이스가 그런 팩토리로 쓰기에 완벽하다. Supplier<T>를 인자로 받는 메서드는 보통 bounded wildcard type (아이템 31)으로 입력을 제한해야 한다.

Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

 

의존성 주입이 유연함과 테스트 용이함을 크게 향상해주지만, 의존성이 많은 큰 프로젝트인 경우에는 코드가 장황해질 수 있다. 그 점은 대거, 주스, 스프링 같은 프레임워크를 사용해서 해결할 수 있다.

핵심정리 

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준 다면 싱글톤과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접 만들게 해서도 안 된다. 대신 필요한 자원을 (혹은 그 자 원을 만들어주는 팩토리를) 생성자에 (혹은 정적 팩토리나 빌더에) 넘겨주자.

의존 객체 주입이라 하는 이 기법은 클래스가 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.

 

[아이템 6] 불필요한 객체 생성을 피하라

 

같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다. 재사용은 빠르고 세련되다. 특히 불변 객체(아이템 17)는 언제든 재사용할 수 있다.

String str = new String("bikini"); //따라하지 말 것!

 

이 문장은 실행될 때마다 String 인스턴스를 새로 만든다. 생성자에 넘겨진 "bikini" 자체가 이 생성자로 만들어내려는 String과 기능적으로 완전히 똑같다.

// 개선된 버전
String str = "bikini"

 

자바 9에서 deprecated 된 Boolean(String) 대신 Boolean.valueOf(String) 같은 static 팩토리 메서드(아이템 1)를 사용할 수 있다. 생성자는 반드시 새로운 객체를 만들어야 하지만 팩토리 메서드는 그렇지 않다.

만드는데 메모리나 시간이 오래 걸리는 객체 즉 "비싼 객체"를 반복적으로 만들어야 한다면 캐시 해두고 재사용할 수 있는지 고려하는 것이 좋다.

정규 표현식으로 예제로 살펴보자. 문자열이 로마 숫자를 표현하는지 확인하는 코드는 다음과 같다.

 static boolean isRomanNumeral(String s) {
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
 }

 

String.matches가 가장 쉽게 정규 표현식에 매치가 되는지 확인하는 방법이긴 하지만 성능이 중요한 상황에서 반복적으로 사용하기에 적절하지 않다.

String.matches는 내부적으로 Pattern 객체를 만들어 쓰는데 그 객체를 만들려면 정규 표현식으로 유한 상태 기계로 컴파일하는 과정이 필요하다. 즉 비싼 객체다.

성능을 개선하려면 Pattern 객체를 만들어 재사용하는 것이 좋다.

public class RomanNumber {

    private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

    static boolean isRomanNumeral(String s) {
        return ROMAN.matcher(s).matches();
    }

}

 

하지만 이 코드도 문제가 있는데, isRomanNumeral 메서드가 호출되지 않는다면, ROAM이라는 필요 없는 객체를 만든 셈이 된다. 게으른 초기화(lazily initializing)(아이템 83)를 사용해서 최적화할 수 있지만 추천하진 않는다. 보통 지연 초기화는 측정 가능한 성능 개선 없이 구현을 복잡하게 만든다.(아이템 67)

어댑터

public class UsingKeySet {

    public static void main(String[] args) {
        Map<String, Integer> menu = new HashMap<>();
        menu.put("Burger", 8);
        menu.put("Pizza", 9);

        Set<String> names1 = menu.keySet();
        Set<String> names2 = menu.keySet();

        names1.remove("Burger");
        System.out.println(names2.size()); // 1
        System.out.println(menu.size()); // 1
    }
}

 

불변 객체인 경우에 안정하게 재사용하는 것이 매우 명확하다 하지만 몇몇 경우에 분명하지 않은 경우가 있다. 오히려 반대로 보이기도 한다. 어댑터를 예로 들면, 어댑터는 인터페이스를 통해서 뒤에 있는 객체로 연결해주는 객체라 여러 개 만들 필요가 없다.

Map 인터페이스가 제공하는 keySet은 Map이 뒤에 있는 Set 인터페이스의 뷰를 제공한다. keySet을 호출할 때마다 새로운 객체가 나올 거 같지만 사실 같은 객체를 리턴하기 때문에 리턴 받은 Set 타입의 객체를 변경하면, 결국에 그 뒤에 있는 Map 객체를 변경하게 된다.

오토 박싱 : 오토 박싱은 기본 타입과 그에 대응하는 박싱 된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

public class AutoBoxingExample {

    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);
        System.out.println(System.currentTimeMillis() - start);
    }
}

 

위 코드에서 sum 변수의 타입을 Long으로 만들었기 때문에 불필요한 Long 객체를 2의 31 제곱 개만큼 만들게 되고 대략 6초 조금 넘게 걸린다. 타입을 프리미티브 타입으로 바꾸면 600 밀리 초로 약 10배 이상의 차이가 난다.

불필요한 오토 박싱을 피하려면 박스 타입보다는 프리미티브 타입을 사용해야 한다.

이번 아이템으로 인해 객체를 만드는 것은 비싸며 가급적이면 피해야 한다는 오해를 해서는 안된다. 특히 방어적인 복사(Depensive copying)를 해야 하는 경우에도 객체를 재사용하면 심각한 버그와 보안성에 문제가 생긴다. 객체를 생성하면 그저 스타일과 성능에 영향을 줄 뿐이다.

 

[아이템 7] 다 쓴 객체 참조를 해제하라

 

// Can you spot the "memory leak"?
public class Stack {

    private Object[] elements;

    private int size = 0;

    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        this.ensureCapacity();
        this.elements[size++] = e;
    }

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        return this.elements[--size]; // 주목!!
    }

    /**
     * Ensure space for at least one more element,
     * roughly doubling the capacity each time the array needs to grow.
     */
    private void ensureCapacity() {
        if (this.elements.length == size) {
            this.elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 

스택에 계속 쌓다가 많이 빼냈다고 치자, 그래도 스택이 차지하고 있는 메모리는 줄어들지 않는다. 왜냐면 저 스택의 구현체는 필요 없는 객체에 대한 레퍼런스를 그대로 가지고 있기 때문이다. 가용한 범위는 size 보다 작은 부분이고 그 값 보다 큰 부분에 있는 값들은 필요 없이 메모리를 차지하고 있는 부분이다.

다음과 같이 코드를 수정할 수 있다.

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }

        Object value = this.elements[--size];
        this.elements[size] = null;
        return value;
    }

 

스택에서 꺼낼 때 그 위치에 있는 객체를 꺼내 주고 그 자리를 null로 설정해서 다음 GC가 발생할 때 레퍼런스가 정리되게 한다. 실수로 해당 위치에 있는 객체를 다시 꺼내는 경우에 NullPointerException이 발생할 수 있긴 하지만, 그 자리에 있는 객체를 비우지 않고 실수로 잘못된 객체를 돌려주는 것보다는 차라리 괜찮다. 프로그래밍 에러는 언제든지 가능한 한 빨리 포착하는 것이 유익하다. (Fail-Fast?)

그렇다고 필요 없는 객체를 볼 때마다null로 설정하는 코드를 작성하지는 말자. 객체를 Null로 설정하는 건 예외적인 상황에서나 하는 것이지 평범한 일이 아니다. 필요없는 객체 레퍼런스를 정리하는 최선책은 그 레퍼런스를 가리키는 변수를 특정한 범위(스코프) 안에서만 사용하는 것이다. (로컬 변수는 그 영역 넘어가면 쓸모 없어져서 정리되니까요.) 변수를 가능한 가장 최소의 스콥으로 사용하면 자연스럽게 그렇게 될 것이다. (하지만 위에 코드처럼 size라는 멤버 변수와 elements를 쓰는 경우엔 역시 자연스럽게 그렇게 되진 않으니까.. 즉 예외적인 상황이라 그래서 명시적으로 null로 설정하는 코드를 써줘야 했던 겁니다.)

그럼 언제 레퍼런스를 null로 설정해야 하는가? 메모리를 직접 관리할 때. Stack 구현체ㅓ럼 elements라는 배열을 관리하는 경우에 GC는 어떤 객체가 필요 없는 객체인지 알 수 없다. 오직 프로그래머만 elements에서 가용한 부분 (size 보다 작은 부분)과 필요없는 부분 (size 보다 큰 부분)을 알 수 있다. 따라서, 프로그래머가 해당 레퍼런스를 null로 만들어서 GC한테 필요없는 객체들이라고 알려줘야 한다.

메모리를 직접 관리하는 클래스는 프로그래머가 메모리 누수를 조심해야 한다.

캐시

캐시를 사용할 때도 메모리 누수 문제를 조심해야 한다. 객체의 레퍼런스를 캐시에 넣어 놓고 캐시를 비우는 것을 잊기 쉽다. 여러 가지 해결책이 있지만, 캐시의 키에 대한 레퍼런스가 캐시 밖에서 필요 없어지면 해당 엔트리를 캐시에서 자동으로 비워주는 WeakHashMap을 쓸 수 있다.

또는 특정 시간이 지나면 캐시 값이 의미가 없어지는 경우에 백그라운드 쓰레드를 사용하거나 (아마도 ScheduledThreadPoolExecutor), 새로운 엔트리를 추가할 때 부가적인 작업으로 기존 캐시를 비우는 일을 할 것이다. (LinkedHashMap 클래스는 removeEldestEntry라는 메서드를 제공한다.)

콜백

세 번째로 흔하게 메모리 누수가 발생할 수 있는 지점으로 리스너와 콜백이 있다.

클라이언트 코드가 콜백을 등록할 수 있는 API를 만들고 콜백을 뺄 수 있는 방법을 제공하지 않는다면, 계속해서 콜백이 쌓이기 할 것이다. 이것 역시 WeahHashMap을 사용해서 해결할 수 있다.

메모리 누수는 발견하기 쉽지 않기 때문에 수년간 시스템에 머물러 있을 수도 있다. 코드 인스택션이나 heap profiler 같은 디버깅 툴을 사용해서 찾아야 한다. 따라서 이런 문제를 예방하는 방법을 학습하여 미연에 방지하는 것이 좋다.

 

[아이템 9] try - finally 보다는 try - with - resources를 사용하라

 

자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. InputStream, OutputStream 등이 좋은 예다. 

자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다. 이런 자원 중 상당수가 안정망으로 finalizer를 활용하고는 있지만 finalizer는 글 믿을만하지 못하다.(아이템 8)

지저분한 try - finally 방식.

static void copy(String src, String dst) throws IOException {
    IunputStream in = new FIleInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally{
        in.close();
    }
}

 

짧고 매혹적인 try - with - resources 방식.  

static void copy(String src, String dst) throws IOException {
    try(InputStream in = new FileInputStream(src);
    OuputStream out = new FileOutputStream(dst)){
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    }

}

 

 

참고
- 이펙티브 자바 3판
- 백기선 님 유튜브 이펙티브 자바 강의

댓글