생각해보기
이펙티브 자바 -1- 본문
객체의 생성과 파괴
1. 생성자 대신 정적 팩터리 메서드를 고려하라
정적 팩터리 메서드란
생성자와 별도로 static 으로 객체를 반환하는 것
예시
public static Boolean ValueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
정적 팩터리 메서드가 생성자 보다 좋은 이유
- 이름을 가질 수 있다 : 생성자 매개 변수와 생성자 자체만으로는 객체의 특성을 설명을 잘 못하는 데, 정적 팩터리 메서드는 이름을 가질 수 있으므로 객체의 특성을 쉽게 설명할 수 있다
- 호출 될 때 마다 인스턴스를 새로 생성하지 않아도 된다 : 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 메서드는 인스턴스의 생성을 통제할 수 있다. 이는 객체를 효율적으로 사용할 수 있다.
- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다 : 팩토리 패턴과 같이 반환할 클래스를 자유롭게 선택할 수 있게 만듦으로써 유연성을 제공해 줄 수 있다.
- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다 : 반환 타입의 하위 객체를 반환할 수 있기 때문에 조건에 맞게 반환할 수 있는 유연성이 있다.
- 정적팩터리 메서드를 작정하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다 : 이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다
* 서비스 제공자 프레임 워크
제공자는 서비스의 구현체다. 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로 부터 분리해준다.
서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이루어진다.
- 구현체의 동작을 정의하는 서비스 인터페이스
- 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
- 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 사용자 서비스 API
- 서비스의 인스턴스를 생성하는 팩터리 객체를 설명하는 서비스 제공자 인터페이스 (추가적으로 쓰임)
클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있고, 조건은 하지 않으면 기본 구현체를 반환하거나 지원하는 구현체를 순회하면서 반환한다. 이러한 서비스 접근 API가 유연한 정적 팩토리를 사용할 수 있다
정적 팩터리 메서드의 단점
- 상속을 하려면 public이나 protected 생성자가 필요한데 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다(생성자를 의도적으로 private 만들어 정적 팩터리 메서드를 사용하도록 강제한 경우)
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다 : 생성자 처럼 API 설명에 명확히 드러나지 않으므로 문서화를 잘해야 한다
정적 팩터리 명령 방식
- from : 매개 변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
- ex) Date d = Date.from(instant);
- of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
- ex) Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf : from과 of의 더 자세한 버전
- ex) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
- instance 혹은 getInstance : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다
- ex) StackWalker luke = StackWalker.getInstance(options);
- create 혹은 newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다
- ex) Object newArray = Array.newInstance(classObject, arrayLen);
- getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 Type
- ex) FileStore fs = Files.getFileStore(path)
- newType : newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. "Type"은 팩터리 메서드가 반환할 Type
- ex) BufferedReader br = Files.newBufferdReader(path);
- type : getType과 newType의 간결한 버전
- List<Complaint> litany = Collections.list(legacyLitany);
정리
정적 팩터리 메서드와 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; // 선택
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;
}
}
단점 : 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 매개변수가 추가되는 경우 유지보수가 힘들다.
자바빈즈 패턴 - 매개변수 없는 생성자로 객체를 만든후, 세터 메서드들을 호출해 값을 설정하는 방식
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 servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
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 void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
// 호출시
class NutritionFactsRunner{
public static void main(String args[]{
NutritionFacts cocalCola = new NutritionFacts();
cocalCola.setServingSize(240);
cocalCola.setServings(8);
cocalCola.setCalories(100);
cocalCola.setSodium(35);
cocalCola.setCarbohydrate(27);
}
}
단점 : 객체 하나를 만들기 위해서는 메서드 여러개를 호출해야 하고, 객체가 완전히 완성되기 전까지 일관성이 무너진 상태에 놓인다. 일관성이 무너지는 문제 때문에 클래스를 불변으로 만들 수 없으며 스레드 안전성을 얻으려면 추가 작업이 필요하다.
빌더 패턴 - 자바빈즈 패턴과 점층적 생정자 패턴 장점 혼합
public class NutritionFacts {
private int servingSize; // 필수
private int servings; // 필수
private int calories;
private int fat;
private int sodium;
private int carbohydrate;
public static class Builder {
// 필수 매개변수
private int servingSize;
private int servings;
// 선택 매개변수
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
private Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
public Builder servingSize(int servingSize) {
this.servingSize = servingSize;
return this;
}
public Builder servings(int servings) {
this.servings = servings;
return this;
}
public Builder calories(int calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
public NutritionFacts build() {
NutritionFacts nutritionFacts = new NutritionFacts();
nutritionFacts.fat = this.fat;
nutritionFacts.sodium = this.sodium;
nutritionFacts.servings = this.servings;
nutritionFacts.carbohydrate = this.carbohydrate;
nutritionFacts.servingSize = this.servingSize;
nutritionFacts.calories = this.calories;
return nutritionFacts;
}
}
}
// 호출시
class NutritionFactsRunner{
public static void main(String args[]{
NutritionFacts cocalCola = new NutritionFacts.Builder(240, 8)
.calories(100)
.sodium(35)
.carbohydrate(27)
.build();
}
}
일관성을 유지하면서 사용하기 쉽고 읽기 쉽다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기 좋다
정리
생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 것이 더 낫다. 읽고 쓰기가 간편하고 안전하다.
3. private 생성자나 열거 타입으로 싱글턴 임을 보증하라
싱글턴이란 인스턴스를 오직 하나만 생성할 있는 클래스를 말한다.
싱글턴을 만드는 방식은 둘 중 하나다. 두 방식 모두 생성자를 private으로 감추어 두고, 유일한 인스턴스에 접근하는 수단으로 public static 멤버를 마련한다.
1. public static 멤버가 final 필드 방식
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
해당 클래스가 싱글턴 임을 명확히 나타낼 수 있다. 또한 간결하다
2. 정적 팩터리 방식의 싱글턴
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
API를 바꾸지 않아도 싱글턴이 아니게 변경할 수 있다(getInstance 내부만 바꾸면 된다), 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
* 이러한 장점들이 필요 없을 때는 public 필드 방식이 좋다
* 추가
원소가 하나인 열거 타입 선언 (추천)
public enum Singleton {
INSTANCE;
public void doSomething(){...}
}
간편하고, 직렬화 할 수 있다. 복잡한 직렬화 상황이나 리플렉션 공격에도 제 2의 인스턴스가 생기는 것을 막아준다.
단, 싱글턴이 Enum 외의 클래스를 상속해야 한다면 사용할 수 없다.
* 추가
홀더 클래스를 사용하여 jvm 특성을 이용한 싱글턴(추천) - lazyHolder
public class Singleton {
private Singleton() {
}
// lazyHolder
// static class?
// 클래스의 역할은 인스턴스를 만드는 것, 클래스 자체가 인스턴스 일 수 없다
// 따라서 static이라는 키워드가 클래스에 붙는 것은 인스턴스를 생성하는 방식이 달라지는 것이다
// 내부 클래스의 static을 붙이면, 외부 인스턴스 없이 내부 클래스의 인스턴스를 바로 생성할 수 있다.
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.instance;
}
}
이너 클래스 SingletonInstance는 Singleton의 getInstance에 접근할때 생성된다. 따라서 lazy하게 생성가능,
(초기에는 아무런 상태가 없기에 LazyHolder 클래스를 초기화하지 않지만, getInstance 메서드가 호출될 때 LazyHolder 가 로딩되며 초기화가 진행된다)
내부변수 Singleton은 final이면서 static이니까 무조건 1개인게 보장된다. 따라서 sync하게 사용할 수 있다.
4. 인스턴스화를 막으려거든 private 생성자를 사용하라
정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때, final 클래스와 관련한 메서드들을 모아 놓는 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 것이 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 생성자를 추가해준다. 따라서 사용자가 의도치 않게 인스턴스를 만들어 사용할 수 있다.
추상 클래스로 유틸리티 클래스를 만들어 제한 하는 것은 하위 클래스를 만들어 인스턴스화할 수 있으며 사용자가 오해하여 사용할 수 있다.
유틸리티 클래스는 private 기본 생성자로 클래스의 인스턴스화를 막는 것이 좋다. 상속도 불가능하게 하므로 효과적이다.
5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
사용하는 자원에 따라 동작이 달라지는 클래스는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다. 사용하는 자원이 변경가능하다면 자원을 내부에서 생성하지 말고, 클래스 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 것이 좋다(의존성 주입). 의존성을 주입 받음으로써 클래스를 유연하고 재사용 가능하게 만들어 준다.
6. 불필요한 객체 생성을 피해라
똑같은 기능의 객체를 매번 생성하는 것보다 객체 하는 재사용하는 편이 나을 때가 많다. 재사용 객체는 생성 비용을 줄여줄 뿐만아니라 객체가 불변이라면 안전하다.
박싱된 기본 타입보다는 기본 타입을 사용하고, 의도하지 않은 오토박싱이 숨어들지 않도록 주의하라
기존 객체를 재사용해야 한다면 새로운 객체를 만들지 마라
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);
}
}
이 코드에서 스택이 커졌다가 줄어들었을 때 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수 하지 않는다.
- 이 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.
- 여기서 다 쓴 참조란 문자 그대로 앞으로 다시 쓰지 않을 참조를 뜻한다.
- 앞의 코드에서는 elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다. 활성 영역은 인덱스가 size보다 작은 원소들로 구성된다.
객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체 뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체)를 회수해가지 못한다. 그래서 단 몇 개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.
개선된 코드
public class Stack {
// ...
public Object pop() {
if (size == 0) {
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
}
다 쓴 참조를 null 처리하면 다른 이점도 따라온다. 만약 null 처리한 참조를 실수로 사용하면 즉시 NullPointerException으로 프로그램의 오류를 발견할 수 있다.
모든 객체를 다 쓰자마자 null로 변경할 필요는 없다. 오히려 코드가 지저분해진다. 객체 참조를 null로 처리하는 경우는 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다. 변수의 범위를 최소가 되게 정의하자.
*메모리 누수를 줄이고자 WeakHashMap을 사용하는 것도 좋은 방법이다.
WeakHashMap (약한 참조 해시맵)
WeakHashMap의 작동 방식을 이해하려면 JVM의 GC와 관련하여 WeakReference 를 조금은 이해할 필요가 있다. Java에서는 세 가지 주요 유형의 참조(Reference) 방식이 존재한다.
- 강한 참조 (Strong Reference)
– Integer prime = 1; 와 같은 가장 일반적인 참조 유형이다. prime 변수 는 값이 1 인 Integer 객체에 대한 강한 참조 를가진다. 이 객체를 가리키는 강한 참조가 있는 객체는 GC대상이 되지않는다. (new로 새로운 객체를 만들어 해당 객체를 참조하는 방식) - 부드러운 참조 (Soft Reference)
– SoftReference<Integer> soft = new SoftReference<Integer>(prime); 와 같이 SoftReference Class를 이용하여 생성이 가능하다. 만약 prime == null 상태가 되어 더이상 원본(최초 생성 시점에 이용 대상이 되었던 Strong Reference) 은 없고 대상을 참조하는 객체가 SoftReference만 존재할 경우 GC대상으로 들어가도록 JVM은 동작한다. 다만 WeakReference 와의 차이점은 메모리가 부족하지 않으면 굳이 GC하지 않는 점이다. 때문에 조금은 엄격하지 않은 Cache Library들에서 널리 사용되는 것으로 알려져있다. - 약한 참조 (Weak Reference)
– WeakReference<Integer> soft = new WeakReference<Integer>(prime); 와 같이 WeakReference Class를 이용하여 생성이 가능하다. prime == null 되면 (해당 객체를 가리키는 참조가 WeakReference 뿐일 경우) GC 대상이 된다. 앞서 이야기 한 내용과 같이 SoftReference와 차이점은 메모리가 부족하지 않더라도 GC 대상이 된다는 것이다. 다음 GC가 발생하는 시점에 무조건 없어진다. (WeakReference을 이용해 new로 만든 객체를 참조하는 방식)
8. finalizer와 cleaner 사용을 피해라
자바는 두 가지 객체 소멸자를 제공한다(finalizer, cleaner). finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 즉, finalizer와 cleaner 로는 제때 실행되어야하는 작업은 절대 할 수 없다.
이 소멸자 함수를 얼마나 신속히 실행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 첨차만별이다.
finalizer와 cleaner 대신 종료해야할 자원을 담고 있는 객체의 클래스에서 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출해라.(예외 발생해도 제대로 종료되도록 *try-with-resources 사용)
* try-with-resources : try에 자원 객체를 전달하면, try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능
9. try-finally보다는 try-with-resources를 사용하라
자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. 자원 닫기는 클라이언트가 놓치기 쉬워 예측할 수 없는 성능 문제로 이어지기도 한다. (상당수는 안전망으로 finalizer를 활용하지만 믿음직스럽지 않음)
전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다.(finally 에서 close 호출)
기존의 try-finally
public class Item {
private static final int BUFFER_SIZE = 0; // 편의상 0으로 지정
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();
}
}
}
자원이 둘 이상이면 try-finally 방식은 너무 지저분하다.
개선된 try-with-resources
public class Item {
private static final int BUFFER_SIZE = 0; // 편의상 0으로 지정
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);
}
}
}
}
이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다
정리
회수하는 자원을 다룰 때 try-finally 말고, try-with-resources를 사용하자. 코드는 더 짧고 분명해지고 정확하고 쉽게 자원을 회수할 수 있다.
'자바' 카테고리의 다른 글
이펙티브 자바 -3- (0) | 2022.01.03 |
---|---|
이펙티브 자바 -2- (0) | 2021.12.30 |
모던 자바 인 액션 -16- (0) | 2021.12.28 |
모던 자바 인 액션 -15- (0) | 2021.12.27 |
모던 자바 인 액션 -14- (0) | 2021.12.27 |