본문 바로가기

JAVA

이펙티브 자바 #1

반응형

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

장점

  1. 이름을 가질수 있다.
  2. 호출될 때 마다 인스턴스를 새로 생성하지 않아도 된다.
  3. 반환 타입의 하위 타입 객체를 반환할수 있다.
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래가 존재하지 않아도 된다.
Date d = Date.from(instant);

Set<RanK> fceCards = EnumSet.of(JACK, QUEEN, KING);

BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

StackWalker luke = StackWalker.getInstance(options);

Object newArray = Array.newInstance(classObject, arrayLen);

FileStore fs = Files.getFileStore(path);

BufferedReader br = File.newBufferedReader(path);

List<Complaint> litany = Collections.list(lefacyLitany);

정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니, 상대적인 장단점을 이해하고 사용하는 것이 좋다.
정적 팩토리를 사용하게 유리하는 경우가 더 많으므로 무작정 public 생성자를 제공하는 습관이 있다면 고치자.

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

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

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

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

생성 방식 3가지

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

    public void leaveTehBuilding() { ... }

}
  • private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할때 딱 한번만 호출(리플랙션은 예외)
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();

    private Elvis() { ... }
    public static Elvis getInstance() { retrun INSTANCE; }

    public void leaveTehBuilding() { ... }

}
  • Elvis.getInstance 는 항상 같은 객체의 참조를 반환하므로 제2의 Elvis 인스턴스란 결코 만들어지지 않는다.(리플랙션는 예외)
public enum Elvis {
    INSTANC;

    public void leaveTehBuilding() { ... }
}
  • 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.

인스턴스화를 막을려거든 private 생성자를 사용

  • 단순히 정적 메서드와 정적 필드만을 담은 클래스를 만들때 (유틸리티 클래스 같은)
public class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
}

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

  • 많은 클래스가 하나 이상의 자원에 의존한다.
public class SpellChecker {
    private static final Lexicon dictionary = ...;

    private SpellChecker() {}

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

정적유틸리티 잘못 사용 - 유연하지 않고 테스트 어려움

public class SpellChecker {
    private final Lexicon dictionary = ...;

    private SpellChecker(...) {}
    public static SpellChecker INSTANCE = new SpellChecker(...);

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

싱글톤을 잘못 사용 - 유연하지 않고 테스트 어려움

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

의존 주입은 유연성과 테스트 용이성의 높여준다

  • 사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
  • 대신 클래스가(SpellChecker)가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원 (dictionary)을 사용해야 한다.
  • 인스턴스를 생성할때 생성자에 필요한 자원을 넘겨주는 방식으로 의존객체를 주입해주면된다.

클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적유틸리티 클래스를 사용하지 않는 것이 좋다.
클래스가 직접 만들게 해서도 안된다.
대신 필요한 자원을 생성자에 넘겨준다. 의존객체 주입은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.

불필요한 객체 생성을 피하라.
  • 똑같은 기능의 객체를 매번 생성하기 보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

  • 재사용은 빠르다.

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

  • 불변 객체만 아니라 가변 객체라 해도 사용중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

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은 입력받은 정규표현식에 해당하는 유한 상태머신을 만들기 때문에 인스턴스 생성 비용이 높다.
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) {
            retrun ROMAN.matcher(s).matches();
    }
}

-정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해 두고, 나중에 isRomanNumeral() 메서드가 호출될때마다 이 인스턴스를 재사용한다.

오토박싱은 기본타입과 박싱된 기본 타입을 섞어 쓸때 자동으로 상호 변환해주는 기술

오토박싱은 기본타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

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

    return sum;
}
  • sum 변수를 long아 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 약231개나 만들어진다. (long 타입인 i가 Long 타입인 sum에 더해질 때마다)

  • sum의 타입을 long으로 바꾸면 빨라진다.

  • 박싱된 기본타입 보다는 기본타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.

다 쓴 객체는 참조를 해제하라.
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object o) {
        ensureCapacity();
        elements[size++] = o;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf*elements, 2 * size + 1);
        }
    }
}
  • 메모리 누수의 위치?
  • 스택이 커졌다가 줄어들었을 때, 스택에서 꺼내진 객체들을 가비지 컬렉터에 회수하지 않는다. (프로그램에서 그 객체들을 더 이상 사용하지 않는다 해도)
  • 스택이 그 객체들의 다 쓴 참조(obsolete reference)를 여전히 가지고 있기 때문이다.
  • 다 쓴 참조란, 문자 그대로 앞으로 다시 쓰지 않을 참조를 뜻한다.
  • elements 배열의 '활성 영역'밖의 참조들이 모두 여기에 해당한다.
  • 활성영역은 인덱스가 size보다 작은 원소들로 구성된다.
  • 객체 참조를 하나를 살려두면 가비지 컬렉터는 그 객체뿐만 아니라 그 객체가 참조하는 모든 객체(또 그 객체들이 참조하는 모든 객체..)를 회수해가지 못한다.
  • 그래서 단 몇개의 객체가 매우 많은 객체를 회수되지 못하게 할수 있고, 잠재적으로 성능에 악 영향을 줄수 있다.

