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

[Effective Java] 이펙티브자바 정리: 2장 객체 생성과 파괴

by 책 읽는 개발자_테드 2022. 1. 24.
반응형

목차

· 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

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

· 아이템 3. private 생성자나 열거 타입으로 싱글턴을 보증하라

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

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

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

· 아이템 7. 다 쓴 객체 참조를 해제하라

· 아이템 8. finalizer와 cleaner 사용을 피하라

· 아이템 9. try-finally보다는 try-with-resource를 사용하라

 


 

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


· 클래스의 인스턴스를 얻기 위해 정적 팩터리 메서드(static factory method)를 제공할 수 있다.

- 전통적인 수단: public 생성자

- 디자인 패턴(Gamma95)의 팩터리 메서드와는 다르다. 이와 일치하는 패턴은 없다.

 

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다. 

- 생성자에 넘기는 매개변수와 생성자 자체 만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 반환될 객체의 특성을 쉽게 묘사할 수 있다. ex) 값이 소수인 Integer를 반환하는  BigInteger.probablePrime

- 여러 개의 생성자가 있을 때, 엉뚱한 생성자를 호출하는 실수를 방지할 수 있다. 

- 클래스 설명 문서를 찾아보지 않고도 의미를 유추할 수 있다.

 

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

-  이를 이용하여 불변 클래스(immutable class 아이템 17)는 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 재활용하는 방식으로 불필요한 객체 생성을 피할 수 있다.

- 생성비용이 크고, 자주 요청되는 객체라면 성능을 상당히 끌어올릴 수 있다. 플라이웨이트 패턴[Gamma97]도 이와 비슷한 기법이다. 

- 반복되는 요청에 같은 객체를 반환하는 식으로 어느 인스턴스를 살아 있게 할지 철저히 통제할 수 있다. 인스턴스 통제는 플라이웨이트 패턴의 근간이 되며, 열거 타입의 인스턴스가 하나만 만들어짐을 보장한다.

 

인스턴스 통제 이유

더보기
더보기
더보기

 

이를 통해 클래스를 싱글턴으로 만들 수도, 인스턴스화 불가로 만들수도 있다.

불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다(a==b일 때만 a.equals(b)가 성립).

TODO: 플라이웨이트패턴

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

- 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성을 제공한다.

- 유연성을 응용하여 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

- 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크(아이템20)를 만드는 핵심 기술이기도 하다.

더보기
더보기
더보기

자바 컬렉션 프레임워크는 핵심 인터페이스들에 수정 불가나 동기화 등의 기능을 덧붙인 45개의 유틸리티 구현체를 제공하며, java.util.Collections에서 정적 팩터리 메서드를 통해 얻도록 한다. 45개의 클래스를 공개하지 않기 때문에 API 외견을 훨씬 작게 만들 수 있었고, 프로그래머가 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.

 

4. 입력 매개변수에 따라 매번 다른 객체를 반환할 수 있다.

- 클라이언트를 반환되는 객체들의 존재를 모른다. 따라서 다음 버전에서 원하는대로 수정할 수 있으므로, 유지보수가 편리하다.

ex) EnumSet 클래스는 Public 생성자 없이 정적 팩터리만 제공하며, 원소가 64개 이하면 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환한다. (OpenJDK기준)

 

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

- 이런 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다. ex)JDBC

- 제공자(provider): 서비스의 구현체로 클라이언트에서 제공하는 역할을 프레임워크가 통제하여, 클라이언트의 구현체로부터 분리해준다.

 

정적 팩터리 메서드의 단점

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

- 상속보다 컴포지션을 사용(아이템 18)하도록 유도하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.

 

2. 저정 팩터리 메서드는 프로그래머가 찾기 어렵다.

- 생성자처럼 API 설명에 명확히 드러나지 않으므로, 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.

- API 문서를 잘 써놓고, 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화하자.

생성자
일반 메서드

 

 

