1. 생성자 대신 정적 팩터리 메서드를 고려하라
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
Java
복사
Boolean 래퍼타입의 정적 생성 팩토리 메서드
정적 팩터리 메서드를 사용하기 이전에는 어땟는가?
전통적인 public 접근자의 생성자를 이용해 객체를 생성했었다.
정적 팩터리 메서드를 사용하면 뭐가 좋은가?
•
생성자가 이름을 가질 수 있다.
◦
BigInteger(int, int, Random) 보다는 BigInteger.pobablePrime이 명확한 의미를 가진다.
•
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
◦
생성자를 통해서 만들어진 객체는 매번 새로운 객체를 만들어낸다.
◦
하지만 정적 팩터리 메소드를 통한 호출은 미리 생성해둔 객체를 전달할 수 있다.
▪
때문에 불변의 객체를 미리 만들어 둘 수 있다!
◦
이런 클래스를 인스턴스 통제 클래스라 한다.
•
반환 타입의 하위 타입 객체를 반환할 수 있다.
◦
반환할 객체를 자유롭게 선택할 수 있게 하는 유연성을 선물한다.
▪
이 유연성을 유지하면 구현 클래스를 공개하지 않고도 객체를 반환할 수 있다.
◦
컬렉션 프레임워크는 45개의 클래스를 공개하지 않기 때문에 API 외견을 훤씬 작게 만들 수 있었다.
▪
API가 작아진 것은 물론 개념적인 무게를 낮춰 익혀야 하는 개념의 수와 난이도를 낮췄다.
•
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
◦
반환 타입이 하위타입이기만 하다면 어떤 클래스의 객체를 반환하든 상관없다.
•
정적 팩터리 메서드를 작성하는 시점에서 반환할 객체의 클래스가 존재하지 않아도 된다.
◦
이런 유연함은 서비스 제공자 프레임워크를 만다는 근간이 된다.
◦
서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다.
▪
구현체의 동작을 정의하는 서비스 인터페이스
▪
제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
▪
클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API
◦
서비스 제공자 프레임워크는 여러 변형이 있고
▪
브리지 패턴, 의존 객체 주입(DI)도 강력한 서비스 제공자라고 생각할 수 있다
정적 팩터리 메서드의 단점?
•
상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
◦
이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 제약을 지켜야한다는 점에서 오히려 장점으로 받아들일 수 있다.
•
정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
◦
철저한 문서화가 필요하다.
// from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
Date d = Date.from(instant);
// of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
// instance 혹은 getInstance: 매개변수로 명시한 인스턴스를 반환, 같은 인스턴스임을 보장하진 않는다.
StackWalker luke = StackWalker.getInstance(options);
// create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스 생성을 보장
Object newArray = Array.newInstance(classObject, arrayLen);
// getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용 ("Type"은 팩터리 메서드가 반환할 객체의 타입)
FileStore fs = Files.getFileStore(path);
// newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용
BufferedReader br = Files.newBufferedReader(path);
// type: getType과 newType의 간결한 버전
List<Complaint> litany = Collections.list(legacyLitany);
Java
복사
결론
•
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
•
그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
2. 생성자 매개변수가 많다면 빌더를 고려하라
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;
private NutritionFacts(Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
public static class Builder {
// 필수 매개변수
private final int servingSize;
private final int servings;
// 선택 매개변수
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
// 필수 매개변수만을 담은 Builder 생성자
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 선택 매개변수의 setter, Builder 자신을 반환해 연쇄적으로 호출 가능
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
// build() 호출로 최종 불변 객체를 얻는다.
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
Java
복사
생성자를 만들어야 하는데 매개변수가 너무 많다…
•
모두 동일한 타입의 서로 다른 값들을 저장하는 필드 변수들로 인해서 너무나도 길어진 생성자!
◦
점층적으로 필요한 값들의 인자만 받아서 생성하자! (점층적 생성자 패턴)
▪
아 그런데… 너무 생성자가 많아져서 코드를 읽기도 어렵고 유지보수가 너무 어렵다…
▪
그리고 인자를 잘못줘도 런타임에서 발견되기 때문에 사전에 오류를 잡아내기가 어렵다…
◦
그러면 Setter를 통해서 값의 삽입을 진행하면 어떨까? (자바빈즈 패턴)
▪
아 그런데 이러면 모든 값들을 재설정 해야하니 final타입으로 변수를 생성할 수 없어 불변성을 가지는 객체를 만들 수 없구나...!
▪
객체를 얼리고 사용하면 되지만, 실수해서 얼리지 않으면 런타임 오류가 생겨버린다…
어디 생성할때 명확하게 어떤 매개 변수에 어떤 값이 들어가고 불변으로도 만들 수 있는 방법이 없을까?
대신 귀여운 빌더패턴을 드리겠습니다.
•
빌더 패턴은 파이썬, 스칼라, 자바스크립트 등에 있는 명명된 선택적 매개변수를 흉내낸 패턴 (코틀린도 있답니다.)
•
원한다면 디폴드 값을 넣을 수 도있어요!
•
추가적으로 빌더에 외부의 공격에 대비한 필드 검사를 추가해야합니다!
Lombok으로 쉽게 빌더 패턴을 생성할 수 있어요!
@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;
}
Java
복사
결론
•
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.
•
매개변수 중 다수가 필수가 아니거나 같은 타입이면 더 그렇다.
•
빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.
3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴이란?
•
메모리 내에 오직 하나만의 인스턴스를 생성할 수 있는 디자인패턴의 일종
•
스프링에서 생성되는 빈도 싱글톤의 형태로 생성됩니다. (옵션에 따라 다름)
•
싱글턴은 무상태성을 가져야하며 싱글턴을 사용하는 클래스는 테스트가 어렵다!
싱글톤의 장점
•
한번의 생성으로 객체의 재사용이 가능하기 때문에 메모리 낭비를 방지할 수 있음
•
싱글톤으로 생성된 객체는 전역성을 띄기 때문에 공유가 용이하다.
싱글톤의 단점
•
싱글턴을 사용하는 클래스는 테스트하기 어렵다.
•
싱글턴은 올바르게 구현하기가 어렵다.
•
싱글턴은 상속받기가 어렵다.
•
멀티스레드 환경에서 하나의 인스턴스를 보장하기 어렵다.
전통적인 싱글톤을 만드는 방법
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Java
복사
•
멀티스레드 환경에서 하나의 인스턴스를 보장하기 어렵다
조금 개선된 싱글톤 생성 방법
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
Java
복사
•
synchronized 예약어를 통해서 하나의 인스턴스만을 보장하지만 성능이 떨어진다.
•
동기화가 필요한 시점은 오로지 인스턴스의 생성을 진행할 때 뿐이기 때문에 매번 동기화를 하는것은 비효율적임
조금 더 개선된 싱글톤 생성 방법
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static synchronized Singleton getInstance() {
return uniqueInstance;
}
}
Java
복사
방법1 - 인스턴스를 처음부터 만들어준다.
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
Java
복사
방법2 - DCL을 사용하여 동기화 코드를 줄인다.
가장 이상적인 방법 (Enum 이용)
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
Java
복사
•
Enum은 메모리내에 단 하나의 인스턴스만을 JVM이 보장한다.
◦
또한 리플렉션을 이용한 값 변경에도 인스턴스가 생기는 일을 완벽하게 막아준다.
◦
심지어 직렬화도 용이하다!
•
때문에 대부분의 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
4. 인스턴스화를 막으려거든 private 생성자를 사용하라
가끔 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을때가 있을 것이다.
•
객체지향 적으로 사고하지 않는 이들이 종종 남용하는 방식이기에 그리 곱게 보이지는 않지만, 분명 나름의 쓰임새가 있다.
•
java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아놓을 수 있다.
추상클래스를 만들면 되는게 아닐까?
•
추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
◦
하위 클래스를 만들어 인스턴스화 하면 그만이기 때문!
생성자를 private로 만들어서 클래스의 인스턴스화를 막자
public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용)
private UtilityClass() {
throw new AssertionError(); // 생성자 내부 호출 시 명시적인 에러 던지기
}
...
}
Java
복사
•
생성자가 private이기 때문에 상속이 불가능하다.
여담
•
정적 메소드, 변수는 별도의 메모리 영역에 보관되기 때문에 GC대상이 아니다!
•
때문에 메모리를 조금이라도 아껴서 사용할 때 용이하다.
5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
•
많은 클래스가 하나 이상의 자원에 의존한다.
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker() {} // 객체 생성 방지
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
Java
복사
정적 유틸리티를 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다
public class SpellChecker {
private static final Lexicon dictionary = ...;
private SpellChecker(...) {}
public static SpellChecker INSTANCE = new SpellChecker(...);
public static boolean isValid(String word) { ... }
public static List<String> suggestions(String typo) { ... }
}
Java
복사
싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.
public class SpellChecker {
private final Lexicon dictionary;
private SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { return true; }
public List<String> suggestions(String typo) { return null; }
}
Java
복사
의존성 주입을 통해서 유연성과 테스트 용이성을 높여준다.
자바8 Supplier<T>
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
Java
복사
함수형 인터페이스를 통해서 제공이 가능하다.
정리
•
클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.
•
의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 기막히게 개선해준다.
6. 불필요한 객체 생성을 피하라
문자열 객체
// 매번 새로운 인스턴스를 생성
String s = new String("bikini");
// 한번 생성된 인스턴스를 재활용 함
String s = "bikini";
Java
복사
정적 타입
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
Java
복사
•
정적으로 생성된 메서드나 필드변수는 별도의 메모리 공간 (static 메모리)에 생성되기 때문에 매번 새로운 인스턴스를 생성하지 않고 캐싱된 객체를 이용할 수 있다.
오토박싱
private static long sum() {
Long sum = 0L;
for(long i=0; i<=Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
Java
복사
•
우리 자바 프로그래머가 가장 조심해야 하는 문제라고 생각한다!
•
오토박싱은 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
오해 금지
•
객체 생성은 비싸니 피해야 한다로 오해하면 안 된다.
•
특히나 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
◦
프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
•
그렇다고 단순히 객체 생성을 피하기 위해 자신만의 객체 풀(pool)을 만들지는 말자.
•
요즘 JVM의 GC는 상당히 잘 최적화 되어서, 가벼운 객체를 다룰 때는 직접 만든 객체 풀보다 훨씬 빠르다.
방어적 복사
•
방어적 복사가 필요한 상황에서 객체를 재사용했을 때 피해가, 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.
7. 다 쓴 객체 참조를 해제하라
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]; // 정답은 여기!
}
/**
* 원소를 위한 공간을 적어도 하나 이상 확보한다.
* 배열 크기를 늘려야 할 때마다 대략 두 배씩 늘린다.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
Java
복사
메모리 누수가 일어나는 곳을 찾아봐요!
•
GC는 마크앤 스윕을 통해서 레퍼런스 카운트를 검사하는데 배열 내 해제되지 못한 레퍼런스 카운팅 때문에 영원히 해제되지 못한채 구천을 떠돌다 결국 OOM을 맞이할 것이다.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
Java
복사
메모리를 꼭 해제해주도록 합시다!
•
다 쓴 참조를 null 처리 하면 다른 이점도 따라온다. 만약 null 처리한 참조를 실수로 사용하려면 프로그램은 즉시 NullPointerException을 던지며 종료된다.
◦
다 쓴 참조를 해제하는 가장 좋은 방법은 스코프 밖으로 밀어내는 것
메모리 누수가 발생하는 이유
•
일반적으로 자기 메모리를 직접 관리하는 클래스라면 항시 메모리 누수에 주의해야 한다.
◦
원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null 처리해줘야 한다.
•
캐시 역시 메모리 누수를 일으키는 주범이다.
◦
객체 참조를 캐시에 넣고 이 사실을 잊은체 그 객체를 다 쓴뒤로도 한참을 그냥 놔두는 일을 자주 접할 수 있다.
•
메모리 누수의 세 번째 주범은 바로 리스너 혹은 콜백이다.
◦
클라이언트 코드가 콜백을 등록할 수 있는 API를 만들고 콜백을 뺼 수 있는 방법을 제공하지 않는다면, 계속해서 콜백이 쌓이기만 할 것이다.
◦
이것 역시 WeakHashMap을 사용해서 콜백을 Weak 레퍼런스로 저장하면 GC가 이를 즉시 수거해 해결할 수 있다.
8. finalizer와 cleaner 사용을 피하라
자바는 두 가지 객체 소멸자를 제공한다.
•
finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 대부분 불필요하다.
•
cleaner는 finalizer보다는 덜 위험하지만 여전히 예측할 수 없고, 느리며, 보통은 불필요하다.
◦
C++ 프로그래머에게 소멸자와는 다른 개념
finalizer와 cleaner는 호출된 후 언제 실행될 지 알 수 없다.
•
즉, 제때 실행되어야 하는 작업을 절대 할 수 없다.
•
성능저하도 일으킨다.
finalizer나 cleaner를 대신해줄 묘안 AutoCloseable
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private static class State implements Runnable {
int numJunkPiles;
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
@Override
public void run() {
System.out.println("방 청소");
numJunkPiles = 0;
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override
public void close() throws Exception {
cleanable.clean();
}
}
Java
복사
•
인터페이스로 구현하면 close가 호출된다.
finalizer와 cleaner는 언제 쓸까?
•
자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안정망 역할
•
네이티브 피어와 연결된 객체 에서 사용
◦
네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 의미함
9. try-finally 보다는 try-with-resource를 사용하라
public static String firstLineOfFile(String path) throw IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
Java
복사
과거의 코드 try-finally
•
디버깅이 어려워질 수 있다.
•
가독성이 떨어진다. (try-finally문 중첩시)
•
제대로 close하지 않으면 자원이 회수되지 못하는 문제가 발생할 수 있다.
public static String firstLineOfFile(String path) throw IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (Exception e) {
return defaultVal;
}
}
Java
복사
현대적인 더 나은 코드 try-with-resource
•
try-with-resource를 사용하면 AutoCloseable인터페이스가 구현된 객체의 close 메서드를 자동으로 호출한다.
•
close에서 발생한 예외는 숨겨지고 첫 번째 예외가 기록된다.
◦
이렇게 숨겨진 예외들은 스택 추적 내역에 suppressed 꼬리표를 달고 출력된다.
◦
자바7에서 Throwable에 추가된 getSuppressed 메서드를 쓰면 프로그램 코드에서 가져올 수 있다.