-
2장 - 객체 생성과 파괴📕 book/이펙티브 자바 2022. 10. 25. 13:18
📌 생성자 대신 정적 팩터리 메서드를 고려하라
클라이언트는 클래스의 인스턴스를 얻는 전통적인 수단으로는 public 생성자이다.
클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다.
정적 팩터리 메서드 (static factory method)
- 해당 클래스의 인스턴스를 반환한다 (객체를 생성하는 메서드)
- ex) Optional의 of() : ‘new’ 키워드 대신 ‘of()’ 메서드를 이용해 객체를 만든다.
장점
1. 이름을 가질 수 있다.
public class Hat { private String color; private int cost; private Hat(String color, int cost) { this.color= color; this.cost = cost; } public static Hat createRedHat(int cost) { return new Hat("red", cost); } public static Hat createBlueHat(int cost) { return new Hat("blue", cost); } }
위 코드가 있을 때, 외부에서 인스턴스를 얻을 때
Hat hat = new Hat(”red”, 3000); 보다
Hat hat = Hat.createRedHat(3000); 이런 식으로 호출하는 것이 의미 파악하기에 더 쉽다.
2. 호출될 때 마다 인스턴스를 새로 생성하지 않아도 된다.
- 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피한다.
- 따라서, 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다.
public class Book { private String type; private static final Book poemBook = new Robot("poem"); private static final Book novelBook = new Robot("novel"); public Book(String type) { this.type = type; } public static Book getInstancePoemBook() { return poemBook; } public static Book getInstanceNovelBook() { return novelBook; } } Book novelBook = Book.getInstanceNovelBook();
이런 식으로 인스턴스를 캐싱해서 사용한다.
인스턴스 통제 클래스
- 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스를 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있다.
- 이런 클래스를 인스턴스 통제 클래스라고 한다.
인스턴스를 통제하는 이유 ❓
- 이를 통해 클래스를 싱글턴 으로 만들 수도
- 인스턴스화 불가 로 만들 수 도 있다.
- 또한, 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장 할 수 있다.
- (a == b 일 때만, a.equals(b) 가 성립)
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
⇒ 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’을 선물한다.
생성자를 사용하면 하위 클래스를 반환할 수 없지만 정적 팩토리 메서드를 사용하면 하위 클래스를 마음대로 리턴할 수 있다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
- ex) EnumSet 클래스
- 원소의 수에 따라 두 가지 하위 클래스중 하나의 인스턴스를 반환
- 원소가 64개 이하면 RegularEnumSet 인스턴스 반환
- 원소가 65개 이상이면 JumboEnumSet 인스턴스 반환
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
- 서비스 제공자 프레임워크를 만드는 근간이 된다.
단점
- 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들수 없다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 지어야 한다.
📌 생성자에 매개변수가 많다면 빌더를 고려
정적 팩터리와 생성자는 매개변수가 많을 때 적절히 대응하기 어렵다는 제약이 있다.
❓매개변수가 많다면???
- 점층적 생성자 패턴
- 자바빈즈 패턴
- 빌더 패턴
점층적 생성자 패턴
public class NutritionFacts { private final int calories; private final int fat; private final int sodium; public NutritionFacts(int calories) { this(calories, 0, 0); } public NutritionFacts(int calories, int fat) { this(calories, fat, 0); } public NutritionFacts(int calories, int fat, int sodium) { this.calories = calories; this.fat = fat; this.sodium = sodium; } }
확장성이 좋지 않고 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.
자바빈즈 패턴
매개변수가 없는 생성자로 객체를 만든 후, setter 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식
public class NutritionFacts { private final int calories = 0; private final int fat = 0; private final int sodium = 0; public NutritionFacts() {} public void setCalories(int calories) { this.calories = calories; } public void setFat(int fat) { this.fat = fat; } public void setSodium(int sodium) { this.sodium = sodium; } }
인스턴스를 만들기 쉽고, 더 읽시 쉬운 코드가 되었다.
하지만 심각한 단점이 존재
- 객체 하나를 만들려면 메서드를 여러개 호출해야 하고, 객체가 완전히 생성되기 전까지 일관성이 무너진 상태 이다.
- 클래스를 불변으로 만들 수 없다.
빌더패턴
점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비
public class NutritionFacts { private final int calories; private final int fat; private final int sodium; public static class Builder { //필수 매개변수 private final int calories; //선택 매개변수 - 기본값으로 초기화 private int fat = 0; private int sodium = 0; public Builder(int calories) { this.calories = calories; } public Builder fat(int val) { fat = val; return this; } // 빌더의 세터 메서드 들 public Builder sodium(int val) { sodium = val; return this; } public NutritionFacts build() { return new NutritionFacts(this); } } private NutritionFacts(Builder builder) { calories = builder.calories; fat = builder.fat; sodium = builder.sodium; } }
NutritionFacts 클래스는 불변이며, 모든 매개변수의 기본값들을 한곳에 모아 둔다.
- 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.(method chaining)
이 코드는 쓰기 쉽고 읽기 쉽다.
계층적으로 설계된 클래스와 함께 쓰기에 좋다.
하지만, 객체를 만들기 전에 빌더부터 만들어야 하고 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다.
📌 private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴 (singleton)
- 인스턴스를 오직 하나만 생성할 수 있는 클래스
싱글턴을 만드는 방식
- private 생성자, public static 멤버가 final 필드인 방식
- private 생성자, 정적 팩토리 메서드를 public static 필드로 제공
- 원소가 하나인 Enum 타입 선언
private 생성자, public static 멤버가 final 필드인 방식
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { } public void leaveTheBuilding() { } ... } Elvis elvis1 = Elvis.INSTANCE; Elvis elvis2 = Elvis.INSTANCE; elvis1 == elvis2 // true; elvis.equals(elvis2) // true;
public, protected 생성자가 없으므로 Elvis 클래스가 초기화 될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
예외 - 딱 한 가지
- 리플렉션 API인 AccessibleObject.setAccessible 을 사용해 private 생성자를 호출할 수 있다.
private 생성자, 정적 팩토리 메서드를 public static 필드로 제공
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() { } public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() { } ... }
Elvis.getInstance() 는 항상 같은 객체의 참조를 반환한다.
- 제 2의 Elvis 인스턴스는 결코 만들어지지 않는다.
장점
- 마음이 바뀌면 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
- 정적 팩토리 메서드(getInstance)가 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.
- 원하면 정적 팩터리를 제네릭 싱글턴 팩터리 로 만들 수 있다.
- 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
- supplier - 매개변수 ❌, 리턴 값 ⭕
- Elvis::getInstance ⇒ Supplier<Elvis>
원소가 하나인 열거(Enum)타입 선언
public enum Elvis { INSTANCE; public void leaveTheBuilding() { } ... }
public 필드 방식과 비슷하지만, 더 간결하고 추가 노력 없이 직렬화할 수 있다.
대부분 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
📌 인스턴스화를 막으려거든 private 생성자를 사용하라
static 메서드와 static 필드만을 담은 클래스
→ 객체지향적이지 않다, 그러나 나름 쓰임새가 있다.
- java.lang.Math나 java.util.Arrays처럼 기본 타입 값 이나 배열 관련 메소드 들을 모아놓은 것
- java.util.Collections처럼 특정 인터페이스 를 구현하는 객체를 생성해주는 정적 메소드 or 팩토리 를 모아놓은 것
- final 클래스와 관련한 메소드들을 모아놓을 때도 사용한다.
- (final 클래스를 상속해 하위 클래스에 메소드를 넣는 것은 불가능하기 때문이다.)
추상클래스로 만드는 것은 인스턴스화를 막을 수 없다.
- 하위 클래스를 만들어 인스턴스화 하면 그만
컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때 뿐이다.
⇒ private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
- 이는 상속을 불가능하게 하는 효과도 있다.
- 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막힘.
📌 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
의존 객체 주입
- 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식
public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dictionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) {...} public List<String> suggests(String typo) {...} }
의존 객체 주입은 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.
이 패턴의 괜찮은 변형으로 생성자에 자원 팩터리를 넘겨주는 방식이 있다.
- 팩터리 : 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
- 즉, 팩터리 메서드 패턴을 구현한 것.
- ex) Supplie<T> 인터페이스
Mosaic create(Supplier<? extends Tile> tileFactory) {...}
Supplier<T>를 입력으로 받는 메서드는 명시한 타입(Tile)의 하위 타입이라면 무엇이든 생성할 수 있는 팩토리를 넘길 수 있다.
의존 객체 주입이 유연성과 테스트 용이성을 개선해주지만, 의존성이 수 천개 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 한다.
- Dagger, Guice, Spring같은 의존 객체 주입 프레임워크를 사용하면 이런 어질러짐을 해소할 수 있다.
📌 불필요한 객체 생성을 피하라
똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
- 재사용은 빠르고 세련되다
- 불변 객체는 언제든 재사용할 수 있다.
생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.
- ex) Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
생성자는 호출할 때 마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않음!
정규 표현식
String.mathes 메서드는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
static boolean isRomanNumeral(String s) { return s.matches("^(?=.)M*(C[MD]|D?C{0,3}(X[CL]|L?X{0,3})(I[XV]|V?I{0,3}$"); }
- 위 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져 곧바로 가비지 컬렉션의 대상이 된다.
public class RomanNumerals { 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}$"); static boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } }
- 성능 개선을 위해 불변인 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고, 나중에 메서드가 호출될 때마다 이 인스턴스를 재사용한다.
오토박싱 (Auto Boxing)
오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술
- 오토박싱은 기본타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아님
private static long sum() { Long sum = 0L; for (long i = 0 ; i < Integer.MAX_VALUE ; i++) { sum += i; } return sum; }
sum 변수를 long 이 아닌 Long으로 선언해서
불필요한 Long 인스턴스가 2^{31} 개나 만들어 진다.
( long 타입의 i 가 Long 타입인 sum에 더해질 때 마다 불필요한 Long 인스턴스가 생성된다 )
⇒ 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자
📌 다 쓴 객체 참조를 해제하라
자바처럼 가비지 컬렉터를 갖춘 언어를 사용하면 메모리 관리에 신경쓰지 않아도 된다고 오해할 수 있다.
⇒ 절대 아님 ❌
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(); //원소를 위한 공간을 at least 1개 이상 확보한다. 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); } }
위 코드는 어떤 테스트도 거뜬히 통과할 것이고 문제가 없어 보인다.
하지만 오래 실행하다보면 점차 가비지 컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하된다.
⇒ ‘메모리 누수’
public Object pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; // 이 부분 }
위 코드에서는 스택이 커졌다가 줄어들 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다.
- pop()이 동작하면 스택에서 객체가 빠져나가고 스택이 줄어든다.
- 스택에서 꺼내진 객체들은 가비지 컬렉터가 회수하지 않는다
- 더 이상 그 객체가 필요하지 않더라도
why❓
- 스택이 그 객체들의 다 쓴 참조(obsolete reference) 를 여전히 가지고 있기 때문
- 다 쓴 참조 : 앞으로 다시 쓰지 않을 참조
가비지 컬렉션 언에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다.
- 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다.
❗ 해결 방법 : 해당 참조를 다 썼을 때 null 처리(참조 해제)하면 된다!!
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 다 쓴 참조 해제 return result; }
객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.
- 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효범위(scope) 밖으로 밀어내는 것
null 처리를 해야 하는 경우
- 자기 메모리를 직접 관리하는 클래스
- 위의 Stack 클래스는 자기 메모리는 직접 관리한다.
- elements 배열로 저장소 풀을 만들어 원소들을 관리
- 배열의 활성 영역에 속한 원소들은 사용되고 비활성 영역은 쓰이지 않음.
그러므로, 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더 쓰지 않을 것을 가비지 컬렉터에 알려야 함. - 캐시객체 참조를 캐시에 넣고, 그 사실을 잊은 채 객체를 다 쓴 후에도 캐시에 놔두는 일이 있다.해법
- 엔트리가 살아있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만든다.
- 다 쓴 엔트리는 그 즉시 자동으로 제거된다
- WeakHashMap은 이러한 상황에서만 유용하다..!
- 리스너 혹은 콜백
콜백을 등록만 하고 명확히 해지하지 않으면 콜백이 계속 쌓여만 간다.
콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해 간다.
ex) WeakHashMap에 키로 저장한다.
약한 참조 (Weak Reference)- 약한 참조만 가진 객체는 GC의 대상이 된다
- 주로 캐시에 활용
- java.lang 패키지의 WeakReference 클래스를 사용
- WeakReference<Integer> weak = new WeakReference<Integer>(item);
부드러운 참조(Soft Reference) : 메모리가 부족하면 GC의 대상이 된다. 부족하지 않으면 대상 ❌
- SoftReference<Integer> soft = new SoftReference<>(item);
- 원본은 없고 대상을 참조하는 객체만 존재
강한 참조 (Strong Reference) : 일반적인 참조로 강한 참조를 가진 객체는 GC의 대상 ❌- 일반적인 유형의 참조 ( Integer item = 1 )
📌 finalizer 와 cleaner 사용을 피하라
자바는 두 가지 객체 소멸자를 제공
- finalizer
- cleaner
finalizer
finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요
클래스에 finalizer를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.
cleaner
finalizer 보단 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요
사용하지 않아야 하는 이유
- 이 둘은 즉시 수행된다는 보장이 없다.
⇒ 따라서, 제때 실행되어야 하는 작업은 절대 할 수 없다. - 수행 시점 뿐 아니라 수행 여부조차 보장하지 않는다.
⇒ 상태를 영구적으로 수정하는 작업에서는 절대 finalizer, cleaner에 의존하면 안된다. - 심각한 성능 문제
- finalizer 가 가비지 컬렉터의 효율을 떨어뜨린다.
- finalizer를 사용한 클래스는 finalizer 공격에 노출되 심각한 보안 문제를 일으킬 수 있다.
📌 try-finally 보다 try-with-resources 를 사용
try-finally는 제대로 자원을 회수하도록 짜기 어렵고 중첩 try문의 경우 둘 다 예외가 발생해도 내부 예외만 출력한다는 문제가 있다.
⇒ try-with-resources 형태를 사용해주는 것이 좋다.
try-with-resoureces 는 자원 사용 후 자동으로 close()해주고 예외 발생시 모두 발생할 수 있다.
close()를 사용하니 AutoClosable을 implement 해줘야 한다.
- 코드의 실행 위치가 try문을 벗어나면 try(..) 에서 선언된 객체의 close() 메소드들을 호출한다!!
좋지 않은 방법 ❌
static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try{ return br.readLine(); } finally { br.close(); } }
책에서 권장 방법 📚
static String firstLineOfFile(String path) throws IOException { try(BufferedReader br = new BufferedReader(new FileReader(path))){ return br.readLine(); } catch (IOException e) { return dfaultVal; } }
좋은 방법 👍
static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try(br){ return br.readLine(); } catch (IOException e) { return dfaultVal; } }
참고 자료
'📕 book > 이펙티브 자바' 카테고리의 다른 글
7장 - 람다와 스트림 (0) 2022.11.07 6장 - 열거타입과 애너테이션 (0) 2022.11.04 5장 - 제네릭 (0) 2022.11.01 4장 - 클래스와 인터페이스 (0) 2022.11.01 3장 - 모든 객체의 공통 메서드 (0) 2022.10.26