Java
책 <이펙티브 자바>
팅리엔
2024. 3. 18. 02:47
- 생성자 대신 정적 팩터리 메서드를 고려하라.
- 장점
- 이름을 가질 수 있다.
- 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
- 반환 타입의 하위 타입 객체를 반환할 수 있다. (반환할 객체의 클래스를 자유롭게 선택할 수 있다.)
- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. (인터페이스를 반환해도 된다.)
- 단점
- 상속을 하려면 public이나 protected 생성자가 필요해서, 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다. 클래스를 인스턴스화 하는 방법을 API 문서에 잘 써놓아야 한다.
- 흔히 사용되는 명명 방식: from, of, valueOf, instance, getInstance, create, newInstance, getType, newType, type
- 장점
- 생성자에 매개변수가 많다면 빌더를 고려하라. : 읽고 쓰기가 훨씬 간편하고, 더 안전하게 완전한 객체를 만들 수 있다.
- private 생성자나 열거 타입으로 싱글턴임을 보증하라.
- 싱글턴을 만드는 방법
- public static final 필드 방식의 싱글턴 (public static final Elvis INSTANCE = new Elvis();)
- 정적 팩터리 방식의 싱글턴 (public static Elvis getInstance() { return INSTANCE; })
- 원소가 하나인 열거 타입 선언
- 싱글턴을 만드는 방법
- 인스턴스화를 막으려거든 private 생성자를 사용하라.
- 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라. : 의존 객체 주입은 유연성과 테스트 용이성을 높여준다.
- 불필요한 객체 생성을 피하라. : 똑같은 기능의 객체를 매번 생성하기보다 객체 하나를 재사용하는 편이 나을 때가 많다.
- 다 쓴 객체 참조를 해제하라. : 객체 참조 하나를 살려두면 가비지컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. 해당 참조를 다 썼을 때 null 처리(참조 해제) 해준다. 그러나 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다. 다 쓴 참조를 해제하는 가장 좋은 방법은 다 쓴 참조 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
finalizer와 cleaner 사용을 피하라.- try-finally보다는 try-with-resources를 사용하라. : 코드는 더 깔끔해지고, 예외 정보도 더 유용하다.
- equals는 일반 규약을 지켜 재정의하라.
- 꼭 필요한 경우가 아니라면 재정의하지 말자. 다음 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.
- 각 인스턴스가 본질적으로 고유하다. (값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스 e.g. Thread)
- 인스턴스의 동등성을 검사할 일이 없다. (e.g. Pattern)
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다. (e.g. Set 구현체)
- 값이 같은 인스턴스가 둘 이상 만들어지지 않는다. (e.g. 싱글턴)
- 언제 재정의해야 하는가? 동등성을 확인해야 하는데 상위 클래스가 equals를 재정의하지 않았을 때. 주로 값 클래스들. (e.g. Integer, String)
- equals를 재정의 하면 동등성을 비교할 수 있고, Map의 키와 Set의 원소로 사용할 수 있게 된다.
- equals 일반 규약
- x.equals(x)는 true다.
- x.equals(y)가 true면 y.equals(x)도 true다.
- x.equals(y)가 true이고 y.equals(z)도 true면, x.equals(z)도 true다.
- x.equals(x)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
- x.equals(null)은 false다.
- 양질의 equals 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
- equals를 재정의할 땐 hashCode도 반드시 재정의한다.
- 꼭 필요한 경우가 아니라면 재정의하지 말자. 다음 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.
- equals를 재정의하려거든 hashCode도 재정의하라.
- 그렇지 않으면 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 땐 문제가 된다.
- toString을 항상 재정의하라. : toString을 재정의한 클래스는 그 클래스를 사용한 시스템을 디버깅하기 쉽다.
clone 재정의는 주의해서 진행하라.- Comparable을 구현할지 고려하라.
- 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
- 클래스와 멤버의 접근 권한을 최소화하라.
- 권한을 풀어주는 일을 자주 하게 된다면 컴포넌트를 더 분해해야 하는 것은 아닌지 다시 고민해본다.
- 상위 클래스의 메서드를 재정의할 때는 그 접근 수준을 상위 클래스보다 좁게 설정할 수 없다. 단지 테스트만을 위해 적당한 수준까지는 넓혀도 괜찮다. 테스트만을 위해 공개 API로 만들어서는 안 된다.
- public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다.
- public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라.
- 하지만 package-private 클래스나 private 중첩 클래스에는 종종 필드를 노출하는 편이 나을 때도 있다.
- 변경 가능성을 최소화하라.
- 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 스레드 안전하여 따로 동기화할 필요가 없다. 불변 객체는 안심하고 공유할 수 있다.
- 불변 클래스를 만드는 규칙
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다. 하위 클래스에서 부주의하게 혹의 나쁜 의도로 객체의 상태를 변하게 만드는 사태를 막아준다.
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다. 외부에서 그 컴포넌트의 참조를 얻을 수 없게 한다. 방어적 복사를 수행하라.
- 모든 클래스를 불변으로 만들 수는 없다. 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화하자.
- 불변 클래스의 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.
- 상속보다는 컴포지션을 사용하라.
- 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 상속을 쓰면 내부 구현이 노출된다.
- 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다.
- 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
- 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이 때 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도한대로 동작하지 않을 것이다. + private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
- 상속을 금지하는 방법
- 클래스를 final로 선언한다.
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어준다.
- 추상 클래스보다는 인터페이스를 우선하라.
- 인터페이스는 기존 클래스에 쉽게 구현해 넣을 수 있다. 추상 클래스는 커다락 제약을 안게 된다.
- 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 고려하자.
- 인터페이스는 구현하는 쪽을 생각해 설계하라.
- 기존 인터페이스에 디폴트 메서드로 새 메서드를 추가하는 일은 꼭 필요한 경우가 아니라면 피한다. 추가하려는 디폴트 메서드가 기존 구현체들과 충돌할 수도 있다.
- 인터페이스는 타입을 정의하는 용도로만 사용하라.
- 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 알려주는 것이다. 상수 공개용 수단으로 사용하지 말자.
- 태그 달린 클래스보다는 클래스 계층구조를 활용하라.
- 두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스는 잘못되었다.태그 달린 클래스보다는 클래스 계층구조를 활용하라.
class Figure { enum Shape { RECTANGLE, CIRCLE }; // 태그 필드 final Shape shape; // 다음 필드들은 RECTANGLE일 때만 쓰인다. double length; double width; // 다음 필드는 CIRCLE일 때만 쓰인다. double radius; }
- 두 가지 이상의 의미를 표현할 수 있으며, 그 중 현재 표현하는 의미를 태그 값으로 알려주는 클래스는 잘못되었다.태그 달린 클래스보다는 클래스 계층구조를 활용하라.
- 멤버 클래스는 되도록 static으로 만들라.
- 중첩 클래스는 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
- 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 바깥 인스턴스 없이는 생성할 수 없다. 바깥 인스턴스와의 관계 정보는 비정적 멤버 클래스의 인스턴스 안에 만들어져 메모리 공간을 차지하며, 생성 시간도 더 걸린다. 또한 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못한다.
- 개념상 중첩 클래스의 인스턴스가 바깥 클래스의 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다. 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만든다.
- 톱레벨 클래스는 한 파일에 하나만 담으라.
- 로 타입은 사용하지 말라.
- 로 타입이란 제네릭 타입에서 타입 매개변수를 사용하지 않을 때를 말한다. List<E>의 로 타입은 List이다. 로 타입을 쓰면 제네릭이 안겨주는 타입 안정성과 표현력을 모두 잃게 된다. 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
- 비검사 경고를 제거하라.
- 배열보다는 리스트를 사용하라.
- 배열은 공변이고(Sub[]는 Super[]의 하위타입), 리스트는 불공변이다(List<Sub>는 List<Super>의 하위타입이 아님). 그 결과 배열은 런타임에는 타입 안전하지 않지만 컴파일 타임에는 안전하다. 제네릭은 그 반대다. 모두 에러를 컴파일 시에 발견하길 원할 것이다.
- 이왕이면 제네릭 타입으로 만들라. : 클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
- 이왕이면 제네릭 메서드로 만들라.
- 한정적 와일드카드를 사용해 API 유연성을 높이라.
- 펙스 공식 PECS: producer-extends, consumer-super
// E 생산자(producer) 매개변수에 와일드카드 타입 적용 // src 매개변수는 Stack이 사용할 E 인스턴스를 생산하므로 생산자이다. public void pushAll(Iterable<? extends E> src) { for (E e : src) { push(e); } } // E 소비자(consumer) 매개변수에 와일드카드 타입 적용 // dst 매개변수는 Stack으로부터 E 인스턴스를 소비하므로 소비자이다. public void popAll(Collection<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
- Comparable은 언제나 소비자이므로, 일반적으로 Comparable<E>보다는 Comparable<? super E>를 사용하는 편이 낫다.
- 반환 타입에는 한정적 와일드카드 타입을 사용하면 안 된다. 반환 타입으로 쓰면 클라이언트 코드에서도 와일드카드 타입을 써야만 한다.
- 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하라. 이때 비한정적 태입 매개변수라면 비한정적 와일드카드로 바꾸고, 한정적 타입 매개변수라면 한정적 와일드카드로 바꾸면 된다.
- 펙스 공식 PECS: producer-extends, consumer-super
- 제네릭과 가변인수(varargs)를 함께 쓸 때는 신중하라.
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 그 결과 제네릭과 varargs를 혼용하면 타입 안정성이 깨진다.
static void dangerous(List<String>... stringLists) { List<Integer> intList = List.of(42); Object[] objects = stringLists; objects[0] = intList; // 힙 오염 발생 String s = stringLists[0].get(0); // ClassCastException }
- 가변인수 메서드를 호출하면 가변인수를 담기 위한 배열이 자동으로 하나 만들어진다. 그 결과 제네릭과 varargs를 혼용하면 타입 안정성이 깨진다.
- 타입 안전 이종 컨테이너를 고려하라.
-
// 타입 안전 이종 컨테이너 패턴 - API public class Favorites { public <T> void putFavorite(Class<T> type, T instance); public <T> T getFavorite(Class<T> type); } // 타입 안전 이종 컨테이너 패턴 - 클라이언트 public static void main(String[] args) { Favorites f = new Favorites(); f.putFavorite(String.class, "Java"); f.putFavorite(Integer.class, 0xcafebabe); f.putFavorite(Class.class, Favorite.class); String favoriteString = f.getFavorite(String.class); int fovoriteInteger = f.getFavorite(Integer.class); Class<?> favoriteClass = f.getFavorite(Class.class); }
-
- int 상수 대신 열거 타입을 사용하라.
- 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 딱 하나씩만 만들어 public static final 필드로 공개한다.
- 열거 타입에는 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있다.
- 열거 타입의 상수별 동작을 구현하는 데 switch 문은 코드가 장황해지고 개발자가 실수로 빼먹을 수 있으므로 적합하지 않다. 대신 전략 패턴을 사용한다.
- ordinal 메서드 대신 인스턴스 필드를 사용하라.
- 열거 타입은 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환하는 ordinal이라는 메서드를 제공한다.
- 하지만 상수 선언 순서를 바꾸는 순가 ordinal 값이 달라진다. 열거 타입 상수에 연결된 값은 인스턴스 필드로 정의하라.
- 열거 타입을 집합 형태로 사용하려면
비트 필드 대신EnumSet을 사용하라.-
public class Text { public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } public void applyStyles(Set<Style> styles) { ... } } // 사용하기 text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
-
- ordinal 인덱싱 대신 EnumMap을 사용하라.
-
class Plant { enum LiftCycle { ANNUAL, PERENNIAL, BIENNIAL } } // 사용하기 Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LiftCycle.class); for (Plant.LifeCycle lc : Plant.LiftCycle.values()) { plantsByLifeCycle.put(lc, new HashSet<>()); }
-
- 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라.
- 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다.
-
public interface Operation { double apply(double x, double y); } public enum BasicOperation implements Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } } private final String symbol; BasicOperation(String symbol) { this.symbol = symbol; } }
- 명명 패턴보다 애너테이션을 사용하라.
- 예컨대 JUnit 3까지는 테스트 메서드 이름을 test로 시작해야 했다.
- @Override 애너테이션을 일관되게 사용하라.
- 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.
- 구체 클래스에서 상위 클래스의 추상 메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다.
- 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라.
- 마커 인터페이스란, 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표현해주는 인터페이스다. e.g. Serializable 인터페이스
- 마커 인터페이스가 애너테이션보다 낫다.
- 마커 인터페이스를 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있지만 마커 애너테이션은 그렇지 않다.
- 마커 인터페이스는 적용 대상을 더 정밀하게 지정할 수 있다.
- 언제 마커 인터페이스를, 언제 마커 애너테이션(EnumType.TYPE)을 써야 할까?
- 클래스와 인터페이스 외의 프로그램 요수(모듈, 패키지, 필드, 지역변수 등)에 마킹해야 할 때는 애너테이션을 쓸 수 밖에 없다.
- "이 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있을까?"의 답이 "그렇다"이면 마커 인터페이스를 써야 한다. 그러면 컴파일타임에 오류를 잡아낼 수 있다.
- 익명 클래스보다는 람다를 사용하라.
- 자바 8에서 추상 메서드를 하나만 가지는 인터페이스(함수형 인터페이스)가 생겼다. 람다식을 사용해 이 함수형 인터페이스의 인스턴스를 만들 수 있게 되었다.
- 람다는 익명 클래스와 개념은 비슷하지만 코드는 훨씬 간결하다.
- 타입을 명시해야 코드가 더 명확할 때만 제외하고는, 람다의 모든 매개변수 타입은 생략하자. 컴파일러가 타입을 추론해준다.
-
// 익명클래스 - 낡은 기법 Collections.sort(words, new Comparator<String>() { public int compare(String s1, String s2) { return Integer.compare(s1.length(), s2.length()); } }); // 람다식을 함수 객체로 사용 Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length())); // 람다 자리에 비교자 생성 메서드 사용 Collections.sort(words, comparingInt(String::length)); // List 인터페이스의 sort 메서드 사용 words.sort(comparingInt(String::length));
- 람다는 이름이 없고 문서화도 못 한다. 따라서 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아지면 람다를 쓰지 말아야 한다. 한 줄일 때 가장 좋고 세 줄 안에 끝내는 게 좋다.
- 람다는 함수형 인터페이스에서만 쓰인다.
- 람다는 자신을 참조할 수 없다. 람다에서의 this 키워드는 바깥 인스턴스를 가리킨다. 반면 익명 클래스에서의 this는 익명 클래스의 인스턴스 자신을 가리킨다.
- 람다도 익명 클래스처럼 직렬화 형태가 구현별로(가령 가상머신별로) 다를 수 있다. 따라서 람다를 직렬화하는 일은 극히 삼가야 한다. 직렬화해야만 하는 함수 객체가 있다면(가령 Comparator처럼) private static inner 클래스를 사용하자.
- 람다보다는 메서드 참조를 사용하라.
- 람다는 익명클래스보다 간결하다는 점에서 낫다. 그런데 메서드 참조를 사용하면 더 짧고 명확하게 작성할 수 있다.
- 자바 8에서 모든 기본 타입의 박싱 타입(Interger 등)은 람다와 기능이 같은 정적 메서드를 제공한다.
- 표준 함수형 인터페이스를 사용하라.
- java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨져 있다. 필요한 용도에 맞는 게 있다면, 직접 구현하지 말고 표준 함수형 인터페이스를 활용하라.
- 총 43개의 기본 인터페이스가 있지만, 주요 6개
- Operator: 인수와 반환값의 타입이 같음
- Predicate: 인수 하나를 받아 boolean 반환
- Function: 인수와 반환값의 타입이 다름
- Supplier: 인수는 받지 않고 값을 반환
- Consumer: 인수를 하나 받고 반환값은 없음
-
인터페이스 함수 시그니처 예 UnaryOperator<T> T apply(T t) String::toLowerCase BinaryOperator<T> T apply(T t1, T t2) BigInteger::add Predicate<T> boolean test(T t) Collection::isEmpty Function<T> R apply(T t) Arrays::asList Supplier<T> T get() Instance::now Consumer<T> void accept(T t) System.out::println
- 직접 만든 함수형 인터페이스에는 항상 @FunctionalInterface 애너테이션을 사용하라. 그 인터페이스가 람다용으로 설계된 것임을 알려주며, 누군가 실수로 메서드를 추가하지 못하게 막아준다.
- 스트림은 주의해서 사용하라.
- 자바 8에서 스트림 API는 다량의 데이터 처리 작업(순차적이든 병렬적이든)을 돕고자 추가되었다.
- 핵심 개념
- 스트림은 데이터 원소의 유한 or 무한 시퀀스를 뜻한다.
- 스트림 파이프라인은 이 원소들로 수행하는 연산 단계를 표현하는 개념이다.
- 스트림 파이프라인은 지연 평가(lazy evaluation) 된다. 평가는 종단 연산(마지막 연산)이 호출 될 때 이뤄지며, 종단 연산에 쓰이지 않는 데이터 원소는 계산에 쓰이지 않는다.
- 스트림을 처음 쓰기 시작하면 모든 반복문을 스트림으로 바꾸고 싶은 유혹이 일겠지만, 스트림과 반복문을 적절히 조합하는 게 최선이다. 기존 코드는 스트림을 사용하도록 리팩터링하되, 새 코드가 더 나아 보일 때만 반영하자.
- 함수 객체로는 할 수 없지만 코드 블록으로는 할 수 있는 일들이 있다.
- 코드 블록에서는 범위 안의 지역변수를 읽고 수정할 수 있다. 하지만 람다에서는 final 변수만 읽을 수 있고, 지역 변수를 수정할 수 없다.
- 코드 블록에서는 return 문을 사용해 메서드에서 빠져나가거나, break나 continue 문으로 블록 바깥의 반복문을 종료하거나 반복을 한 번 건너뛸 수 있다. 또한 메서드 선언에 명시된 검사 예외를 던질 수 있다.
- 다음 일들에는 스트림이 적절하다.
- 원소들의 시퀀스를 일관되게 변환한다.
- 원소들의 시퀀스를 필터링한다.
- 원소들의 시퀀스를 하나의 연산을 사용해 결합한다. (더하기, 연결하기, 최솟값 구하기 등)
- 원소들의 시퀀스를 컬렉션에 모은다. (아마도 공통된 속성을 기준으로 묶어가며)
- 원소들의 시퀀스에서 특정 조건을 만족하는 원소를 찾는다.
- 스트림에서는 부작용 없는 함수를 사용하라.
- 반환 타입으로는 스트림보다 컬렉션이 낫다.
- 스트림 병렬화는 주의해서 적용하라.
- 스트림을 잘못 병렬화하면 프로그램을 오동작하게 하거나 성능을 급격히 떨어뜨린다.
- 대체로 스트림의 소스가 ArrayList, HashMap, HashSet, ConcurrentHashMap의 인스턴스거나 배열, int 범위, long 범위일 때 병렬화의 효과가 가장 좋다.
- 참조 지역성이 낮으면(이웃한 원소의 참조들이 메모리에 연속해서 저장되어 있지 않으면), 스레드는 데이터가 주메모리에서 캐시메모리로 전송되어 오기를 기다리며 대부분 시간을 멍하니 보내게 된다.
- 종단 연산에서 수행하는 작업량이 파이프라인 전체 작업에서 상당 비중을 차지하면서, 순차적인 연산이라면 파이프라인 병렬 수행의 효과는 제한될 수 밖에 없다.
- 종단 연산 중 병렬화에 적합한 것
- 축소(reduction) (파이프라인에서 만들어진 모든 원소를 하나로 합치는 작업 - reduce 메서드 중 하나, mix, max, count, sum 같은 메서드)
- 조건에 맞으면 바로 반환하는 메서드 (anyMatch, allMatch, noneMatch)
- Stream의 collect 메서드는 병렬화에 적합하지 않다.
- 종단 연산 중 병렬화에 적합한 것
- 매개변수가 유효한지 검사하라.
- 메서드 몸체가 시작되기 전에 매개변수의 값을 검사해야 한다. (오류는 가능한 한 빨리 잡아야 한다.)
- public과 protected 매서드는 매개변수 값이 잘못됐을 때 던지는 예외를 문서화해야 한다. (@throws)
- 클래스 수준 주석은 그 클래스의 모든 public 메서드에 적용되므로 각 메서드에 일일이 기술하는 것보다 훨씬 깔끔한 방법이다.
- @Nullable이나 이와 비슷한 애너테이션을 쓸 수도 있지만 표준적인 방법은 아니다.
- 자바 7 : java.util.Objects.requireNonNull을 사용해 null 검사를 할 수 있다.
- 자바 9 : checkFromIndexSize, checkFromToIndex, checkIndex를 사용해 범위 검사를 할 수 있다.
- public이 아닌 메서드라면 단언문(assert)을 사용해 매개변수 유효성을 검증할 수 있다.
private static void sort(long a[], int offset, int length) { assert a != null; assert offset >= 0 && offset <= a.length; assert length >= 0 && length <= a.length - offset; ... }
- 메서드가 직접 사용하지는 않으나 나중에 쓰기 위해 저장하는 매개변수는 특히 더 신경 써서 검사해야 한다. 클라이언트가 돌려받은 객체를 사용하려 할 때 예외가 발생하고, 이 객체를 어디서 가져왔는지 추적하기 어려워 디버깅이 힘들어질 수 있다.
- 유효성 검사 비용이 지나치게 높거나 실용적이지 않을 때, 또는 계산 과정에서 암묵적으로 검사가 수행될 때는 검사를 생략해도 된다.
- 매개변수에 제약을 두는 게 좋다는 게 아니다. 메서드는 최대한 범용적으로 설계해야 한다.
- 적시에 방어적 복사본을 만들라.
- 클라이언트가 당신의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍해야 한다.
- 어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 자기도 모르게 내부를 수정하도록 허락할 수 있다.
// 이 클래스는 얼핏 보면 불변처럼 보이지만 불변이 아니다. // Date가 가변이기 때문이다. // 이를 보호하려면 생성자에서 받은 가변 매개변수 각각을 방어적으로 복사해야 한다. // 접근자는 가변 필드의 방어적 복사본을 반환해야 한다. public final class Period { private final Date start; private final Date end; public Period(Date start, Date end) { if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + "가 " + end + "보다 늦다."); } this.start = start; this.end = end; // 위 코드는 아래처럼 수정되어야 한다. /* this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if (start.compareTo(end) > 0) { throw new IllegalArgumentException(start + "가 " + end + "보다 늦다."); } */ } public Date start() { return start; // 위 코드는 아래처럼 수정되어야 한다. /* return new Date(start.getTime()); */ } public Date end() { return end; // 위 코드는 아래처럼 수정되어야 한다. /* return new Date(end.getTime()); */ } }
- 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사해야 한다. 멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
- 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안 된다.
- 접근자는 가변 필드의 방어적 복사본을 반환해야 한다.
- 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰한다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시한다.
- 메서드 시그니처를 신중히 설계하라.
- 메서드 이름을 신중히 짓자.
- 항상 표준 명명 규칙을 따라야 한다.
- 같은 패키지에 속한 다른 이름들과 일관되게 짓는 게 최우선이다. 그 다음에 개발자 커뮤니티에서 널리 받아들여지는 이름을 사용한다.
- 긴 이름은 피하자.
- 애매하면 자바 라이브러리 API 가이드를 참조한다.
- 편의 메서드를 너무 많이 만들지 말자.
- 메서드가 너무 많은 클래스는 익히고, 사용하고, 문서화하고, 테스트하고, 유지보수 하기 어렵다.
- 아주 자주 쓰일 경우에만 별도의 약칭 메서드를 둔다. 확신이 서지 않으면 만들지 말자.
- 매개변수 목록은 짧게 유지하자.
- 4개 이하가 좋다.
- 같은 타입의 매개변수 여러 개가 연달아 나오는 경우가 특히 해롭다.
- 여러 메서드로 쪼갠다.
- 매개변수 여러 개를 묶어주는 도우미 클래스를 만든다. 일반적으로 이런 도우미 클래스를 static inner 클래스로 만든다.
- 앞 두 가지 방법을 합친 것으로, 빌더 패턴을 메서드 호출에 응용한다. 매개변수가 많은데 그 중 일부를 생략해도 괜찮을 때 유용하다. 먼저 모든 매개변수를 하나로 추상화한 객체를 정의하고, 클라이언트에서 이 객체의 setter 메서드를 호출해 필요한 값을 설정하게 하는 것이다. 이 때 각 setter 메서드는 매개변수 하나 혹은 서로 연관된 몇 개만 설정하게 한다. 클라이언트는 먼저 필요한 매개변수를 다 설정한 다음, execute 메서드를 호출해 앞서 설정한 매개변수들의 유효성을 검사한다. 마지막으로 설정이 완료된 객체를 넘겨 원하는 계산을 수행한다.
- 매개변수의 타입으로는 클래스보다는 인터페이스가 낫다. 인터페이스 대신 클래스를 사용하면 클라이언트에게 특정 구현체만 사용하도록 제한하는 꼴이며, 혹시라도 입력 데이터가 다른 형태로 존재한다면 명시한 특정 구현체의 객체로 옮겨 담느라 비싼 복사 비용을 치러야 한다.
- boolean보다는 원소 2개짜리 열거 타입이 낫다. 코드를 읽고 쓰기가 더 쉽고, 나중에 선택지를 추가하기도 쉽다.
- 메서드 이름을 신중히 짓자.
- 다중정의(overloading)는 신중히 사용하라.
- 메서드를 재정의(overriding)했다면 어떤 메서드가 호출될지 해당 객체의 런타임 타입에 따라 결정된다. 메서드를 재정의한 다음 하위클래스의 인스턴스에서 그 메서드를 호출하면 재정의한 메서드가 실행된다.
- 메서드를 다중정의(overloading)했다면 어떤 메서드가 호출될지 컴파일 타임에 결정된다. 객체의 런타임 타입은 전혀 중요치 않다.
- 헷갈릴 수 있는 코드는 작성하지 않는 게 좋다. 안전하고 보수적으로 가려면 매개변수 수가 같은 다중정의는 만들지 말자. 가변인수(varargs)를 사용하는 메서드라면 다중정의를 아예 하지 말아야 한다.
- 다중정의하는 대신 메서드 이름을 다르게 지어주는 걸 고려하자.
- 가변인수(varargs)는 신중히 사용하라.
- 성능에 민감한 상황이라면 가변인수가 걸림돌이 될 수 있다. 가변인수 메서드는 호출될 때마다 배열을 새로 하나 할당하고 초기화한다.
- 예를 들어 해당 메서드 호출의 95%가 인수를 3개 이하로 사용한다면, 인수가 0개인 것부터 3개인 것까지를 정의하고 마지막으로 인수 4개 이상(varargs)인 메서드를 제공하면, 다중정의 메서드가 5%의 호출을 담당하도록 할 수 있다.
- null이 아닌, 빈 컬렉션이나 배열을 반환하라.
- 옵셔널 반환은 신중히 하라.
- 값을 반환하지 못할 가능성이 있고, 호출할 때마다 반환값이 없을 가능성을 염두에 둬야 한다면 옵셔널을 반환해야 할 수도 있다. 하지만 옵셔널 반환에는 성능 저하가 뒤따르니 null을 반환하거나 예외를 던지는 편이 나을 수도 있다.
- 박싱된 기본 타입을 담은 옵셔널을 반환하는 대신 OptionalInt, OptionalLong, OptionalDouble을 반환하자.
- 공개된 API 요소에는 항상 문서화 주석을 작성하라.
- 공개된 모든 클래스, 인터페이스, 메서드, 필드 선언에 문서화 주석을 달아야 한다.
- how가 아닌 what을 기술해야 한다.
- 지역변수의 범위를 최소화하라.
- 지역변수의 유효 범위를 최소로 줄이면 코드 가독성과 유지보수성이 높아지고 오류 가능성은 낮아진다.
- 메서드를 작게 유지하고 한 가지 기능에 집중한다. 한 메서드에서 여러 가지 기능을 처리한다면 그중 한 기능과만 관련된 지역변수라도 다른 기능을 수행하는 코드에서 접근할 수 있을 것이다.
- 지역변수가 가장 처음 쓰일 때 선언한다.
- 거의 모든 지역변수는 선언과 동시에 초기화해야 한다. 초기화에 필요한 정보가 충분하지 않다면 충분해질 때까지 선언을 미뤄야 한다.
- 반복 변수의 값을 반복문이 종료된 뒤에도 써야하는 상황이 아니라면 while 문보다 for 문을 쓰는 게 낫다.
- 전통적인 for 문보다는 for-each 문을 사용하라.
- for-each 문은 반복자와 인덱스 변수를 사용하지 않는다. 코드가 깔끔해지고 오류가 날 일도 없다. 하나의 관용구로 컬렉션과 배열을 모두 처리할 수 있어서 어떤 컨테이너를 다루는지는 신경쓰지 않아도 된다.
- 라이브러리를 익히고 사용하자.
- 바퀴를 다시 발명하지 말자. 일반적으로 라이브러리는 당신의 코드보다 품질이 좋고 점차 개선될 가능성이 크다.
- 정확한 답이 필요하다면 float와 double은 피하라.
- 금융 계산에는 BigDecimal, int, long을 사용해야 한다.
- 박싱된 기본 타입보다는 기본 타입을 사용하라.
- 기본 타입은 간단하고 빠르다.
- 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.
- 언박싱 과정에서 NullPointerException을 던질 수 있다.
- 다른 타입이 적절하다면 문자열 사용을 피하라.
- 문자열은 다른 값 타입을 대신하기에 적합하지 않다. 기본 타입, 참조 타입, 열거 타입을 쓰고 없다면 새로 클래스를 작성하라.
- 문자열 연결은 느리니 주의하라.
- 성능을 포기하고 싶지 않다면 String 대신 StringBuilder를 사용하자.
- 객체는 인터페이스를 사용해 참조하라.
- 적합한 인터페이스만 있다면 매개변수, 반환값, 변수, 필드를 전부 인터페이스 타입으로 선언하라. 프로그램이 훨씬 유연해진다.
- 객체의 실제 클래스를 사용해야 할 상황은 오직 생성자로 생성할 때(new)뿐이다.
- 적합한 인터페이스가 없다면 클래스의 계층구조 중 필요한 기능을 만족하는 가장 덜 구체적인(상위의) 클래스를 타입으로 사용하자.
- 리플렉션보다는 인터페이스를 사용하라.
- 리플렉션을 이용하면 컴파일 당시에 존재하지 않던 클래스도 이용할 수 있다는 장점이 있다.
- 리플렉션의 단점
- 컴파일타임 타입 검사가 주는 이점을 누릴 수 없다. 존재하지 않는 / 접근할 수 없는 메서드를 호출하려 시도하면 (주의해서 대비 코드를 작성하지 않았다면) 런타임 오류가 발생한다.
- 리플렉션을 이용하면 코드가 지저분하고 장황해진다.
- 성능이 떨어진다.
- 리플렉션은 되도록 객체 생성에만 사용하고, 생성한 객체를 이용할 때는 적절한 인터페이스나 컴파일타임에 알 수 있는 상위클래스로 형변환해 사용해야 한다.
- 네이티브 메서드는 신중히 사용하라.
- JNI(Java Native Interface)는 자바 프로그램이 네이티브 메서드를 호출하는 기술이다. 네이티브 메서드란, C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다.
- 성능을 개선할 목적으로 네이티브 메서드를 사용하는 것은 권장하지 않는다. 네이티브 메서드가 성능을 개선해주는 일은 많지 않다.
- 최적화는 신중히 하라.
- 마음에 새겨야 할 격언 세 개
- 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다.
- 자그마한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다.
- 최적화를 할 때는 다음 두 규칙을 따르라. 첫 번째, 하지 마라. 두 번째, 아직 하지 마라. 완전히 명백하고 최적화되지 않은 해법을 찾을 때까지는 하지 마라.
- 빠른 프로그램보다는 좋은 프로그램을 작성하라. 좋은 프로그램이지만 성능이 나오지 않는다면 그 아키텍처 자체가 최적화할 수 있는 길을 안내해줄 것이다. 좋은 프로그램은 정보 은닉 원칙을 따르므로 개별 구성요소의 내부를 독립적으로 설계할 수 있다. 따라서 시스템의 나머지에 영향을 주지 않고도 각 요소를 다시 설계할 수 있다.
하지만 성능을 무시하라는 뜻은 아니다. 구현상의 문제는 나중에 최적화해 해결할 수 있지만, 아키텍처의 결함이 성능을 제한하는 상황이라면 시스템 전체를 다시 작성하지 않고는 해결하기 불가능할 수 있다. 그러니 설계 단계에서 성능을 반드시 염두에 두어야 한다. - 성능을 제한하는 설계를 피하라. 완성 후 가장 변경하기 어려운 설계 요소는 바로 컴포넌트끼리, 혹은 외부 시스템과의 소통 방식이다. e.g. API, 네트워크 프로토콜, 영구 저장용 데이터 포맷.
- API를 설계할 때 성능에 주는 영향을 고려하라. e.g. public 타입을 가변으로 만들면 불필요한 방어적 복사를 수없이 유발할 수 있다, 컴포지션으로 해결할 수 있음에도 상속으로 설계하면 상위 클래스에 영원히 종속되며 그 성능 제약까지도 물려받게 된다.
- 잘 설계된 API는 성능도 좋은 게 보통이다. 그러니 성능을 위해 API를 왜곡하려고 하지 말자. 왜곡된 API와 이를 지원하는 데 따르는 고통은 영원할 것이다.
- 각각의 최적화 시도 전후로 성능을 측정하라. 가장 먼저 어떤 알고리즘을 사용했는지 살펴보라. 알고리즘을 잘못 골랐다면 다른 최적화는 아무리 해봐야 소용이 없다.
- 마음에 새겨야 할 격언 세 개
- 일반적으로 통용되는 명명 규칙을 따르라.
- 예외는 진짜 예외 상황에만 사용하라.
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
- 예외는 일상적인 제어 흐름용으로 쓰여선 안 된다.
- 특정 상태에서만 호출할 수 있는 상태 의존적 메서드를 제공하는 클래스는 상태 검사 메서드도 함께 제공해야 한다. 또는,
- 상태검사 메서드 제공하기
- 빈 옵셔널 반환하기
- null 같은 특수한 값을 반환하기
- 멀티스레드에서는 옵셔널이나 특정값 사용. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
- 성능이 중요한 상항에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정값 사용.
- 다른 모든 경우엔 상태 검사 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다. 상태 검사 메서드 호출을 깜빡 잊었다면 상태 의존적 메서드가 예외를 던질 것이다.
- 복구할 수 있는 상황에서 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라.
- 검사 예외
- 호출하는 쪽에서 복구하리라 여겨지는 상황
- 호출자가 그 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제하게 된다.
- 호출자가 예외 상황에서 벗어나는 데 필요한 정보를 알려주는 메서드를 함께 제공해야 한다.
- 런타임 예외
- 복구가 불가능하거나 더 실행해봐야 득보다 실이 많은 상황
- 이 예외를 잡지 않은 스레드는 적절한 오류 메시지를 내뱉으며 중단된다.
- 프로그래밍 오류
- 에러
- JVM이 더 이상 수행을 계속할 수 없는 상황
- Error 클래스를 상속해 하위 클래스를 만드는 일은 자제한다. 당신이 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 한다.
- 검사 예외
- 필요 없는 검사 예외 사용은 피하라.
- 검사 예외는 API 사용자에게 예외 처리 부담을 준다. 더구나 검사 예외를 던지는 메서드는 스트림 안에서 직접 사용할 수 없기 때문에 자바 8부터는 부담이 더욱 커졌다.
- 검사 예외를 회피하는 가장 쉬운 방법은 적절한 결과 타입을 담은 옵셔널을 반환하는 것이다. 이 방식의 단점은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다.
- 또 다른 방법으로, 검사 예외를 던지는 메서드를 2개로 쪼개 비검사 예외로 바꿀 수 있다. 첫 번째 메서드는 예외가 던져질지 여부를 boolean 값으로 반환한다.
- 표준 예외를 사용하라.
- 자바 라이브러리는 충분한 수의 예외를 제공한다.
e.g. IllegalArgumentException, IllegalStateException, NullPointerException, IndexOutOfBoundsException, ConcurrentModificationException, UnsupportedOperationException - Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자. 여러 성격의 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.
- 자바 라이브러리는 충분한 수의 예외를 제공한다.
- 추상화 수준에 맞는 예외를 던져라.
- 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 수행하려는 일과 관련 없어 보이는 예외가 튀어나오면 프로그래머를 당황시키고, 내부 구현 방식을 드러내게 된다.
- 메서드가 던지는 모든 예외를 문서화하라.
- 검사 예외든, 비검사 예외든.
- 예외의 상세 메시지에 실패 관련 정보를 담으라.
- 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
- 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동하지 말자. 최종 사용자에게는 친절한 안내 메시지를 보여줘야 하는 반면, 예외 메시지는 가독성보다는 담긴 내용이 훨씬 중요하다.
- 가능한 한 실패 원자적으로 만들라.
- 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다. (실패 원자적)
- 가장 간단한 방법은 불변 객체로 설계하는 것이다.
- 가변 객체의 메서드를 실패 원자적으로 만드는 갖아 흔한 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다.
- 마지막 방법은 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법이다.
- 실패 원자적으로 만들 수 있더라도 항상 그리 해야 하는 것은 아니다. 실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산도 있다.
- 예외를 무시하지 말라.
- 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꿔놓도록 한다.
- 공유 중인 가변 데이터는 동기화해 사용하라.
- 여러 스레드가 가변 데이터를 공유한다면 그 데이터를 잃고 쓰는 동작은 동기화해야 한다.
- 과도한 동기화는 피하라.
- 동기화 메서드나 동기화 블록 안에서는 제어를 절대로 클라이언트에게 양도하면 안 된다. 예를 들어 동기화된 영역 안에서는 재정의할 수 있는 메서드는 호출하면 안 되며, 클라이언트가 넘겨준 함수 객체를 호출해서도 안 된다.
- 기본 규칙은 동기화 영역에서는 가능한 한 일을 적게 하는 것이다.
- 스레드보다는 실행자, 태스크, 스트림을 애용하라.
- java.util.concurrent 패키지 등장으로 한 줄로 작업 큐를 생성할 수 있게 되었다.
(ExecutorService exec = Executors.newSingleThreadExecutor();)
- java.util.concurrent 패키지 등장으로 한 줄로 작업 큐를 생성할 수 있게 되었다.
- wait과 notify보다는 동시성 유틸리티를 애용하라.
- java.util.concurrent를 활용하자.
- 스레드 안전성 수준을 문서화하라.
- 지연 초기화는 신중히 사용하라.
- 대부분의 필드는 지연시키지 말고 곧바로 초기화해야 한다.
- 성능 혹은 위험한 초기화 순환을 막기 위해 꼭 지연 초기화를 써야 한다면 올바를 지연 초기화 기법을 사용하자. 인스턴스 필드에는 이중검사 관용구를, 정적 필드에는 지연 초기화 홀더 클래스 관용구를 사용하자. 반복해 초기화해도 괜찮은 인스턴스 필드에는 단일검사 관용구도 고려 대상이다.
- 프로그램의 동작을 스레드 스케줄러에 기대지 말라.
- 자바 직렬화의 대안을 찾으라.
- 시스템을 밑바닥부터 설계한다면 JSON이나 프로토콜 버퍼 같은 대안을 사용하자.
- 신뢰할 수 없는 데이터는 역직렬화하지 말자.
- Serializable을 구현할지는 신중히 결정하라.
- Serializable을 구현할 때의 문제
- 릴리스한 뒤에는 수정하기 어렵다.
- 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다. 그래서 그 직렬화 형태를 영원히 지원해야 하는 것이다. 달리 말하면, 기본 직렬화 형태에서는 클래스의 private과 package-private 인스턴스 필드들마저 API로 공개되는 꼴이 된다.
- 클래스 내부 구현을 손보면 원래의 직렬화 형태와 달라지게 된다. serialVersionUID가 바뀐다.
- 버그와 보안 구멍이 생길 위험이 높아진다.
- 객체는 생성자를 사용해 만드는 게 기본이다. 직렬화는 언어의 기본 매커니즘을 우회하는 객체 생성 기법인 것이다.
- 해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다.
- 릴리스한 뒤에는 수정하기 어렵다.
- 상속용으로 설계된 클래스와 인터페이스는 대부분 Serializable을 구현하면 안 된다. 하위 클래스에 커다란 부담을 지우게 된다.
- 내부 클래스는 직렬화를 구현하지 말아야 한다.
- Serializable을 구현할 때의 문제
- 커스텀 직렬화 형태를 고려해보라.
- readObject 메서드는 방어적으로 작성하라.
- 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라.
- 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라.