ch2 객체 생성과 파괴 - LenKIM/everyone-is-effective-java-study GitHub Wiki

아래 자료는 Slipp 스터디에서 제공되었음을 명시합니다.

https://www.slipp.net/wiki/pages/viewpage.action?pageId=30771479

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

  • 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다.
  • 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
public static Boolean valueOf(boolean b) {
  return (b ? TRUE : FALSE);
}

장점 1. 이름을 가질 수 있다.

  • 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못하지만 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
  • 하나의 시그니처로는 생성자를 하나만 만들 수 있지만, 이름을 가질 수 있는 정적 팩터리 메서드에는 이런 제약이 없다. (e.g. BigInteger(int, int, Random) vs BigInteger#probablePrime(int, Random))

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

  • 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
  • 대표적인 예인 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.
  • 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷한 기법이라 할 수 있다. (e.g. java.lang.Integer#valueOf(int))

tips)

플라이웨이트 패턴?

final Integer n1 = 1;
final Integer n2 = 1;
System.out.println(n1 == n2); // true

final Integer n3 = 128;
final Integer n4 = 128;
System.out.println(n3 == n4); // false

?

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

  • 반환할 객체의 클래스를 자유롭게 선택할 수 있는 '엄청난 유연성'을 선물한다.
  • API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
  • API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.

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

  • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 심지어 다음 릴리스에서는 또 다른 클래스의 객체를 반환해도 된다. (e.g. EnumSet#noneOf(Class<E>))
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);
}
  • EnumSet.nonOf 의 인자로 무엇을 받는가에 따라 반환되는 값이 달라진다.

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

  • 이런 유연함은 서비스 제공자 프레임워크(service provider framework)를 만드는 근간이 된다.
  • 대표적인 서비스 제공자 프레임워크로는 JDBC(Java Database Connectivity)가 있다.
  • JDBC의 getConnection() 메서드에서 나오는 Connection 객체는 DB 마다 다르다.

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

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

  • API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.

정적 팩터리 메서드에 흔히 사용하는 명명 방식

명명 설명
from 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
of 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
valueOf from과 of의 더 자세한 버전
instance 혹은 getInstance (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
create 혹은 newInstance instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
getXXX getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다.(Array.newInsatance(classobject, arrayLen))
newXXX newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. (Files.newBufferedReader(path))
XXX getXXX과 newXXX의 간결한 버전(Collections.list(legacyLitany))

참고 자료

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

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

점층적 생성자 패턴

  • 프로그래머들은 이럴 때 **점층적 생성자 패턴(telescoping constructor pattern)**을 즐겨 사용했다.
  • 점층적 생성자 패턴은 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);

자바빈즈 패턴

  • 매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.
  • 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성(consistency)이 무너진 상태에 놓이게 된다.
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
  • 이러한 단점을 완화하고자 생성이 끝난 객체를 수동으로 '얼리고(freezing)', 얼리기 전에는 사용할 수 없도록 하기도 한다.
  • 이 방법을 쓴다고 하더라도 객체 사용 전에 프로그래머가 freeze 메서드를 확실히 호출해줬는지를 컴파일러가 보증할 방법이 없어서 런타임 오류에 취약하다.

빌더 패턴

  • 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴(Builder pattern)
  • 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개 변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
        .calories(100)
        .sodium(35)
        .carbohydrate(27)
        .build()
  ;
  • 이 클라이언트 코드는 쓰기 쉽고, 무엇보다도 읽기 쉽다. 빌더 패턴은 **명명된 선택적 매개변수(named optional parameters)**를 흉내 낸 것이다.
  • Lombok - @Builder
@Builder
public 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 void main(String[] args) {
        NutritionFacts.builder()
                .calories(100)
                .sodium(35)
                .carbohydrate(27)
                .build()
        ;
    }
}
  • Kotlin - 명명된 선택적 매개변수
class NutritionFacts(servingSize: Int, servings: Int, calories: Int, fat: Int, sodium: Int, carbohydrate: Int)

fun main(args: Array<String>) {
    NutritionFacts(servingSize = 240, servings = 8, calories = 100, fat = 0, sodium = 35, carbohydrate = 27)
}

참고 자료

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

  • 싱글턴(singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
  • 싱글턴의 전형적인 예로는 함수와 같은 무상태(stateless) 객체나 설계상 유일해야 하는 시스템 컴포넌트를 들 수 있다.
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
  • 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.

public static final 필드 방식

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }
}
  • private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출된다.

정적 팩터리 방식

public class Elvis {
    private static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }
    
    public static Elvis getInstance() {
        return INSTANCE;
    }
}

열거 타입 방식

public enum Elvis {
    INSTANCE;
}
  • 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.
  • 조금 부자연스러워 보일 수 있으나 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

참고 자료

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

  • 이따금 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있을 것이다.
  • final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다.
  • 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다.
  • 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
  • private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
public class UtilityClass {
    // 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
    private UtilityClass() {
        throw new AssertionError();
    }
}
  • 명시적 생성자가 private이니 클래스 바깥에서는 접근할 수 없다.
  • 이 방식은 상속을 불가능하게 하는 효과도 있다.

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

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
  • 이 조건을 만족하는 간단한 패턴이 있으니, 바로 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이다.
  • 스프링 프레임워크에서 @Autowire 와 비슷하지만, 사용함에 주의해야 한다.
  • 팩터리 메서드 패턴을 구현할 때 생성자로 Supplier<T> 를 활용해 클라이언트는 자신이 명시한 타입의 하위타입이라면 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다. Mosaic create(Supplier<? extends Tile> tileFactory) { ... }

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

  • 똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
  • 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
  • '비싼 객체'가 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }
    return sum;
}
//끔찍하게 느리다는 사실을 이해하라!
//객체가 만들어지는 위치를 찾았는가?
  • 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

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

  • 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.

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

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

  • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
  • 자바 9에서는 finalizer를 사용 자체(deprecated) API로 지정하고 cleaner를 그 대안으로 소개했다.
  • finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
  • finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.

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

  • 자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. (e.g. InputStream#close(), OutputStream#close(), java.sql.Connection#close())

try-finally

  • 믿기 어렵겠지만 훌륭한 프로그래머조차 잘못을 흔히 저지른다.
// 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
static String firstLineOfFile(String path) throws IOException {
  BufferedReader br = new BufferedReader(new FileReader(path));
  try {
    return br.readLine();
  } finally {
    br.close();
  }
}

여기서 만약, 하나의 자원을 더 사용하게 된다면?

// 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
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-with-resources 가 생겼다.

try-with-resources

  • 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
  • 짧고 읽기 수월할 뿐 아니라 문제를 진단하기도 훨씬 좋다.
// 코드 9-3 try-with-resources - 자원을 회수하는 최선책! (48쪽)
static String firstLineOfFile(String path) throws IOException {
  try (BufferedReader br = new BufferedReader(
    new FileReader(path))) {
    return br.readLine();
  }
}

// 코드 9-4 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다! (49쪽)
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);
  }
}

// 코드 9-5 try-with-resources를 catch 절과 함께 쓰는 모습 (49쪽)
static String firstLineOfFile(String path, String defaultVal) {
  try (BufferedReader br = new BufferedReader(
    new FileReader(path))) {
    return br.readLine();
  } catch (IOException e) {
    return defaultVal;
  }
}

참고 자료

⚠️ **GitHub.com Fallback** ⚠️