Lee's Grow up

[EFFECTIVE JAVA 3/E] 객체 생성자 파괴 본문

PROGRAMMING/JAVA

[EFFECTIVE JAVA 3/E] 객체 생성자 파괴

효기로그 2019. 11. 4. 10:15
반응형

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

아이템 목록


  1. 생성자 대신 정적 팩토리 메소드를 고려하라
  2. 생성자에 매개변수가 많다면 빌더를 고려하라
  3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
  4. 인스턴스화를 막으려거든 private 생성자를 사용하라
  5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
  6. 불필요한 객체 생성을 피하라
  7. 다 쓴 객체 참조를 해제하라
  8. finalizer와 cleaner 사용을 피하라
  9. try - finally 보다는 try - with - resources를 사용하라

1. 생성자 대신 정적 팩토리 메소드(static factory method)를 고려하라


  • 클래스는 생성자와 별도로 그 클래스의 인스턴스를 반환하는 단순한 정적 메소드를 제공할 수 있다.
  • 정적 팩토리 메소드는 디자인 패턴의 팩토리 메소드와 다르다. 어떤 디자인 패턴중 일치하는 패턴은 없다.
      public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE : Boolean.FALSE; }
    

장점

  1. 이름을 가질 수 있다
    생성자 자체와 넘겨주는 매개변수 많으로 생성자의 특성을 쉽게 알 수가 없다.
    LeeGrowUp() , LeeGrowUp(int) , LeeGrowUp(int,String) 해당 생성자를 보고 각각의 생성자가 어떤 역할을 하는지 특성을 알수 있는가? 반면 _정적 팩토리_는 이름만 잘 지으면 반환될 객체의 특성을 묘사할 수 있다.
    • BigInteger(int, int,Random)BigInteger.probaleprime( ) 중 어떤 것이 소수 BigInteger 를 반환한다는 의미가 명확한지 생각해 보자.
  2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다. 이 덕분에 불변 클래스는 불필요한 객체 생성을 피할 수 있고, 인스턴스 통제 클래스로 만들 수 있다. 인스턴스 통제 (instance-controlled) 클래스는 인스턴스를 통제할 수 있는 클래스를 말한다.
    • 싱글톤(singleton), 인스턴스화 불가 (noninstantiable), 불변 값 클래스에서 동치인 인스턴스가 단 하나임을 보장할 수 있다 ( a == b 일 때만 a.equals(b)가 성립 ) , 또한 인스턴스 통제는 플라이웨이트 패턴의 근간이 된다.
  3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. 이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 유연성을 선물하고, API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 객체를 반환할 수 있어 API를 작게 유지할 수 있다. 이는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. ( 반환 타입의 하위타입이면 된다. )
    • EnumSet 클래스는 생성자 없이 정적 팩토리만 제공하는데, 원소의 수에 따라 하위 클래스 중 하나의 인스턴스 반환 ex ) EnumSet 원소가 64개 이하이면 RegularEnumSet, 65개 이상이면 JumboEnumSet를 반환
  5. 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    • 이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다 ( JDBC 등 ) 이런 서비스 제공자 프레임워크 패턴에는 브리지 패턴(Bridge pattern), 의존 객체 주입(dependency injection, 의존성 주입) 등이 있다.

단점

  1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
    상속보다 컴포지션을 사용하도록 유도하고, 불변타입으로 만드려면 이 제약을 지켜야 한다는 점에서 강점일수도 있다.
  2. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다. 생성자처럼 API에 명확히 드러나지 않는다, 정적 메소드는 메소드일 뿐으로 docs에서 특별하게 취급하지 않는다. 따라서 사용자가 인스턴스화하려고 했는데, 생성자가 없으면 static factory method를 찾으면 된다. 아래는 정적 팩토리 메소드의 명명 방식들이다.
    • from : 매개변수 하나 받아서 인스턴스화 ex Date.from ()
    • of : 여러개의 매개변수를 받아서 인스턴스화 ex EnumSet.of ()
    • instance / getInstance : 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다.
    • create / newIntance : getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장
    • getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 사용
    • newType : newInstance와 같으나, 생성할 클래스가 아닌 클래스에 팩토리 메소드를 정의할때 사용
    • type : getTypenewType의 간결한 버전

