Effective Java 3rd - Ch02
포스트
취소

Effective Java 3rd - Ch02

Java 기초를 다지기 위해 효과적인 자바란 책을 공부 중입니다.

제2장 객체 생성과 파괴

  • 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 법
  • 올바른 객체 생성 방법
  • 불필요한 생성을 피하는 방법
  • 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령

1) 생성자 대신 정적 팩토리 메서드를 고려하라

코드 2-0 디자인 패턴의 팩토리 메소드와 다르다

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

장점

  • 이름을 가질 수 있다 : 충분한 설명
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 됨 : 플라이웨이트 패턴과 유사
  • 반환 타입의 하위타입 객체를 반환할 수 있음 : 유연성
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음
  • 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨
    • ex) JDBC : 서비스 인터페이스, 제공자 등록 API, 서비스 접근 API

단점

  • 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다
  • 정적 팩토리 메서드는 프로그래머가 찾기 어렵다

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

코드 2-1 점층적 생성자 패턴 - 확장하기 어렵다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 NutritionFacts( int servingSize, int servings ){
    this( servingSize, servings, 0 );
  }

  public NutritionFacts( int servingSize, int servings, int calories ){
    this( servingSize, servings, calories, 0 );
  }

  public NutritionFacts( int servingSize, int servings, int calories
        , int fat ){
    this( servingSize, servings, calories, fat, 0 );
  }

  public NutritionFacts( int servingSize, int servings, int calories
        , int fat, int sodium ){
    this( servingSize, servings, calories, fat, sodium, 0 );
  }

  public NutritionFacts( int servingSize, int servings, int calories
        , int fat, int sodium, int carbohydrate ){
    this.servingSize = servingSize;
    this.servings = servings;
    this.calories = calories;
    this.fat = fat;
    this.sodium = sodium;
    this.carbohydrate = carbohydrate;
  }

}

/////////////////////

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

코드 2-2 자바빈즈 패턴 : 일관성이 깨지고, 불변(immutable)으로 만들 수 없다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NutritionFacts {
  // 매개변수들은 기본값으로 초기화 된다
  private int servingSize  = -1;    // 필수
  private int servings     = -1;    // 필수
  private int calories     = 0;
  private int fat          = 0;
  private int sodium       = 0;
  private int carbohydrate = 0;

  public NutritionFacts() {}
  // setter methods
  public void setServingSize(int val)  { servingSize = val; }
  public void setServings(int val)     { servings = val; }
  public void setCalories(int val)     { calories = val; }
  public void setFat(int val)          { fat = val; }
  public void setSodium(int val)       { sodium = val; }
  public void setCarbohydrate(int val) { carbohydrate = val; }

  // 일단 생성하고 내부변수를 채우는 전략
  // ==> 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 됨
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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 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 val){
      this.calories = val; return this;
    }
    public Builder fat(int val){
      this.fat = val; return this;
    }
    public Builder sodium(int val){
      this.sodium = val; return this;
    }
    public Builder carbohydrate(int val){
      this.carbohydrate = val; return this;
    }

    public NutritionFacts build(){
      return new NutritionFacts(this);
    }
  }

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

/////////////////////

// NOTE: 쓰기 쉽고, 무엇보다 읽기 쉽다
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
          .calories(100).sodium(35).carbohydrate(27).build();

코드 2-4 계층적으로 설계된 클래스와 잘 어울리는 빌더 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class Pizza {
  public enum Topping { HAM, MUSHROOM, INION, 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();

    // 하위 클래스는 이 메서드를 재정의(overriding) 하여
    // "this"를 반환하도록 해야 한다
    protected abstract T self();
  }

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

코드 2-5 뉴욕 피자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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;
  }
}

////////////////////////

NyPizza pizza = new NyPizza.Builder(SMALL)    // 필수 매개변수
        .addTopping(SAUSAGE).addTopping(ONION).build();

코드 2-6 칼초네 피자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 build() { return new Calzone(this); }

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

  private Calzone(Builder builder) {
    super(builder);
    sauceInside = builder.sauceInside;
  }
}
////////////////////////

Calzone pizza = new Calzone.Builder()     // 필수 없음
        .addTopping(HAM).sauceInside().build();

:bangbang:   핵심정리

  • 생성자나 정적 팩토리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다
  • 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다

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

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

  • 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문

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

1
2
3
4
5
6
7
8
public class Elvis {
  public static final Elvis INSTANCE = new Elvis();

  // public 이나 protected 생성자가 없어서 시스템에서 하나뿐임이 보장됨
  private Elvis() { ... }     // INSTANCE 초기화 때 한번만 호출됨

  public void leaveTheBuilding() { ... }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Elvis {
  public static final Elvis INSTANCE = new Elvis();
  private Elvis() { ... }

  // 항상 같은 INSTANCE 를 반환하므로 한층 안전해짐
  public static Elvis getInstance() { return INSTANCE; }

  public void leaveTheBuilding() { ... }

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

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

1
2
3
4
5
6
7
8
// 대부분의 상황에서 싱글턴을 만드는 가장 좋은 방법
// ==> 원소(INSTANCE)가 하나뿐인 enum 타입
// 단, 클래스 상속은 불가능
public enum Elvis{
  INSTANCE;

  public void leaveTheBuilding() { ... }
}

4) 인스턴스화를 막으려거든 private 생성자를 사용하라

정적 메서드와 정적 필드만 담은 클래스도 나름 용도가 있다

