기술서적

EFFECTIVE JAVA3 - 2장. 객체 생성과 파괴

skysoo1111 2022. 4. 15. 22:27

EFFECTIVE JAVA3를 읽고 내용을 정리해보려고 한다.

이번 장은 객체의 생성과 파괴를 다룬다.

  1. 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법
  2. 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령

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

여기서 얘기하는 정적 팩터리 메서드는 디자인 패턴의 팩토리 메서드와 다르다.

장점 👍

1. 이름을 가질 수 있다.

❌ Bad
BigInteger(int, int, Random)

✅ Good
BigInteger.probablePrime(int, int, Random)

"값이 소수인 BigInteger를 반환한다" 라는 보다 명확한 의미 전달이 가능하다.

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

이 덕분에 불변 클래스는 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. (객체 생성의 비용이 큰 경우 성능을 상당히 올릴 수 있다.)
🔸 불변 클래스: 멤버 변수를 공개하지 않는다.

✅ Good
public final class Boolean implements Serializable, Comparable<Boolean> {

  public static final Boolean TRUE = new Boolean(true);
  public static final Boolean FALSE = new Boolean(false);

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

3. 반환 타입의 하위 타입 객체를 반활 할 수 있는 능력이 있다.

// java 8 이전에는 인터페이스에 정적 메서드를 선언할 수 없었다.
// 인터페이스와 동반 클래스의 예
Collection<String> empty = Collections.emptyList();

public class Collections {
    public static final List EMPTY_LIST = new EmptyList<>();

    public static final <T> List<T> emptyList() {
            return (List<T>) EMPTY_LIST;
    }
}

// java 8 이후에는 인터페이스에 정적 메서드를 선언할 수 있다.
Stream<String> chosunKings = Stream.of("태조", "정종"};

public interface Stream<T> extends BaseStream<T, Stream<T>> {
    public static<T> Stream<T> of(T t) {
            return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
    }
}


// java 9 이후에는 private 정적 메서드까지 허용한다.

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

✅ Good
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
}

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

이런 유연함은 서비스 제공자 프레임워크(3개의 핵심 컴포넌트)를 만드는 근간이 된다.

  1. 서비스 인터페이스 : 구현체의 동작을 정의
  2. 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
  3. 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용

✅ JDBC 서비스 제공자 프레임워크
1. 서비스 인터페이스 - Connection

2. 제공자 등록 API - DriverManager.registerDriver

3. 서비스 접근 API - Drivermanager.getConnection
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) 

4. 서비스 제공자 인터페이스 - Driver

단점 👎

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

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

API 문서화를 잘하고 메서드 이름도 알려진 규약을 따라 짓도록 하자

- from
Date d = Date.from(instance);
- of
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf
BigInteger prime = BigInteger.valueOf(Integer,MAX_VALUE);
- instance or getInstance
StackWalker luke = StackWalker.getInstance(options);
- create or new Instance
Object newArray = Array.newInstance(classObject, arrayLean);
- getType
FileStore fs = Files.getFileStore(path);
- newType
BufferedReader br = Files.newBufferdReader(path);
- type
List<Complaint> litany = Collections.List(legacyLitany);

각자의 장단점이 있으나 정적 팩토리를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관이 있다면 고치자.

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

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

2-1. 점층적 생성자 패턴 - 확장하기 어렵다!
이는 각 매개변수가 무엇인지, 몇 개인지, 순서까지 고려해야 한다.

NutritionFacts cocaCola = new NutritionFacts(240, 8,100,0,35,27);

2-2. 자바빈즈 패턴 - 일관성이 깨지고, 불변으로 만들 수 없다.
위 패턴보다 인스턴스를 만들기 쉽고, 가독성이 좋아졌지만 객체 하나를 만들기 위해 setter 메서드 여러개를 호출해야 하고, 일관성을 유지하기 어렵다.

2-3. 빌더 패턴 - 점층적 생성자 패턴과 자바빈즈 패턴의 장점만 취했다.

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

1. 메서드 체이닝이 가능해 가독성이 좋다. 
2. 필수 매개변수와 선택적 매개변수의 명시적 구분이 가능하고 사용이 편리하다.

가변 객체에도 불변식은 존재할 수 있으며, 불변은 불변식의 극단적인 예라고 볼 수 있다.