자주 사용하는 정적 팩터리 메서드 명명 방식 

· from - 매개변수를 하나만 받아서 해당 타입의 인스턴스를 반환하는 메서드

예시) Date date = Date.from(dateStr);

 

· of - 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드

예시) Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

 

· valueOf - from과 of의 더 자세한 버전

예시) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

 

· instance(getInstance) - 매개변수를 받는다면, 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다. 싱글턴일 수도 있다.

예시) StackWalker luke = StackWalker.getInstance(options);

 

· create(newInstance) - instance/getInstance와 같지만, 매번 새로운 인스턴스를 반환함을 보장한다.

 

· getType - getInstance와 맥락은 같으나 특정 Type을 반환할 때 사용

예시) Steak steak = Food.getSteak(Meet.BEEF);

 

· newType - newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스의 팩터리 메서드를 정의 할 때 사용

예시) Steak steak = Food.newSteak(Meet.BEEF);

 

· Type - getType, newType의 같결한 버전

예시) Steak steak = Food.steak(Meet.BEEF);

 

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


· 정적 팩터리와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.

· 이러한 문제의 대안으로 점층적 생성자 패턴(telescoping constructor pattern)과 자바빈즈 패턴이 등장했지만, 한계가 존재한다.

더보기

점층정 생성자 패턴: 필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개를 받는 생성자, 선택 매개변수를 2개 받는 생성자 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식이다. 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다는 단점이 있다.

 

자바빈즈 패턴: 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드들을 호출해 원하는 매개변수의 값을 성정하는 방식이다. 점층적 생성자 패턴에 비해 인스턴스를 생성하기 쉽고, 더 읽기 쉬운 코드를 만든다. 하지만 객체 하나를 만들 때 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이는 단점이 있다. 점층적 생성자 패턴에서는 매개변수들이 유효한지 생성자에서만 확인하면 일관성을 유지할 수 있었는데, 그 장치가 완전히 사라진 것이다.

 

· 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴으로 이러한 문제를 해결할 수 있다.

- 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자나 정적 팩터리를 호출해 빌더 객체를 얻고, 빌더 객체가 제공하는 세터 메서드들로 원하는 선택 매개변수를 설정하고, 매개변수가 없는 build 메서드를 호출해 객체를 얻는다.

 

빌더 패턴 예시

class NutritionFacts{
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

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

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

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

        public Builder calories(int calories){
            this.calories = calories;
            return this;
        }

        public Builder fat(int fat){
            this.fat = fat;
            return this;
        }

        public Builder sodium(int sodium){
            this.sodium = sodium;
            return this;
        }

