2장 - 객체 생성과 파괴
💡 생성자 대신 정적 팩터리 메서드를 고려하라
“정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로,
무작정 public 생성자를 제공하던 습관이 있다면 고치자”
1. 정적 팩터리 메서드
- 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드
- ex)
Boolean.valueOf
메서드
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
2. 정적 팩터리 메서드의 장점 5가지
a) 이름을 가질 수 있다.
- 생성자
BigInteger(int, int, Random)
과 정적 팩터리 메서드BigInteger.probablePrime
중 어느 쪽이 ‘값이 소수인BigInteger
를 반환한다’는 의미를 더 잘 설명할 것 같은가?
b) 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
- ex)
Boolean.valueOf(boolean)
메서드는 객체를 아예 생성하지 않음
c) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- ex) 자바 컬렉션 프레임워크의 유틸리티 구현체들 대부분은 단 하나의 인스턴스화 불가 클래스인
java.util.Collections
에서 정적 팩터리 메서드를 통해 얻음
d) 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없음
e) 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
3. 정적 팩터리 메서드의 단점 2가지
a) 상속을 하려면 public
이나 protected
생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
b) 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- 생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 함.
from, of, valueOf, getType
등 널리 알려진 규약을 따라 짓는 식으로 이 단점을 완화해줘야 함
💡 생성자에 매개변수가 많다면 빌더를 고려하라
“생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하자”
1. 빌더 패턴의 가독성
- 매개변수를 다양화하여 생성자를 여러개 만드는 점층적 생성자 패턴(telescoping constructor pattern)은 클라이언트 코드를 작성하거나 읽기 어렵다.
- 예를 들어 클라이언트가 실수로 매개변수의 순서를 바꿔 건네줘도 컴파일러는 알아채지 못하고, 결국 런타임에 엉뚱한 동작을 하게 됨
- 빌더 패턴은 메서드 체이닝 방식으로 매개변수의 가독성을 높여줌
// 점층적 생성자 패턴 : 각 매개변수가 무엇인지 헷갈림
new NutrionFacts(240, 8, 100, 0, 35, 27);
new NutrionFacts(100, 7, 120, 60);
// 빌더 패턴 : 매개변수가 무엇인지 표현되어 있음
new NutrionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
2. 빌더 패턴의 안전성
- 매개변수가 없는 생성자로 객체를 만든 후, 세터(setter) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식인 자바빈즈 패턴(JavaBeans pattern)은 객체가 완전히 생성되기 전까지 일관성(consistency)이 무너진 상태에 놓임
- 반면에 빌더 패턴은 데이터 일관성, 객체 불변성 등을 만족시킴
// 자바빈즈 패턴
NutrionFacts cocaCola = new NutrionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
💡 private 생성자나 열거 타입으로 싱글턴임을 보증하라
“싱글턴을 만드는 세 가지 방법 : public static final 필드 방식, 정적 팩터리 방식, 열거 타입 방식”
1. public static final 필드 방식
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... }
}
// 사용할 때
Elvis.INSTANCE;
- 장점
- 해당 클래스가 싱글턴임이 API에 명백히 드러남
- 간결함
2. 정적 팩터리 방식
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis();
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() { ... }
}
// 사용할 때
Elvis.getInstance();
- 장점
- 마음이 바뀌면 API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있음
- 원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있음
- 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있음
3. 열거 타입 방식
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
// 사용할 때
Elvis.INSTANCE;
- 대부분 상황에서 이 방법이 가장 좋은 방법이다
- 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다
💡 인스턴스화를 막으려거든 private 생성자를 사용하라
“private 생성자 : 인스턴스화 방지, 상속 방지”
1. 인스턴스화를 막으려는 상황
java.lang.Math
처럼 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때java.util.Collections
처럼 특정 인터페이스의 구현체를 생성해주는 정적 팩터리를 모아놓고 싶을 때final
클래스와 관련한 메서드들을 모아놓을 때
2. private 생성자
- 클래스에 생성자를 명시하지 않으면 컴파일러가 자동으로 매개변수를 받지 않는
public
생성자를 만들어 준다. (즉, 인스턴스화 가능)- 사용자는 이 생성자가 자동생성된 것인지 구분할 수 없음
- 실제로 공개된 API들에서도 의도치 않게 인스턴스화할 수 있게 된 클래스가 종종 있음
private
생성자로 위의 상황을 방지할 수 있음
public class UtilityClass {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용).
private UtilityClass() {
throw new AssertionError(); // 클래스 안에서 실수로라도 생성자 호출하지 않도록 방지
}
}
- 이 방식은 상속을 불가능하게 만들기도 함. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를
private
으로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 수 없음
💡 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
“의존 객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.”
1. 의존 객체
- ex) 맞춤법 검사기 클래스(SpellCheker)를 만들 때, 사전 객체(dictionary)를 사용해야함
- 맞춤법 검사기 클래스가 사전 객체에 의존하는 것
- 맞춤법 검사기는 영어 사전, 한글 사전 등 여러 종류의 사전을 지원해야함.
- 위와 같이 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않음
2. 의존 객체 주입
public class SpellChecker {
private final Lexicon dictionary;
public SpellChecker(Lexicon dictionary) {
this.dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid(String word) { ... }
public List<String> suggestions(String typo) { ... }
}
- 의존 객체 주입 : 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식
- 클래스는 여러 자원 인스턴스를 지원할 수 있고, 클라이언트는 원하는 자원을 사용할 수 있음
- 의존 객체 주입이 유연성과 테스트 용이성을 개선해주긴 하지만, 의존성이 수 천개나 되는 큰 프로젝트에서는 코드를 어지럽게 만들기도 함
- 이는 대거(Dagger), 주스(Guice), 스프링(Spring) 같은 의존 객체 주입 프레임워크를 사용하여 해결하자
💡 불필요한 객체 생성을 피하라
“불필요한 객체를 생성하게 되는 4가지 예시”
1. new String(String);
- 위 문장은 실행될 때마다
String
인스턴스를 새로 만든다. - 위 문장이 반복문 안에 있다면 쓸데없는
String
인스턴스가 수백만 개 만들어 질 수도 있는 것
// 이렇게 쓰자 : 이 방식은 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함
String s = "bikini";
2. new Boolean(String);
- 위 문장도 마찬가지로 실행될 때 마다
Boolean
인스턴스를 새로 만든다. - 대신 정적 팩터리 메서드를 사용하자
// 이렇게 쓰자
Boolean.valueOf(String);
3. String.matches(String)
String.matches
메서드가 만드는 정규표현식용Pattern
인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다.Pattern
은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다
String.matches
를 사용하는 나쁜 예와 좋은 예를 살펴보자
// 나쁜 예 : 호출될 때 마다 matches가 실행 & 내부적으로 Pattern 생성
static boolean isRomanNumeral(String s) {
return s.matches("정규식");
}
/*
좋은 예 : Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고,
isRomanNumeral 메서드가 호출될 때 마다 재사용
*/
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("정규식");
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
4. 오토박싱(auto boxing)
- 오토박싱 : 프로그래머가 기본 타입(ex.
long
)과 박싱된 기본 타입(ex.Long
)을 섞어 쓸 때 자동으로 상호 변환해주는 기술 - 오토박싱으로 성능이 저하되는 예시를 살펴보자
private static long sum() {
Long sum = 0L;
for(long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i; // 여기서 long타입(i)을 Long타입(sum)에 더하기 위해 Long인스턴스가 생성된다.
return sum;
}
sum
변수를long
이 아닌Long
으로 선언해서 불필요한Long
인스턴스가 약 2^31개나 만들어 지게 된다.
→ 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자
💡 다 쓴 객체 참조를 해제하라
“메모리 누수는 겉으로 잘 드러나지 않아 시스템에 수년간 잠복하는 사례도 있다”
1. 자기 메모리를 직접 관리하는 클래스
- 가비지 컬렉터는 프로그래머가 의도치 않게 살려둔 객체를 알아채기 어렵다.
- 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐아니라 그 객체가 참조하는 객체(그리고 또 그 객체들이 참조하는 모든 객체 ...)를 회수해가지 못한다.
- 단 몇 개의 객체가 매우 많은 객체를 회수하지 못하게 할 수 있다는 것
→ 객체 참조를 담는 배열 등을 사용해서 자기 메모리를 직접 관리하는 클래스에서는 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 null
처리 해줘야 한다.
2. 캐시
- 객체 참조를 캐시에 넣어두고, 객체를 다 썼는데도 한참 그냥 놔두는 경우를 조심하자
→ WeakHashMap
를 활용하는 등 상황에 따라 메모리 누수를 방지하자
3. 리스너(listener) 혹은 콜백(callback)
- 클라이언트가 콜백을 등록만하고 명확히 해지하지 않는 경우에 뭔가 조치하지 않는 한 콜백은 계속 쌓여갈 것이다.
→ 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 수거해감.
ex) WeakHashMap
에 키로 저장
💡 finalizer와 cleaner 사용을 피하라
“
finalizer
와cleaner
는 예측할 수 없고, 일반적으로 불필요하다.”
1. 수행시점과 수행여부
- 수행시점 : 예측 불가,
finalizer
와cleaner
는 즉시 수행된다는 보장이 없음- 따라서 객체의 소멸 시점에 의존하는 동작을
finalizer
와cleaner
에 맡기면 안 됨 - ex) 파일 닫기
- C++
destructor
의 동작을 기대하면 안 됨
- 따라서 객체의 소멸 시점에 의존하는 동작을
- 수행여부 : 자바 언어 명세는
finalizer
나cleaner
의 수행 여부조차 보장하지 않음- 따라서 상태를 영구적으로 수정하는 작업을 절대
finalizer
나cleaner
에 맡기면 안 됨 - ex) DB같은 공유 자원의 영구 락(lock) 해제
- 따라서 상태를 영구적으로 수정하는 작업을 절대
2. 성능과 보안 이슈
- 성능 : 느림
finalizer
와cleaner
는 가비지 컬렉터의 효율을 떨어뜨림
- 보안 : finalizer 공격에 취약함
3. 대체제
AutoCloseable
을 구현해서 사용하자!finalizer
나cleaner
를 대신하여 그저AutoCloseable
을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면close
메서드를 호출하면 된다.
💡 try-finally보다는 try-with-resources를 사용하라
1. try-finally
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
- 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 위와 같이
try-finally
가 쓰였다. - 위 코드에서 예외는
try
블록과finally
블록 모두에서 발생할 수 있는데, 예컨대 기기에 물리적인 문제가 생긴다면try
블록 안의readLine
메서드가 예외를 던지고, 같은 이유로close
메서드도 실패할 것이다.
→ 이런 상황에서 스택 추적 내역에는 두 번째 예외의 정보만 남게 되어 디버깅에 어려움을 준다.
- 더하여 이 방법은 자원을 두 개 이상 사용하는 상황(아래 예시)에서 코드가 지저분해진다.
// 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다!
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
2. try-with-resource
// 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다!
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
try-with-resource
버전이 훨씬 간결하고 가독성이 좋다.- 또한
try-finally
방식과는 다르게 숨겨진 예외들도 그냥 버려지지 않고, 스택 추적 내역에 ‘숨겨졌다(suppressed)’는 꼬리표를 달고 출력된다.
'📚 Reading > Tech' 카테고리의 다른 글
이펙티브 자바 - 7장. 람다와 스트림 (0) | 2023.01.24 |
---|---|
이펙티브 자바 - 6장. 열거 타입과 애너테이션 (0) | 2023.01.24 |
이펙티브 자바 - 5장. 제네릭 (0) | 2023.01.24 |
이펙티브 자바 - 4장. 클래스와 인터페이스 (0) | 2023.01.24 |
이펙티브 자바 - 3장. 모든 객체의 공통 메서드 (0) | 2023.01.24 |