객체 생성과 파괴

철근콘크리트·2020년 12월 29일
0

Effective JAVA

목록 보기
1/1

객체를 만들어야 하는 경우와 그렇지 않은 경우에 대해 알아보자.

🔥 생성자 대신 정적 팩터리 메서드를 고려하라.

  • 클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다.

    ( public 방법 말고도 정적 팩터리 메서드를 사용할 수 있다. )



✔ 정적 팩터리 메서드 장점 다섯 가지.

1) 첫 번째, 이름을 가질 수 있다.

2) 두 번째, 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.

  • 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

  • Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다.

  • 플라이웨이트 패턴이 이와 유사하다.

3) 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

  • 반환할 클래스를 자유롭게 선택할 수 있는 '엄청난 유연성'을 가진다.

  • API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.

4) 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

5) 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

  • 이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다. 대표적인 서비스 제공자 프레임워크로는 JDBC가 있다.

  • 서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다. 구현체의 동작을 정의하는 서비스 인터페이스, 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API(provider registration API), 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API가 그 주인공이다.




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

: 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

✔ 프로그래머들이 자주 사용하는 3가지 Pattern에 대해 알아보자.

  • 점층적 생성자 패턴 ( telescoping constructor pattern)
    • 사용하고 싶은 변수를 정해 매개변수를 늘리거나 줄여준다.
    • 단점 : 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려워진다.
  • 자바빈즈 패턴 ( JavaBeans Pattern)
    • 매개변수가 없는 생성자로 객체를 만든 후 세터(setter) 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.
    • 단점 : 객체 하나를 만들려면 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다. 자바빈즈 패턴에서는 클래스를 불변으로 만들 수 없다.
  • 빌더 패턴 ( Builder Pattern )
    (점층적 생성자 패턴과 자바빈즈 패턴을 보완해 나온 내용이 빌더 패턴이다. )
    • 클라리언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다. 그 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.

    • 빌더 패턴은 (파이썬과 스칼라에 있는) 명명된 선택적 매개변수를 흉내 낸 것이다.

    • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

    • 단점 : 빌더 패턴은 객체를 만들려면, 그에 앞서 빌더부터 만들어야 한다. 빌더 생성 비용이 크지는 않지만 성능에 민감한 상황에서는 문제가 될 수 있다. 또한 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있다.

      🔥 생성자나 정적 팩터리 방식으로 시작했다가 나중에 매개변수가 많아지면 빌더 패턴으로 전환할 수도 있지만, 이전에 만들어둔 생성자와 정적 팩터리가 아주 도드라져 보일 것이다. 그러니 애초에 빌더로 시작하는 편이 나을 때가 많다 .


생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.
빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 휠씬 안전하다.





🔥 private 생성자나 열거 타입으로 싱글턴임을 보증하라.

싱글턴이란? 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
    	private Elvis() { ... }
        private void leaveTheBuilding(){ ...}

}

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.



싱글턴을 만드는 2가지

첫번째 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다.

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
    	private Elvis() { ...}
        
        public void leaveTheBuilding() {... }
}

private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출된다.
public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다. 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.


두번째 방법에서는 정적 팩터리 메서드를 public static 멤버로 제공한다.
public class Elvis { 
	private static final Elvis INSTANCE = new Elvis();
    	private Elvis() { ... }
        public static Elvis getInstance(){ return INSTANCE; }
        
        public void leaveTheBuilding(){ ... }

}

public 필드 방식의 큰 장점은 해당 클래스가 싱글턴임이 API에 명백히 드러난다는 것이다.
public static 필드가 final 이니 절대로 다른 객체를 참조할 수 없다.


세번째 방법은 원소가 하나인 열거 타입을 선언하는 것이다.
private enum Elivis {

  INSTANCE;           
  public void leaveTheBuilding(){ ... }

}

public 필드 방식과 비슷하지만, 더 간결하고, 추가 노력 없이 직렬화할 수 있고, 심지어 아주 복잡한 질렬화 상황이나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 완벽히 막아준다.