        public Builder carbohydrate(int carbohydrate){
            this.carbohydrate = carbohydrate;
            return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }
    }
    
    private NutritionFacts(Builder builder){
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

 

빌더 패턴 사용법

· 이 클라이언트 코드는 쓰기 쉽고 읽기 쉽다.

NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
                                            .calories(100)
                                            .sodium(35)
                                            .carbohydrate(27).build();

 

· 빌더 패턴은 파이썬과 스칼라에 있는 명명된 선택적 매개변수(named optional parameters)를 흉내 낸 것이다.

· 빌더의 세터 메서드들을 빌더 자신을 반환하기 떄문에 연쇄적으로 호출할 수 있다. 이러한 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 플루언트 API 혹은 메서드 연쇄라 한다.

 

· 잘못된 매개변수를 일찍 발견하기위해 빌더의 생성자와 메서드에서 입력 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식을 검사하자.

- 불변(immutable 혹은 immutability): 어떠한 변경도 허용하지 않는다는 뜻으로 변경을 허용하는 가변(mutable) 객체와 구분하는 용도로 쓰인다.

 

· 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

public abstract class Pizza{
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>>{
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping){
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();

        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();
    }

}

 

public class NyPizza extends Pizza{
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder>{
        private final Size size;

        public Builder(Size size){
            this.size = Objects.requireNonNull(size);
        }

        @Override public NyPizza build(){
            return new NyPizza(this);
        }

        @Override protected Builder self(){
            return this;
        }
    }

    private NyPizza(Builder builder){
        super(builder);
        size = builder.size();
    }

}

 

public class Calzone extends Pizza{
    private final boolean sauceInside;

    public static class Builder extends Pizza.Builder<Builder>{
        private boolean sauceInside = false;

        public Builder sauceInside(){
            sauceInside = true;
            return this;
        }

        @Override public Calzone builde(){
            return new Calzone(this);
        }

        @Override protected Builder self(){ return this; }       
    }
    private Calzone(Builder builder){
        super(builder);
        sauceInside = builder.sauceInside;
    }
}
// 객체 생성
NyPizza pizza = new NyPizza.Builder(SMALL)
                .addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone = new Calzone.Builder()
                .addTopping(HAM).sauceInside().build();

 

· Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 여기에 추상 메서드인 self를 더해 하위 클래스에서 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.

 

· 각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환한다. 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반한 타입이 아닌, 그 하위 타입을 반환하는 기능을 공변반환 타이핑(covariant return typing)이라 한다. 이 기능을 이용하면 클라이언트가 형변환에 신경 쓰지 않고 빌더를 사용할 수 있다.

 

· 빌더 패턴은 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있고, 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

 

단점

· 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.

· 코드가 장황하다.

 

+ 추가적인 방법

1. 필드 값만 들어있는 클래스를 만들어 DI한다.

2. Map 값을 클래스 필드로로 옮긴다.

 

아이템 3. private 생성자나 열거 타입으로 싱글턴을 보증하라


· 싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

ex) 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트

 

싱글턴의 문제점

· 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려워 질 수 있다.

- 타입을 인터페이스로 정의한 후 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.

 

싱글턴을 만드는 방식

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

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

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

· 문제점:

 1. 권한이 있는 클라이언트에서 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출 할 수 있다.

 2. 생성되는 시점을 조절할 수 없다. (클래스가 다른 자원(DB커넥션 등)에 의존해야 한다면 이용 불가능)

- 방어 방법: 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던진다.

 

2. 정적 팩터리 메서드를 public static 멤버로 제공하는 방식

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

· 장점:

  1. API를 바꾸지 않고도 싱글턴이 아니게 변꼉할 수 있다. 

  ex) 유일한 인스턴스를 반환하던 팩터리 메서드가 호출하는 스레드별로 다른 인스턴스를 넘겨줄 수 있다.

  2. 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (아이템30)

  3.정적 팩터리의 메서드 참조를 공급자(supplier)로  사용할 수 있다.

  ex) Elvis::getInstance를 Supplier<Elvis>로 사용할 수 있다. (아이템 43, 44)

 

· 문제점: 리플렉션을 통한 예외는 똑같이 적용된다.  

 

· 두 방식 모두 생성자는 private로 감추고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련한다.

· 둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면, Serializable을 구현한다고 선언하는 것만으로는 부족 하다.

- 모든 인스턴스 필드를 transient로 선언하고, readResolve 메서드를 제공해야 한다. (아이템 89)

- 이렇게  하지 않으면, 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어 진다.

 

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

 

3. 원소가 하나인 열거 타입을 선언한다.

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

· 장점:

  1. 간결하다.

  2. 추가 노력 없이  직렬화할 수 있다.

  3. 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

· 대부분의 상황에서 싱글턴을 만드는 가장 좋은 방법이다.

· 문제점: 싱글턴이 Enum 외의 클래스를 상속해야 한다면, 이 방법은 사용할 수 없다.

- 열거 타입이 다른 인터페이스를 구현하도록 선언할 수 없다.

 

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


· 정적 메서드와 정적 필드만 담은 클래스는 객체 지향적으로 사고하지 않는 사람들이 종종 남용하는 방식이지만, 나름의 쓰임새가 있다.

1. java.lang.Math, java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.

2. java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수 있다.

