본문 바로가기
Java/Effective Java

[이펙티브 자바] - 객체의 생성과 파괴

by bloodFinger 2020. 12. 31.

블로그의 내용은 '이펙티브 자바'를 참고하였습니다.

책의 내용정리와 저의 생각을 포스팅 합니다. 

 

 

 

 

이번 장은 객체의 생성과 파괴를 다룬다. 

  • 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는법 
  • 올바른 객체 생성 방법과 불필요한 생성을 피하는 방법
  • 제때 파괴됨을 보장하고 파괴 전에 수행해야 할 정리 작업을 관리하는 요령

 

 

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라 

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.

아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보장하라.

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라.

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

아이템 6. 불필요한 객체 생성을 피하라.

아이템 7. 다 쓴 객체 참조를 해제하라.

아이템 8.finallizer와 cleaner 사용을 피해라.

아이템 9. try-finally 보다는 try-with-resources 를 사용하라.

 

 


아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라.

일반적으로 자주 사용하는 setter는 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.

 

또한 점층적 생성자 패턴도 쓸수있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

 

빌더 패턴

public class NutritionFacts2 {
	
	private final int a;
	private final int b;
	private final int c;
	private final int d;
	
	public static class Builder {
		private final int a;
		private final int b;
		
		private int c=0;
		private int d=0;
		
		public Builder(int a , int b){
			this.a = a;
			this.b = b;
		}
		
		public Builder c(int val){
			c = val;
			return this;
		}
		
		public Builder d(int val){
			d = val;
			return this;
		}
		
		public NutritionFacts2 build() {
			return new NutritionFacts2(this);
		}
	}
	
	private NutritionFacts2(Builder builder) {
		a = builder.a;
		b = builder.b;
		c = builder.c;
		d = builder.d;
	}
	
	
	public static void main(String[] args) {
		NutritionFacts2 nf2 = new NutritionFacts2.Builder(2, 3).c(4).d(5).build();
		System.out.println("nf2 : " + nf2.a + "/" + nf2.b +"/"+ nf2.c +"/" + nf2.d);
	}
}

 

 

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더  낫다.

 

아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보장하라.

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라.

 

단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들고 싶을 때가 있다.

객체 지향적으로 사고하지 않는 이들이 종종 남용하닌 방식이라 하지만 , 나름 쓰임새가 있다.

 

예를들어 java.lang.Math , java.lang.Arrays 처럼 기본타입 값이나 배열 관련 메서드 놓아두거나

java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드를 모아 놓을수 있다.(자바8부터)

마지막으로 final 클래스와 관련된 메서드들을 모아놓을 때도 사용한다.

 

추상클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.

왜냐하면 하위 클래스를 만들어서 인스턴스화 하면 그만인다.

 

다행히 인스턴스화를 막는 방법은 간단하다.

public class Test1 {
	
	private Test1(){
		throw new AssertionError();
	}
	
	public static void main(String[] args) {
		Test1 t = new Test1();
	}
}

결과

그런데 생성자가 분명 존재하는데 호출할 수 없으니 앞의 코드에 적절한 주석을 달아주자!

 

이방식은 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데 , 이를 private으로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀버린다.

 

 

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.

public class SpellChecker {
    private static final Lexicon dictionary = ... ;
    
    private SpellChecker(Lexicon dictionary) {} //객체생성 방지

    public boolean isValid(String word) {...}
    public List<String> suggestions(String typo) {...}
}

 

public class SpellChecker {
    private final Lexicon dictionary = ...;
    
    private SpellChecker(...) {} 
    public static SpellChecker  INSTANC = new SpellChecker(...);

    public boolean isValid(String word) {...}
    public List<String> suggestions(String typo) {...}
}

위에 두 방식 모두 사전을 단 하나만 사용한다고 가정한다는 점에서 그리 훌륭하지 않다.

 

그렇다면 , dictionary 필드에서 final 한정자를 제거하고 다른 사전으로 교체하는 메서드를 추가할 수 있지만 , 아쉽게도 이 방식은 오류를 내기 쉬우며 멀티쓰레드 환경에서 는 쓸 수 없다.

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.

 

 

