2017년 2월, '이펙티브 자바 2판’을 공부하고 정리한 내용들을 블로그에 올리지 못했는데, 3판이 나왔다는 얘기를 듣고 다시 공부하면서 나를 위해 블로그에 정리하여 남긴다.
규칙 1. 생성자 대신 정적 팩터리 메서드를 고려하라
정적 팩터리 메서드(static factory method)란 클래스의 인스턴스를 반환하는 단순한 static method를 말한다. 팩토리 메서드 패턴과 다르다.
Tip
|
객체? 인스턴스? 인스턴스화?
비슷한 개념이지만 정확히 구별하면 인스턴스(instance)가 객체(object)보다 큰 의미이다. 객체는 어떤 클래스를 사용해서 만들어진 것을 의미한다. 그리고 그 객체가 메모리에 할당되어 실제 메모리를 차지하는 것을 인스턴스라고 한다. 아래 코드에서 객체와 인스턴스를 구별해보자.
|
-
public 생성자
public Boolean(String str) { this.value = "true".equalsIgnoreCase(str); }
TipNPE를 피하는 방법위 코드를 보면
str
의 equalsIgnoreCase 가 아닌 문자 이터럴의 equalsIgnoreCase 를 사용한다. 이와 같은 방법은str
이 null 일 경우 발생할 NPE(NullPointException)를 피할 수 있다. -
정적 팩터리 메서드
public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
Note
|
boxing 과 unboxing
Java에서 primitive type과 wrapper class는 서로 boxing/unboxing이 가능하다.
|
정적 팩터리 메서드 장점
java.utils.Collections
클래스에서 정적 팩터리 메서드의 장점이 많이 보여 코드의 일부를 추가하였다.
이름을 가질 수 있다
public static final <K,V> Map<K,V> emptyMap() { ... }
생성자와 달리 반환되는 객체의 특성을 잘 설명한 메서드명을 통해 이해하기 쉬운 코드를 작성할 수 있다.
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다
public class Collections {
public static final Map EMPTY_MAP = new EmptyMap<>();
// static factory method
public static final <K,V> Map<K,V> emptyMap() {
return (Map<K,V>) EMPTY_MAP;
}
}
인스턴스를 미리 만들어 놓거나 이미 만든 인스턴스를 캐싱하여 재사용하면서 불필요한 인스턴스화를 피할 수 있다.
(특히 인스턴스화 비용이 큰) 동일한 객체가 요청되는 일이 잦을 때 적용하면 성능을 크게 개선할 수 있다.
이 기법을 활용한 좋은 사례로 Boolean.value(boolean)
를 둘 수 있으며 Flyweight 패턴과 유사하다.
인스턴스 통제(instance-controlled) 클래스
반복적인 요청에도 같은 인스터스를 반환하는 정적 팩터리 방식의 클래스는 어떤 시점에 어떤 객체가 얼마나 존재할지를 정밀하게 제어할 수 있다. 인스턴스 통제는 플라이웨이트 패턴의 근간이 되며, enum은 인스턴스가 하나만 만들어짐을 보장한다. 이런 클래스를 인스턴스 통제 클래스 라고 부른다. 인스턴스를 통제하는 이유는 아래와 같다.
-
Singleton pattern 적용 가능
-
Non-instantiable class 생성 가능
Example. Utility classpublic class UtilityClass { private UtilityClass() { throw new AssertionError(); } }
-
public class Complex { private final double re; private final double im; private Complex(double re, double im) { this.re = re; this.im = im; } public static Complex valueOf(double re, double im) { return new Complex(re, im); } }
-
enum이 이 기법을 사용
-
equals()
대신==
연산자 사용 가능
-
자신의 인스턴스만 반환하는 생성자와는 달리, 서브타입 객체도 반환 가능하다
이러한 유연성을 응용하면 구현 세부사항을 감출 수 있으므로 아주 간결한 API가 가능하다. 인터페이스 기반 프레임워크(interface-based framework) 구현에 핵심 기술로, 이 프레임워크에서 인터페이스는 정적 팩터리 메서드의 반환값 자료형으로 이용된다.
public class Collections {
public static final Map EMPTY_MAP = new EmptyMap<>();
public static final <K,V> Map<K,V> emptyMap() {
return (Map<K,V>) EMPTY_MAP;
}
private static class EmptyMap<K,V> extends AbstractMap<K,V> implements Serializable { ... }
}
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다
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
클래스를 보면 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.
사용자는 어떤 것이 반환되던지 알 필요가 없으며, 단지 EnumSet
의 하위 클래스를 반환해주기만 하면 된다.
정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다
TODO: 이 부분은 읽어도 모르겠다.. 다음번 다시 읽을 때 추가로 정리해야겠다.
제네릭 클래스의 인스턴스를 생성하는 코드를 간결하게 해준다.
정적 팩토리 메서드를 사용하면 컴파일러가 타입 추론(type inference)으로 제네릭 클래스의 인스턴스화를 간결하게 해준다.
// before
Map<String, List<String>> m = new HashMap<String, List<String>>();
// after: >= 1.6
Map<String, List<String>> m = HahsMap.newInstance();
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
하지만, jdk7에서 제공된 다이아몬드 연산자를 사용하면 아래와 작성할 수 있다.
Map<String, List<String>> m = new HashMap<>();
더이상 장점이라 할 수 없으므로 3판에서는 이 내용이 제거된 것 같다.
정적 팩터리 메서드 단점
-
정적 팩터리 메서드만 제공하는 클래스는 public 이나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다.
어찌보면 상속(inheritance)보다 합성(composition)을 사용하도록 유도하므로 장점으로 받아들일 수도 있다.
// 정적 팩터리 메서드만 가진 클래스 public class Collections { Collections() {} } // 상속을 통한 하위 클래스 public class CustomCollections extends Collections { public CustomCollections() { super(); // 불가능 } } // "Favor object composition over class inheritance" public class CustomCollections { private Collections collections; }
Tip언제 상속? 합성?-
상속(inheritance)을 사용하는 경우:
is-a
관계 -
합성(composition)을 사용하는 경우:
has-a
관계
-
-
개발자가 찾기 어렵다.
생성자와는 달리 정적 팩터리 메서드는 다른 메서드와 섞여 잘 구분되지 않고 어떤 정적 팩터리 메서드가 있는지 개발자가 알아야한다. 대안으로 흔히 사용되는 네이밍을 통해 구별하기 쉽게 할 수 있다.
Table 1. 정적 팩터리 메서드에 흔히 사용하는 네이밍 Naming Description from
인자를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
of
여러 인자를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf
from
과of
의 더 자세한 버전, 자신의 매개변수와 같은 값을 갖는 인스턴스를 반환BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
getInstance or instance
매개변수에 맞는 인스턴스 반환. 싱글톤인 경우 하나의 인스턴스 반환.
StackWalker luke = StackWalker.getInstance(options);
newInstance or create
매번 새로운 인스턴스를 반환하는 것을 보장
Object newArray = Array.newInstance(classObject, arrayLen);
getType
getInstance
와 유사하나 팩토리 메서드가 다른 클래스에 있을 때 사용FileStore fs = Files.getFileStore(path);
newType
newInstance
와 유사하나 팩토리 메서드가 다른 클래스에 있을 때 사용BufferedReader br = Files.newBufferedReader(path);
type
getType 과 newType 의 간결한 버전
List<Item> items = Collections.list();
02
Yeongjun Kim <opid911@gmail.com> :revdate: 2019-09-27 :toc: :page-draft:
규칙 2. 생성자에 매개변수가 많다면 빌더를 고려하라
이 장에서는 선택적 인자가 많을 때 객체 생성하는 방법들의 특징을 살펴보고, 왜 빌더Builder 를 고려해야하는지 얘기한다.
-
점층적 생성자 패턴telescoping constructor pattern
-
자바빈즈 패턴JavaBeans pattern
-
빌더 패턴Builder pattern
점층적 생성자 패턴
기본적으로 필수 인자만 받는 생성자를 정의하고, 선택적 인자를 받는 생성자를 추가하는 방법이다. 객체를 생성할 때 인자 갯수에 맞는 생성자를 골라 호출해야한다.
public class Person {
private final String name; // 필수
private final int age; // 필수
private final String mail;
private final String city;
private final String state;
public Person(String name, int age) {
this(name, age, "");
}
public Person(String name, int age, String mail) {
this(name, age, mail, "");
}
public Person(String name, int age, String mail, String city) {
this(name, age, mail, city, "");
}
public Person(String name, int age, String mail, String city, String state) {
this.name = name;
this.age = age;
this.mail = mail;
this.city = city;
this.state = state;
}
}
-
설정할 필요가 없는 필드에도 인자를 전달해야 해야 한다.
-
인자 수가 늘어날수록 가독성이 떨어진다.
자바빈즈 패턴
인자 없는 생성자로 객체를 할당받고, setter를 통해 나머지 값들을 설정하는 방법이다. 객체 생성도 쉽고 위 방법보다 가독성이 좋다.
public class Person {
private String name;
private int age;
private String mail;
private String city;
private String state;
public Person() {}
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
....
}
Person me = new Person();
me.setName("yeongjun.kim");
me.setAge(27);
-
한번에 객체 생성을 끝낼 수 없으므로, 객체 일관성이 일시작으로 깨질 수 있다.
-
변경 불가능 클래스를 만들 수 없다. 해결하기 위해서 추가 구현할 코드가 많아진다.
-
setter를 추가로 만들어줘야한다. (내생각)
Tipsetter 생성방법보통 IDEA에 getter/setter를 자동으로 추가해주는 기능이 존재한다. 혹은 lombok의
@Setter
어노테이션을 활용할 수 있다.
빌더 패턴
필수 인자들을 생성자(또는 정적 팩터리 메서드)에 전달하여 빌더 객체를 만들고, 선택적 인자들을 추가한 뒤, 마지막에 build()
를 호출하여 Immutable 객체를 만드는 방법이다.
public class Person {
private final String name;
private final int age;
private final String mail;
private final String city;
private final String state;
// 빌더 객체
public static class Builder {
// 필수 인자
private final String name;
private final String age;
// 선택적 인자 - 기본값으로 초기화
private final String mail = "";
private final String city = "";
private final String state = "";
public Builder(String of, int age) {
this.name = name;
this.age = age;
}
public Builder mail(String mail) {
this.mail = mail;
return this;
}
public Builder city(String city) {
this.city = city;
return this;
}
public Builder state(String state) {
this.state = state;
return this;
}
public Person build() {
return new Person(this);
}
}
private Person(Builder builder) {
this.name = name;
this.age = age;
this.mail = mail;
this.city = city;
this.state = state;
}
}
Person me = Person.Builder("yeongjun.kim", 27)
.mail("opid911@gmail.com")
.build();
특징
-
빌더 클래스(Builder)는 빌더가 만드는 객체 클래스(Person)의 정적 맴버 클래스로 정의한다({#item22}[규칙 22]).
public class Person { public static class Builder { ... } }
-
불변식을 적용할 수 있으며,
build()
에서 불변식이 위반되었는지 검사할 수 있다.public class Person { public static class Builder { ... public Person build() { Person result = new Person(this); if(/* result의 값 검사 */) { throw new IllegalStateException(/* 위반 원인 */); } return result; } } }
-
빌더 객체에서 실제 객체로 인자가 복사된 다음에 불변식들을 검사할 수 있다는 것, 그리고 그 불변식을 빌더 객체의 필드가 아니라 실제 객체의 필드를 두고 검사할 수 있다는 것은 중요하다({#item39}[규칙 39]).
-
불변식을 위반한 경우,
build()
는IllegalStateException
을 던져야 한다({#item60}[규칙 60]). -
예외 객체를 살펴보면 어떤 불변식을 위반했는지 알아낼 수도 있어야 한다({#item63}[규칙 63]).
cf. 불변식을 강제하는 방법
-
불변식이 적용될 값 전부를 인자로 받는 setter를 정의하는 방법.
-
setter는 불변식이 만족하지 않으면 *IllegalArgumentException*을 던짐.
-
build()가 호출되기 전에 불변식을 깨뜨리는 인자가 전달되었다는 것을 신속하게 알 수 있는 장점.
public class Person {
...
public static class Builder {
public Builder setNameAndAge(String name, int ate) {
if(name == null) {
throw new IllegalArgumentException();
}
return this;
}
...
public Person build() {
return new Person(this);
}
}
...
}
-
메서드마다 하나씩, 필요한 만큼 varargs 인자를 받을 수 있다.
public class Person {
public static class Builder {
public Builder names(String... names) {
this.names = names;
return this;
}
public Builder foramily(String... names) {
this.farther = names[0];
this.marther = names[1];
return this;
}
}
...
}
-
유연하다. (e.g. 객체가 만들어질 때마다 자동적으로 증가하는 일련번호 같은 것을 채울 수 있다)
-
인자가 설정된 빌더는 훌륭한 [Abstract Factory][dp-abstract-factory]다. JDK1.5 이상을 사용하는 경우, 제네릭 자료형 하나면 어떤 자료형의 객체를 만드는 빌더냐의 관계 없이 모든 빌더에 적용할 수 있다.
public interface Builder<T> {
public T build();
}
public class Person {
public static class Builder implements Builder<Person> {
...
public Person build() {
return new Person(this);
}
}
}
**e.g.** *Code at package `java.util.stream`*
Stream.builder().add(1).add(2).add(3).build();
-
빌더 객체를 인자로 받는 메서드는 보통 *한정적 와일드카드 자료형~bounded wildcard type~*을 통해 인자의 자료형을 제한한다([규칙 28](#items28)).
Tree buildTree(Builder<? extends Node> nodeBuilder) {...}
-
자바가 제공하는 추상적 팩토리로는 Class 객체가 있으며, 이 객체의 newInstance() 가 build 메서드 구실을 한다.
**하지만,** newInstance()는 항상 무인자 생성자를 호출하려 하는데, 문제는 그런 생성자가 없을 수도 있다는 것. TO-DO
문제점
-
빌더 객체를 만드는 오버헤드가 문제가 될 수 있다(성능이 중요한 상황). 그러니 인자 갯수가 통제할 수 없을 정도로 많아지만 빌더 패턴을 적용하자.
요약
빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특시 대부분의 인자가 선택적 인자인 상황에 유용하다.
Tip
|
lombok 활용하여 빌더 만들기
|
Unresolved directive in 01-09.adoc - include::03.adoc[]
규칙 4. 인스턴스화를 막으려거든 private 생성자를 사용하라
생성자를 생략하면 컴파일러는 자동으로 인자없는 public
생성자를 만든다.
이 생성자를 직접 선언하고 접근자를 private
바꾸어 인스턴스화를 막을 수 있다.
또한, 구현부에 AssertionError()
를 던져 혹시나 클래스내에서 생성자를 사용할 경우를 방지한다.
public class Utils {
private Utils() {
throw new AssertionError();
}
}
Note
|
유틸리티성 클래스는 인스턴스를 만들 필요가 없기 때문에 static 메서드와 상수만 가지게 된다. lombok에서 `@UtilityClass`는 어노테이션을 붙힌 클래스 내에 모든 메서드를 static으로, 필드를 상수로 만든다. 그리고 생성자를 private 으로 변경해 인스턴스화를 막는다.
|
규칙 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
Unresolved directive in 01-09.adoc - include::06.adoc[]
규칙 7. 다 쓴 객체 참조는 해제하라
아래 예제는 메모리 누수를 일으키는 코드이다.
public class Stack {
private Object[] element = new Object[16];
private int size = 0;
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
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);
}
}
}
스택이 커졌다가 줄어들 때, index 값이 size보다 큰 곳에 있는 요소들(쓰레기 값)은 GC가 처리하지 못한다. 스택이 그런 객체에 대한 만기 참조obsolete reference를 제거하지 않기 때문이다. 만기 참조란 다시 이용되지 않을 참조reference를 말한다.
자동적으로 쓰레기 객체를 수집하는 언어에서 발생하는 메모리 누수 문제(≒ 의도치 않은 객체 보유~unintentional object retention~)는 찾아내기 어렵다.
해결방안
만기 참조를 제거하는 가장 좋은 방법은, 해당 참조가 보관된 변수의 유효범위socpe를 최대한 좁게 만들어 벗어나게 두는 것이다([규칙 45](#item45)).
위 예제 Stack과 같이 자체적으로 메모리는 관리하는 경우에는, 쓸 일이 없는 객체 참조는 반드시 null로 바꿔준다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
흔히 메모리 누수가 발견되는 곳
-
자체적으로 관리하는 메모리가 있는 클래스
-
캐시cache: 객체 참조를 캐시 안에 넣어 놓고 잊어버리는 일이 많기 때문. (수명이 키에 대한 외부 참조의 수명에 따라 결정되는 상황에는 WeakHashMap 활용)
-
리스너listener등의 역호출자callback - 콜백을 명시적으로 제거하지 않을 경우, 적절한 조치를 취하기 전까지 메모리는 점유된 상태. 해결방안으로 콜백에 대한 약한 참조~weak reference~만 저장하는 것(WeakHashMap)
Unresolved directive in 01-09.adoc - include::08.adoc[]