2-4. 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴
재귀적 타입 한정을 이용하는 제네릭 타입을 사용하고 추상 메서드인 self를 더해 하위 클래스에서 형변화 하지 않고도 메서드 체이닝을 지원할 수 있다.

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

    // Pizza.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입이다. 
    // 여기에 추상 메서드인 self를 추가하여 하위 클래스에서는 형변환하지 않고도 메서드 연쇄를 지원할 수 있다.
    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();
        }

        // 각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 선언한다.
        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;

    // super의 builder도 상속받아
    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;
    }
}

NyPizza nyPizza = new NyPizza.Builder(NyPizza.Size.SMALL)
                             .addTopping(Pizza.Topping.SAUSAGE)
                             .addTopping(Pizza.Topping.ONION).build();

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

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트 테스트가 어려워진다. (mock 구현으로 대체할 수 없기 때문)

3-1. public static final 필드 방식의 싱글턴

// private 생성자는 public static final 필드를 초기화 할 때 한번만 호출된다.
public class Elvis { 
    public static final Elvis INSTANCE = new Elvis();     
    private Elvis() {...}

    public void leaveTheBuilding() {...}
}

👍 장점
해당 클래스가 싱글턴임이 API에 명백히 드러난다.

3-2. 정적 팩토리 방식의 싱글턴

// 항상 같은 객체의 참조를 반환 하므로 오직 하나의 인스턴스만 가진다. 
public class Elvis { 
  private static final Elvis INSTANCE = new Elvis(); 
  private Elvis() {...} 
  public static Elvis getInstance() { 
      return INSTANCE; 
  } 

  public void leaveTheBuilding() {...}
}


👍 장점
1. API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점이다.
2. 원한다면 정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다.(아이템 30)
3. 정적 팩토리의 메서드 참조를 공급자로 사용할 수 있다는 점이다.(아이템 43,44)

위에서 언급한 두 방식의 장점인 이유가 뭔가?

권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다. (3-1, 3-2 모두 해당 됨)
이를 막으려면 생성자를 수정하여 두번째 객체가 생성될 때 예외를 던지자.

위 두 방식으로 만든 싱글턴을 직렬화하려면 단순히 Serializable을 구현하는 것으로 부족하다.
모든 인스턴스 필드를 transient라고 선언하고 readResolve() 메서드를 제공해야 한다. (아이템 89)

3-3. 열거 타입 방식의 싱글턴 - 바람직한 방법

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() {...}
}

👍 장점
1. public 필드 방식과 비슷하지만 더욱 간결하다.
2. 추가 노력 없이 직렬화가 가능하다.
3. 리플렉션 공격에서도 완벽히 막아준다.

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

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

  1. java.util.Arrays 처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 때
  2. java.util.Collections 처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩토리)를 모아놓을 때
  3. final 클래스와 관련한 메서드들을 모아 놓을 때

정적 멤버만 담은 클래스는 좋지는 않지만 위와 같은 경우 나름의 쓰임새가 있다. 하지만 생성자를 명시하지 않는 경우 컴파일러에 의해 자동으로 public 생성자가 만들어진다.

// 정적 멤버 클래스에서 public 생성자가 만들어지는 것을 막기위한 명시적 생성자
public class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
}

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

많은 클래스가 하나 이상의 자원에 의존한다.

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.

5-3. 의존 객체 주입은 유연성과 테스트 용이성을 높여준다.

// 인스턴스를 생성할 때 필요한 자원을 주입하는 방식이다.
public class SpellChecker {
  private final Lexicon dictionary;
  private SpellChecker(Lexicon dictionary) {
    this.dictionary= Objects.requireNonNull(dictionary);
  }
  public boolean isValid(String word) { ... }
}

👍 이 패턴의 쓸만한 변형이 팩토리 메서드 패턴이다.
생성자에 자원 팩토리를 넘겨주는 방식으로 여기서 팩토리란, 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다.

자바8의 Supplier 인터페이스가 팩토리를 표현한 완벽한 예이다.

// Supplier<T>를 입력 받는 메서드는 한정적 와일드 카드를 사용해 팩토리의 입력 매개 변수를 제한해야 한다.

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

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 필요한 자원(또는 자원을 만드는 팩토리)을 생성자(또는 정적 팩토리 or 빌더)에 넘겨주자.