대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글터을 만드는 가장 좋은 방법이다. 단, 만드려는 싱글턴이 Enum 외에 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.



🔥 인스턴스화를 막으려거든 private 생성자를 사용하라.

  • 추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다. private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
public class UtilityClass{
	//기본 생성자가 만들어지는 것을 막는다.(인스턴스화 방지용)
    private UtilityClass(){
    	throw new AssertionError();
    }

}

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




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

1) 싱글턴을 잘못 사용한 예 - 유연하지 않고 테스트하기 어렵다.

public class SpellChecker {
	private final Lexicon dictionary = ...;
    	
        private SpellChecker(...){}
        public static SpellChecker INSTANCE = new SpellChecker(...);
        
        public boolean isValid(String world){...}
        public List<String> suggestions(String type){...}


}

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




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){ ... }

}

이 패턴의 쓸만한 변형으로는, 생성자에 자원 팩터리를 넘겨주는 방식이다. 팩터리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 넘겨주는 방식이다.



🔥 불필요한 객체 생성을 피하라.

  • Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
    (생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.)

1) 정규 표현식을 잘 사용하면 성능을 훨씬 더 끌어올릴 수 있다.

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})$");

}

String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다. 이 메서드가 내부에서 만드는 정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.

2) 값비싼 객체를 재사용해 성능을 개선한다.

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 인스턴스를 static final 필드로 끄집어내고 이름을 지어줘 코드의 이미가 훨씬 잘 드러난다.

개선된 isRomanNumeral 방식의 클래스가 초기화된 후 이 메서드를 한 번도 호출하지 않는다면 ROMAN 필드는 쓸데없이 초기화된 꼴이다.

예컨대 Map 인터페이스의 keyset 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다.

불필요한 객체를 만들어내는 또 다른 예로 오토박싱(auto boxing)을 들 수 있다. 오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.

아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 여러분만의 객체 풀을 만들지는 말자. 물론 객체 풀을 만드는 게 나은 예가 있다. 데이터베이스 연결 같은 경우 생성 비용이 워낙 비싸니 재사용하는 편이 낫다.
( 요즘 JVM의 가비지 컬렉터는 상당히 잘 최적화되어서 가벼운 객체용을 다룰 때는 직접 만든 객체 풀보다 훨씬 더 빠르다.)


🔥 다 쓴 객체 참조를 해제하라.

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

  • 해법은 간단하다. 해당 참조를 다 썼을 때 null 처리하면 된다.

  • 그렇다면 null 처리는 언제 해야 할까? Stack 클래스는 왜 메모리 누수에 취약한 걸까? 바로 스택이 자기 메모리를 직접 관리하기 때문이다.

  • 자기 메모리를 직접 관리하는 클래스라면 프로그래머는 항상 메모리 누수에 주의해야 한다. 원소를 다 사용한 즉시 그 원소가 참조한 객체들을 다 NULL 처리해줘야 한다.

  • 캐시 역시 메모리 누수를 일으키는 주범이다.
    (캐시를 만들 때 보통은 캐시 엔트리의 유효 기간을 정확히 정의하기 어렵기 때문에 시간이 지날수록 엔트리의 가치를 떨어뜨리는 방식을 혼히 사용한다. 이런 방식에서는 쓰지 않는 엔트리를 이따금 청소해줘야 한다.-> LinkedHashMap은 removeEldestEntry 메서드를 써서 후자의 방식으로 처리한다.

  • 메모리 누수의 세 번째 주범은 바로 리스너(listener) 혹은 콜백(callback)이라 부르는 것이다.
    (클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다.)



🔥 finalizer와 cleaner 사용을 피하라.

  • 자바는 두 가지 객체 소멸자를 제공한다. 그 중 finailzer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다. cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
  • finalizer나 cleaner를 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별이다.

  • 클래스에 finalizer를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.

  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안된다.

0개의 댓글