(ing...)Effective java 2nd. Joshua Bloch

2019-01-15·programming

Table of Contents

1장 서론

  • 명료하고, 정확하고, 사용이 편하며, 안정적이고, 유연하며, 유지보수가 쉬운 프로그램을 만드는 방법을 다룬다. 그런 프로그램을 만들 수 있게 되고 나면, 필요한 성능을 얻는 것은 상대적으로 쉽다.

2 장 객체의 생성과 삭제

규칙 1 생성자 대신 정적 팩터리 메소드를 사용할 수 없는지 생각해보라

  • 첫번째 장점은, 생성자와는 달리 이름이 있다는 것이다. 생성자의 인자는 어떤 객체가 생성되는지를 설명 못하지만, 메서드 이름을 잘 짓기만 한다면 사용하기도 쉽고, 클라이언트 코드의 가독성도 높아진다.

  • 두번째 장점은, 생성자와 달리 호출할 때마다 새로운 객체를 생성할 필요가 없다는 것이다. 가령 같은 객체를 반복해서 반환할 수 있으므로 객체가 얼마나 어느시점에서 존재할지를 정밀하게 제어할 수 있다.

  • 세번째 장점은, 생성자와는 달리 반환값 자료형의 하위 자료형 객체를 반환할 수 있다는 것이다.

  • 네 번째 장점은, 형인자 자료형(parameterized type)객체를 만들 때 편리하다는 것이다.

    // Usually
    Map<String, List<String>> f = new HashMap<String, List<String>>();
    
    // Good
    public static <K, V> newInstance(){ return new HashMap<K, V>() }; Map<String,List<String>> m = HashMap.newInstacne();
    
  • 정적 팩터리 메서드만 있다면 가장 큰 문제는 public/protected 생성자가 없어 하위 클래스를 만들 수 없다는 것이다.

규칙 2 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

  • 인자에 따라 생성자를 여러개 만드는 패턴은 인자 수가 늘어나면 사용도 어렵고, 읽기도 어렵다.

  • 인자 없는 객체를 만들고 setter 를 호출하는 JavaBeans 패턴이 있다. 그러나 1회 호출로 생성이 끝나지 않아 일관성이 없거나, 변경불가능한 클래스가 아니므로 thread safe 에 대해 고려해야한다. 이를 위해 객체를 freezing 하는 방법은 까다로우며 거의 쓰이지 않는다.

  • 빌더로 객체를 생성하면 객체생성 이후 불변한지 필드별로 검사 할 수 있다.

  • 요약하자면 빌더 패턴은 인자가 많은 생성자나, 정적 팩터리가 필요한 클래스를 설계 할 때, 특히 대부분의 인자가 선택적 인자인 상황에 유용하다.

규칙 3 private 생성자나 enum 자료형은 싱글턴 패턴을 따로도록 설계하라

  • 싱글턴을 구현하는 방법이 두가지 있다. 두 방법다 생성자를 private으로 선언하고, 객체는 static 멤버를 통해 이용하는 것이다.

  • 팩토리 메소드를 사용하는 방법의 한 가지 장점은, API를 변경하지 않고도 싱글턴 패턴을 포기할 수 있다는 것이다.

  • jdk 1.5 부터는 원소가 하나뿐인 enum 자료형을 통해 실글턴을 구현할 수도 있다. 사실 위에 나열한 두가지 방법은 리플렉션을 통해 private 생성자를 호출할 수 있지만, enum 방식은 그럴 수 없다. 아직 널리 사용되는 접근법은 아니다.

    public enum Elvis {
        INSTANCE
    
        public void foo() {...}
    }
    

규칙 4 객체 생성을 막을 때는 private 생성자를 사용하라

  • 때로는 정적 메서드나 필드만 모은 클래스를 만들고 싶을 때가 있다. 이런 클래스는 악명이 높은데, 객체 지향적으로 생각하지않으려는 사람들이 남용하는 경향이 있기 때문이다. 하지만 이런 클래스들도 분명 필요할 때가 있다.

  • 하지만 생성자를 생략하면 컴파일러는 자동으로 인자 없는 public 기본 생성자를 만들어버린다. 따라서 private 생성자를 클래스에 넣어서 객체 생성을 방지하자는 것이다.