  • 예) java.lang.Math, java.util.Arrays, java.util.Collections

:bangbang:   생성자를 명시하지 않으면, 컴파일러가 자동으로 기본 생성자를 만들어준다

코드 4-1 인스턴스를 만들 수 없는 유틸리티 클래스

1
2
3
4
5
6
7
8
9
public class UtilityClass {
  // 기본 생성자가 만들어지는 것을 막는다 (인스턴스화 방지용)
  // 상속도 불가능
  private UtilityClass() {
    throw new AssertionError();
  }

  ... // 나머지 코드 생략
}

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

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

1
2
3
4
5
6
7
8
9
public class SpellChecker {
  // 단 하나의 사전만 사용한다고 가정
  private static final Lexicon dictionary = ...;

  private SpellChecker() {}   // 객체 생성 방지

  public static boolean isValid(String word) { ... }
  public static List<String> suggestions(String typo) { ... }
}

코드 5-1 정적 유틸리티를 잘못 사용한 예2 : 싱글턴으로 구현한 경우

1
2
3
4
5
6
7
8
9
public class SpellChecker {
  private final Lexicon dictionary = ...;

  private SpellChecker(...) {}   // 객체 생성 방지
  public static SpellChecker INSTANCE = new SpellChecker(...);

  public boolean isValid(String word) { ... }
  public List<String> suggestions(String typo) { ... }
}

단 하나의 사전만 사용하는게 가능한가? (어리석은 생각)

  • 다른 사전으로 교체할 수 있도록 하자

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

  • 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식이 필요
  • 팩토리 메서드 패턴

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

1
2
3
4
5
6
7
8
9
10
public class SpellChecker {
  private final Lexicon dictionary;

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

  public boolean isValid(String word) { ... }
  public List<String> suggestions(String typo) { ... }
}

참고: Why should one use Objects.requireNonNull()?

:bangbang:   핵심정리

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다

6) 불필요한 객체 생성을 피하라

하지 말아야 할 극단적인 예 (낭비)

  • “bikini” 자체와 생성된 String 인스턴스가 똑같다
  • 생성 비용이 비싼 객체라면 캐싱하여 재사용하길 권고
1
2
3
4
5
String s = new String("bikini");  // 완전히 쓸데없는 행위
Boolean b = Boolean(String);

String s = "bikini";              // 개선된 버전
Boolean b = Boolean.valueOf(String);  // 팩토리 메소드

코드 6-1 성능을 훨씬 더 끌어올릴 수 있다

1
2
3
4
5
6
7
// 소요시간 1.1us
static boolean isRomanNumeral(String s) {
  // Pattern 인스턴스는 한번 쓰고 버려져서 가비지 콜렉션 대상이 됨
  // 특히, 정규표현식을 위한 유한 상태 머신을 만들기 때문에 생성 비용이 높다
  return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
          + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

코드 6-2 값비싼 객체를 재사용해 성능을 개선한다

1
2
3
4
5
6
7
8
9
10
11
// 소요시간 0.17us (6.5배 빨라짐)
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();    // Pattern 재사용
  }
}

코드 6-3 끔찍이 느리다! 객체가 만들어지는 위치를 찾았는가?

1
2
3
4
5
6
7
8
9
private static long sum() {
  // **NOTE : long 으로만 바꿔도 '6.3초'에서 '0.59초'로 빨라진다
  Long sum = 0L;
  for( long i=0; i<=Integer.MAX_VALUE; i++ )
    // **NOTE : 더해질 때마다 Long 인스턴스가 생성된다
    sum += i;   // Long 으로 오토 박싱

  return sum;
}

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

단, 방어적 복사(50번 항목)가 필요한 경우에는 재사용을 하지 마라 (복사를 하라)

언제 터져 나올지 모르는 버그와 보안 구멍을 위해 방어적 복사를 해야할 경우도 있다

7) 다 쓴 객체 참조를 해제하라

가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다

코드 7-1 메모리 누수가 일어나는 위치는 어디인가?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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;
  }

  // 활성 영역은 size 보다 작은 index 의 원소들
  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);
  }
}

해법은 간단하다. 해당 참조를 다 썼을 때 null 처리(참조 해제) 하면 된다.

코드 7-2 제대로 구현한 pop 메서드

1
2
3
4
5
6
public Object pop() {
  if( size == 0 ) throw new EmptyStackException();
  Object result = elements[--size];   // 참조는 result 에게 이양
  elements[size] = null;    // elements 에 의한 다 쓴 참조 해제
  return result;
}

그렇다고 모든 객체를 다 쓰자마자 일일이 null 처리하려고 하지는 마라. 프로그램을 필요 이상으로 지저분하게 만들 뿐이다. 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