(자바 8 부터는 이런 메서드를 인터페이스에 넣을 수 있다)

3. final 클래스와 관련된 메서드를 모아 놓을 때 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 넣는 건 불가기 때문이다.

 

· 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 따라서 private 생성자를 추가해 클래스의 인스턴스화를 막아서 사용할 수 있다. (생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다)

// 인스턴스를 만들 수 없는 유틸리티 클래스
public class UtilityClass {
	// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
    // 악의적 리플렉션을 막을 수 있다.
    private UtilityClass() {
    	throw new AssertionError();
    }
}

· 생성자가 분명 존재하는데 호출할 수는 없다니, 그다지 직관적이지 않으므로 앞의 코드처럼 적절한 주석을 달아준다.

· 이 방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스를 생성자를 호출해야하는데, 이를 private 선언으로 막아버렸기 때문이다.

 

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


· 많은 클래스가 하나 이상의 자원에 의존한다. 사용하는 자원에 따라 동작이 달라지는 클래스에 경우 클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다.

 

· 이 조건을 만족하는 간단한 패턴으로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이있다. 이는 의존 객체 주입의 한 형태다. 아래는 사전(dictionary)에 의존하는 맞춤법 검사기 클래스다.

public class SpellChecker {
    private final Lexicon dictionary;
    
    // 여기서 의존성 주입을!
    public SpellChecker(Lexicon dictionary){
    	this.dictionary = Objects.requireNotNull(dictionary);
    }
    
    public static boolean isVaild(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

 

·  의존 객체 주입 패턴의 장점:

  1. 자원이 몇 개든 의존 관계가 어떻근 상관없이 잘 동작한다.

  2. 불변(아이템17)을 보장하여 여러 클라이언트가 의존 객체를 안심하고 공유할 수 있도록 한다.

 

· 의존 객체 주입은 생성자, 정적 팩터리 모두에 똑같이 응용할 수 있다.

· 이 패턴의 쓸만한 변형으로, 생성자에 자원 팩터리를 넘겨주는 방식이 있다. 즉, 팩터리 메서드 패턴(Gamma95)을 구현하는 것이다.

- 팩터리: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체

 

· 자바 8에서 소개한 Supplier<T> 인터페이스는 팩터리를 표현한 완벽한 예다.

· Supplier<T>를 입력으로 받는 메서드는 일반적으로 한정적 와일드카드 타입(bounded wildcard type, 아이템 31)을 사용해 팩터리의 타입 매개변수를 제한해야한다. 이 방식을 사용해 클라이언트는 자신이 명시한 타입의 하위 타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.

- 예시) 클라이언트가 제공한 팩터리가 생성한 타일들로 쿠성된 모자이크를 만드는 메서드

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

 

· 의존 객체 주입은 유연성과 테스트 용이성을 개선해준다.

· 하지만 의존성이 많은 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다. 이럴떄 대거, 주스, 스프링 같은 의존 객체 주입 프레임워크를 사용하여 이 문제를 해결할 수 있다.

 

· 클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴정적 유틸리티 클래스는 사용하지 않는 것이 좋다. 이 자원들을 클래스가 직접 만들게 해서도 안된다.

- 정적 유틸리티를 잘못 사용한 예: 유연하지 않고 테스트하기 어렵다.

public class SpellChecker {
	private static final Lexicon dictionary = ...;
    
    private SpellChecker() {} // 인스턴스화 방지 (아이템 4 참고)
    