규칙 5 불필요한 객체는 만들지 말라

// bad
String s = new String("foo");

// good
String s = "foo"; // 값이 같다면 JVM에서 재사용
  • 생성자 대신 정적 팩터리 메스드를 이용하면 불필요한 객체 생성을 피할 수 있다. Boolean(String)보다는 Boolean.valueOf(String)쪽이 대체로 바람직하다.

  • 초기화 지연은 객체 생성 비용이 클 때 고려할 수 있으나, 구현이 복잡해서 추천할만한 방법은 아니다.

  • 객체 표현형 대신 기본 자료형을 사용하자.

  • 가령 데이터베이스 접속하는 비용이리 충분히 높으므로, 이런 객체들은 객체 풀을 관리하는 것이 이치에 맞다. 다만 생성 비용이 크지 않으면 이러한 기법은 사용하지 않는 것이 오히려 좋다.

규칙 6 유효기간이 지난 객체 참조는 폐기하라

  • 만기 참조가 되는경우 명시적으로 null 을 지정하여 메모리 누수를 막자
puvlic class Stack {
    private Object[] elements;
    private int size = 0;
    (...)
    public Object pop() {
        return elements[--size]; // leak
    }
}
  • 하지만 위와 같은 케이스 때문에 객체 사용이 끝나면 즉시 그 참조를 null 처리 해야한다는 강박관념에 사로잡히는 경우가 있다. 객체 참조를null 처리하는 것은 규범이라기보단 예외적인 조치가 되어야 한다. 만기 참조를 제거하는 가장 좋은 방법은 보간된 변수가 scope를 벗어나게 두는 것이다.

  • 일반적으로 자체적으로 관리하는 메모리가 있는 클래스를 만들 때 누수를 조심하자.

  • listener등의 callback 에서 역 호출자를 client에서 명시적으로 제거하지 않는 경우등이 있을 수 있다. 이럴 때에는 callback 에 대한 약한 참조(weak reference)만 저장하는 것이다. WeakHashMap의 키로 저장하는 것이 그 예다.

규칙 7 종료자(finalizer) 사용을 피하라

  • finalizer 는 예측 불가능하고, 대체로 위험하고, 일반적으로 불필요하다.

  • 즉시 실행되리라는 보장이 전혀 없다. 자바 명세에도 그런 문구는 없다. 따라서 중요한 작업을 종료자 안에서 처리하면 안된다. 실행시점은 GC 알고리즘에 좌우된다.

  • finally 에서 명시적으로 호출하는 방법을 택하자.

4장 클래스와 인터페이스

규칙 13 클래스와 멤버의 접근 권한은 최소화하라

  • 은닉과 캡슐화는 소프트웨어 설계의 기본 원칙 가운데 하나다.

  • private package 로 선언된 최상위 레벨 클래스나 인터페이스를 사용하는 클래스, 즉 사용자 클래스가 하나뿐이면 최상위 레벨 클래스를 사용자 클래스의 private 중첩 클래스로 만들 것을 고려해보기 바란다.

  • 테스트 때문에 접근 권한을 열어주고 싶을 때도 있다. 어느 선까지는 괜찮다. public 클래스의 private 을 private package로 만드는 것 까지는 괜찮지만, 그 이상은 곤란하다.

  • 객체 필드는 절대로 public으로 선언하면 안된다. final 참조를 public 으로 선언하면, 필드에 저장될 값을 제한할 수 없게 된다. 그리고 이러한 변경 가능 public 필드를 가진 클래스는 다중 스레드에 안전하지 않다.

  • 공개 API의 일부가 되는 경우, 쉽게 삭제하거나 수정할 수 없게 된다.

