02. 객체 생성과 파괴
#
주요 포인트- 올바른 객체 생성 시기와 생성 법
- 불필요한 객체 생성을 피하는 법
- 제때 파괴됨을 보장하는 법
- 파괴 전 정리 작업 관리하는 법
#
Item 01. 생성자 대신 정적 팩터리 메서드를 고려하라- 클래스 생성자와는 별도로, 정적 팩터리 메소드로 객체 생성을 관리 가능
#
일반 생성자보다 정적 팩터리 메소드가 좋은 이유#
1) 이름을 가질 수 있음- 생성되는 의미를 더욱 상세하게 묘사할 수 있음
BigInteger(int, int, Random)
vsBigInteger.probablePrime()
- 후자가 설명력이 더 강함
#
2) 호출될때 마다 인스턴스를 새로 생성하지 않아도 됨- 불변 클래스는 인턴스를 미리 만드는 캐싱 전략으로 재활용 가능
Boolean(boolean)
vsBoolean.valueOf(boolean)
- 후자는 구현이 미리 만들어 놓은 객체를 리턴
#
3) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력을 가짐- 개발자가 해당 API를 통해 하위 타입이 무엇인지에 대해 알아야 할 내용을 줄일 수 있음
#
4) 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있음- 예시:
EnumSet
클래스는 정적 팩터리만 제공- 원소 수에따라
RegularEnumSet
혹은JumboEnumSet
중 하나를 반환
- 원소 수에따라
#
5) 정적 팩터리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 됨- 클라이언트를 구현체 관리로 부터 분리
- 지원 가능한 구현체만 반환이 가능해짐
#
정적 팩터리 메소드의 단점#
1) 정적 팩터리 메소드만 구성된 클래스는 하위 클래스를 만들 수 없음- 상속을 하려면 public, protected 생성자가 필요함
#
2) 정적 팩터리 메서드는 프로그래머가 찾기 어려움- API 문서를 잘 써놓고 메서드의 이름도 널리 알려진 규약을 따를 수 밖에 없음
#
정적 팩터리 메소드에 쓰이는 잘알려진 컨벤션from
: 매개 변수 하나를 받아 해당 타입의 인스턴스 반환of
: 매개 변수 여러개를 받아 적합한 타입의 인스턴스 반환valueOf
: from과 of의 더 자세한 버전instance
orgetInstance
: 같은 인스턴스임을 보장하진 않음create
ornewInstance
: 매번 새로운 인스턴스임을 보장getType
: 구현된 클래스 본인이 아닌, 다른 클래스의 인스턴스를 반환newType
: newInstance + getType 특징을 가짐type
: getType, newType 의 간결 버전
#
01 핵심정리- 반드시 쓰라는게 아니고 장단점을 파악해 쓰자
- 무작정 public 생성자 보단 정적 팩터리 메서드를 고려하는 습관을 가져보자
#
Item 02. 생성자에 매개변수가 많다면 빌더를 고려하라- 매개변수가 많으면 정적 팩터리와 생성자가 관리하기 힘듦
- 대안1) 점층적 생성자 패턴 (Telescoping Constructor Pattern)
- 필수 매개변수, 필수 매개변수 + 선택 매개변수 하나, ... 이런식으로 모든 경우의수 생성자를 만듦
- 생성자가 너무 많아짐 -> 가독성 저하
- 대안2) JavaBeans Pattern
- 매개 변수가 없는 생성자로 객체를 만들고, 이후 setter로 매개변수 설정
- 객체를 불변으로 만들 수 없음
- 객체가 세팅될 때까지의 일관성(consistency)가 무너진 상태임
- 고로 매개변수가 많으면 Builder Pattern 을 사용하자
#
빌더 패턴 특징- 필수 매개 변수를 지정할 수 있음 (생성자 패턴의 장점)
- 선택적 매개 변수를 생성단계에서 지정할 수 있어 일관성(consistency)가 보장 (자바빈즈 패턴 단점 보완)
- 사용자 입장에서 가독성이 점층적 생서자 패턴보다 좋아짐
- 계층적으로 설계된 클래스와 잘 어울림
#
빌더 패턴 단점- 빌더부터 만들어야하는 코스트가 발생 (lombok
@Builder
로 보완 가능) - 매개변수가 작으면 그냥 점층적 생성자가 나음
#
02 핵심정리- 매개 변수가 많다면 빌더 패턴을 권장
#
Item 03. private 생성자나 열거 타입으로 싱글턴임을 보증하라- 싱글턴 패턴
- 인스턴스를 오직 하나만 생성할 수 있는 클래스
- 상태가 없는 객체나 시스템 설계상 하나의 객체만 존재해야하는 경우 사용
- 하지만 싱글턴은 이를 사용하는 클래스가 테스트에 애를 먹을 수 있음
#
싱글턴 만드는 방식- 공통적으로 private으로 생성자를 감춤
#
1) public static 멤버가 final 필드인 방식- private 생성자라 public static final인 Elvis 객체 INSTANCE 가 하나임이 보장됨
- 장점
- 간결
- 싱글턴임을 API로 바로 알 수 있음
- 단점
- 리플렉션 가능, 생성 시 예외로 방어해야 함
- 직렬화 시 추가 구현
#
2) 정적 팩터리 메서드를 public static 멤버로 제공- 장점
- 변경에 용이
- 싱글턴으로 사용하고 있지만, getInstance 부분만 수정하면 싱글턴 아닌 다른 무언가로 바꾸기 용이
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있음 (item 30)
- 정적 팩터리의 메서드 참조를 supplier로 사용할 수 있음 (item 43, 44)
- 변경에 용이
- 단점
- 리플렉션 가능, 생성 시 예외로 방어해야 함
- 직렬화 시 추가 구현
#
위의 방식의 직렬화- 위 방식의 싱글턴 클래스는
Serializable
구현 선언만으로는 직렬화 할 수 없음 - 모든 필드를 transient라 선언 후 readResolve 메서드를 제공해야함 (item 89)
- 이렇게 안하면 역직렬화 시, 새 객체가 만들어짐
#
3) enum으로 싱글턴 만들기- 가장 권장되는 방식
- 장점
- 간결
- 추가 노력없이 직렬화 가능
- 리플렉션 공격 방어
- 단점
- 상속 불가
#
Item 04. 인스턴스화를 막으려거든 private 생성자를 사용하라- private으로 선언하고 밖에서 호출 불가
- 그리고 생성자 내부에서 Assertion을 던지면 클래스 내부에서 인스턴스화도 방지
#
Item 05. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라- 맞춤법 검사기 - 사전의 예로 설명하고 있음
- 맞춤법 검사기는 사전을 의존함
- 하지만 싱글턴(item 3)이나 정적 유틸리티 클래스(item 4)로 사전을 맞춤법 검사기에 사용하기엔 사전이 여러개인 경우 부적합함
- 클래스에서 의존하는 다른 클래스가 여러 타입일 경우, 생성자로 넘겨줘서 의존하는 방식
맞춤법 검사기를 의존 객체주입으로 사용한 예
- 이의 변형으로,
Factory Method Pattern
이 있음- 팩터리를 생성자로 넘겨줘서 의존하는 방식
- 팩터리는 호출마다 새로운 인스턴스를 만드는 객체
- 예제로 Java 8의
Supplier<T>
인터페이스 가 있음
- 의존 객체 주입의 장점
- 테스트 용이성
- 유연성
- 단점
- 의존성이 많다면 코드가 어지러워짐
#
05 핵심정리클래스가 내부적으로 하나 이상 존재하며, 해당 클래스가 다른 동작에 영향을 준다면 의존 객체 주입을 사용하자.
#
Item 06. 불필요한 객체 생성을 피하라성능적으로
같은 기능의 객체를 여러번 생성하는 것 보단, 하나를 만들어 재사용하는 편이 좋음
Boolean(String)
대신Boolean.valueOf(String)
팩터리 메서드 사용- 생성 비용이 비싼 객체도 캐싱해서 사용
- 객체가 불변이라면 재사용해도 안정적
- 기본타입 사용을 권하고, 의도치 않은 오토 박싱은 피하자
- 오토 박싱이란 primitive type을 자동적으로 Wrapper class 로 변환해주는 것
- 객체 생성은 비싸니 피해야 한다는 게 아니다
- item 50. 방어적 복사와는 대조적인 내용임
#
Item 07. 다 쓴 객체 참조를 해제하라- 가비지 컬렉션 (GC) 을 갖춘 Java라도 메모리 관리는 유념해야함
- 메모리 누수는 GC 활동과 메모리 사용량이 늘어나 성능저하를 불러 일으킴
- 심할땐 디스크 페이징이나 OutOfMemoryError (OOM) 를 일으켜 앱이 종료됨
- 예제에서 문제가 있는
pop()
에서 메모리 누수가 발생함- elements의 참조를 따로 해제 하지 않아 pop이 되었지만 해당 Object는 GC의 대상이 되지 않음
- 개선된 pop에서는 null처리를 하여 해결
- 강박적으로 다쓴 객체를 null처리할 필요는 없고, 위 처럼 예외적인 경우에만 null처리 하면됨
#
메모리 누수 주의 케이스- 메모리를 직접 관리하는 경우
- null 처리
- 캐시
- WeakHashMap 을 사용
- key에 해당하는 value 객체가 존재하지 않게 될 경우, 자동으로 key에서 제거 GC 대상이 됨
- WeakHashMap 을 사용
- listener or callback
- 사용자가 콜백을 등록만하고 해제하지 않음
- 콜백을 weak reference로 등록
#
07 핵심정리- 철저한 코드리뷰 혹은 힙 프로파일러 같은 디버깅 도구로 예방가능
- 메모리 누수 관리에 대해 예방법을 익혀두는 것이 매우 중요
#
Item 08. finalizer 와 cleaner 사용을 피하라- 자바에서 제공하는 객체 소멸자, finalizer 와 cleaner
- finalizer 는 예측할 수 없고 상황에 따라 위험할 수 있어 일반적으로 불필요
- 기본적으로 쓰지말기
- cleaner 도 finalizer 대안으로 java 9 에 나왔지만 예측할 수 없고, 느리고, 불필요
- c++ 의 destructor(파괴자) 와는 다른 개념으로
- 자원 해제를 활용하려면 try-with-resources 로 해결하자 (Item 09)
- finalizer, cleaner 둘다 언제 제때 실행해야하는지 보장할 수 없음
- AutoCloseable 를 구현 + try-with-resources 활용으로 자원 해제를 보장해줄 수 있음
#
08 핵심정리- clenaer, finalizer는 안정망 역할 or 중요하지 않은 네이티브 자원 회수 용도로만 활용하자~
- 물론 그냥 안쓰는게 최고
#
Item 09. try-finally 보단 try-with-resources를 사용하라- 기존 자원해제는 try-finally로 활용하는 추세였지만 Java 7에서 try-with-resources 를 사용할 수 있음
- try-finally에서는 finally 에서 예외가 발생하면 기존 예외를 잡아먹는 문제가 있음
- AutoCloseable 을 통한 try-with-resources 구현은 try의 예외 부분을 잡아먹지 않는다