    public static boolean isVaild(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

 

- 싱글턴을 잘못 사용한 예: 유연하지 않고 테스트하기 어렵다.

public class SpellChecker {
	private final Lexicon dictionary = ...;
    
    private SpellChecker() {} // 인스턴스화 방지 (아이템 4 참고)
    public static SpellChecker INSTANCE = new SpellChecker(...);
    
    public static boolean isVaild(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

 

 

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


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

 

·  생성자 대신 정적 팩터리 매서드를 제공하는 불변 클래스에서는 불필요한 객체 생성을 피할 수 있다.

ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

 

·  가변 객체라 해도 수용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

 

·  생성 비용이 아주 비싼 객체가 반복해서 필요하다면, 캐싱하여 재사용하길 권한다.

ex) 아래는 주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드다. 하지만 String.matches 메서드를 사용한다는 데 문제가 있다.

public class RomanNumerals {
    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는 성능이 중요한 상황에서 반복해 사용하기에 적합하지 않다.

1. 메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.

2. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.

 

TODO: 유한 상태 머신이란?

 

- 불변인 Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 재사용할 수 있다.

아래 코드는 성능만 좋아진 것이 아니라 코드도 더 명확해졌다.

public class RomanNumerals {
    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 메서드가 처음 호출될 때 필드를 초기화하도록 지연 초기화로 불필요한 초기화를 없앨 수는 있지만, 권하지 않는다.

- 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을 때가 많다(아이템 67).

 

· 객체가 불변이라면 재사용해도 안점함이 명백하지만, 훨씬 덜 명확하거나, 직관에 반대되는 상황도 있다.

ex) 어댑터[Gamma95]를 예로 들면, 어댑터는 인터페이스를 통해 뒤에 있는 객체로 연결해주는 view라 여러 개 만들 필요가 없다.

- 어댑터: 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체

 

다음과 같이 같은 인스턴스를 대변하는 여러 개의 인스턴스를 생성하지 말자.

Map<String, Object> map = new HashMap<>();
map.put("Hello", "World");

Set<String> set1 = map.keySet();
Set<String> set2 = map.keySet();

assertThat(set1).isSameAs(set2); // TRUE

set1.remove("Hello");
System.out.println(set1.size()); // 1
System.out.println(set1.size()); // 1

 

· 오토박싱은 불필요한 객체를 만들어 성능에 해로울 수 있다.

- 오토박싱: 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술

ex) 다음은 sum 변수를 long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 약 231개나 만들어 진다.

public class Sum {
    private static long sum() {
        Long sum = 0L;
        for (long i = 0; i <= Integer.MAX_VALUE; i++) {
            sum += i;
        }
        return sum;
    }
}

 

· 객체 생성은 무조건 비싸니 피해야 하는 것은 아니다. 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것은 일반적으로 좋다.

 

· 데이터베이스 연결 같은 생성 비용이 매우 비싼 경우가 아니라면 객체 풀을 만들지 말자. 일반적으로 자체 객체 풀은 코드를 헷갈리게 만들고, 사용량을 늘리고, 성능을 떨어뜨린다.

 

· 이번 아이템은 방어적 복사(defensive copy)를 다루는 아이템 50과 대조적이다.

- 기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라 vs 새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라

- 방어적 복사가 필요한 상황에서 객체를 재사용했을 때 피해가 필요 없는 객체를 반복 생성했을 때 피해보다 훨씬 크다.

- 방어적 복사에 실패하면 버그와 보안 구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.

 

 

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


· 아래 스택 클래스는 메모리 누수가 발생한다.

- 이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.

- 드물긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료된다.

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 메서드에서 메모리 누수가 발생한다.

- 스택이 커졌다가 줄어들 때, 스택에서 꺼내진 객체들은 프로그램에서 더 이상 사용하지 않더라도 가비지 컬렉터가 회수하지 않는다.

- 꺼내진 객체들이 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.

- elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.

- 다쓴 참조: 앞으로 다시 쓰지 않을 참조

 

·  객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다.

- 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고, 잠재적으로 성능에 악영향을 줄 수 있다.

 

해결 방법

· 참조를 다 썼을 때 null(참조 해제)한다.

public Object pop() {
	if (size == 0) 
    	throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // 다 쓴 참조 해제
    return result;
}

· 다쓴 참조를 null 하면 프로그램 오류를 조기에 발견할 수도 있다.

- null 처리한 참조를 실수로 사용하려 하면 프로그램은 즉시 NullPointerException을 던지며 종료된다.

- null 처리하지 않았다면, 아무 내색 없이 무언가 잘못된 일을 수행할 수 있다.

 

· 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.

- 변수의 범위를 최소로 되게 정의했다면(아이템 57) 이 일은 자연스럽게 이뤄진다.

- null 처리를 해야하는 경우는 앞선 코드의 스택 처럼 자기 메모리를 직접 관리하는 클래스이다. 비활성 영역에서 참조하는 객체가 더 이상 쓸모없다는 것을 프로그래머만 알지 가비지 컬렉터는 알지 못하기 때문이다. 이럴 때는 null 처리를 하여 가비지 컬렉터에 직접 알려야한다.

 

· 캐시 역시 메모리 누수를 일으키는 주범이다.

- 객체 참조를 다 쓴 뒤 함참을 그냥 놔두는 일을 자주 접할 수 있다.

- 해결 방법1: 외부에서 키를 참조하는 동안만 엔트리가 살아 있는 캐시가 필요하다면, WeakHashMap을 사용해 캐시를 만든다.

다 쓴 엔트리는 즉시 자동으로 제거된다.

- 해결 방법2: 캐시를 만들 때 보통 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식이 흔히 사용된다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해야한다. ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용하거나 캐시에 새 엔트리를 추가할 때 부수 작업으로 수행할 수 있다.

- LinkedHahshMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다.

- 더 복잡한 캐시를 만들고 싶다면 java.lang.ref 패키지를 직접 활용해야 한다.

 

아이템 8. finalizer와 cleaner 사용을 피하라


· 자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공한다.

· finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.

- 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.

· 자바 9에서 finalizer가 deprecated되고, cleaner를 그 대안으로 소개했지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

· C++에서의 파괴자(destructor)와 자바의 finalizer, cleaner는 다른 개념이다.

- C++의 파괴자는 비메모리 자원을 회수하는 용도로 쓰이고, 자바에서는 try-with-resources와 try-finally를 사용해 해결한다.

 

finalizer와 cleaner의 부작용

·  finalizer와 cleaner는 즉시 수행된다는 보장이 없고, 실행되기까지 얼마나 걸릴지 알 수 없다.

- 따라서 파일 닫기와 같이 제때 실행되어야 하는 작업을 둘에게 맡기면 중대한 오류를 일으킬 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문이다. 새로운 파일을 열지 못해 프로그램이 실패할 수 있다.

 

·  자바 언어 명세는 finalizer나 clearner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.

·  System.gc나 System.runFinalization 메서드가 finalize와 cleaner가 실행될 가능성을 높여줄 수는 있지만, 보장해주진 않는다.

- System.runFinalizerOnExit과 Runtime.runFinalizerOnExit 메서드가 이를 보장해줄 수 있지만, 두 메서드는 ThreadStop이라는 심각한 결함이 있다.

 

·  finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다. 경고조차 출력하지 않는다. 

- 해당 객체는 마무리가 덜 된 상태로 남아 다른 스레드에서 훼손된 객체를 사용할 수도 있다.

(cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하므로 이런 문제는 발생 x)

 

·  finalizer와 cleaner는 심각한 성능 문제를 동반한다.

- 저자의 컴퓨터에서 AutoCloseable 객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns가 걸린 반면, finalizer를 사용하면 550ns가 걸렸다. cleaner도 이와 비슷하다.

 

· finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다.

- finalizer 공격: 생성자나 직렬화 과정에서 예외가 발생하면, 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.

- 이 finalizer는 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집 못하게 막을 수 있다.

- 이렇게 일그러진 객체가 만들어지면, 이 객체의 메서드를 호출해서 허용되지 않은 작업을 수행할 수 있다.

- 방어 방법1: final 클래스는 하위 클래스를 만들 수 없어 이 공격에서 안전하다.

- 방어 방법2: final이 아닌 클래스를 finalizer 공격으로부터 방어하려면, 아무 일도 하지 않는 finalize 메서드를 만들고 final 로 성언하자.

 

종료해야 할 자원을 담고 있는 객체의 클래스(파일이나 스레드 등)에서 finalizer와 Cleaner를 대신하는 방법

· AutoCloseable을 구현하고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출한다.

- 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources 사용

· 각 인스턴스는 자신이 닫혓는지 추적하는 것이 좋다.

- 즉, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던진다.

 

 finalizer와 Cleaner의 용도

1. 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할을 한다.

- finalizer나 cleaner가 즉시 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 아예 안하는 것보다는 낫다.

- FileInputStream, FileOutputStream, ThreadPoolExecutor 등 자바라이브러리 일부에서 안전망 역할의 finalizer를 제공한다.

 

2. 네이티브 피어와 연결된 객체에서 자원 회수용으로 사용한다.

- 네이티브 피어: 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체다. 자바 객체가 아니므로, 가비지 컬렉터는 그 존재를 알지 못한다.

- 성능 저하를 감당할 수 있고, 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 적합한 작업이다. 이에 해당하지 않으면, 앞서 설명한 close 메서드를 사용한다.

 

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


· 자바 라이브러이에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.

ex) InputStream, OutputStream, java.sql.Connection

 

· 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.

- 이런 자원 중 상당수는 안전망으로 finalizer를 활용하지만, finalizer는 믿음직하지 못하다.

 

·  전통적으로 자원을 닫는 수단으로 try-finally가 쓰였지만, 자원이 둘 이상이면 코드가 너무 지저분해진다. 

    // 자원 하나 회수
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }

 