해당 참조를 다 썼을때 null처리(참조 해제) 하면된다.

public Object pop() {
    if (size == 0) 
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; //다 쓴 참조 해제
    return result;
}
  • 다 쓴 참조를 null처리하면, null 처리한 참조를 실수로 사용하면 NullPointerException을 던진다.
  • 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.
  • 다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효범위(scope) 밖으로 밀어내는 것이다.
  • 변수의 범위를 최소가 되게 정의했다면, 자연스럽게 이뤄진다.

null 처리는 언제 해야 하나?

  • Stack 클래스가 메모리 누수에 취약한 점은 스택이 바로 자기 메모리를 직접 관리하기 때문이다.
  • 스택은(객체 자체가 아니라 객체 참조를 담는) elements 배열로 저장소 풀을 만들어 원소들을 관리한다.
  • 배열의 활성영역에 속한 원소들이 사용되고 비활성 영역은 쓰이지 않는다.
  • 문제는 가비지 컬렉터는 이 사실을 알 길이 없다.
  • 가비지 컬렉터가 보기에 비활성 영역에서 참조하는 객체도 똑같이 유효한 객체다.
  • 비활성 영역의 객체가가 더 이상 쓸모없다 건 프로그래머만 아는 사실이다.
  • 프로그래머는 비활성 영역이 되는 순간 null 처리해서 해당 객체를 더는 쓰지 않을 것임을 가비지 컬렉터에 알려야 한다.

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

  • 캐시 역시 메모리 누수를 일으키는 주범이다.

  • 캐시 외부에서 키(key)를 참조하는 동안만(값이 아니다) 엔트라가 살아 있는 캐시가 필요한 상황이라면 WeakHashMap을 사용해 캐시를 만든다.

  • 다 쓴 엔트리는 그 즉시 자동으로 제거될 것이다.

  • 단, WeakHashMap은 이러한 상황에서만 유용하다는 사실을 기억해야 한다.

  • 리스너(listener) 또는 콜백(callback)도 메모리 누수를 일으키는 주범이다.

  • 클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 뭔가 조치해주지 않는 한 콜백은 계속 쌓여갈 것이다.

  • 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해 간다. 예) WeakHashMap에 키로 저장하면된다.

메모리 누수는 겉으로 잘 드러나지 않는다. 철저한 코드 리뷰나 힙 프로파일러 같은 디버깅 도구를 활용한다.

finalizer와 cleaner 사용을 피하라.

자바 두가지 객체 소멸자를 제공

  • finalizer는 예측할수 없고, 상황에 따라 위험할수 있어 일반적으로 불필요하다.

  • cleaner는 finalizer보다 덜 위험하지만, 여전히 예측할수 없고, 느리고 일반적으로 불필요하다.

  • 자바 언어 명세는 finalizer와 cleaner의 수행 시점뿐만 아니라 수행 여부 조차 보장하지 않는다.

  • 프로그래 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안된다.

  • 파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner를 대신으로 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면된다.

  • 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources를 사용해야 한다.

  • 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋다. close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렀다면 IllegalStateException을 던지는 것이다.

cleaner는(자바8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자.
이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

try-with-resources를 사용하라.

  • 자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.

  • InputStream

  • OutputStream

  • java.sql.Connection

  • 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.

  • 이러한 자원 중 상당수가 안정망으로 finalizer를 활용하고 있지만, finalizer는 그릴 믿지 못하하다.

static string firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while((n = in.read(buf)) >= 0) 
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}
  • try-finally 문을 제대로 사용한 코드에도 결점이 있다.
  • 예외는 try 블록과 finally 블록 모두 발생할수 있는데, 기기에 물리적인 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 수 있다.
  • 두번째 예외가 첫번째 예외를 완전히 집어삼겨 버린다. 그러면 스택 추적 내역에 첫번째 예외에 관한 정보가 남지 않는다.

이러한 문제들은 try-with-resources로 해결된다.

  • 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
static String firstLineOfFile(String path) throws IOException {
    try ( BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}
static void copy(String src, String dst) throws IOException {
    try (InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst)) {

            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while((n = in.read(buf)) >= 0) 
                out.write(buf, 0, n);
        }
}
  • firstLineOfFile 메서드에서 readLine과 close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다.
  • 프로그래머에게 보여줄 예외 하나만 보존되고 여러개의 다른 예외가 숨겨질수도 있다.
  • 숨겨진 예외들도 그냥 버려지지 않고, 스택 추적 내역에 숨겨져있다는 꼬리표를 달고 출력된다.
  • catch 절도 중첩하지 않고 다수의 예외를 처리한다.
static String firstLineOfFile(String path) {
    try ( BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}

꼭 회수해야하는 자원을 다룰 때는 try-with-resources를 사용한다.

반응형

'JAVA' 카테고리의 다른 글

이펙티브 자바 # 3  (0) 2021.07.30
도메인 모델링 3가지 개념  (0) 2020.09.17