[이펙티브자바] 2장. 객체 생성과 파괴

이펙티브 자바 2장. 객체 생성과 파괴

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

클래스의 인스턴스를 얻는 전통적인 수단인 public 생성자 이외에도 static factory method를 통해 더 효율적으로 클래스를 제공할 수 도 있다.(장단점 존재)

  • 장점
    1. 이름을 가질 수 있다.
      • 이름만 잘지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
      • 한 클래스에 시그니처가 같은 생성자가 여러개 필요할 것 같으면, 입력 매개변수의 순서를 바꾸기 보다는 정적 팩터리 메서드로 변경하고 차이가 드러나는 이름을 지어주자
    2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
      • 같은 객체가 자주 요청되는 상황에서 성능을 끌어올려 준다.
      • 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아있게 할지 통제할 수 있다.
    3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
      • 반환할 객체의 클래스를 자유롭게 선택할 수 있게하는 유연성 제공(java.util.Collection 에서도 이를 통해 api를 작게 유지하고, 사용자가 편리하게 사용가능하다.)
    4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다
      • 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.(EnumSet 클래스의 경우 원소의 수에 따라 반환되는 인스턴스(하위 클래스의 인스턴스)가 다르다.)
      • 사용자는 이를 알필요도 없고 그냥 사용하면 되고, 그렇기때문에 개발자는 상황에 맞게 하위클래스를 마음대로 변경 할 수 있다.
    5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
      • JDBC
  • 단점
    1. 하위클래스를 만들 수 없다.
      • 상속 불가
    2. 프로그래머가 찾기 어렵다
  • 결론

    정적팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 장단점을 이해하고 각각 유리한 경우 선택적으로 사용해야한다.

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

선택적 매개변수가 많을 경우 생성자, 정적 팩터리를 사용하여 구현하면 어려움이 있을 수 있다.

  • 생성자 패턴의 경우 가독성이 떨어져, 잘못된 데이터 전달로 오류를 유발할 수 있다.
  • 자바빈즈 패턴의 경우 불완전한 객체가 생성될 수 있어 에러를 유발할 수 있다

따라서 이럴경우 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더패턴을 사용하면 좋다.

  • 빌더패턴
    • 클라이언트는 필요한 객체를 직접 만드는 대신, ‘필수 매개변수’만으로 생성자를 호출해 “빌더객체”를 얻는다.
    • “빌더객체”의 Setter 메서드를 사용하여 나머지 ‘선택 매개변수’를 입력하고 .build() 를 호출하여 필요한 객체를 얻는다.
    • 이때 빌더객체의 Setter 메서드는 빌더 자신을 반환하기 때문에 연쇄적으로 호출(method chaining)할 수 있다.
        User user1 = new User.Builder(SN10525, pw1234).email(taeho.kim20@snowcorp.com).gender(M).build(); 
      
    • 계층적으로 설계된 클래스와 함께 쓰기에 좋다.(하위클래스의 메서드가 상위클래스에서 정의한 타입이 아닌 하위에서 정의한 타입을 반환(공변 반환 타이핑)할 수 있음)
  • 단점
    • 빌더가 생성되야하기 때문에 성능에 민감한 경우 문제가 될 수 있다.
    • 코드 양이 늘기 때문에 매개변수가 많은 경우에만 사용하는것이 좋다.
  • 결론

    처리해야할 매개변수가 많다면 빌더패턴을 선택하는 것이 더 낫다.

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

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 설계상 유일해야 하는 시스템 컴포넌트와 같은 경우가 이에 해당한다.

일반적으로 싱글턴을 구현하는 방법으로는

  • public static final 로 인스턴스를 바로 제공하는 방식
  • public static 메서드로 private static final 인스턴스를 제공하는 방식

이 일반적이다. 하지만 이 경우 리플랙션 공격에 취약하고, 직렬화과정에서 모든 인스턴스 필드를 transient라고 선언하고, readResolve 메서드를 제공하지 않으면 역직렬화시 제2의 인스턴스가 생길 위험이 있다.

이러한 문제점이 발생하지 않는 방법으로는 원소가 하나인 열거타입을 선언하는 것이다.

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

이와 같은 방법으로 앞서 소개된 위험을 해결 할 수 있으며, 간결하게 작성할 수 있다.

하지만 Enum 외의 클래스를 상속해야한다면 사용할 수 없다는 단점이 있다.

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

일반적으로 인스턴스 생성시 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다. 따라서, 인스턴스화를 막고싶을 경우 private 생성자를 사용하면 클래스 바깥에서는 접근할 수 없게 된다.

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

클래스가 하나 이상의 자원에 의존하며 사용하는 자원에 따라 동작이 달라지는 경우 싱글톤(item3)이나 정적유틸리티(item4) 을 사용하는 것은 부적합하다.

이런 경우 의존 객체 주입을 사용해야한다. (인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식)