	// 자원 복수개 회수
	static void copy(String src, String dst) throws IOException {
        InputStream 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 블록과 finally 블록 모두에서 발생할 수 있는데, 예컨대 기기에 물리적인 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 것이다.

이런 상황이라면 두 번째 예외가 첫 번째 예외를 완전히 집어삼켜 버린다. 그러면 스택 추적 내역에 첫 번째 예외에 관한 정보는 남지 않게 되어, 실제 시스템에서의 디버깅을 몹시 어렵게 한다(일반적으로 문제를 진단하려면 처음 발생한 예외를 보고 싶을 것이다).

 

· 자바7에서 등장한 try-with-resources은 try-finally의 결점을 해결한다.

- 이 구조를 사용하려면 하당 자원이 AutoCloseable 인터페이스를 구현해야 한다.

- 단순히 void를 반환하는 close 메서드 하나만 정의한 인터페이스다.

 

· 다음은 앞선 코드를 try-with-resources를 사용해 재작성한 코드다.

    static String firstLineOfFile(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        }
    }

 

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

· 앞선 코드에서 확인할 수 있듯이 try-with-resources 버전이 짧고 읽기 수월하며, 문제를 진단하기도 훨씬 좋다.

- firstLineOfFile 메서드를 살펴보자. readLine과 (코드에는 나타나지 않는)close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다.

- 이렇게 숨겨진 예외들도 스택 추적 내역에 '숨겨졌다(suppressed)'는 꼬리표를 달고 출력된다.

- 자바 7에서 Throwable에 추가된 getSuppressed 메서드를 이용하면 프로그램 코드에서 가져올 수도 있다.

 

· try-with-resources도 catch 절을 쓸 수 있다.

- 이를 통해 try 문을 더 중첩하지 않고 다수의 예외를 처리할 수 있다.

    static String firstLineOfFile(String path, String defaultVal) {
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            return br.readLine();
        } catch (IOException e) {
            return defaultVal;
        }
    }

 

출처

이펙티브 자바 3/E

https://velog.io/@lychee/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-6.-%EB%B6%88%ED%95%84%EC%9A%94%ED%95%9C-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%EC%9D%84-%ED%94%BC%ED%95%98%EB%9D%BC

반응형

댓글