02장 객체 생성과 파괴 (ITEM 01 ~ 09)

 

ITEM 01 생성자 대신 정적 팩터리 메서드를 고려하라.

 

클래스 인스턴스화는 생성자뿐만 아니라 정적 팩터리 메서드를 통해서도 제공할 수 있다.

생성자 대신 정적 팩터리 메서드를 사용하면 얻을 수 있는 장점은 다음과 같다.

  • 메서드명을 가질 수 있어 어떤 성격의 객체를 생성해주는것인지 명시될 수 있다.
  • 호출될 때마다 생성자 처럼 인스턴스를 새로 생성하지 않아도 되므로 싱글턴 패턴이나 캐싱을 구현할 수 있다.
  • 반환 타입의 하위 타입 객체를 반환할 수 있다.
  • 클라이언트가 new 연산자를 통해 인스턴스화를 직접하지 않으므로 클라이언트는 인스턴스화되는 클래스와의 결합도가 낮아진다.

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

 

내 생각) 이는 하위 클래스가 같은 패키지에 존재하면 상위 디폴트 생성자에 protected 접근제어자를, 이외에 경우 public 접근제어자를 선언하여 해결하면 될듯하다.

 

결론적으로 정적 팩터리 메서드로 제공하는게 더 유리한경우가 많으니 정적 팩터리 메서드를 애용하자.

 

 

ITEM 02 생성자에 매개변수가 많다면 빌더를 고려하라.

 

책에는 다음의 경우 빌더를 고려하라고 명시되어있다.

 

(1) 정적 팩터리 메서드로 넘기는 인수양이 많은 경우

(2) 클라이언트 코드에서 setter로 객체의 상태 값의 일관성을 맞추는 경우.

 

(2) 번 경우는 상태값 들을 final 한정자로 선언할 수 없다는 단점이 존재하고 setter 코드가 하나라도 빠지면 객체의 상태값 일관성이 깨질 수 있다.

 

내 생각) 이는 빌더를 통해 해결할 수 있지만 클라이언트 코드에서 빌더를 사용하면 인스턴스화 하고자하는 클래스의 프로퍼티들이 노출되는것이므로 캡슐화가 깨져버린다. 정적 팩터리 메서드는 상태값들을 final 한정자로 선언을 할 수 있음과 동시에 상태값 set 외에 추가적인 처리가 이루어 질수 있기 때문에 이에 대해 클라이언트로 부터 감춰 캡슐화가 깨지지 않는다. 그래서 나는 빌더보단 정적 팩터리 메서드를 제공하는것을 선호한다. 하지만 문제는 (1) 번 경우 인수양이 많다면 어떤  값이 어떤 프로퍼티에 set되는지 알기 힘들다는 것이다. 이는 IDE의 도움을 받으면 인수자리에 프로퍼티명이 표시되기 때문에 어느정도 극복할 수 있다고 생각된다. (IDE말고 메모장에 개발하는사람은 없겠지...)

 

 

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

 

책에서는 싱글턴임을 보증하기 위해 원소가 하나뿐인 열거 타입으로 싱글턴을 만드는것을 가장 추천한다. 더 간결하고, 리플렉션 공격에도 제2의 인스턴스화가 되는것을 방지해준다.

 

내 생각) 열거형으로 사용하면 열거형 존재 목적을 해치는게 아닌가 싶다.

열거형을 통한 싱글턴 패턴이 개발팀 내에 공유가 되지 않는다면 코드 가독성을 위해서라도 일반적인 private 생성자와 정적 팩터리 메서드로 싱글패턴을 구현하는게 나을듯 싶다.

 

 

ITEM 04 인스턴스화를 막으려거든 private 생성자를 사용하라.

 

클래스에 private 생성자만 존재한다면 당연히 인스턴스화를 막을 수 있다. 뿐만 아니라 상속을 불가능하게 하는 효과가 있다. 하위 클래스에서는 인스턴스화시 상위 클래스의 생성자를 호출해야하지만 상위 클래스 생성자는 private로 감춰져있기 때문에 상속이 불가능하기 때문이다.

 

 

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

 

싱글턴 패턴은 내부 프로퍼티 자원 생성이 내부에 명시되어 있으며  final 한정자로 선언되어있어 불변하다.  즉 input값에 따라 동작이 달라지는 클래스에는 적합하지 않다. 이런경우 단순하게 의존 객체 주입 패턴을 사용하면된다. DI(의존성 주입)은 유연성을 높이는것 뿐만아니라 가장 중요한 객체간 의존성도 낮춰준다.

 

 

ITEM 06 불필요한 객체 생성을 피하라.

 

불필요한 객체 생성은 두가지 예가 존재한다.

 

 

1. 상수풀을 사용하지 않는 경우

String bad1 = new String("BAD"); //(1)
String bad2 = new String("BAD"); //(2)
String good1 = "GOOD";    //(3)
String good2 = "GOOD";    //(4)

 

(1), (2)와 같이 new String()를 사용한다면 호출될 때마다 중복되는 문자열을 넘기기더라도 새로운 String 인스턴스를 만든다. 즉 (1)과 (2)는 아이디가 다르다.

 

(3), (4)와 같이 문자열 리터럴을 사용하면 String 상수 풀에 저장되므로 중복되는 문자일경우 재사용되어 효율적이다.

 

 

2. 기본타입과 박싱된 기본타입을 혼용한경우

 

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

 

위의 코드에서 i 변수가 sum에 더 해질 때 마다 오토박싱되어 성능에 영향을 끼칠 수 있다. sum 타입을 long 으로 변경해준다면 6.3 에서 0.59초로 빨라진다.(저자 컴퓨터)

 

 

ITEM 07 다 쓴 객체는 참조를 해제하라.

 

Java는 가비지 컬렉터가 참조하지 않는 객체는 수거해준다. 반대로 참조중인 객체는 수거해가지 않는다. 즉 개발자가 실수로 다쓴 객체지만 참조를 해제하지 않으면 결국 미래에는 메모리 누수가 일어날 수 있다.

 

이런 경우는 자료구조의 Stack으로 예로들 수 있다.

Stack을 한번 pop하여 head가 바닥방향으로 한칸 이동한경우에 pop 로직내에 pop된 item을 stack이 참조하지 않도록 null을 할당해야한다. 만약 null을 할당하지 않는다면 추후 이 아이템이 외부에서 사용하지 않더라도 stack이 계속 참조하고있어 메모리 누수가 일어날 수 있다.

 

 

ITEM 08 finalizer와 cleaner 사용을 피하라.

 

객체 소멸자인 finalizer와 cleaner는 예측할 수 없고 느리고 심각한 성능 문제를 동반한다. 또한 보안 문제도 일으킬수 있다.

 

내 생각) 사실 객체 소멸자인 finalizer와 cleaner를 사용해본적이 없다. 미래에 불가피하게 사용할 경우 그때 다시 깊게 읽어보자.

 

 

ITEM 09 try-finally 보다는 try-with-resources 를 사용하라.

 

회수해야 하는 자원을 다룰 때는 꼭 try-with-resources 를 필수로 사용하자. 예외는 없다. 코드 가독성도 올라가고 쉽고 안전하게 자원도 회수할 수 있다.

 

 

참고 자료

 

Effective Java 3rd

 

 

'📚 Book > Effective Java' 카테고리의 다른 글

03 모든 객체의 공통 메서드 (ITEM 10 ~ 14)  (0) 2021.08.04