클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용하려면...

인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식을 사용한다.

//의존 객체 주입(팩토리 메서드 패턴) - 유연함과 테스트 용이성을 높여준다.
public class SpellChecker {
    private final Lexicon dictionary;
    
    public SpellChecker(Lexicon dictionary) {
    	this.dictionary = dictionary;
    }

	public boolean isValid(String word) {...}
    public List<String> suggestions(String typo) {...}
}

 

 

아이템 6. 불필요한 객체 생성을 피하라.

String s = new String("biki");
//이 문장은 실행될 때마다 String 인스턴스를 새로 만든다.
String s = "biki";
//이 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.

우리는 아이템 1. 에서 불변 클래스에서 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다. 라는 점을 배웠다.

 

예컨데, Boolean(String) 생성자 대신 Boolean.ValueOf(String) 팩터리 메서드를 사용하는 것이 좋다.

왜냐하면 생성자는 호출할 때마다 새로운 객체를 만들지만 , 팩터리 메서드는 전혀 그렇지 않다.

 

 

아이템 7. 다 쓴 객체 참조를 해제하라.

자바 처럼 가비지 컬렉터를 갖춘 언어로 넘어오면 프로그래머의 삶이 평안해진다.

다 쓴 객체를 알아서 회수해가니 말이다.

그런데 자칫 메모리 관리에 더 이상 신경 쓰지 않아도 된다고 오해할 수 있는데, 절대 사실이 아니다.

 

Stack 클래스를 보면서 찾아보자

public class Stack{ 
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITAL_CAPACITY = 16;
    
    /** 
    * 생성자
    */
    public Statck(){
    	elements = new Object[DEFAULT_INITAL_CAPACITY];
    }
    
    public void push(Object e) {
    	ensureCapacity();
        elements[size++] = e;
    }
    
    public Object pop(){
    	if(size == 0) 
        	throw new EmptyStackException();
        return elements[--size];
    }
    
   /** 
    * 원소가 꽉찼을때 2배로 늘리는 메소드
    */
    public void ensureCapacity() {
    	if(elements.length == size) {
        	elements = Arrays.copyOf(elements , 2*size+1);
        }
    }
    
}

 

이 스택을 사용하는 프로그램을 오래 실행하다 보면 점차 가비지컬렉션 활동과 메모리 사용량이 늘어나 결국 성능이 저하될 것이다. 상대적으로 드믄 경우긴 하지만 심할 때는 디스크 페이징이나 OutOfMemoryError를 일으켜 프로그램이 예기치 않게 종료되기도 한다.

 

가비지 컬렉션 언어에서는 (의도치 않게 객체를 살려두는) 메모리 누수를 찾기가 아주 까다롭다. 객체 참조 하나를 살려두면 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체(그리고 또 그 객체들이 참조하는 모든 객체) 를 회수해 가지 못한다. 그래서 단 몇개의 객체가 매우 많은 객체를 회수되지 못하게 할 수 있고 잠재적으로 성능에 악영향을 줄 수 있다.

 

해법은 간단한다. 다 쓴 객체를 null로 참조 해제 하는 것이다.

public Object pop(){
    if(size == 0) 
        throw new EmptyStackException();
    Object obj = elements[--size];
    elements[size] = null;  //다 쓴 객체 참조 해제
    return obj;
}

 

하지만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

다쓴 참조를 해제하는 가장좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.

여러분이 변수의 범위를 최소가 되게 정의했다면(아이템57) 이 일은 자연스럽게 이뤄진다.

 

이거 말고도 캐시 또한 메모리 누수를 일으키는 주범이다.

 

 

아이템 8.finalizer와 cleaner 사용을 피해라.

 

  • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
  • cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고 , 일반적으로 불필요하다.
  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
  • finalizer 와 cleaner는 심각한 성능 문제도 동반한다.
  • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안문제를 일으킬 수도 있다.

정리

cleaner(자바 8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자.

물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

 

 

 

아이템 9. try-finally 보다는 try-with-resources 를 사용하라.