14 public 클래스 안에는 public 필드를 두지 말고 접근자 메서드를 사용하라

  • public 으로 선언하기보다는 setter/getter 를 선언하자. 그래야 클래스 표현을 자유로이 수정할 수 있게 된다.

  • 하지만 package-private 클래스나 private 중첩 클래스는 데이터 필드를 공개하더라도 잘못이라 말할 수 없다. 클래스가 추상화하려는 내용을 제대로 기술하기만 하면 말이다.

15 변경 가능성을 최소화하라

  • 객체 상태를 변경하는 메서드를 제공하지 않으며, 계승할 수 없도록 final 로 선언하고 필드들 final 혹은 private 으로 선언하자. 변경 가능한 컴포넌트에 대한 독립적 접금권을 보장하는데 가령 수정시 방어적 복사본을 만들어 반환하는 방법이 있다.
public final class Complex {
    private final double re;
    private final double im;

    public Complex add(Complex c) { return new Complex( ...);}
}
  • 변경 불가능 객체는 단순하며 thread safe 하다. 객체는 다른 객체에 구성요소로 훌륭하다.

  • 단점은 값마다 별도의 객체를 만들어야 한다는 것이다.

  • 변경 가능한 클래스로 만들 타당한 이유가 없다면, 반드시 변경 불가능 클래스로 만들어야 한다. 그러기 어려우면, 변경 가능성을 최대한 제한해라.

16 계승하는 대신 구성하라

  • 계승(Inheritance)는 캡슐화 원칙을 위반한다. 상위 클래스의 구현에 의존하면서, 상위클래스의 변경이 하위클래스는 사이드이팩트로 이어질 수 있다.

  • 계승하는 대신, 계승하고자 하는 객체를 참조하여 사용해라. 기존 클래스는 구현하고자 하는 클래스의 일부(component)가 된다.

  • 계승은 하위 클래스가 상위 클래스의 자료형(subtype)이 확실한 경우에만 바람직하다. IS-A 관계가 성립할 때 계승을 사용하자.

17 계승을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 계승을 금지하라

  • 재정의 기능 메서드를 내부적으로 어떻게 사용하는지 가이드 문서에 남기라는 것이다.

  • 계승을 허용할 때 따라야할 제약사항 중 하나는, 생성자는 재정의 가능 메서드를 호출해서는 안 된다는 것이다.

  • 계승에 맞도록 설계하고 문서화하지 않는 클래스에 대한 하위 클래스는 만들지 않아야 한다.

18 추상 클래스 대신 인터페이스를 사용하라

  • 여러가지 구현을 허용하는 자료형을 만드는 방법 두가지가 인터페이스와 추상클래스이다. 둘의 차이는 추상 클래스는 구현된 메서드를 포함할 수 있고 인터페이스는 아니라는 점이다.

  • 인터페이스는 믹스인(mixin)을 정의하는 데 이상적이다. 믹스인은 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다.

  • 인터페이스는 비 계층적인 자료형 프레임워크를 만들 수 있도록 한다.

        public interface Singger {} 
        public interface Songwritter{}
        public interface SinggerSongwritter extends Singger, Songwriter {...}
    
  • 인터페이스 안에는 메서드 구현을 둘 수 없지만, 그렇다고 프로그래머가 사용할 수 있는 코드를 제공할 방법이 없는 것은 아니다. 추상 골격 구현(abstract skeletal implementation) 클래스를 중요 인터페이스마다 두면, 인터페이스의 장접과 추상 클래스의 장접을 결합할 수 있다. 관습적으로 이름은 AbstactInterface와 같이 정한다. 본뜨고자 하는 인터페이스를 abstract 로 선언하여 일부 구현과 재정의가 필요한 메소드에 익셉션( UnsupportedOperationExcetion)을 던지는 구현을 하므로써 재정의를 강제할 수 있다.

    public abstract class AbstractMapEntry<K,V> implments Map.Entry<K,V> {
        public V setValue(V value){ throw new UnsupportedOperationException();}
    
        @Override ...
    }
    
  • 인터페이스는 신중하게 설계해야 한다. 인터페이스 공개되고 널리 구현된 다음에는, 인터페이스의 수정이 거의 불가능하기 때문이다.