////
Search

2장 - 객체 생성과 파괴

Created
2022/09/03 11:37
Tags
Java

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.Mathjava.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 메서드를 쓰면 프로그램 코드에서 가져올 수 있다.