이펙티브 자바(Effective Java)

이펙티브 자바(Effective Java)

생성일
Nov 11, 2024 10:38 AM
최종 편집 일시
Last updated November 13, 2024
태그
JAVA

2장

객체의 생성과 파괴

  1. 정적 팩터리 메서드를 고려하라
    1. 생성자보다 좋은 점
    2. 이름을 가질 수 있다.
      1. 이름만 잘 지으면 반환될 객체의 특성을 짐작할 수 있게 해준다. (한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.)
    3. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
      1. 불변 클래스(immutable class)는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
    4. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    5. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
    6. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
    7. 단점
    8. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    9. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
      1. 이 때문에 메서드 이름을 널리 알려진 규약을 따라 짓는 식으로 사용하는 것이 좋다.
        1. from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드 Date d = Date.from(instant);
        2. of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
        3. valueOf: from과 of의 더 자세한 버전 BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
        4. instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. StackWalker luke = StackWalker.getInstance(options);
        5. create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. Object newArray = Array.newInstance(classObject, arreyLen);
        6. getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. “Type”은 팩터리 메서드가 반환할 객체의 타입이다. FileStore fs = Files.getFileStore(path);
        7. newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. BufferedReader br = Files.newBufferedReader(path);
        8. type: getType과 newType의 간결한 버전 List<Complaint> litany = Collections.list(legacyLitany);

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

점층적 생성자 패턴, 자바빈즈 패턴(setter 활용)보다는 빌더 패턴 사용이 불변성을 해치지도 않고 가독성이 좋아진다.
 

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

싱글턴은 인스턴스를 오직 하나만 생성할 수 있는 클래스이다. 그런데 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 타입을 이터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문이다.
싱글턴을 만드는 방식은 보통 둘 중 하나다.
public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() {...} public void leaveTheBuilding() {...} }
private 생성자는 Elvis.INSTANCE를 초기화할 때 닥 한 번만 호출된다. public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 저체 시스템에서 하나뿐임이 보장된다.
다만 이 방식은 리플렉션을 사용해서 AccessibleObject.setAccessible로 private 생성자를 호출할 수 있는 문제가 있다. 이를 방지하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지게 하면 된다.
public class Elvis { private static final Elvis INSTANCE = new Elvis(); private Elvis() {...} public static Elvis getInstance() { return INSTANCE; } public void leaveTheBuilding() {...} }
Elvis.getInstance()는 항상 같은 객체의 참조를 반환하므로 제 2의 Elvis 인스턴스란 결코 만들어지지 않는다.(다만 이 방식 또한 리플렉션을 사용한 억지 수정은 똑같이 가능하다.)
이 때 싱글턴 클래스를 직렬화하려면 readResovle 메서드를 추가로 선언해야 역직렬화 했을 때 새로운 인스턴스가 탄생하지 않는다.
private Object readResolve() { return INSTANCE; }
또 다른 방식으로 싱글턴을 만드려면 열거 타입을 사용하면 된다.
public enum Elvis { INSTANCE; public void leaveTheBuilding() {...} }
이넘을 사용하면 간결하고, 직렬화 가능하고 리플렉션 공격에서도 자유롭다.
따라서 대부분의 상황에서는 원소가 하나 뿐인 열거 타입을 사용하는 것이 싱글턴을 만드는 가장 좋은 방법이 된다.

인스턴스화를 막으려면 private 생성자를 사용하라

컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이므로 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
public class UtilClass { private UtilClass() { throw new AssertionError(); } ... }
이 방식은 또한 상속을 불가능하게 하는 효과도 있다. 모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데, 이를 private으로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 수가 없게 된다.

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

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다.
public class SpellChecker { private final Lexicon dictionary; public SpellChecker(Lexicon dictionary) { this.dicthionary = Objects.requireNonNull(dictionary); } public boolean isValid(String word) {...} public List<String> suggestions(String typo) {...} }
이러한 의존 객체 주입 방식은 생성자, 정적 팩터리, 빌더 모두에 똑같이 응용할 수 있다.
팩터리 메서드 패턴은 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 만드는 것을 말한다. 자바 8의 Supplier<T> 인터페이스가 팩터리이다.
예를 들어 Mosaic create(Supplier<? extends Tile> tileFactory) {...} 같은 느낌이다.
스프링에서는 이런 생성자 주입을 권장하고 있다.

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

Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다. 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
생성비용이 비싼 비싼 객체들이 있다.
static boolean isRomanNumeral(String s) { return s.matches(정규식); }
여기서 s.matches는 내부에서 Pattern 인스턴스를 쓰는데 이는 한 번 쓰고 버려져서 곧바로 GC 대상이 된다. Pattern은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.
이를 개선하려면 불변인 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱해두고 중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용해야 한다.
public class isRomanNumerals { private static final Pattern ROMAN = Pattern.compile(정규식); static boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); } }

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

close를 사용해 직접 닫아줘야 하는 자원이 꽤 많다. 예를 들어 InputStream, OutputStream, java.sql.connection 등이 있다. 이런 자원 중 상당 수가 안전망으로써 finalizer를 활용하고 있지만 finalizer는 그리 믿을만하지 못하다.
static String firstLineOfFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } } // 복수의 자원이 필요한 경우 static void copy(String src, String dist) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dist)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } }

3장

equals는 일반 규약을 지켜 재정의하라

다음과 같은 상황에서는 재정의하지 않는 것이 최선이다.
  1. 각 인스턴스가 본질적으로 고유하다.
    1. 값을 표현하는 것이 아닌 동작하는 개체를 표현하는 클래스가 여기 해당한다.
    2. Thread가 좋은 예이고 Object의 equals 메서드는 이에 맞게 구현되었다.
  1. 인스턴스의 논리적 동치성을 검사할 일이 없다.
    1. java.util.regex.Pattern은 equals를 재정의해서 두 Pattern의 인스턴스가 같은 정규표현식을 나타내는지 검사하는(논리적 동치성) 방법도 있다.
    2. 설계자가 이 방식이 필요하지 않다고 판단한다면 Object.equals만으로 해결된다.
  1. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 맞는다.
    1. 대부분의 Set 구현체는 AbstractSet이 구현한 equals를 상속받아 쓰고, List 구현체들은 AbstractList로부터, Map 구현체들은 AbstractMap으로부터 상속받아 그대로 쓴다.
  1. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
    1. 이런 경우에는
      1. @Override public boolean equals(Object o) { throw new AssertionError(); }

equals를 재정의 해야만 할 때는 언제일까?

상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때이다.
Integer나 String처럼 값 클래스는 객체가 같은지가 아니라 값이 같은지를 알고 싶을 것이다.
equals를 논리적 동치성을 확인하도록 재정의하면 Map의 키, Set의 원소로도 사용이 가능해진다.

equals 메서드 재정의에 대한 일반 규약

equals 메서드는 동치관계를 구현하며, 다음을 만족한다.
  1. 반사성: null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.
    1. 객체는 자기 자신과 같아야 한다는 뜻이다.
  1. 대칭성: null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
    1. 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다.
  1. 추이성: null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y) == true고 y.equals(z) == true면 x.equals(z)도 true 이다.
    1. 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면 첫 번째 세 번째 객체도 같아야 한다는 뜻이다.
  1. 일관성: null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
    1. 두 객체가 같다면 (어느하나 또는 두 개 모두가 수정되지 않는 한 ) 앞으로도 영원히 같아야 한다는 뜻이다.
    2. 불변 객체는 한 번 다르면 끝까지 달라야 한다.
    3. 클래스가 불변이든 가변이든 equals의 판당에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다.
  1. null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.