결론

정적 팩토리 메소드와 public 생성자도 각자의 쓰임새가 있으니 장단점을 이해하고 사용하자, 그래도 정적 팩토리를 사용하는게 더 유리한 경우가 많으므로 public 생성자를 제공하던 습관이 있다면 고치자

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


  • 생성자와 정적 팩토리 메소드는 동일한 단점을 가진다, 매개변수가 많이 필요한 경우이다.
2-1. 점층적 생성자 패텬
NutritionFacts tomato = new NutritionFacts('tomato', 100, 1, 2, 3, 4); 
NutritionFacts banana = new NutritionFacts('banana', 200, 4, 3, 2, null);

N번째 인자에 넘기는 값이 무엇인지 해당 생성자의 시그니처를 봐야 알 수 있다. 또한 매개변수가 많아질 수록 많은 그에 따른 생성자를 작성해야 하고, 복잡해진다.

2-2. 자바빈 패턴
NutritionFacts tomato = new NutritionFacts(); 
tomato.setName("tomat"); 
tomato.setFat(23);

현재 VO로 많이 사용하는 방식이며, 객체 하나를 만들려면 메소드를 여러 번 호출해야 하고, 객체가 완전히 생성되기 전까지 일관성(consistency)가 무너진 상태에 놓이게 됩니다. 또한 쓰레드 안정성을 얻으려면 추가 작업이 필요

2-3. 빌더 패턴
NutritionFacts tomato = new NutritionFacts.Builder() 
            .name("tomato") 
                .fat(23) 
                .build();

필수 인자를 빌더의 생성자에 인자로 선언해주고, 나머지는 빌더의 세터로 사용. 체이닝 기법을 사용하기 때문에 가독성이 좋고, 각각의 인자가 무엇을 의미하는지 명확하다. 또한 객체의 불변화 가능

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


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

3-1. public static file 필드 방식의 싱글턴

public static final LeeGrowUp INSTANCE = new LeeGrowUp(); 이와 같이 선언하게 되면 한번만 호출되기 때문에 LeeGrowUp.INSTANCE 는 싱글턴이 보장된다.

3-2. static factory method 방식의 싱글턴
public class LeeGrowUp { 
    private static final LeeGrowUp INSTANCE = new LeeGrowUp(); 
    public static LeeGrowUp getinstance( return LeeGrowUp; ) 
}

이 방식은 API의 변경이 유연하며, 정적 팩토리를 제네릭 싱슬턴 팩토리르 만들수 있다는 점이다, 또한 정적 팩토리의 메소드 참조를 공급자로 사용할 수 있다는 점이다. 이러한 장점들이 굳이 필요하지 않다면 public 필드 방식이 좋다.

3-3. enum 타입 방식의 싱글턴 - 바람직한 방법
public enum LeeGrowUp{ 
    INSTANCE; 
}

대부분의 상황에서는 원소가 하나뿐인 enum 타입이 싱글턴을 만드는 가장 좋은 방법이지만, 싱글턴이 Enum외의 클래스를 상속해야 한다면 이방법은 사용 불가

3-1,3-2의 경우 직렬화 또는 리플렉션의 공격에대한 별도의 처리가 필요하지만 3-3의 경우는 위 두가지 상황을 추가 노력 없이 방지해준다.

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


  • 정적 메소드와 정적 필드만을 담은 클래스를 만들고 싶을 경우 ex java.lang.Matha,java.util.Array
  • 컴파일러가 자동으로 기본 생성자를 추가해주므로, 사용자가 해당 클래스를 인스턴스화 할 가능성이 생긴다. 그렇기 때문에 private 생성자 를 명시적으로 추가해주자. 또한 상속도 불가능하게 막을 수 있다.

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


많은 클래스가 하나 이상의 자원에 의존한다. 이럴 때 정적 유틸리티 클래스나 싱글턴으로 구현하면 문제가 발생 아래 2가지의 예제이다.