public class SpellChecker {
    private final Lexicon dictionary;
    
    public SpellChecker (Lexicon dictionary) {
        this.dictionary = Object.requireNonNull(dictionary)
    }
}

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

똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 것이 좋을때가 많다.

String s = new String("bikini")

위와 같이 “bikini” 자체가 String의 인스턴스인데 불필요하게 String 객체를 매번 재생성할 필요는 없다.

private static long sum() {
    Long sum = 0L;
    for (long i = o; i<= Integer.MAX_VALUE; i++){ sum += i; }
    return sum;
}

위의 경우 sum이 Long으로 i가 long선언되어 있기 때문에 둘을 더할때마다 오토박싱(자동 형변환)이 일어나 Long 객체가 불필요하게되고(불필요한 객체 생성) 이는 성능 저하를 야기한다.

특히, ‘비싼 객체’의 반복적 생산은 지양하고 캐싱하여 재사용하는 편이 좋다.

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

위의 경우 정규식을 변환해주는 전 과정을 한번에 실행한다. 위의 코드가 반복적으로 실행될시 성능 저하가 일어날 수 있다.

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();
	}
}

하지만 위의 경우 String.matches 내부에서 사용되는 Pattern 인스턴스를 ROMAN으로 저장해 놓고, 재사용하여 Pattern 인스턴스의 재사용을 통해 성능을 개선할 수 있다.

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

자바는 가비지 컬렉터를 갖춘언어로 대부분의 경우 알아서 메모리를 관리해준다. 하지만 그렇지 못한 예외의 경우에 발생할 수 있는 메모리 누수에 미리 대비할 필요가 있다.

  1. 메모리를 직접 관리하는 경우 프로그램상에서는 더 이상 쓰이지 않는 참조가 있다면 null을 주어 해제해야 한다.(null을 주면 잘못된 참조 사용시 NPE를 유발시켜 잘못된 로직을 미리 잡을 수 있다.) 가비지 컬렉터는 객체 참조 하나를 상려두면 그 객체가 참조하는 모든 객체까지 회수 할 수 없기때문에 메모리 누수정도가 빠르게 늘어날 수 있다.
  2. 캐시를 사용하는 경우 WeakHashMap을 사용하면 다 쓴 엔트리는 자동으로 삭제될 수 있다. 하지만 캐시 엔트리의 유효기간을 정의하기 어렵기 때문에 보통은 시간이 지날수록 가치를 떨어트리는 방식을 사용한다.
  3. 리스너와 콜백에서 명확히 해지하지 않는경우 약한참조로 저장하면 명확히 해지하지 않아도 가비지컬렉터가 수거해 간다.

Item 8. finalizer 와 cleaner 사용을 피하라

자바에서 제공하는 객체소멸자인 finalizer와 cleaner의 사용을 피하는 것이 좋다.

  1. 예측할 수 없고, 여전히 느리고, 상황에 따라 위험할 수 있다.
  2. C++ 에서 사용되는 destructor와는 다른 개념이다.
    • 자바에서는 가비지 컬렉터가 접근할 수 없게 된 객체를 회수
  3. 실행되기까지 얼마나 걸리는지(수행시점) 알 수 없으므로 제때 실행되어야 하는 작업은 절대 할 수 없다.
  4. 실행 여부조차 보장하지 않기때문에(접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수 있는 위험이 존재) 상태를 영구적으로 수정하는 작업에서는 절대 사용해서는 안된다.
  5. 심각한 성능문제를 야기한다(가비지 컬렉터의 효율을 떨어뜨린다)
  6. finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다.
  • 대안

    파일이나 스레드등 종료해야할 자원을 담고 있는 객체의 클래스에서 효과적으로 자원을 회수하기 위해서는 finalizer와 cleaner 대신 AutoCloseable을 구현하고, 클라이언트에서 인스턴스를 다 쓰고나면 close 메서드를 호출해야 한다.(close 메서드에서 이 객체가 더이상 유효하지 않다고 기록하여, 닫힌후 호출됬을경우 에러를 발생시켜야 한다)

Item 9. try-finally 보다는 try-with-resources를 사용하라

자바라이브러리에는 close 메소드를 호출해 직접 닫아야 하는 자원이 많고, 그럴경우 AutoCloseable 인터페이스를 구현해야한다. AutoCloseable 인터페이스는 try-with-resources를 사용하는 것이 바람직하다. try-finally를 사용해서 close 메소드를 호출할 경우 만약 try, finally 모두에서 예외가 발생한다면 try에서의 에러는 감추어지고 finally에서의 에러만 나오기 때문에 추적이 쉽지 않기 때문이다. 또한 catch 문도 같이 사용할 수 있어 예외처리에도 효과적이며 코드를 간결하게 구성할 수 있다.


© 2020. All rights reserved.