정적 팩토리 메서드와 팩토리 메서드의 차이는?
정적 팩토리 메서드 : 객체 생성의 역할을 하는 클래스 메서드
팩토리 메서드 : 객체의 생성을 서브 클래스에서 결정

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

앞에서 언급한 정적 팩토리 메서드의 사용 역시 불필요한 객체 생성을 막는 방법이 된다. (Boolean.valueOF(String)이 좋은 예)

❌ Bad
String s = new String("bikini");

✅ Good
String s = "bikini";

> 불변 객체: 재할당은 가능하지만, 한번 할당하면 내부 데이터를 변경할 수 없는 객체

6-2. 값비싼 객체를 재사용해 성능을 개선한다.
인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱하고 재사용해라

지연 초기화(아이텝 83)는 코드를 복잡하게 만들고 성능은 크게 개선되지 않을 때가 많기 때문에 권하지는 않는다.

불필요한 객체를 만드는 예

/* 
 * keySet()은 호출할 때마다 같은 Set 뷰를 반환한다.
 * 따라서 keySet이 뷰 객체를 여러개 만들 필요도 이득도 없다.
*/
@Test
    public void 불필요한_객체_생성(){
        Map<Integer, String> temps = new HashMap<>();
        temps.put(1, "1");
        temps.put(2, "2");
        temps.put(3, "3");

        Set<Integer> tempsKeySet1 = temps.keySet();
        Set<Integer> tempsKeySet2 = temps.keySet();
        assertThat(tempsKeySet1).contains(1,2,3);

        tempsKeySet2.remove(3);
        assertThat(tempsKeySet2).doesNotContain(3);

}

/* 
 * 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.
 * 박싱된 기본 타입보다는 기본 타입을 사용하고 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
*/ 

이번 아이템은 객체 생성은 비싸니 피해야 한다가 아니라, 생성 비용이 비싼 객채는 재사용하자 이므로 단순히 객체 생성을 피하고자 객체 풀을 만들어 쓰지는 말자.

반드시 새로 만들어서 사용해야 할 객체를 재사용 했을 때의 피해가, 필요 없는 객체를 반복 생성했을 대의 피해보다 훨씬 크다. (아이템 50 "새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라")

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

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

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

참조 객체 하나를 살려두면 그 객체가 참조하는 모든 객체를 회수하지 못한다.

🔸 비활성 영역

  1. 자기 메모리를 직접 관리하는 클래스에서의 객체 참조
  2. 캐시
  3. 리스너 혹은 콜백

🔸 비활성 영역을 가비지 컬렉터에서 처리하는 방법

  1. 참조 객체의 null 처리
  2. WeakhashMap 사용

단, 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 즉, 자기 메모리를 직접 관리하는 클래스인 경우에는 가비지 컬렉터가 알 수 없는 (비활성 영역의 데이터) 참조 객체들을 null 처리 해주면 된다.

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

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

  1. finalizer
  2. cleaner

🔸 절대 사용하면 안될 때

  1. 제때 실행되어야 하는 작업
  2. 상태를 영구적으로 수정하는 작업에서는 절대 의존하면 안된다.
  3. System.gc나 System.runFinalization 메서드에 현혹되지 말자. 실행 가능성은 높여주나 역시나 보장해주지는 않는다.

하지만 finalizer, cleaner는 즉시 수행된다는 보장이 없어 예측할 수 없고 일반적으로 불필요하다.

  1. 예외 무시
  2. 심각한 성능 문제
  3. 보안 문제

8-1. cleaner를 안전망으로 활용하는 AutoCloseable 클래스

❌ Bad
public class Adult {
    public static void main(String[] args)  {
        new Room(99);
        System.out.pringln("안녕~");
    }
}

✅ Good - try-with-resources
public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.pringln("안녕~");
        }
    }
}

finalizer나 cleaner 대신 AutoCloseable을 구현해주고 try-with-resources 를 사용하라

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

try-finally를 사용하는 경우 아래와 같은 단점들이 존재한다.

  1. 자원이 둘 이상이면 코드가 지저분해진다.
  2. 자원의 사용처 / close 모두 예외를 던질 때 두번째 예외가 첫번째 예외를 집어 삼키므로 디버깅을 어렵게 한다.

9-4. 복수의 자원을 처리하는 try-with-resources 짧고 매혹적이다!

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-finally 말고, try-with-resources를 사용하자. 예외는 없다.