규칙 1 생성자 대신 정적 팩터리 메서드를 사용할 수 없는지 생각해 보라
- 클래스를 통해 객체를 만드는 일반적인 방법은 public으로 선언된 생성자 (constructor)를 이용하는 것이다.
- Design Patterns(디자인 패턴)에 등장하는 팩터리 메서드(Factory Method) 개념과 다르다는 점에 유의하자.
public class Sample {
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
}
public class Sample {
public static BigInteger probablePrime(int bitLength, Random rnd) {
if (bitLength < 2)
throw new ArithmeticException("bitLength < 2");
return (bitLength < SMALL_PRIME_THRESHOLD ? smallPrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd) : largePrime(bitLength, DEFAULT_PRIME_CERTAINTY, rnd));
}
}
- 장점
- 생성자와는 달리 정적 팩터리 메서드에는 이름이 있다는 것이다.
- 정적 팩터리 메서드는 이름을 잘 짓기만 한다면 시용하기도 쉽움
- 클라이언트 코드의 가독성(readability)도 높아짐
- 생성자와는 달리 호출할 때마다 새로운 객체를 생성할 필요는 없다는 것이다.
- 개체 통제 클래스를 작성하는 이유
- 개체 수를 제어하면 싱글턴(singleton) 패턴을 따르도록 할 수 있다.
- 객체 생성이 불기능한 클래스를 만들 수도 있다.
- 변경이 불가능한 클래스의 경우 두 개의같은 객체가 존재하지 못하도록 할 수도 있다.
- 정적 팩터리 메서드를 사용하면 같은 객체를 반복해서 반환할 수 있으므로 어떤 시점에 어떤 객체가 얼마나 존재할지를 정밀하게 제어할 수 있다.
- 생성자와는 달리 변환값 자료형의 하위 자료형 객체를 반환할 수 있다든 것이다.
- 반환되는 객체의 클래스를 훨씬 유연하게 결정할 수 있다.
- 서비스 제공자 프레임워크 : 다영한 서비스 제공자들이 하나의 서비스를 구성하는 시스템 (JDBC)
- 서비스 인터페이스(service interface)
- 제공자 등록 API (provider registration API)
- 서비스 접근 API (service access API) : 클라이언트에게 실제 서비스 구현체를 제공한다.
- 형인자 자료형(parameterized type) 객체를 만들 때 편하다는 점이다.
- 생성자와는 달리 정적 팩터리 메서드에는 이름이 있다는 것이다.
public static <K, V> HashMap<K, V> new newInstance() {
return new HashMap<K, V>();
}
import java.sql.*;
public class JavaDatabaseConnectivity {
public static void main(String[] args) throws SQLException {
// 서비스 제공자 프레임워크는 다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템
// 1. 서비스 인터페이스 (java.sql.Connection)
// 4. 서비스 제공자 인터페이스 (Driver)
Driver driver = new com.mysql.cj.jdbc.Driver();
// 2. 제공자 등록 API (구현체를 시스템에 등록하여 클라이언트가 쓸 수 있도록 함)
DriverManager.registerDriver(driver);
// 3. 서비스 접근 API (구현체에 대한 접근이 가능하도록 한다)
// getConnection() '유연한 정적 팩터라'
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306", null);
Statement statement = connection.createStatement();
statement.execute("");
}
}
- 단점
- public나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다는 것이다.
public class Foo {
public static Foo newInstance() {
return new Weakness();
}
private Foo() {
}
}
public class FooSub extends Foo {
private WeaknessSub() {
// 상위클래스 생성자 호출을 할 수 없음
super();
}
}
- 정적 팩터리 메서드가 다른 정적 메서드와 확연히 구분되지 않는다는 것이다.
| 메서드명 | 설명 |
|---|---|
| valueOf | 인자로 주어진 값과 같은 값을 갖는 객체를 반환한다는 뜻이다. |
| of | valueOf를 더 간단하게 쓴 것이다. |
| getInstance | 인자에 기술된 객체를 반환하지만, 인자와 같은 값을 갖지 않을 수도 있다. |
| newInstance | getInstance와 같지만 호출할 때마다 다른 객체를 반환한다. |
| getType | getInstance와 같지만, 반환될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 사용한다. |
| newType | newInstance와 같지만, 반환될 객체의 클래스와 다른 클래스에 팩터리 메서드가 있을 때 시용한다. |
규칙 2 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라.
- 정적 팩터리나 생성자는 같은 문제를 갖고 있다. 선택적 인자가 많은 상황에 적응하지 못한다는 것.
// 점층적 생성자 패턴 - 더 많은 인자 개수에 잘 적응하지 못한다
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;
}
}
- 선택적 인자가 많은 상황
- 점층적 생성자 패턴(telescoping constructor pattern)
- 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고, 무엇보다 읽기 어려운 코드가 되고 만다.
- 클라이언트가 두 개 인자의 순서를 실수로 뒤집어도 컴파일러는 알지 못하며, 프로그램 실행 도중에 문제가 생기게 되는 것이다.
- 자바빈 패턴
- 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로, 객체 일관성(consistency)이 일시적으로 깨질 수 있음
- 생성자의 인자가 유효한지 검사하여 일관성을 보장하는 단순한 방법을 여기서는 사용할 수 없음
- 변경 불가능(immutable) 클래스를 만들 수 없다는 것
- 점층적 생성자 패턴(telescoping constructor pattern)
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;}
}
- 빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특히 대부분의 인자가 선택적 인자인 상황에 유용
- 생성자와 마찬가지로, 빌더 패턴을 사용하면 인자에 불변식(invariant)을 적용할 수 있음
- 빌더 객체는 여러 개의 varargs 인자를 받을 수 있다는 것
- 빌더 패턴은 하나의 빌더 객체로 여러 객체를 만들 수 있음
- 빌더 객체는 어떤 필드의 값은 자동으로 채울 수도 있음
- 단점
- 객체를 생성하려면 우선 빌더 객체를 생성
- 실무에서 빌더 객체를 만드는 오버헤드가 문제가 될 소지는 없어 보이지만, 성능이 중요한 상황에선 그렇지 않을 수도 있음
-
빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 특히 대부분의 인자가 선택적 인자인 상황에 유용
- Q : 불변식이란(Invariant)?
- A : an invariant is a condition that can be relied upon to be true during execution of a program, or during some portion of it. It is a logical assertion that is held to always be true during a certain phase of execution.
- R : https://en.wikipedia.org/wiki/Invariant_(computer_science)
규칙 3 private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라
// public final 필드를 이용한 싱글턴
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public void leaveTheBuilding( ) { }
}
// 정적 펙터리를 이용한 싱글턴
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { }
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding( ) { }
}
// Enum 싱글턴 - 이렇게 하는 쪽이 더 낫다
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { }
}
- 클래스를 싱글턴으로 만들면 클라이언트를 테스트하기가 어려워질 수가 있음
- serialize된 객체가 역직렬화(deserialize)될 때마다 새로운 객체가 생기게 된다.
-
원소가 하나뿐인 enum 자료형이야말로 싱글턴을 구현하는 가장 좋은 방법
- 참고 싱글턴
규칙 4 객체 생성을 막을 때는 private 생성자를 사용하라
- 원소가 하나뿐인 enum 자료형이야말로 싱글턴을 구현하는 가장 좋은 방법
- 객체를 만들 수 없도록 하려고 클래스를 abstract로 선언해 봤자 소용
- private 생성자를 클래스에 넣어서 객체 생성을 방지하자는 것
// 객쳬룰 만둘 수 없는 유틸리티 클래스
public class UtilityClass {
// 기본 생성자가 자동 생성되지 못하도록 하여 객체 생성 방지
private UtilityClass() {
throw new AssertionError();
}
}
규칙 5 불필요한 객체는 만들지 말라
- JDK 1.5부터는 쓸데없이 객체를 만들 새로운 방법이 더 생겼다. 자동 객체화 (autoboxing)라는 것인데, 프로그래머들이 자바의 기본 자료형(primitive type) 그 객체 표현형을 섞어 사용할 수 있도록 해 준다.
- 객체 표현형 대신 기본 자료형을 사용하고, 생각지도 못한 자동 객체화가 발생하지 않도록 유의하라는 것이다.
- 직접 관리하는 객체 풀(Object pool)을 만들어 객체 생성을 피하는 기법은 객체 생성 비용이 극단적으로 높지 않다면 사용하지 않는 것이 좋다.
- 코드가 어지러워짐
- 메모리 요구량이 증가 하며, 성능도 떨어짐
- 최신 JVM은 고도로 최적화된 쓰레기 수집기를 갖고 있어서, 가벼운(lightweight) 객체라면 객체 풀보다 월등한 성능을 보여줌
- 방어적 복사가 요구되는 상황에서 객체를 재사용하는 데 드는 비용은 쓸데없이 같은 객체를 여러 벌 만드는 비용보다 훨씬 높다는 것에 유의하자.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
규칙 6 유효기간이 지난 객체 참조는 폐기하라
- 만기 참조란, 다시 이용되지 않을 참조
- 자동적으로 쓰레기 객체를 수집하는 언어에서 발생하는 메모니 누수 문제 (의도치 않은 객체 보유)
- 쓸 일 없는 객체 참조는 무조건 null
- 만기 참조가 몇 개라도 있으면 굉장히 많은 객체가 쓰레기 수집에서 제외될 가능성이 있음
- 객체 참조를 null 처리하는 것은 규범(norm)이라기보단 예외적인 조치가 되어야 한다.
- 자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해아 한다.
- 더 이상 사용되지 않는 원소 안에 있는 객체 참조는 반드시 null로 바꿔 주어야 한다.
- 캐시도 메모리 누수가 흔히 발생하는 장소다.
- WeakHashMap은 캐시 안에 보관되는 항목의 수명이 키에 대한 외부 참조의 수명에 따라 결정되는 상황에서만 적용 가능하다는 것을 기억하자.
- 메모리 누수가 흔히 발견되는 또 한 곳은 리스너 등의 역호출자다.
Q : ‘더 이상 사용되지 않는 원소 안에 있는 객체 참조는 반드시 null로 바꿔 주어야 한다.’ 잘못된 사례는 ?
규칙 7 종료자 사용을 피하라
- 종료자(finalizer)는 예측 불가능하며, 대체로 위험하고, 일반적으로 불필요
- 긴급한(time-critical) 작업을 종료자 안에서 처리하면 안됨
- 중요 상태 정보(critical persistent state)는 종료자로 갱신하면 안됨
- 종료자는 그런 자원을 발견하게 될 경우 반드시 경고 메시지를 로그(log)