$1. 생성자 대신 static 팩토리 메소드 사용을 고려하자.
클래스의 인스턴스를 생성하는 일반적인 방법은 public 생성자를 제공하는 것이지만, 보다 유용한 방법인 클래스에 public static 팩토리를 두는 것이다.
[java]
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
상기와 같은 static 팩토리 메소드의 장단점은 다음과 같다.
1. static 팩토리 메소드의 한 가지 장점은, 생성자와 달리 자기 나름의 이름을 가질 수 있다.
자바 클래스는 생성자 이름이 클래스 이름과 같은 하나의 생성자만 가질 수 있다. 따라서 개발자들은 매개변수를 이용하여 여러 생성자를 만들곤 한다. 하지만 이는 좋은 방법이 아니다. 이와 같이 하나의 클래스에 동일한 시그너처를 갖는 여러 개의 생성자가 필요한 경우 생성자 대신 static 팩토리 메소드를 사용하되, 메소드 간의 차이점을 부각시키도록 신중하게 이름을 선정한다.
2. static 팩토리 메소드의 두 번째 장점은, 생성자와 달리 호출될 때마다 매번 새로운 객체를 생성할 필요가 없다.
동일한 객체가 자주 사용될 때, 특히 객체 생성시 자원이나 시간이 많이 든다면 이 방법은 프로그램 성능을 크게 향상시킬 수 있다.
static 팩토리 메소드는 여러 번 호출되더라도 이미 생성된 동일 객체를 반환할 수 있으므로, 클래스에서는 언제든지 인스턴스들의 존재를 직접 제어 할 수 있다. 인스턴스 제어 클래스라 불리는 이와 같은 클래스를 만드는 이유는 몇 가지가 있다.
인스턴스를 제어하면 싱글톤 또는 인스턴스 생성 불가 클래스로 만들 수 있으며, 또한 불변 클래스에서 두 개의 동일한 인스턴스가 생기지 않도록 해준다.
3. 자신의 클래스 인스턴스만 반환하는 생성자와 달리, static 팩토리 메소드는 자신이 반환하는 타입의 어떤 서브타입 객체도 반환할 수 있다.
접근 범위가 public이 아닌 클래스의 객체를 생성하고 반환할 수 있는 API가 있다. 이 기법은 static 팩토리 메소드의 반환 타입으로 인터페이스를 사용하는 인터페이스 기반 프레임워크에 적합하다.
1.5 버전에 추가된 java.util.EnumSet클래스에는 public 생성자는 없고, static 팩토리 메소드들만 있으며, 주어진 열거형 타입의 크기에 따라 둘 중 하나의 객체를 생성하고 반환한다. 즉 64개 이하의 요소수인 경우 RegularEnumSet 인스턴스를 반환하며, 65개 이상이면 JumboEnumSet 인스턴스를 반환한다.
이처럼 유연한 static 팩토리 메소드는 자바 데이터베이스 연결 API(JDBC)와 같은 서비스 제공자 프레임워크의 근간이 된다. 서비스 제공자 프레임워크(Service provider framework)는 세 가지 핵심 컨포넌트로 구성된다. 첫 번쨰는 제공자가 구현하는 서비스 인터페이스이며, 두 번째는 프레임워크 시스템에서 구현체를 등록하는데 사용하는 제공자 등록 API이며 끝으로, 서비스 인스턴스를 얻기 위해 클라이언트가 사용하는 서비스 접근 API가 있다. 또한 선택가능한 내 번쨰 요소는 서비스 제공자 인터페이스라는 것으로, 제공자가 자신의 서비스 구현체 인스턴스를 생성하기 위해 구현한다.
4. static 팩토리 메소드의 네 번째 장점은, 매개변수화 타입의 인스턴스를 생성하는 코드를 간결하게 해준다.
매개변수화 클래스의 생성자를 호출할 때는 타입 매개변수를 지정해야 한다.
[JAVA]
Map<String, List<String>> m = new HashMap<String, List<String>>();
이와 같이 코딩하면 타입 매개변수가 늘어나는 경우 타이핑할 분량이 많아지고 복잡해져서 매우 힘들어진다. 그러나 static 팩토리를 사용하면 컴파일러가 타입 매개변수를 해결하도록 할 수 있는데 이를 타입 추론이라고 한다.
만일 HashMap에서 다음의 static 팩토리 메소들를 제공한다고 가정해 보자
[JAVA]
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
위에서 선언한 메소드를 다음과 같이 간결한 형태로 사용할 수 있다.
[JAVA]
Map<String, List<String>> m = HashMap.newInstance();
5. static 팩토리 메소드의 가장 큰 단점은, 인스턴스 생성을 위해 static 팩토리 메소드만 갖고 있으면서 public이나 protected 생성자가 없는 클래스의 경우는 서브 클래스를 가질 수 없다.
자바 표준 컬렉션 프레임워크에 구현된 클래스의 어느 것도 서브 클래스를 가질 수 없다.
6. static 팩토리 메소드의 두 번째 단점은, 다른 static 메소드와 쉽게 구별 할 수 없다는 것이다.
static 팩토리 메소드의 공통 명칭 사용 예
- valueOf - 자신의 매개변수와 같은 값을 갖는 인스턴스를 반환한다.
- of - valueOf를 줄인 형태의 이름이며, EnumSet에서 사용한다.
- getInstance - 매개변수에 나타난 인스턴스를 반환하지만, 매개변수와 같은 값을 갖지 않을 수 도 있다. 싱글톤인 경우 getInstance는 매개변수가 없고, 오직하나의 인스턴스만 반환한다.
- newInstance - getInstance와 유사하나 반환되는 각 인스턴스가 서로 다르다.
- getType - getInstance와 유사하나 팩토리 메소드가 다른 클래스에 있을 떄 사용한다.
- newType - newInstance와 유사하나 팩토리 메소드가 다른 클래스에 있을 때 사용한다.
$2. 생성자의 매개변수가 많을 때는 빌더를 고려하자
static 팩토리 메소드와 생성자는 공통적인 제약이 있다. 즉, 선택 가능한 매개변수가 많아질 경우 신축성있게 처리하지 못한다는 것이다.
이를 처리하는 방법중 하나는 텔리스코핑 생성자 패턴을 사용하는 것이다. 하지만 이 방법은 매개변수가 많을 때는 클라이언트 코드 작성이 힘들고 코드의 가독성도 떨어진다.
[JAVA] : 텔리스코핑
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;
}
}
매개변수가 많은 생성자의 두 번째 대안으로 자바빈즈 패턴이 있다. 이 패턴에서는 매개변수가 없는 생성자를 호출해서 객체를 생성한 후 세터 메소드를 호출해서 각각의 필수 필드와 선택 필드 값을 지정한다,
[JAVA] : 자바빈즈 패턴
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() { }
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; }
}
자바빈즈 패턴은 심각한 단점을 갖고 있다. 여러 번의 메소드 호출로 나누어져 인스턴스가 생성되므로, 생성과정을 거치는 동안 자바빈 객체가 일관된 상태를 유지하지 못할 수 있다. 따라서 자바빈즈 패턴은 불변 클래스를 만들 수 있는 가능성을 배제하므로 스레드에서 안전성을 유지하려면 프로그래머의 추가적인 노력이 필요하다.
세번째 방법은 위의 두가지 방법을 결합한 빌더 패턴의 형태로써, 원하는 객체를 바로 생성하는 대신 클라이언트는 모든 필수 매개변수를 갖는 생성자를 호출하여 빌더 객체를 얻고, 빌더 객체의 세터 메소드를 호출하여 필요한 선택 매개변수들의 값을 설정한다. 끝으로 클라이언트는 매개변수가 없는 build 메소드를 호출하여 불변 객체를 생성한다.
[JAVA]
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 carbohydrate = 0;
private int sodium = 0;
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val) { calories = val; return this; }
public Builder fat(int val) { fat = val; return this; }
public Builder carbohydrate(int val) { carbohydrate = val; return this; }
public Builder sodium(int val) { sodium = val; return this; }
public NutritionFacts (Builder builder) {
return new NutritionFacts(this);
}
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
}
생성자 대비 빌더의 또 다른 장점은, 여러개의 가변인자 매개변수를 가질 수 있다는 것이다. 또한 빌더 패턴은 유연성이 좋다. 하나의 빌더는 여러개의 객체를 생성하는데 사용될 수 있으며, 이러한 과정 중에 빌더의 매개변수는 다양하게 조정될 수 있다. 빌더를 사용하면 일부 필드의 값을 자동으로 설정할 수 있으며, 매개변수 값이 설정된 빌더는 훌륭한 추상 팩토리를 만든다. 즉, 클라이언트 코드에서는 그런 빌더를 메소드로 전달하여 그 메소드에서 하나 이상의 객체를 생성하게 할 수 있다.
생성자나 static 팩토리 메소드에서 많은 매개변수를 갖게 될 클래스를 설계할 때는 빌더 패턴이 좋은 선택이다. 특히 선택 매겨변수가 대부분인 경우가 그렇다.
$3. private 생성자나 enum 타입을 사용해서 싱글톤의 특성을 유지하자
싱글톤은 정확히 하나의 인스턴스만 생성되는 클래스이다.
자바 1.5 버전 이전에는 싱글톤을 구현하는 방법이 두 가지 있었으며, 두 방법 모두 생성자를 private으로 하고, 유일한 인스턴스에 접근할 수 있도록 public static 멤버를 외부에 제공한다.
그중 한 가지 방법에서는 다음과 같이 멤버가 final 필드이다.
[JAVA]
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
private 생성자는 딱 한번만 호출되어 public static final 필드인 Elvis.INSTANCE를 초기화한다. 클라이언트는 이것을 변경할 수 있는 방법이 없다. 한 가지 예외가 있다면, 허가된 클라이언트가 AccessibleObject.setAccessible 메소드를 사용해서 private 생성자를 재귀적으로 호출할 수 있다는 것이다.
싱글톤을 구현하는 두 번쨰 방법은 static 팩토리 메소드를 public 멤버로 두는 것이다.
[JAVA]
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
몇 번이 호출되건 Elvis.getInstance에서는 항상 같은 객체 참조를 반환하며, 또 다른 Elvis인스턴스는 절대 생성되지 않는다.
이렇게 구현된 싱글톤 클래스를 직렬화 가능하도록 하려면 어떻게 해야 할까? 우선, 클래스 선언부에 implements Serializable을 추가해야 한다. 그리고 싱글톤을 보장하기 위해 모든 인스턴스 필드를 transient로 선언해야 하며, readResolve 메소드를 추가해야 한다. 그렇지 않으면 직렬화된 인스턴스가 역직렬화 될 때마다 새로운 인스턴스가 생성된다.
[JAVA]
private Object readResolve() {
// 하나의 진짜 Elvis를 반환하고 가비지 컬렉터가 가짜 Elvis를 처리하도록 한다.
return INSTANCE;
}
자바 1.5 이후 버전에서는 싱글톤을 구현하는 세 번쨰 방법이 있다.
[JAVA]
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
이 방법은 public 필드 방법과 기능적으로 동일하지만 더 간단하다. 그리고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원되고, 인스턴스가 여러 개 생기지 않도록 확실하게 보장해 준다. 이방법이 싱글톤을 구현하는 가장 좋은 방법이다.
$4. private 생성자를 사용해서 인스턴스 생성을 못하게 하자
static 메소드와 static 필드만을 모아 놓은 클래스를 만들 필요가 종종 있을 것이다. 예를 들면 java.lang.Math나 java.util.Arrays 클래스처럼 산술 연산에 필요한 기본형 값이나 배열에 관련된 메소드들을 모아 놓는데 사용될 수 도 있으며, java.util.Collections처럼 특정 인터페이스를 구현하는 객체들에 사용되는 static 메소드를 모아 놓는데 사용될 수 있다.
이러한 클래스들은 인스턴스를 생성하지 못하게 설계되었다. 하지만 명시적으로 지정한 생성자가 없을 떄는 컴파일러가 디폴트 생성자를 만들어 준다. 우리가 의도하는 것은 인스턴스를 생성할 수 없게 하는것이다. 이를 위해 간단한 이디엄이 있다. 즉, 디폴트 생성자는 우리가 명시적으로 지정한 생성자가 전혀 없을 때만 자동으로 만들어진다. 따라서 우리가 private 생성자를 정의하면 인스턴스 생성이 불가능한 클래스를 만들 수 있다.
[JAVA]
public class UtilityClass {
private UtilityClass() {
throw new AssertionError();
}
}
명시적으로 정의한 생성자가 private이므로 이 클래스 외부에서는 생성자 호출이 불가능하다.
위의 클래스의 단점은 서브 클래스를 만들 수 없다는 부작용이 있다. 서브 클래스 인스턴스를 생성할 때 생성자를 호출하면, 수퍼 클래스의 디폴트 생성자가 호출된다. 그러나 위 클래스의 서브 클래스를 만들면 수퍼 클래스의 생성자가 private이라 호출이 불가능해지므로 컴파일 시에 에러가 발생한다.
$5. 불필요한 객체의 생성을 피하자
기능적으로 동일한 객체를, 필요할 때마다 매번 새로 생성하기보다는 하나의 객체를 재사용하는 것이 좋을 때가 많다.
해서는 안될 극단적인 예로 다음 코드가 있다.
[JAVA]
String s = new String("stringette");
위의 코드는 String 인스턴스를 불필요하게 생성한다. 개선된 코드는 아래와 같다.
[JAVA]
String s = "stringette";
여기서는 실행될 때마다 새로운 인스턴스를 생성하지 않고 하나의 String 인스턴스를 사용하며, 같은 JVM에서 실행되는 어떤 코드에서도 동일한 문자열 리터럴을 갖도록 재사용 된다.
불변 클래스의 불필요한 객체 생성을 막으려면 생성자보다는 static 팩토리 매소드를 사용하는 것이 좋다.
[JAVA]
public class Person {
private final Date birthDate;
// 다른 부분은 생략
// 절대 이렇게 하지 말라
public boolean isBabyBoomer() {
// 불필요한 객체 생성
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
...
...
return ...
}
}
개선된 코드는 아래와 같다.
[JAVA]
public class Person {
private final Date birthDate;
// 다른 부분은 생략
static {
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
}
public boolean isBabyBoomer() {
return ...
}
}
$6. 쓸모 없는 객체 참조를 제거하자
자바와 같이 가비지 컬렉션을 자동으로 해주는 언어로 프로그램을 개발하면 객체들이 사용하던 메모리가 자동으로 회수된다고 생각하여 메모리 관리를 전혀 고려하지 않아도 된다는 생각을 가질 수 있다. 그러나 이러한 생각은 잘못된 것이다.
[JAVA]
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;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
// 배열에 요소를 저장하는데 필요한 공간을 확인하고 배열이 커질 필요가 있을 때는
// 그 크기를 2배로 늘린다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
상기 코드는 메모리 누출이 생긴다. 만일 스택이 커졌다가 줄어들면 스택에서 꺼냈던 객체들은 가비지 컬렉션되지 않을 것이다. 이렇듯 가비지 컬렉션이 지원되는 프로그래밍 언어에서 생기는 메모리 누출(의도하지 않은 객체 유지)은 우리가 모르는 사이에 진행된다.
이와 같은 문제에 대한 해결책은 매우 간단하다. 즉, 쓸모 없는 참조를 null로 만드는 것이다. 앞의 Stack 클래스에서는 스택에서 요소를 꺼내는 즉시로 그 요소의 참조가 쓸모 없게 된다.
[JAVA]
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 쓸모없는 참조를 제거한다.
return result;
}
쓸모없는 참조를 null 값으로 바꾸면 부수적인 장점이 생긴다. 이런 참조들이 나중에 실수로 사용된다면 NullPointerException 예외가 생긴다.
하지만 이와 같이 처리하는 것이 반드시 바람직하지 않다. 꼭 필요할 때만 예외적으로 행해야 한다. 이를 판단하는 기준으로는 자신의 메모리를 스스로 관리하는 부분에서만 사용한다. 즉 자신의 메모리를 자기가 관리한다면, 프로그래머는 항상 메모리 누출에 주의해야 한다.
메모리 누출이 흔히 생기는 또 다른 근원은 캐시(clone)이다. 객체 참조를 케시에 저장하면 저장했다는 것을 잊어 버리고 객체가 더 이상 필요 없을 때까지 캐시에 내버려 두기 쉽다. 이런일은 백그라운드 스레드(Timer 또는 ScheduledThreadPoolExecutor)로 처리하거나, 또는 새 EldestEntry 메소드를 사용하자.
메모리 누출의 세 번째 근원은 리스너와 콜백이다. 만일 클라이언트가 콜백을 등록하되 말소는 하지 않는 API를 구현한다면 콜백은 계속 누적될 것이다. 콜백이 신속하게 가비지 콜렉션 되도록 하는 가장 좋은 방법은 약한 참조만을 저장 유지하는 것이다. 예들 들어 WeakHashMap의 키로만 콜백을 저장한다.
메모리 누출은 잘 드러나지 않으므로 코드를 철저하게 검사하거나 힙 프로파일러와 같은 디버깅 도구의 도움을 받아야만 원인을 찾을 수 있다. 따라서 정답은 코드를 잘짜는 것이다.
$7. 파이널라이저의 사용을 피하자
파이널라이저는 예측 불가에다가 위험하기도 하고 일반적으로는 불필요하다. 하지만 몇가지 적합한 용도가 있다.
파이널라이저는 신속하게 실행된다는 보장이 없기 때문에 실행시간이 매우 중요한 작업을 절대 하지 말아야 한다. 예를 들면 파이널라이저에서 파일을 닫는 것은 매우 심각한 실수이다.
파이널라이저가 얼마나 빨리 실행되는가는 주로 가비지 컬렉션 알고리즘에 달려있으며, 그 알고리즘은 JVM 종류에 따라 다양하다. 또한 클래스에 파이널라이저를 사용하면 간혹 인스턴스들의 메모리 회수와 재활용이 지연될 수 있다.
이를 해결하기 위한 System.gc와 System.runFinalization 메소드들을 사용하고픈 유혼에 빠지지 말자. 만일 파이널라이즈를 하는 동안 catch 되지 않은 예외가 발생하면 그 예외는 무시되고 그 객체의 파이널라이즈는 종결된다.
파이널라이즈를 사용하면 엄청난 성능 저하가 발생한다. 그러면 파일이나 스레드처럼 종결 작업이 필요한 자원을 갖는 객체들의 클래스에서는 파이널라이저 대신 무엇을 써야 할까? 작업이나 자원을 정상적으로 종료하는 메소드만 별도로 추가하면 된다. 그리고 더 이상 필요 없는 각 인스턴스에 대해서 그 클래스의 클라이언트가 종료 메소드를 호출하도록 하면 된다. 이 때 주의 할것은 각 인스턴스에서 자신의 종료 여부를 유지 관리해야 한다. 즉, 종료 메소드에서는 해당 객체가 더 이상 유효하지 않다는 것을 private 필드에 기록해야 하며, 다른 메소드에서 이 필드 값을 확인하여 객체 종료후 호출되었다면 IllegalStateException 예외를 발생시켜야 한다.
가급적 종료 메소드는 try-finally와 함께 사용하여 확실하게 실행되도록 하자.
그렇다면, 파이널라이저는 어떤 경우에 사용하면 좋을까? 첫번째는 생성된 객체를 갖고 있는 코드에서 그 객체의 종료 메소드 호출을 빠뜨렸을 경우에 "안전망"의 역할을 하는 것이다. 즉, 클라이언트가 종료 메소드 호출에 실패하는 그런 경우를 대비해서 파이널라이저를 사용한다. 두번째는 네이티브 피어와 관련이 있다. 네이티브 피어는 네이티브 메소드를 통해 일반 자바 객체가 자신의 일을 위임하는 네이티브 객체이다. 네이티브 피어는 일반 자바 객체가 아니므로, 그것과 연관된 자바 피어 객체가 소멸되면 가비지 컬렉터가 알지 못하며 재활용 할 수도 없다. 따라서 이런 경우 파이널라이저가 적합할 수 있다.