Stack 클래스는 왜 메모리 누수에 취약한 걸까?

스택이 자기 메모리를 직접 관리하기 때문이다. 이 스택은 (객체 자체가 아니라 객체 참조를 담는) elements 배열로 저장소 풀을 만들어 원소들을 관리한다. 일반적으로 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항시 메모리 누수에 주의해야 한다.

:bangbang: 메모리 누수의 주범

  • 자기 메모리를 직접 관리하는 클래스
    • :arrow_right:   다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리
  • 캐시
    • :arrow_right:   WeakHashMap 고려
    • :arrow_right:   ScheduledThreadPoolExecutor 같은 백그라운드 스레드를 활용해 주기적으로 청소
    • :arrow_right:   새 엔트리를 추가할 때 부수 작업으로 수행, ex) LinkedHashMap 의 removeEldestEntry()
  • 리스너 혹은 콜백 (등록만 하고 명확히 해지하지 않는 경우)
    • :arrow_right:   WeakHashMap 고려

8) finalizer 와 cleaner 사용을 피하라

자바는 두가지 객체 소멸자를 제공 (가비지 컬렉터에 의존)

  • finalizer
    • 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요. 성능 저하
    • :arrow_right:   기본적으로 쓰지 말아야 한다
  • cleaner
    • finalizer 보다 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요
1
2
- finalizer 와 cleaner 로는 제때 실행되어야 하는 작업은 절대 할 수 없다
- System.gc 나 System.runFinalization 등의 메서드에 현혹되지 말자

파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서의 묘안은 무엇인가 ?

  • AutoCloseable 을 구현해 주고
  • 클라이언트에서 인스턴스를 다 쓰고 나면 close 메소드를 호출

:bangbang:   finalizer 와 cleaner 의 용도

  • close 메서드를 호출하지 않는 것에 대한 안전망 역활
  • 네이티브 피어(네이티브 메서드를 통해 기능을 위임한 네이티브 객체)와 연결된 객체에서 사용
    • 자바 객체가 아니라서 가비지 콜렉터가 그 존재를 알지 못함

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Room implements AutoCloseable {
  private static final Cleaner cleaner = Cleaner.create();

  // 청소가 필요한 자원. 절대 Room 을 참조해서는 안된다!
  private static class State implements Runnable {
    int numJunkPiles;   // 방(Room) 안의 쓰레기 수 (수거할 자원)

    State(int numJunkPiles) {
      this.numJunkPiles = numJunkPiles;
    }

    // close 메서드나 cleaner 가 호출한다
    @Override public void run() {
      System.out.println("방청소");
      numJunkPiles = 0;
    }
  }

  // 방의 상태 : cleanable 과 공유한다
  private final State state;
  // cleanable 객체 : 수거 대상이 되면 방을 청소한다
  private final Cleaner.Cleanable cleanable;

  public Room(int numJunkPiles) {
    state = new State(numJunkPiles);
    cleanable = cleaner.register(this, state);
  }

  @Override public void close() {
    cleanable.clean();
  }
}

////////////////////////

// 잘 짜인 클라이언트 코드
public class Adult {
  public static void main(String[] args) {
    try( Room myRoom = new Room(7) ){
      System.out.println("안녕~");
    }
    // **NOTE: '안녕~'을 출력 후, 이어서 '방청소'를 출력한다
  }
}

// 결코 방청소를 하지 않는 잘못된 코드
public class Teenager {
  public static void main(String[] args) {
    new Room(99);
    System.out.println("아무렴");
    // **NOTE: '방청소'는 한번도 출력되지 않는다
  }
}

9) try-finally 보다는 try-with-resources 를 사용하라

전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다

코드 9-1 try-finally : 더 이상 자원을 회수하는 최선의 방책이 아니다

1
2
3
4
5
6
7
8
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 방식은 너무 지저분하다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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();
  }
}

자바 7에서 try-with-resources 구문을 제공하며 문제 해결

  • 이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해 두어야 한다
  • 자바 라이브러리와 서드파티 라이브러리들은 이미 구현 또는 확장해 두었다
  • 닫아야 하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable 을 반드시 구현해라

코드 9-3 try-with-resources : 자원을 회수하는 최선책

1
2
3
4
5
6
static String firstLineOfFile(String path) throws IOException {
  try( BufferedReader br = new BufferedReader(new FileReader(path)) ){
    return br.readLine();
  }
  // AutoCloseable 가 구현되어 있어서 close() 가 필요 없다
}

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

1
2
3
4
5
6
7
8
9
10
11
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);
  }
  // AutoCloseable 가 구현되어 있어서 close() 가 필요 없다
}

코드 9-3 try-with-resources 를 catch 절과 함께 쓰는 모습

1
2
3
4
5
6
7
static String firstLineOfFile(String path, String defaultVal) {
  try( BufferedReader br = new BufferedReader(new FileReader(path)) ){
    return br.readLine();
  } catch( IOException e ){
    return defaultVal;
  }
}

 
 

끝!   읽어주셔서 감사합니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.