5-1. 정적 유틸리가 잘못 사용 된 예
public class SpellChecker { 
    private static final Lexicon dictionary = new Dicionary(); 
    private SpellChecker() { } 
}
5-2. 싱글톤이 잘못 사용 된 예
public class SpellChecker { 
    private final Lexicon dictionary = new Dicionary(); 
    private SpellChecker( ... ) { } 
    public static SpellChecker INSTANCE = new SpellChecker(...); 
}

자원에 따라 동작이 달라야 한다고 가정할 때 위와 같이 사용하면 적합하지가 않다. 이럴 경우 의존성 주입을 통해 필요한 자원을 정적 팩토리 메소드 또는 빌더로 넘겨주는 방식이 좋다.

5-3. 의존 객체 주입 예
public class SpellChecker { 
    private final Lexicon dictionary ; 
    private SpellChecker( Lexicon dictionary ) { 
        this.dictionary = dictionary; 
    } 
}

위 패턴의 변형으로 의존 객체가 아닌 Factroy를 넘겨주는 방식도 있다. 즉 팩토리 메소드 패턴 (Factory Method Pattern)이 있다.

public class SpellChecker { 
    public SpellChecker(Supplier <? extends Lexicon> dicFactory){ 
        this.dictionary = dicFactory.get(); 
    } 
}

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


String str = new String("LeeGrowUp"); 보다 String str = "LeeGrowUp"; 가 훨씬 좋다. 전자의 경우 선언시 새로운 인스턴스가 만들어지고 메모리 영역을 할당받지만, 후자의 경우 상수풀 영역에 올라가기 때문에 같은 문자열의 경우 재사용이 가능하다. 같은 이유로 new Boolean(String) 보다 Boolean.valueOf(String)가 좋다.

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


GC 를 사용하는 언어라고 메모리 관리에 더 이상 신경을 쓰지 않아도 되는게 절대 아니다.
자칫 메모리 누수가 일어나 시스템의 성능에 저하가 발생할 수 있다.

해결 방안

  1. 다쓴 객체는 null처리를 해준다. NullPointerException 을 통한 오류 검출의 이점도 존재, 단 객체 참조를 null처리하는 일은 예외적인 경우에만 해당 : Stack 클래스
  2. 가장 좋은 방법은 변수의 유효 범위 scope 밖으로 밀어내는 것, GC가 자동으로 객체를 소멸
  3. 캐쉬 또한 메모리 누수의 주범이다. 엔트리가 살아 있는 캐쉬가 필요한 상황이 아니라면 WeakHashMap을 사용하여 캐시 생성, 또는 LinkedHashMap.removeEldestEntry 등 사용을 권장. 즉 Weak reference로 사용하거나 Weak reference를 사용해 구현된 API를 이용

8. finalizer와 cleaner 사용을 피하라


  1. 자바에서는 객체의 소멸을 기본적으로 Garbage Collection이 관리하지만, finalizercleaner을 제공해준다. finalizer의 경우 java 9부터 사용 자체를 deprecated로 지정
  2. 자바에서의 finalizercleaner의 경우 C나 C++의 소멸자라고 생각하면 안된다. 자바에서는 언제 실행이 될지 예측할 수 없고, 상황에 따라 위험[Thread Stop]할수가 있고 상당히 성능을 저하 시키기 때문에 일반적인 경우 불필요
  3. 그렇다고 완전 필요 없는건 아니다, AutoCloseable클래스 같은 경우 안전망으로 clener을 사용

결론

cleaner또는finalizer의 경우 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자, 물론 이런 경우라도 불확실성과 성능 저하에는 주의가 필요

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


자바 라이브러리에는 InputStream,OutputStreaclose() 메소드를 호출해 직접 닫아줘야 하는 자원이 많이 존재한다. 그러나 실제 뛰어난 프로그래머라도 실수를 많이 하기 때문에 try-with-resources를 추천

결론

꼭 회수해야 하는 자원을 다룰 때는 try-finally말고 try-with-resources를 사용, 예외는 없다. 코드의 가독성 안정성등이 훨씬 보장된다.

반응형
Comments