아이템 1: 생성자 대신 정적(static) 팩터리 메서드를 고려하라
why?
-
이름을 가질 수 있다
- 반환될 객체의 특성을 쉽게 묘사 가능
- 한 클래스에 시그니처가 같은 생성자가 여럿 필요할 것 같으면 생성자를 static factory method로 바꾸고 차이를 잘 드러내는 이름을 지어주기
class Person {
boolean isKorean;
public Person(boolean isKorean) {
this.isKorean = isKorean;
}
}
이것보단
class Person {
boolean isKorean;
private Person(boolean isKorean) {
this.isKorean = isKorean;
}
public static Person createKorean() {
return new Person(true);
}
public static Person createForeign() {
return new Person(false);
}
}
-
호출될 때마다 인스턴스를 새로 만들 필요가 없음
- 미리 만들거나 만들어서 캐싱, 재사용
- 인스턴스를 통제할 수 있다
- instance-controlled class
- 상황에 따라 싱글톤으로도, noninstantiable…
public class Database {
static Connection connection;
private Database() {}
public static Connection getConnection() {
if (connection == null) {
connection = new Connection();
}
return connection;
}
}
-
반환 타입의 하위 타입 객체를 반환할 수 있음
public class Ramen extends Noodle {}
public class RiceNoodle extends Noodle{}
public class Noodle {
public Noodle() {}
public static Ramen getRamen() {}
public static RiceNoodle getRiceNoodle() {}
}
-
매개변수에 따라 매번 다른 클래스의 객체 반환 가능
- 반환타입의 하위 타입이기만 하면 어떤 클래스든 상관 없음
- 클라 입장에서는 반환 클래스가 어떤 클래스의 인스턴스인지 상관없음
public class Food {
public static Food getRepresentativeNoodle(String country) {
if (country.equals("vietnam")) {
return new RiceNoodle();
} else if (country.equals("italy")) {
return new Pasta();
} else {
}
}
}
-
메서드 작성 시점에는 반환 객체 클래스가 존재하지 않아도 됨
public interface Book {}
public class Library {
public static List<Book> getLibrary() {
return new ArrayList<>();
}
}
- 여기서
BookImpl
은 나중에 구현해도 됨. Library 작성 시점에서는 없어도 됨
- 프레임워크를 만드는 근간 (???)
- 위의 예시로 따지면 사용자가 Book 구현체인 Essay든 Magazine이든 채워넣으면 된다는 이야기인가?
단점
- static factory method만 제공하면 하위 클래스를 만들 수 없음
- 상속하려면 public이나 protected 생성자가 필요하기 때문
- 프로그래머가 찾기 어려움
- 생성자보다 명확히 드러나지는 않음
- 사용자는 인스턴스화할 방법을 일일이 찾아야 함
- 문서와 잘하고 이름도 컨벤션 잘 지켜서 짓기
conventions
- from
- 매개변수 하나 받아서 해당 타입 인스턴스 반환. 형변환
- Date.from
- of
- 여러 매개변수 받아서 적합한 타입의 인스턴스 반환
- EnumSet.of
- valueOf
- instance/getInstance
- 매개변수로 명시한 인스턴스를 반환
- 하지만 같은 인스턴스임을 보장하지 않음
- create/newInstance
- instance/getInstance와 같지만
- 매번 새로운 인스턴스를 생성해 반환함을 보장
- [get|new]{Type}
- [get|new]Instance와 같지만
- 생성할 클래스가 아니라 다른 클래스에 factory method를 정의할 때 씀
- type
아이템 2: 생성자에 매개변수가 많다면 빌더를 고려하라
매개변수가 많을 경우 할 수 있는 짓들
- 점층적 생성자 패턴
- 필드가 n개면 매개변수가 (단순히 생각하면) 0~n개까지인 생성자를 만들 수 있음
- 불편함
- 이게 반복되면 클라 코드를 작성하고 읽기 어려움
- JavaBeans 패턴
- 매개변수 없는 생성자로 객체 생성 수 setter 사용
- 단점
- 객체 하나 만들려면 메서드를 여러 개 호출
- 객체 생성 완료까지 inconsistent
- 매개변수의 유효성을 어떻게 확인할 것인가? 앞에선 생성자에서 한 번에 확인할 수 있는데?
- 쓰레드 안정성을 위한 추가 작업 필요
- immutable하게 만들 수 없음
해결 방안: 빌더 패턴
- 클라가 필요로 하는 객체를 직접 만들지 않음
- 순서
- 필요 매개변수로 생성자나 static factory를 호출해 빌더 객체를 얻음
- 빌더 객체가 제공하는 setter 통해 원하는 매개변수 설정
- 매개변수가 없는 build 메서드 호출
- 필요한 객체 등장! (보통 immutable)
public class Person {
private final int age;
private final String name;
private Person(Builder builder) {
}
public static class Builder {
public Builder(필수_매개변수) {
}
public Builder age(int age) { ... }
public Person build() {
return new Person(this);
}
}
}
장점
- 계층적으로 설계된 클래스와 함께 쓰기 좋음
- abstract class에서는 추상 빌더
- 구체에서는 구체 빌더
- 피자 예시
- abstract class Pizza
- class NyPizza extends Pizza
- class Calzone extends Pizza
- build 메서드는 구체인 하위 클래스 반환
- 클라는 형변환에 신경쓸 필요 없음
- covariant return typing
- 가변인수 매개변수를 여러 개 사용 가능
- 메서드를 나누거나
- 여러 번 호출하도록 하고 내부에서 모을 수도 있음
단점
- 빌더 만들어야 함
- 코드가 길어짐
- 그러나 대개 API는 시간 지날수록 어차피 매개변수 많아짐
아이템 3: private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글턴: 인스턴스를 오직 하나만 생성할 수 있는 클래스
문제점
사용하는 클라 테스트하기가 어려워질 수 있음
만드는 방식
- 공통
- 생성자를 private으로 감추고
- 유일한 인스턴스에 접근할 수 있는 수단 = public static 멤버
- 첫 번째 방식에서는 final 필드
- 두 번째 방식에서는 static factory 메서드
public final
필드 + private
constructor
- private 생성자 뿐이므로 클래스가 초기화될 때 만들어지는 인스턴스가 시스템에서 유일
- 예외: reflection API
- 장점
- public static 팩터리 메서드 + private final 필드
- 이 팩터리 메서드는 항상 같은 객체 참조 반환
- reflection API 예외는 동일
- 장점
- API는 그대로 두고 싱글턴 패턴 아니게 변환 가능. 유연함
- 제네릭 싱글 팩토리로 만들 수 있음(??)
- 메서드 참조를 supplier로 사용 가능(??????)
- 원소가 하나인 enum type 선언
- 위 두가지의 문제점
- 역직렬화할 때 새로운 인스턴스가 만들어질 수 있음
- 해결 방안: transient 선언 + readResolve 메서드 제공
- 반면 enum 타입으로 선언하면 직렬화 문제 해결
- reflection 공격도 막아줌
- 대개 이 방식이 가장 좋은 방식
- 하지만 Enum 외 클래스를 상속해야 하는 경우 사용 불가
아이템 4: 인스턴스화를 막으려거든 private 생성자를 사용하라
- static method와 필드만 담은 유틸리티 클래스를 만들고 싶은 경우
- 쓰임새
- Math, Arrays처럼 기본 타입 값이나 배열 관련 메서드 모아놓기
- Collections처럼 특정 인터페이스 구현하는 객체를 생성해주는 static method
- final class 관련 메서드 모아놓기
문제점
- 이런 유틸 클래스는 인스턴스 만들어 쓰는 게 목적이 아님
- 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자 만들어줌.
해결 방법
private 생성자를 추가
아이템 5: 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
- 많은 클래스가 하나 이상의 자원에 의존
- static util classs나 singleton으로 구현하는 경우가 있음
- 하지만 사용하는 자원에 따라 동작이 달라지는 클래스에서는 부적합
- 자원을 교체하는 메서드를 사용할 수 있지만
- 오류 나기 쉽고
- 멀티스레드 환경에서 사용 불가
해결 방안: 인스턴스 생성 시 생성자에 필요한 자원을 넘겨주기
- 의존 객체 주입의 한 형태
- 변형: 자원 factory 넘겨주기
- factory: 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체
단점
- 큰 플젝에서는 코드 복잡해짐
- 해결 방안: Spring 등 DI 프레임웤 사용
아이템 6: 불필요한 객체 생성을 피하라
- 같은 기능의 객체를 매번 생성하는 것보다 객체 하나를 재사용하는 것이 나을 때가 많음
- Static factory 메서드 제공하는 immutable class에서는 static factory 메서드 사용하면 피할 수 있음
- 왜냐면 생성자는 매번 새로 만들지만 static factory 메서드는 그렇지 않기 때문
- 생성 비용이 비싼 객체도 캐싱해 재사용하기
- 예를 들어 Pattern 인스턴스는 Pattern.compile 사용하기
- String.matches는 내부적으로 Pattern 객체를 생성한다
- lazy initialization을 사용할 수도 있겠지만 비추. 성능 향상 그닥…
- Adapter (view)
- 작업은 뒷단에 맡기고 자신은 제2의 인터페이스 역할만
- 예: Map의 keySet
- Map의 Set 뷰 반환
- 매변 새로운 set 인스턴스 만들 것 같지만 같은 set 인스턴스를 반환할 지도 모른다(모른다는 무슨 말이지??????)
- 반환된 객체를 수정하면 다른 모든 객체가 바뀜. 같은 Map 인스턴스를 대변하기 때문
- 따라서 뷰 객체를 하나만 재사용하는 게 이득임
- 오토박싱
- 예: Long에 long을 n번 더하면 불필요한 Long 인스턴스가 n개 만들어짐
- 되도록 기본 타입을 사용하고 의도치 않은 오토박싱이 없도록 하자
유의사항
- 요즘의 JVM은 작은 객체의 생성/회수가 큰 부담이 아님
- 프로그램의 명확성, 간결성, 기능을 위해 만드는 객체는 오히려 좋아
- 아주 무거운 객체 아니고서야 객체 pool을 만들지 말자
- 많은 경우 코드가 복잡, 메모리 사용량 증가, 성능 저하
아이템 7: 다 쓴 객체 참조를 해제하라
- 자바는 메모리 관리에 신경 안써도 된다는 오해 → 사실이 아님
- 책의
Stack
예시
- 객체 하나를 회수 못하면 걔가 참고하고 또 걔가 참조하는 객체들까지 모두 회수 못함
해법
- 참조를 다 썼을 때 명시적으로 null 처리하기
- Stack에서는 pop했을 때 해당 위치의 요소를 null로
- 장점: 나중에 잘못 접근할 일이 없다 (NPE 처리)
- 그러나 null 처리 하는 건 예외적인 경우여야 함
- 가장 좋은 방법은 변수를 유효 scope 밖으로 밀어내는 것
- 변수의 범위를 최소가 되게 정의한다면 자연스레 해결
- Stack의 문제점
- 스택이 자기 메모리를 직접 관리하기 때문
- GC는 원소가 비활성인지 알 방법이 없음
- 자기 메모리를 직접 관리하는 클래스는 항상 mem 누수에 주의해야 함
- 캐시도 누수 주범
- WeakHashMap: 키를 참조하는 동안만 엔트리를 살릴 수 있음
- 주기적으로 entry 청소 (ScheduledThreadPoolExecutor 이용)
- listener, callback
- 클라가 등록만 하고 명확히 해지하지 않는 경우
- weak ref로 저장
아이템 8: finalizer와 cleaner 사용을 피하라
그냥 쓰지 마라
cpp의 destructor와 다르다
- cpp에서는 특정 객체와 관련된 자원을 회수하는 보편적인 방법이지만
- java에선 gc가 해줌, 우리에게 작업 요구하지 않음
문제점들
즉시 수행을 보장 x
- 실행될 때까지 얼마나 걸릴지 알 수 없음
- gc에 달린 문제, 천차만별
- 파일 닫기를 여기서 하면 원하는 것보다 많은 파일이 열려 있을 수 있다
- 특히 finalizer 스레드는 다른 app 스레드보다 우선순위 낮음
실행 여부조차 보장 x
- (접근할 수 없는 객체에 딸린) 종료 작업을 전혀 실행 못하고 프로그램이 중단될 수 있음
- db 락을 여기서 관리하면 죽을 수도…
finalizer 동작 중 예외는 무시됨
- 처리할 작업이 남았어도 그 순간 종료
- 마무리 덜 된 상태로 마무리 될수도
- 그리고 이렇게 훼손된 객체를 누군가 사용하려 한다면?
성능 문제
- AutoClosable, try-with-resources보다 훨씬 느림
보안 문제
- finalizer 공격
- 생성자나 직렬화에서 예외 → finalizer 수행…!
- finalizer 안에서 필드에 자기 참조를 할당하면?
- gc가 회수 못함
- 생성자에서 예외만 던지면 되는데 finalizer 있으면 그렇지 않음
그러면 어떻게 할까?
AutoCloseable
을 구현하고 클라에서는 인스턴스 다쓰면 close
호출하기
- 구체적으로 자신이 닫혔는지 추적하면서 close 내에서는 이 객체가 유효하지 않음 표기
- 나중에 다시 불리면 예외 처리
- 주의: 순환 참조 생기지 않게 할 것
cleaner와 finalizer는 왜 있는데?
- 자원 소유자가 close를 호출하지 않을 것에 대비한 안전망
- native peer
- 일반 자바 객체가 native method 통해 기능을 위임한 native 객체 (?????)
- 자바 객체가 아니라 gc가 그 존재를 모름
- 이 때 cleaner, finalizer가 처리학기 좋음
- 단 성능 저하 감당하고 심각한 자원을 갖고 있을 때만
- 이 때도 자원을 즉시 회수해야 하면 close 메서드 쓸 것
아이템 9: try-finally보단 try-with-resources를 사용하라
- java lib에는 close를 호출해 직접 닫아줘야 하는 게 많음
- 하지만 앞에서 말했듯 finalizer는 별로임
- 전통적으로 try-finally가 사용되었음
- 자원 많아지면 코드 지저분해짐
- 예외 던져지면 close도 실패
- 그러면 close의 예외만 남고 앞의 예외 정보는 x
try-with-resources 사용하기
void function() {
try (Resource1 rc1 = new ...
Resource2 rc2 = new ...) {
}
}
- 이렇게 하면 원래 코드와 close 양쪽에서 모두 예외 발생하면 close 예외는 숨겨지고 원래 코드의 예외가 기록
- catch도 사용 가능!
아이템 10: equals는 일반 규약을 지켜 재정의하라
우선 하면 안되는 경우
- 각 인스턴스가 본질적으로 고유한 경우 = 값 표현이 아니라 동작하는 개체를 표현하는 클래스
- 인스턴스의 logical equality를 검사할 일이 없는 경우
- 상위 클래스의 equals가 하위 클래스에도 맞는 경우
- private class나 package-private class = equals 메서드를 호출할 일이 없는 경우
그러면 언제?
논리적 equality를 확인해야 하늗네 상위 클래스의 equals가 재정의되지 않은 경우. 즉 객체가 같은지가 아니라 값이 같은지 확인하고 싶은 경우
- Map의 키, Set의 원소로도 쓸 수 있음
- 아이템 1의 인스턴스 통제 클래스나 Enum이라면 재정의할 필요 x
- 차피 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않음
규약
- 반사성: x ≠ null → x.equals(x) = true
- 대칭성: x, y ≠ null && x.equals(y) = true → y.equals(x) = true
- 추이성: x, y, z ≠ null && x.quals(y) = true && y.equals(z) = true → x.equals(z) = true
- 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다
- 추상 클래스라면 가능
- 상속 대신 컴포지션을 사용하라
- 추가되는 필드를 private으로 두고 부모 구체 클래스를 반환하는 view 메서드를 두자
- 단 사용할 땐 주의하기
- 일관성: x, y ≠null → x.equals(y)는 항상 true or false
- 수정되지 않았을 경우
- 불변 클래스라면 결과가 항상 같아야 함
- equals 판단 기준에 신뢰할 수 없는 자원이 끼면 안됨 (e.g. IP 주소)
- 메모리에 존재하는 객체만을 사용, deterministic한 계산만 해야 함
- non-null: x≠null → x.equals(null) = false
좋은 equals 메서드 구현 방식
- == 연산자로 자기 자신 참조인지 확인
- instanceof로 올바른 타입인지 확인
- 입력을 올바른 타입으로 형변환
- 핵심 필드들이 모두 일치하는지 하나씩 검사
주의할 점
- 필드 비교 순서가 성능에 영향을 미칠 수 있음
- 다를 가능성이 크거나 비교 cost가 싼 필드를 먼저 비교하자
- 대칭성, 추이성, 일관성을 만족하는지 확인해보기
- equals를 재정의한 경우 hashCode도 반드시 재정의
- Object를 매개변수로 받자
- 타입을 구체적으로 명시한 경우 이는 재정의(override)가 아니라 오버로딩임
- 하위 클래스에서 FP를 뱉을 수 있고 보안도 안좋음
아이템 11: equals를 재정의하려거든 hashCode도 재정의하라
- 안그러면 HashMap이나 HashSet에서 문제 생김
규약을 살펴보자
- equals 비교에 사용되는 정보가 변경되지 않았다면 앱이 실행되는 동안 hashCode 메서드의 반환값은 같아야 함
- equals가 두 객체 같다고 했으면 hashCode도 같아야 함
- equals가 두 객체 다르다고 판단했어도 hashCode의 값이 다를 필요는 없음
- 하지만 달라야 hash table의 성능이 좋아짐
좋은 hashCode 작성하기
- 각 핵심 필드에 대해 다음 result를 계산
과정
- c를 계산
- primitive 타입이면 Type.hashCode
- 참조 타입 필드인 경우
- 클래스의 equals 메서드가 필드의 equals를 재귀적으로 호출해 비교한다면 필드의 hashCode를 재귀적으로 호출
- 복잡해지면 필드의 표준형을 만들어 표준형의 hashCode를 호출
- 이 때 null이면 0
- 배열인 경우 원소 각각을 별도 필드처럼 다루면서 위 두 가지 규칙 적용
- 핵심 원소가 없는 경우 상수 사용
- 모두 핵심 원소면 Arrays.hashCode
- result = 31 * result + c
- 그리고 unit test
- 파생 필드는 계산해서 제외해도 됨
- equals 비교에 사용되지 않는 필드는 반드시 제거
- 불변 클래스고 hash code 계산 비용이 크다면 캐싱 고려
- 성능을 위해 핵심 필드를 생략하면 안됨
- 해시 품질이 나빠져 hast table 성능 저하
- 값 생성 규칙을 API 사용자에게 자세히 드러내지 말자
아이템 12: toString을 항상 재정의하라
toString의 규약: 모든 하위 클래스에서 이 메서드를 재정의해라
- 디버깅하기 쉬움!
- println, printf, 문자열
+
, assert 넘길 때 사용됨
- 로깅도 쉬움
- 객체가 가진 주요 정보를 모두 반환하는 게 좋음 (스스로를 완변히 설명하는 문자열)
반환값의 포맷을 문서화할지 정해야 함
- 값 클래스면 추천 (e.g. 전화번호, 행렬…)
- human-readable
- 그대로 입출력에 사용하거나 csv 등으로 저장할 수도
- 문자열 ↔ 객체 상호 전환할 수 있는 static factory나 생성자 같이 제공하면 좋다!
- 단점: 시작하면 계속 얽매이게 됨
- 하든 안하든 의도는 명확히 하자
- 그리고 toString에 포함된 정보를 얻어올 수 있는 api는 제공하자
아이템 13:clone 재정의는 주의해서 진행하라
- Cloneable 인터페이스 → Object의 protected 메서드인 clone의 동작 방식을 결정
- 이걸 구현, 재정의하는 클래스에서는 public + 반환 타입을 클래스 자신으로 변경하자
- 먼저 super.clone 호출하고 필요한 필드를 적당히 수정
- 여기서 적절히 = deep copy, side effect가 없도록
- Cloneable을 구현한 클래스 사용자는 복제가 제대로 이뤄질 것을 기대한다
주의사항
-
stack
-
HashTable (버킷 내 원소들이 linked list)
- 배열(bucket)뿐만 아니라 linked list도 복사해야 함
-
clone 내에서는 재정의될 수 있는 메서드 호출하면 안됨
- 하위 클래스가 복제 과정 후 원본과 복제본의 상태가 달라질 수 있음 (흠…..?)
- final이나 private
-
재정의할 경우 exception 던지지 마라. 그래야 편함
-
상속용 클래스에서는 clone 구현하지 말거나 final + throws Exception으로 막아놓기
-
스레드 안전 클래스의 경우 동기화 필요
-
근데 clonable은 문제점이 많으니 복사 생성자/팩토리를 제공하는 게 낫다
- 자신과 같은 클래스 인스턴스를 받는 생성자
- 인터페이스를 받아서 처리할 수도 있음
아이템 14: Comparable을 구현할지 고려하라
- compareTo는 대부분 equals와 비슷하나 몇 가지 차이점
- 순서 비교 가능
- 제네릭
- 규약은 거의 equals의 규약(아이템 10)이랑 비슷한데 지키면 좋을 점
- x.compareTo(y) == 0 ↔ x.equals(y)
- 규약을 지키지 못하면 비교를 제대로 활용할 수 없을 수 있다(e.g. TreeSet, TreeMap, Collections, Arrays)
- 얘네는 동치성 비교할 때 compareTo를 써서 엇박 날수도 있다
<
, >
대신 박싱된 기본 타입 클래스의 compare 사용
- 핵심적인 필드부터 비교
- 비교자 생성 메서드를 사용하면 코드가 간결해지지만 성능이 약간 나빠짐(선택)
- 값의 차이를 그대로 return하면 안됨!
- overflow
- 부동소수점 관련 오류
- 대신 static compare나 비교자 생성 메서드 사용
아이템 15: 클래스와 멤버의 접근 권한을 최소화하라
intro
- 잘 설계된 컴포넌트 = 클래스 내부 테이터와 구현 정보를 외부로부터 잘 숨김
- 오직 api로만 외부와 소통
- 개발 속도, 관리 비용, 성능 최적화, 재사용성, 난이도 등에서 장점
- 자바에서 접근성은 다음에 따라 결정
원칙
모든 클래스와 멤버의 접근성을 최대한 좁혀야 함
- top-level class, interface
- public, package-private
- 패키지 외부에서 쓸 일이 없다면 package-private
- 한 번 public으로 가면 계속 호환성 관리해줘야 함
- 접근 수준
- private, package-private, protected, public
- 권한을 자주 풀어줘야 한다면 컴포넌트 분해를 고려하자
- 제약
- 상위 클래스 메서드 재정의할 땐 접근 수준을 상위 클래스보다 좁게 설정할 수 없음
- 리스코프 치환 원칙 때문 (상위 클래스의 인스턴스는 하위 클래스의 인스턴스로 대체해 사용할 수 있어야 한다)
- public 클래스의 필드는 되도록 public이 아니어야 함
- 통제를 잃어버림
- thread-safe하지 않음
- 배열도 조심
- 배열은 private, public 불변 리스트를 추가
- 배열은 private, 복사본 반환하는 메서드 공개
- 자바9에선 모듈 시스템 추가
- 모듈 = 패키지의 묶음 (where 패키지 = class의 묶음)
- 공개할 패키지 정의 가능
- protected나 public이어도 패키지를 공개 안했으면 모듈 외부에서 접근 불가능
- 조심해서 쓰세요
아이템 16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
- public 클래스라면 getter, setter 쓰는 게 맞음
- 근데 package-private 클래스나 private 중첩 클래스는 필드 노출해도 문제 없음
- 표현하려는 추상 개념만 잘 표현해주면 됨
- 차피 클라이언트는 패키지 내부(혹은 private 중첩 클래스라면 이 클래스를 포함하는 클래스) 코드이므로 통제 가능
- public 클래스의 필드가 불변이어도 직접 노출하는 건 나쁜 생각
아이템 17: 변경 가능성을 최소화하라
불변 클래스 = 내부 값을 수정할 수 없는 클래스
적용한 패턴 = 함수형 프로그래밍
만드는 규칙
- setter 미제공
- 클래스 확장 불가능
- 클래스를 private이나 package-private으로 만들고 public 정적 팩터리 메서드 제공
- 모든 필드를 final로
- 모든 필드를 private으로
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없게
- 클라에서 제공한 객체 참조를 가리키거나 이를 그대로 반환 X
장점
- 단순
- thread safe
- 방어적 복사가 필요 없음
- clone 메서드나 복사 생성자 제공 안하는 게 좋음
- 불변 객체끼리는 내부 데이터 공유 가능
- 구조가 복잡해도 불변식을 유지하기 수월
- 실패 원자성 제공
- 메서드에서 예외가 발생한 후에도 객체는 메서드 호출 전화 같이 유효한 상태여야 함
단점
- 값이 다르면 무조건 독립된 객체로 만들어야 함
- 간단한 연산에도 엄청난 시간, 공간복잡도 있을 수도
- 다단계 연산을 쓰면 해결 가능
대안
- 모든 클래스를 불변으로 만들 수는 없으니
- 변경할 수 있는 부분을 최소한으로 줄이자. 가능하다면 private final로
아이템 18: 상속보다는 컴포지션을 사용하라
상속이 항상 최선은 아니다
- 대개 코드를 재사용하기 쉽지만 (확장할 목적으로 설계되었거나 문서화도 잘 된 클래스가 아닌) 일반적인 구체 클래스를 패키지 경계를 넘어 상속하면 위험
- 왜냐면 캡슐화를 깨뜨리기 때문
- 상위 클래스의 구현 방식에 따라 하위 클래스의 동작에 이상 생길 수 있음
- 다음 릴리즈에서 상위 클래스 변경되면 갑자기 하위에서 뭔가 깨질 수 있음
- 그러면 하위 클래스도 수정해야 함…
- 특히 메서드 재정의
- 클라가 상위 클래스 메서드를 직접 호출해 불변식 해칠 수 있음
해결 방법: 컴포지션
기존 클래스가 새로운 클래스의 구성 요소로 쓰인다
- 확장 대신
- 새로운 클래스를 만들고 (Wrapper class)
- private 필드로 기존 클래스의 인스턴스를 참조
- 그럼 새로운 클래스의 메서드는 private 필드에 있는 인스턴스의 메서드 호출
- decorator pattern
장점
- 기존 클래스 내부 구현 방식에 구애받지 않음
- 상위 클래스 api의 결함이 전파되지 않음
단점
- 콜백 프레임웤과는 상성 안맞음
- 콜백 내부에서는 wrapper를 모르니 내부 객체를 참조로 넘기고 호출하게 됨
생각해보자
- (컴포지션 대신) 상속은 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 함
아이템 19: 상속을 고려해 설계하고 문서화하라. 그렇지 않았다면 상속을 금지하라
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남기기
- api 메서드에서 재정의 가능한 메서드를 호출한다면?
@implSpec
- 내부 동작 중간에 끼어들 수 있는 hook을 잘 선별해 protected 메서드로 공개
- 성능 향상 등을 기대할 수 있고 간단해짐
- 상속의 효과 극대화
- 실제 하위 클래스를 만들어 시험해보기
- 너무 많지도, 적지도 않게
- 상속용 클래스 생성자에서는 재정의 가능 메서드를 호출하면 안됨
- 오작동 가능
- 왜냐면 상위 클래스 생성자가 하위 클래스 생성자보다 먼저 호출되기 때문
- 재정의 메서드가 하위 클래스 생성자에서 초기화하는 값에 의존하면 문제!
- clone, readObject는 재정의 가능 메서드를 호출하면 안됨
- 역직렬화, 복제를 올바르게 하기 전에 재정의 메서드를 호출하게 됨
- 상속용으로 설계하지 않은 클래스는 상속 금지하기
- 금지까지는 아니어도 피하자
- 인터페이스 쓰자
- 그래도 해야겠다면 클래스 내부에서 재정의 가능 메서드 호출하지 말고 문서화
아이템 20: 추상 클래스보다는 인터페이스를 우선하라
여러 장점이 있음
- 추상 클래스는 단일 상속, 인터페이스는 선언된 메서드들을 모두 정의하기만 하면 같은 타입 취급
- 기존 클래스에 쉽게 구현해넣을 수 있음
- implements
- 추상 클래스 끼워넣긴 어려움
- mixin 정의에 안성맞춤
- 대상 타입의 주 기능에 더해 선택적 기능을 혼합했다는 뜻
- 반대로 클래스 계층구조에는 믹스인 삽입할 합리적 위치 없음
- 계층구조 없는 타입 프레임웤 만들 수 있음
- 현실에서는 계층을 엄격히 구분하기 어려운 경우도 있음 (가수, 작곡가, 싱어송라이터의 계층을 나눌 수 있나?)
- 인터페이스들을 조합해 또다른 인터페이스를 만들 수도. 조합의 가능성이 넓어짐!
- 기능 중 명백한 건 default 메서드로 제공해 편의 제공 가능
템플릿 메서드 패턴
- 인터페이스와 skeletal impl 클래스를 함께 제공하는 방식으로 두 방식의 장점 모두 취함
- 역할 분담
- 인터페이스: 타입 정의, 필요시 default 메서드 (
XInterface
)
- 추상 클래스가 타입 정의할 때의 제약으로부터 자유롭게 해줌
- skeletal impl 클래스: 나머지 메서드 구현 (
XAbstractInterface
)
- 이후 skeletal impl 클래스 확장
- 개발하기 편리
- 골격 구현 작성
- (인터페이스) 보고 다른 메서드 구현에 사용되는 기반 메서드 선정
- (인터페이스) 기반 메서드를 사용해 직접 구현할 수 있는 걸 모두 default 메서드로
- (골격 구현 클래스) 기반, default로 만들지 못한 메서드 추가
- equals, hashCode 정의 필요하면 여기서
- 이후 골격 구현 상속해서 사용
아이템 21: 인터페이스는 구현하는 쪽을 생각해 설계하라
- java8부터는 default method 추가
- 하지만 불변식을 해치지 않게 작성하긴 어려움
- 예를 들어 Synchronized collection
- 컴파일에 성공해도 기존 구현체에 런타임 오류를 일으킬 수 있음
- 그러니 기존 인터페이스에 default 메서드 추가는 웬만하면 없어야 함
- 메서드 제거나 기존 시그니처 수정 용도가 아님
- 문제 방지법
- 서로 다른 방식으로 최소 3개는 구현해보기
- 클라이언트도 여러 개 만들어보기
- 릴리즈 전에 문제 잡자
아이템 22: 인터페이스는 타입을 정의하는 용도로만 사용하라
인터페이스는 자신을 구현한 클래스의 인스턴스를 참조할 수 있는 타입 역할을 한다.
- 인터페이스의 구현 = 이 인스턴스로 뭘 할 수 있는지 알려주는 것
- 상수 인터페이스는 안티패턴
- 특정 클래스나 인터페이스에 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가해야 함
- 열거 타입이나
- 인스턴스화할 수 없는 유틸 클래스에 담기
아이템 23: 태그 달린 클래스보다는 클래스 계층구조를 활용하라
예시로 사각형이나 원이 될 수 있는 Figure class
- 태그 달린 클래스엔 쓸데 없는 코드가 많음
- 컴파일러는 별 도움 안되고 런타임에 에러 가능성 농후
대신 subtyping을 활용하자
- root 추상 클래스 정의
- 태그 따라 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
- 같은 메서드는 루트 클래스의 일반 메서드로 선언
- 공통 데이터도 루트 클래스로
아이템 24: 멤버 클래스는 되도록 static으로 만들라
nested class의 종류, 언제 쓸까?
- static 멤버 클래스
- 다른 클래스 안에 선언
- 바깥 클래스의 private 멤버에도 접근할 수 있음
- 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스
- 바깥 인스턴스에 접근할 일이 없으면 무조건 static으로 만들자
- 그렇지 않으면 외부 인스턴스로 숨은 외부 참조를 가지고 시간, 공간 낭비
- mem leak 가능성
- (non static) 멤버 클래스
- 바깥 클래스 인스턴스와 암묵적으로 연결
- 정규화된 this(
OuterClass.this
)로 바깥 인스턴스의 인스턴스 메서드나 참조를 가져올 수 있음
- 바깥 클래스 없이 생성할 수 없음
- 어댑터 정의할 때 자주 쓰임
- 인스턴스를 감싸 다른 클래스 인스턴스처럼 보이게
- 예를 들어 컬렉션 뷰
- 익명 클래스
- 쓰이는 시점에 선언과 동시에 인스턴스 생성
- 비정적 문맥에서만 외부 클래스 인스턴스 참조 가능
- 길면 가독성 떨어짐
- 정적 팩토리 메서드 구현할 때
- 지역 클래스
- 지역 변수 선언할 수 있는 곳이면 선언 가능 (유효범위도 같음)
아이템 25: 톱레벨 클래스는 한 파일에 하나만 담으라
- 소스파일 하나에 톱레벨 클래스를 여러 개 선언할 수는 있다
- 하지만 그럴 이유가 없음
- 한 클래스를 여러 방식으로 정의할 수 있고
- 컴파일러에 소스파일 전달하는 순서에 따라 사용하는 클래스가 달라짐
해결 방법
- 톱레벨 클래스를 다른 소스 파일로 분리
- 혹은 정적 멤버 클래스 사용하기
참고: 제네릭 관련 용어 정리
parameterized type | 매개변수화 타입 | List |
---|
actual type parameter | 실제 타입 매개변수 | String |
generic tpye | 제네릭 타입 | List |
formal type parameter | 정규 타입 매개변수 | E |
unbounded wildcard type | 비한정적 와일드카드 타입 | List<?> |
raw type | 로 타입 | List |
bounded type paramter | 한정적 타입 매개변수 | |
recursive type bound | 재귀적 타입 한정 | <T extends Comapable> |
bounded wildcard type | 한정적 와일드카드 타입 | List<? extends Number> |
generic method | 제네릭 메서드 | static List asList(E[] a) |
type token | 타입 토큰 | String.class |
아이템 26: 로(raw) 타입은 사용하지 말라
용어 정리
- 제네릭 클래스/인터페이스 (제네릭 타입)
- 클래스/인터페이스 선언에 타입 매개변수가 쓰인 것
- 매개변수화 타입
- 클래스/인터페이스 이름 다음 꺽쇠 괄호 안에 나오는 타입 매개변수들
List<String>
의 String
- raw 타입
- 제네릭 타입에서 타입 매개변수를 사용하지 않은 것
List<E>
의 List
raw 타입 단점
- 타입 안정성, 표현력이 사라짐
- 컴파일할 때 오류가 발생하지 않고 런타임에 오류 발생
- 그러면 오류가 어디서 발생했는지 알아차리기 힘들 수 있음
특징
그럼 왜 있나?
- 호환성
- raw 타입은 안되지만
List<Object>
와 같은 사용은 ㄱㅊ
- 모든 타입을 허용함을 컴파일러에 명확히 전달
- 제네릭 하위 규칙 때문이기도
List<String>
은 List
의 하위 타입
List<Object>
는 List
의 하위 타입 X
- 원소의 타입을 몰라도 되는 경우에는?
예외
- class literal
- instanceof
- 비한정적 와일드카드 타입 외에는 적용 불가
- 이 경우 명시적 형변환 필요
아이템 27: 비검사 경고를 제거하라
- 쉬우니까 웬만하면 제거하기
- 모든 비검사 경고는 런타임에
ClassCastException
을 일으킬 수 있음
- 타입 Safe 하다고 정말정말 확신할 수 있으면
@SupressWarnings("unchecked")
- 다만 그 범위는 최소로
- 변수 선언 → 메서드 선언 → 클래스 선언
- 그리고 안전한 이유를 주석으로 남기기
아이템 28: 배열보다는 리스트를 사용하라
배열과 제네릭 타입의 차이
- 배열은 공변, 제네릭은 불공변
- Sub가 Super의 하위 타입인 경우
- Sub[]는 Super[]의 하위 타입
- 하지만 List와 List은 서로 하위 타입도, 상위 타입도 아님
- 정확히는 어떤 타입 T1, T2에 대해서도 List과 List는 상/하위 타입 관계에 있지 않음
- 리스트를 사용하면 타입 관련 실수를 컴파일시 알 수 있다
- 런타임시 타입 정보
- 배열은 런타임에도 원소의 타입을 확인
- 제네릭은 런타임에는 타입 정보가 소거됨
- 결론: 배열과 리스트는 상성이 안맞음
- 제네릭 배열은 생성 불가
-
type-safe하지 않음
-
구체적 예시, 아래 코드가 실행될 수 있다면? (실제로는 컴파일 안됨)
List<String>[] stringLists = new List<String>[1];
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stingLists[0].get(0);
- 배열
E[]
대신 컬렉션 List<E>
를 사용하자
- 코드가 좀 길어지고 성능도 좀 나빠질 순 있지만
- 타입 안정성과 상호 운용성이 좋아진다!
아이템 29: 이왕이면 제네릭 타입으로 만들라
- 제네릭을 안쓴다면 타입 관련 런타임 오류를 마주할 수 있음
어떻게?
elements
(배열)를 갖는 스택의 예시
- 클래스 선언에 타입 매개변수 추가하고 클래스 내부에서 이 타입 사용
- 그런데 타입 매개변수(여기선
E
)는 실체화 불가 타입
E
타입으로 배열을 만들 수 없음
new E[length]
는 불가능
해결법
- 제네릭 배열 생성 금지 제약을 우회하기
- Object 배열 생성 → 제네릭 배열로 형변환
- 비검사 형변환이 안전하다면
@SuppressWarnings
로 경고 숨기기
- 필드가 private인지
- 클라나 다른 메서드로 전달되는 일은 없는지 등등
- 코드상
elements
는 E[]
이지만 런타임 타입은 Object[]
- 보통 현업에선 이거 많이 씀
- 필드 타입을
Object[]
로, 원소를 사용할 때는 E
로 형변환
- 이 경우 API에서 반환값을 형변환할 때 오류 발생
- 형변환이 안전하다면
@suppressWarnings
로 숨기기
- 단점: 원소를 사용할 때마다 형변환해줘야 함
아이템 30: 이왕이면 제네릭 메서드로 만들라
- 매개변수화 타입을 받는 static util 클래스는 보통 제네릭
- 예: Set::union
- 불변 객체를 여러 타입으로 활용할 수 있으려면
- 객체 타입 바꿔주는 정적 팩터리 만들어야 함
- 제네릭 싱글턴 팩터리
- 쓸 일이 있을까…..?
- 재귀적 타입 한정 (recursive type bound)
- 자기 자신이 들어간 표현식을 사용해 타입 매개변수의 허용 범위를 한정
- 예: Comparable 인터페이스
- String은 Comparable을, Integer는 Comparable를 구현
- <E extends Comparable> = 모든 타입 E는 자신과 비교할 수 있다
- 장점: 클라가 형변환해줄 필요가 없음
아이템 31: 한정적 와일드 카드를 사용해 API 유연성을 높이라
- 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입 사용하기
class Stack<E> {
public boolean isEmpty();
public void push(E e);
public E pop();
public void pushAll(Iterable<? extends E> src) {
for (E e: src) {
push(e);
}
}
public void popAll(Collection<? super E> dst) {
while (!isEmpty()) {
ds.add(pop());
}
}
}
PECS: Producer-Extends, Consumer-Super
public Chooser(Collection<? extends T> choices)
public static <E extends Comparable<? super E>> E max(List<? extends E> list)
- 반환 타입에는 한정적 와일드카드 타입 사용하지 말기
- 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드카드로 대체하는 것 추천
public void swap(List<?> list, int i, int j);
public <E> void swap(List<E> list, int i, int j);
-
단 첫 번재 코드는 helper 함수 필요로 함
public void swap(List<?> list, ...) {
swapHelper(list, i, j);
}
private <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
-
(근데 이렇게 할거면 차라리 애초에 타입 매개변수 쓰는 게 낫지 않나??)
OKKY - [JAVA] 제네릭 질문
- 즉 메서드 내부에서 타입을 알아야하는경우는 타입파라미터를 쓰고, 타입을 몰라도 되는경우는 와일드카드를 쓰는걸 권합니다.
- 가령 List를 받아서 그 size를 리턴하는경우엔 List가 어떤 요소를 담고있든 중요하지않죠. 이럴땐 타입파라미터가 아니라 와일드카드를 권합니다.
- swap도 그러한 경우. 타입이 뭔지는 중요하지 않음
- 개념적으로는 와일드카드가 맞는데 실제로 와일드카드로 구현하면 기술적 한계때문에 메서드 구현이 안됨
- 기술쪽을 택하면 타입 매개변수로 구현
- 개념을 지키면서 기술의 한계를 넘을때 저렇게 헬퍼 메서드를 만들어서 해결
추가
이펙티브 자바, 쉽게 정리하기 - item 31. 한정적 와일드카드를 사용해 API 유연성을 높이라
- 클래스가 가진 컬렉션 필드에 수용할 때는 많은 정보 중 필요한 정보만 취사선택 하면 된다. (생산자 관점)
- 정보가 더 많아서 나쁠 게 없다. (상위 타입은 하위 타입을 수용할 수 있다. 상속 덕에 필요한 정보가 이미 다 있다.)
- 클래스가 가진 컬렉션 필드를 꺼내올 때는 해당 컬렉션이 가진 정보를 받아줄 객체가 필요하다. (소비자 관점)
- 상위 객체만이 하위 객체를 받아줄 수 있다. (하위 타입은 상위 타입을 수용할 수 없다. 더 많은 정보가 필요하다.)
아이템 32: 제네릭과 가변인수를 함께 쓸 때는 신중하라
- 제네릭 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);
}
- 근데 앞에서 봤듯이 아래 코드는 컴파일 에러
List<String>[] stringLists = new List<String>[1];
- 왜?
- 제네릭이나 매개변수화 타입의 varargs 매개변수 받는 메서드가 유용하기 때문
@SafeVarargs
annotation을 사용하면 메서드 작성자가 타입 안정성 보장
- 정말 확실할 때만, which is
- 메서드가 varargs 제네릭 배열에 아무것도 저장하지 않고
- 배열 혹은 복제본이 밖으로 노출되지 않는다면 = 신뢰할 수 없는 코드가 배열에 접근 못하면
- 요약해서 인수 전달의 역할만 한다면
static <T> T[] toArray(T... args) {
return args;
}
static <T> T[] pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return toArray(a, b);
case 1: return toArray(b, c);
case 2: return toArray(c, a);
}
throw new Error();
}
public static void main(String[] args) {
String[] attributes = pickTwo("이것", "저것", "그것");
}
- varargs 매개변수 배열을 다른 메서드가 접근해도 되는 경우
@SafeVarargs
로 제대로 annotated된 varargs 메서드에 넘기는 경우
- 배열 내용의 일부 함수를 호출만하는 일반 메서드(varargs 받지 않는)에 넘기는 경우
- 대안: varargs 매개변수 대신 List 매개변수로
-
그럼 위 예시는 다음과 같이 됨
static <T> List<T> pickTwo(T a, T b, T c) {
switch(ThreadLocalRandom.current().nextInt(3)) {
case 0: return List.of(a, b);
case 1: return List.of(b, c);
case 2: return List.of(c, a);
}
throw new Error();
}
public static void main(String[] args) {
List<String> attributes = pickTwo("이것", "저것", "그것");
}
아이템 33: 타입 안전 이종 컨테이너 고려하기
public class Favorites {
private Map<Class<?>, Object> favorites = new HashMap<>();
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(Objects.requireNonNumm(type), instance);
}
public <T> T getFavorites(Class<T> type) {
return type.cast(favorites.get(type));
}
}
f.putFavorite(String.class, "Java");
String favoriteString = f.getFavorite(String.class);
- 컨테이너에 값을 넣거나 뺄 때 매개변수화한 키를 함께 제공하는 방식
제약
- Class 객체를 제네릭으로 넘겨야 함
- 실체화 불가 타입에 사용 불가
- String이나 String[]은 저장할 수 있지만 List은 저장 못함
- 왜냐면 List.class를 사용할 수 없기 때문
아이템 34: int 상수 대신 열거 타입을 사용하라
- 열거 타입 = 일정 상수 값들을 정의한 다음 그 외의 값은 허용하지 않는 타입
- int 기반 열거 타입은 단점이 많음
- 타입 안전 보장 x
- 서로 다른 열거 타입을 비교해버릴 수 있음
- 상수값이 바뀌면 클라도 바뀌어야 함
- 디버깅하기 어려움
- int 대신 string 쓰는 것도 별로
- enum 타입을 쓰자!
작동 방식
- 열거 타입 자체도 하나의 클래스
- 상수 하나당 인스턴스를 만듦
- 이걸 public static final 필드로 공개
- 싱글턴
장점
- 타입 안전성
- 상수 추가나 순서 변경해도 ㄱㅊ
- 디버깅도 쉬움
- 메서드나 필드 추가 가능, 인터페이스 구현까지도 가능
- Object 메서드, Comparable, Serializable 구현되어 있음
- 데이터와 연결 가능
- 상수별 메서드 구현
-
열거 타입에 추상 메서드 선연
-
각 상수별 body에서 재정의
enum Operation {
PLUS { public double apply(double x, double y) { return x + y ;} },
public abstact double apply(double x, double y);
}
- 전략 열거 타입 패턴
-
상수에 따라 전략을 선택하도록 함
-
예: 요일(상수)별 임금 정산 계산식(전략)
enum PayrollDay {
SUNDAY(WEEKEND), MONDAY(WEEKDAY),
int pay(int time, int payRate) {
return payType.pay(time, payRate);
}
enum PayType {
WEEKDAY {
int overtimePay(int time, int payRate) { ... }
}
abstract int overtimePay(int time, int payRate);
int pay(int time, int payRate) {
return base + overtimePay(time, payRate);
}
}
}
결론
- 필요한 원소들을 컴파일타임에 알 수 있다면 항상 열거 타입을 사용하자
아이템 35: ordinal 메서드 대신 인스턴스 필드를 사용하라
- 열거 타입 상수에 연결된 정수를 얻고 싶을 때 사용하고 싶겠지만
- 유지보수하기 어려움
- 상수 선언 변경
- 같은 값 추가 불가
- 중간에 값을 비울 수 없음
- 원래 목적은 EnumSet, EnumMap같이 열거 타입 기반 범용 자료구조에 사용하기 위함
- ordinal 사용하지 말고 인스턴스 필드에 저장하자
아이템 36: 비트 필드 대신 EnumSet을 사용하라
- 비트로 상수들을 관리하면 연산은 효율적으로 수행할 수 있지만 단점이 많음
- 정수 열거 상수 사용할 때의 문제점에 더해
- 비트만으로는 해석하기 어려움
- 순회하기 어려움
- 최대 몇 비트가 필요한지 알아야 함
- 대신 EnumSet을 사용하자
- 열거 타입 상수 값으로 구성된 집합을 잘 표현
- 다른 Set 구현체와 함께 사용 가능
- 내부적으로 비트 벡터를 사용해 원소가 64개 이하면 비트 필드에 대등한 성능
아이템 37: ordinal 인덱싱 대신 EnumMap을 사용하라
- 아이템 35와 같은 이유로 ordinal indexing 사용하면 문제가 많음
- 비검사 형변환
- index 자체로는 의미가 없으니 index → label로 변환하는 작업 필요
- 정수는 type-safe하지 않음 (ArrayIndexOutOfRange…)
⇒ EnumMap
을 사용하자
- enum 타입을 키로 사용
- (다차원 관계) ordinal 2번? → EnumMap 2개를 중첩해라
- 예: 물질 상태와 상전이
- ordinal 쓴다면 상태 하나 추가하려면 배열을 한참 고쳐야 함
아이템 38: 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
- 대개 열거 타입의 확장은 좋은 생각은 아님
- 근데 어울리는 경우가 하나 있는데 바로 opcode
- how?
- Operation interface 정의
- XOperation 열거 타입이 위 인터페이스를 구현
- 사용할 때는 Operation 인터페이스를 사용하면 여러 Operation 타입 대체 가능
-
사용 방법 1
private static <T extends Enum<T> & Operation> void func(Class<T> opEnumType) { ... }
public static void main() {
test(ExtendedOperation.class)
}
-
사용 방법 2
private static void test(Collection<? extends Operation> opSet) {
for (Operation op: opSet) { ... }
}
public static void main() {
test(Arrays.asList(ExtendedOperation.values()))
}
- 단점
- 열거타입끼리 구현을 상속할 수 없음
- 상태에 의존하지 않는 경우라면 default 사용
- 공유 부분이 많다면 별도 helper class나 static helper 메서드로 분리를 고려
아이템 39: 명명 패턴보다 애너테이션(annotation)을 사용하라
- 명명 패턴은 단점이 많다. 예: 테스트 작성
- 오타나면 안됨
- 원하는 프로그램 요소에 대해 동작하지 않을 수 있음
- 메서드가 아닌 class의 이름에 Test를 붙인다면 동작 안할 것
- 프로그램 요소를 매개변수로 전달할 벙법이 없음
- 예외가 던져져야 한다는 걸 메서드 이름에? 뭐가 예외인지 어떻게 알건가?
- 메타 애너테이션: 애너테이션 선언에 다는 애너테이션
- @Retention ⇒ 이 애너테이션이 유지되는 범위
- @Target ⇒ 애너테이션을 달 수 있는 범위
- marker annotation: 매개변수 없이 단순히 대상에 마킹한다는 뜻
- 예시
- 결론: 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다!
아이템 40: @Override 애너테이션을 일관되게 사용하라
- 메서드 선언에만
- 의미: 상위 타입의 메서드를 재정의했다
- interface 메서드 재정의할 때도 사용할 수 있음
- 장점
- 버그 예방
- 잘못해서 overloading을 한다거나…
- 이런 경우 Override annotation 달면 컴파일시 실수 잡아줌
- 예외
- 구체 클래스에서 상위 클래스 메서드를 재정의할 때는 꼭 달 필요까진 없다
- 이 경우 구현하지 않은 게 남았다면 컴파일러가 알려줌
- but, 추상 클래스나 인터페이스에서는 상위 클래스나 인터페이스의 메서드를 재정의하는 모든 곳에 Override 다는 게 좋음
아이템 41: 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
마커 인터페이스: 아무 메서드도 담고 있지 않고 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
- 예: Serializable
- 마커 인터페이스의 장점
- 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있음
- 적용 대상을 더 정밀하게 지정할 수 있음
- 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있는 경우
- 마킹하고 싶은 클래스에서만 인터페이스를 구현/확장하면 됨
- 단점
- annotation 시스템의 지원을 받지 못함
- 마커 annotation과의 차이
- 그럼 언제 마커 annotation vs 인터페이스?
- 마커 annotation
- 모듈, 패키지, 필드, 지역변수 등 클래스/인터페이스 외의 프로그램 요소에 마킹해야 할 때
- annotation을 활발히 사용하는 프레임웤을 사용 중인 경우
- 마커 인터페이스
- 마킹된 객체를 매개변수로 받는 메서드를 작성해야될 경우
아이템 42: 익명 클래스보다는 람다를 사용하라
- 자바 8 전까지는 함수 객체를 사용해야 했음
Collections.sort(words, new Comparator<String>(){
public int compare(String s1, String s2) {
}
})
- Comparator 인터페이스는 정렬을 담당하는 추상 전략을 의미
- 구체적 전략은 익명 클래스로 구현
- 이제 람다식 쓰자!
-
추상 메서드 하나짜리 인터페이스들의 인스턴스
Collections.sort(words, (s1, s2) -> ...)
-
s1, s2, 반환값의 타입은 컴파일러가 타입 추론
- 상황에 따라 타입 명시해야 할 때도 있음
- 제네릭은 타입 추론에 사용되기 때문에 여기서 특히 중요
- enum에서 람다를 인스턴스 필드에 저장해 상수별 동작을 구현할 수 있음
public enum Operation {
PLUS("+") {
public double apply(double x, double y) {
return x + y
}
}
Operation(String symbol) {
...
}
}
---
public enum Operation {
PLUS("+", (x, y) -> x + y)
Operation(String symbol, DoubleBinaryOperator op) {
...
}
}
주의할 점
- 이름이 없고 문서화 불가능
- 따라서
- 코드 자체로 동작이 명확히 설명되지 않거나
- 코드 줄 수가 많아지는 경우
- 지양하기
- 사용할 수 없는 곳도 있다
- 람다는 함수형 인터페이스에서만 사용
- 추상 클래스의 인스턴스를 만들 때 람다를 쓸 수 없음
- 이런 경우 익명 클래스 사용
- 자신 참조 불가능
- 직렬화는 웬만하면 피하기
아이템 43: 람다보다는 메서드 참조를 사용하라
- 메서드 참조는 익명 클래스, 람다보다도 간결
- 매개변수가 늘어날수록 메서드 참조로 줄어드는 코드도 늘어남
- 때론 람다가 더 간결할 때도 있음
참조 유형들
- 정적
- before:
map.merge(key, 1, (count, incr) -> count + incr)
- after:
map.merge(key, 1, Integer::sum)
- bound 인스턴스 메서드 참조: 수신 객체를 특정
- before:
Instant then = Instant.now(); t -> then.isAfter(t);
- after:
Instant.now()::isAfter
- unbound 인스턴스 메서드 참조: 수신 객체를 특정하지 않음
- before:
str -> str.toLowerCase()
- after:
String::toLowerCase
- 클래스 생성자
- before:
() -> new TreeMap<K, V>()
- after:
TreeMap<K, V>::new
- 배열 생성자
- before:
len -> new int[len]
- after:
int[]::new
아이템 44: 표준 함수형 인터페이스를 사용하라
- 웬만한 건 표준 함수형 인터페이스에 다 있다
java.util.function
- UnaryOperator, Predicate, Function…
- type, input, output에 따른 변형들까지 있음
- 기본 함수형 인터페이스에 primitive type을 boxing해서 넣지 말자
- 전용 함수형 인터페이스를 만들어야 할 때
- 자주 쓰이며 이름 자체가 용도를 명확히 설명
- 반드시 따라야 하는 규약이 있음
- 유용한 디폴트 메서드 제공 가능
- 작성할 때는 주의해서 설계하고
interface
임을 명심
- 애너테이션 (
@FunctionalInterface
) 잘 붙이기
- 인터페이스가 람다용으로 설계된 것임을 알려줌
- 추상 메서드를 오직 하나만 가져야만 컴파일
- 유지보수할 때 누가 메서드 추가할 가능성 배제
- 주의할 점: 다중정의
- 서로 다른 함수형 인터페이스를 같은 위치의 인수로 받는 메서드들을 다중정의하면 안됨
추가
함수형 인터페이스와 람다
아이템 45: 스트림은 주의해서 사용하라
- 스트림 api는 다량의 데이터 처리 작업을 도움
- 핵심 추상 개념
- stream = 데이터 원소의 유한/무한 시퀀스
- stream pipeline: 이 원소들로 수행하는 연산 단계
- 구성
- source stream
- 중간 연산 (intermediate op)
- 종단 연산 (terminal op)
- 컬렉션에 담기, 특정 원소 선택 등등
- lazy evaluation
- 종단 연산 없으면 아무 일도 하지 않음 (== no-op)
- 병렬 처리 가능하나 효과 볼 수 있는 경우 거의 없음
- 주의
- 잘못(과다) 사용하면 읽기 어렵고 유지보수하기 어려워짐
- 람다 매개변수명을 잘 지어야 이해하기 쉽다
- 사실상 final인 변수만 읽을 수 있고 지역변수 수정은 불가능
- return/break/continue, exception 처리 등이 불가능
- 쓰기 좋은 경우
- 시퀀스를 일관되게 변환
- 필터링
- 하나의 연산을 사용해 결합
- 컬렉션에 모으기
- 특정 조건 만족하는 원소 찾기
- 쓰기 힘든 경우
- stage별 각 단계에서의 값에 동시 접근해야 하는 경우
- 뭐가 나은지 모르겠으면 둘 다 써보고 괜찮은 걸로
아이템 46: 스트림에서는 부작용 없는 함수를 사용하라
- 스트림의 핵심 = 계산을 transformation으로 재구성하는 것
- 각 변환 단계는 순수 함수여야 함
- 즉, 스트림 연산에 건네진 함수 객체는 side effect가 없어야 함
- forEach연산은 스트림 계산 결과를 보고할 때만 사용하고 계산에는 사용하지 말기
- collector는 스트림에 필수적인 개념
- toList, toSet, toCollection, toMap, groupingBy, joining…
- toMap의 옵션들
- 각 원소가 고유한 key에 매핑된 경우
- 병합 함수 제공
- 연관 원소들 중 하나 선택
- 마지막 값 선택
- groupingBy
- 입력으로 classifier
- 출력으로 원소들을 카테고리별로 모은 맵을 담은 수집기
- 리스트 외의 값을 갖는 맵을 생성하려면 downstream 수집기 명시
- 해당 카테고리의 모든 원소를 담은 스트림으로부터 값을 생성
아이템 47: 반환 타입으로는 스트림보다 컬렉션이 낫다
아이템 48: 스트림 병렬화는 주의해서 적용하라
- java는 동시성 프로그래밍 잘 지원하지만
- 병렬 스트림 파이프라인을 사용하려면 주의해야 함
- 다음의 경우 병렬화로 성능 개선 기대 못함
- 데이터 소스가 stream.iterate이거나
- 중간 연산으로 limit 쓰는 경우
- 이 경우 필요없는 연산 할 수 있음 (일단 해놓고 버림)
- 언제 좋은가?
- 스트림 소스가 다음일 경우
- ArrayList, HashMap, HashSet, ConcurrentHashMap, 배열, int, long
- 데이터를 원하는 크기로 쉽게 나눌 수 있기 때문
- 또 locality 높음
- 종단 연산도 중요
- 종단 연산 작업량의 비중이 높다면 성능 향상 기대하기 힘듦
- 가장 적합한 건 reduction
- 잘못된 병렬화는
- 성능 저하, 잘못된 결과, 예상치 못한 동작 야기
- 함수 객체의 규약을 잘 지켜야 함
- 성능 향상 확인법
- 원소 수 * 원소당 수행되는 코드 줄 수 > 수십만인가?
- 검증
- 변경 전후 운영 시스템과 비슷한 환경에서 성능 테스트해보기
- 시스템과 같은 스레드 풀을 사용하기에 다른 부분에 영향 가지 않는지도 봐야 함
아이템 49: 매개변수가 유효한지 검사하라
- 메서드와 생성자 매개변수의 제약은 반드시 문서화하고 메서드 몸체 시작 전에 검사하기
검사 못했을 경우 문제점
-
메서 수핸 중간에 모호한 예외를 던져야 함
-
메서드가 수행되고 잘못된 결과 반환
-
특정 객체를 이상한 상태로 만들어 나중에 메서드와 관련 없는 오류를 만듦
-
public, protected 메서드는 매개변수의 제약과 잘못됐을 때 던지는 예외를 문서화 해야함
-
requireNonNull
메서드도 유용함
-
public, protected가 아니면 우리가 매개변수 잘 넘겨줘야 함
-
나중에 사용하기 위한 매개변수는 더욱 신경쓰기
-
클래스 생성자의 경우 클래스 불변식을 위해 필요
-
예외
- 검사 비용이 높을 때
- 계산과정에서 암묵적으로 수행될 때
아이템 50: 적시에 방어적 복사본을 만들라
- 클라이언트를 믿지 말고 방어적으로 프로그래밍하기
- 흔한 실수: 필드 자체는 private final이지만 외부에서 설정한 필드 인스턴스를 직접 수정하는 경우
- 이 경우 매개변수들을 방어적 복사하기
- 그리고 이 복사본으로 유효성 검사
- 매개변수가 외부에서 확장될 수 있는 타입이면 복사본 만들 때 clone 사용하면 안됨
- getter의 경우 방어적 복사본 반환하기
- 이 경우 clone 사용해도 무방
- 혹은 불변 뷰를 반환
- 클라가 제공한 참조를 내부에 보관하는 경우 이 참조(객체)가 잠재적으로 변경될 수 있는지 따져보기
- 고로, 불변 객체들을 조합해 객체를 구성해야 방어적 복사 할 일이 줄어든다
- 복사를 생략해도 될 때
- 클래스와 클라가 상호 신뢰할 수 있을 때
- 불변식이 깨져도 영향이 호출한 클라로 한정될 때
아이템 51: 메서드 시그니처를 신중히 설계하라
API 설계 요령들!
- 메서드 이름 신중히 짓기
- 표준 명명 규칙
- 이해할 수 있고 같은 패키지 내 다른 메서드와 일관되게
- 커뮤니티에서 널리 받아들여지는 이름
- 긴 이름 피하기
- 편의 메서드를 너무 많이 만들지 않기
- 매개변수 목록은 짧게 유지
- 4개 이하 추천
- 같은 타입 매개변수가 연달아 나오는 건 피하기
- how?
- 여러 메서드로 쪼개기
- 매개변수 여러 개를 묶는 도우미 클래스
- 빌더 패턴 응용
- setter 메서드로 매개변수 설정
- execute 메서드에서 유효성 검사
- 계산 수행
- 매개변수의 타입으로는 클래스보다는 인스턴스
- 유연해짐
- 클래스를 사용하면 클라가 제한됨
- 또 특정 구현체로 옮겨담느라 비용 증가할 수 있음
- boolean보다는 원소 2개짜리 enum 쓰기
아이템 52: 다중정의는 신중히 사용하라
- 재정의한 메서드는 동적으로 선택되고 다중정의한 메서드는 정적으로 선택됨
- 고로 다중정의가 혼동을 일으키는 상황은 피하기
- 매개변수 수가 같은 다중정의는 피하기
- varargs를 사용한다면 아예 다중정의 하지 말기
- 다중정의보다는 메서드 이름 다르게 짓는 게 낫다
- 생성자의 경우 2개 이상인 경우 무조건 다중정의가 됨
- 매개변수 중 하나 이상이 근본적으로 다른 경우(=서로 형변환 불가능한 경우) 헷갈릴 일이 없음
- 참조 메서드와 호출 메서드 다 다중정의 되어있으면 대환장파티
- 헷갈릴만한 매개변수는 형변환해서 정확한 다중정의 메서드 사용하도록 하기
- 혹은 같은 객체를 입력받는 다중정의 메서드들이 모두 동일하게 동작하게 하기
추가
005. LSP와 Generic, 오브젝트 4/6
아이템 53: 가변인수는 신중히 사용하라
- varargs 메서드는 명시한 타입의 인수를 0개 이상 받을 수 있음
- 인수의 개수를 제한(1개 이상이라던가)해야할 수도 있는데 length로 확인하는 방법은 문제가 있음
- 이보다는 매개변수 2개 받기
int min(int firstArg, int... remainingArgs)
- 성능에 민감하다면 다중정의 사용하는 걸 고려
아이템 54: null이 아닌, 빈 컬렉션이나 배열을 반환하라
- 보통 값이 없으면 null을 반환할 때가 많음
- 하지만 이러면 클라가 null을 처리하는 로직 추가해야 함
- 틀린 속설: null 말고 빈 컨테이너 반환하면 성능이 저하된다?
- 이는 보통 수준에서 신경쓸 수준이 못됨
- 빈 컬렉션/배열은 굳이 새로 할당하지 않고도 반환할 수 있음
- (가능성은 적지만) 성능 저하가 있다면?
- 불변인 빈 컬렉션/배열 선언해두고 반환
- 이 경우 실제 성능 개선이 있는지 확인하기
아이템 55: Optional 반환은 신중히 하라
- Optional 등장 이전 반환이 불가능한 경우 가능했던 방법
- 예외 → 진짜 예외적인 경우가 아님, 예외 생성 비용 문제
- null 반환 → 별도 처리해줘야 하고 안할 경우 NPE
- Optional을 반환하는 메서드에서는 절대 null 반환하지 말기
- Optional 반환 선택 기준
- 검사 예외의 취지 생각
- 반환값이 없음을 API 사용자에게 알려줌
- 결과가 없을 수 있으며 클라이언트가 이 상황을 특별하게 처리해야 할 경우 사용하자
주의할 점
isPresent
사용은 신중히
- 웬만한 작업을 할 수 있지만 대부분 적당한 메서드로 대체 가능
- 컨테이너 타입(컬렉션, 스트림, 배열, 옵셔널…)은 옵셔널로 감싸면 안됨
- 박싱된 기본 타입을 담은 옵셔널보다는 전용 옵셔널 (OptionalInt, OptionalLong, OptionalDouble…)
- 옵셔널을 컬렉션의 키/값, 배열의 원소로 사용하는 게 적합한 경우는 거의 없음
- 인스턴스 필드로 사용?
- 이보다는 선택적 필드를 추가한 하위 클래스를 분리
아이템 56: 공개된 API 요소에는 항상 문서화 주석을 작성하라
-
Javadoc은 설명을 추려 API 문서로 변환해줌
-
API를 올바로 문서화하려면 공개된 모든 class, interface, 메서드, 필드 선언에 문서화 주석을 달아야 함
- contructor에는 못 단다
- 공개되지 않은 친구들도 주석 달면 좋다
-
HTML 태그도 알아들음
-
첫 문장은 해당 요소의 summary로 간주
-
메서드 문서화 주석
- 메서드와 클라 사이의 규약을 명료히 기술해야 함
- 상속용이 아니라면 무엇을 하는지를 기술
- precondition, postcondition
- 부작용
-
제네릭 타입이나 제네릭 메서드를 문서화할 때는 모든 타입 매개변수에 주석 달기
-
열거 타입을 문서화할 때는 상수들에도 주석 달기
-
애너테이션 타입을 문서화할 때는 멤버들에도 주석 달기
-
스레드 안전 수준을 API 설명에 반드시 포함
-
직렬화할 수 있는 클래스라면 직렬화 형태도 기술
-
메서드 주석을 삭속시킬 수도 있음
-
복잡한 경우 설명 문서 링크를 제공할 수도
아이템 57: 지역변수의 범위를 최소화하라
- 가장 좋은 방법: 가장 처음 쓰일 때 선언하기
- 거의 모든 지역변수는 선언과 동시에 초기화
- 초기화에 필요한 정보가 부족하다면 충분해질 때까지 선언 미루기
- for문은 범위 최소화를 잘해줌
- 메서드를 작게 유지하고 한 가지 기능에만 집중하는 것도 좋은 방법 == 기능별 메서드 분리
아이템 58: 전통적인 for문보다는 for-each 문을 사용하라
- iterator나 인덱스 변수가 의미 없는 경우가 있음 (원소만 필요한 경우)
for (Element e: elements)
- Iterable 인터페이스를 구현한 객체라면 모두 사용 가능
하지만 사용할 수 없는 경우
- 파괴적 필터링(destructive filtering)
- 컬렉션을 순회하며 선택된 원소를 제거하는 경우
- 자바 8부터는 removeIf 메서드 제공
- 변형(transorming)
- 리스트나 배열을 순회하며 원소의 값 일부나 전체를 교체하는 경우
- 병렬 반복(parallel iteration)
아이템 59: 라이브러리를 익히고 사용하라
- (예) 랜덤 숫자 생성기를 직접 구현할 경우 문제가 많다
- 이럴 때 쓰라고 Random.nextInt가 준비되어 있다
- 장점
- 표준 라이브러리를 쓰면 전문가의 지식과 선배 프로그래머의 경험을 활용할 수 있다!!
- 핵심 로직 개발에 집중 가능
- 따로 노력하지 않아도 성능이 지속해서 개선됨
- 기능의 추가
- 다른 개발자들이 쉽게 이해할 수 있음
- 특히 java.lang, java.util, java.io 및 하위 패키지들
아이템 60: 정확한 답이 필요하다면 float와 double은 피하라
- float, double은 근사치로 계산하도록 설계됨
- 금융 관련 계산이랑은 안맞음
- 대신 BigDecimal, int, long 사용하기
- BigDecimal 단점
아이템 61: 박싱된 기본 타입보다는 기본 타입 사용하라
- 기본 타입과 참조 타입을 크게 구분하지 않고 사용할 수 있게 됐지만 차이가 없는 것은 아님
- 박싱 타입에는 identity가 있음
- 박싱 타입은 nullable
- 기본 타입이 시간과 메모리 사용에 있어 효율적
- 주의할 점
- 박싱 타입에
==
쓰면 오류 발생
- 기본, 박싱 타입 홍뇽하면 박싱이 자동으로 풀리고
- 박싱, 언박싱이 자주 일어날수록 성능 저하
- 박싱 타입은 언제 쓰나?
- 컬렉션의 원소, 키, 값
- 매개변수화 타입이나 매개변수와 메서드의 타입 매개변수
- 리플렉션으로 메서드 호출할 때
아이템 62: 다른 타입이 적절하다면 문자열 사용을 피하라
- 다른 값 타입을 대신하기 부적합
- 예를 들어 키보드 입력을 받을 때 숫자, boolean 등이라면 적절하게 타입 변환해서 사용해라
- 열거 타입 대신하기 부적합
- 혼합 타입을 대신하기 부적합
- delimeter로 join한다거나…
- delimeter가 값에 포함된 경우 문제
- 파싱→ 느림, 귀찮음, 오류 가능성
- 권한 표현에 부적합
- 예: 한 스레드에서 사용해야 하는 value의 key
- 실수로 같은 값을 사용한다거나 할 수 있음
- 보안 취약
아이템 63: 문자열 연결은 느리니 주의하라
- 문자열 연결 연산자로 문자열 n개를 이으면 n^2의 시간
- 성능 저하가 우려되면 StringBuilder 사용하기
아이템 64: 객체는 인터페이스를 사용해 참조하라
- 앞서 아이템 51에서 매개변수 타입으로 클래스 말고 인터페이스 쓰라고 했는데 이를 확장해서,
- 적합한 인터페이스만 있다면 매개변수는 물론 반환값, 변수, 필드를 모두 인터페이스 타입으로 선언해라
- 실제 클래스 사용은 only when 생성자로 생성할 때
- 유연해짐!
- 주의할 점
- 클래스가 interface 규약 외에 특별한 기능을 제공하고 이를 쓴다면 바꿀 애도 이걸 제공해야 함
- 적합한 게 없다면 클래스로…
- cases
- String, BigInteger 등
- 프레임워크 객체
- interface에 없는 특별한 메서드를 제공하는 클래스들
- PriorityQueue::comparator
- 하지만 이렇다 하더라고 가장 덜 구체적인, 상위의 클래스 타입을 사용해라
아이템 65: 리플렉션보다는 인터페이스를 사용하라
- 리플렉션을 이용하면
- 프로그램에서 임의의 클래스에 접근 가능
- Class 객체의 Constructor, Method, Field 인스턴스를 가져올 수 있고
- 클래스의 멤버 이름, 필드 타입, 메서드 시그니처까지도 가져올 수 있음
- 심지어 조작도 가능
- 컴파일시 존재하지 않던 클래스도 이용 가능
단점
- 컴파일타임 타입 검사 이점 못누림
- 지저분한 코드
- 성능 떨어짐
장점 누리기
- 아주 제한된 형태로만 사용해야 이점 취할 수 있음
- 컴파일타임에 이용할 수 없는 클래스를 사용해야 하는 경우
- 이 경우 생성에만 리플렉션 사용하고
- 인터페이스나 상위 클래스로 참조해 사용해라
- 예
Set
의 정확한 클래스를 인수로 받고 인수들을 이 set에 추가하는 경우
- 이 예시에서 리플렉션의 단점들 확인 가능
- 런타임에 별의 별 예외 던질 수 있음
- 장황한 코드
- 인스턴스 생성을 위해 무려 25줄의 코드가 필요…
- 또다른 유용한 경우
- 런타임에 존재하지 않을 수 있는 다른 클래스, 메서드, 필드와의 의존성 관리
- 버전이 여러 개 존재하는 외부 패키지 다룰 때
- 가장 낮은 버전 기준으로 컴파일하고 이후 버전의 클래스나 메서드는 리플렉션으로 접근
- 다만 이 경우에도 런타임에 존재하지 않을 수 있다는 걸 고려해야 함
아이템 66: 네이티브 메서드는 신중히 사용하라
- JNI: Java Native Interface
- C, cpp처럼 네이티브 PL로 작성된 메서드를 호출할 수 있게 해줌
- 쓰임새
- 레지스트리같은 플랫폼 특화 기능 사용
- 네이티브 코드로 작성된 기존 라이브러리 사용
- 성능에 영향 주는 부분만 성능 개선을 위해
- 성능 개선?
- 대부분의 작업에서 자바는 이미 충분한 성능 보여줌
- 다만 아닐 수도 있음 (e.g. BigInteger는 GMP의 성능이 꾸준이 이뤄졌다 함)
- 단점
- 메모리 훼손 오류
- 낮은 이식성
- 어려운 디버깅
- GC가 회수 못하고 추적도 못함
- context switch(라고 해도 되려나?)마다 cost
- 네이티브와 자바 사이의 glue code
아이템 67: 최적화는 신중히 하라
- 전문가가 아니라면 웬만하면 하지 마라
- 차라리 견고한 구조를 위해 노력해라
- 설계 단계에서 성능을 염두에 두긴 해야 함
- 성능을 제한하는 설계 피하기
- API 설계시 성능에 주는 영향을 고려하기
- public 가변 데이터는 불필요한 방어적 복사를 유발
- 컴포지션 대신 상속을 쓰면 성능 제약까지 물려받음
- 인터페이스 대신 구현 타입을 사용하면 나중에 성능 개선된 구현체가 나와도 갈아끼우기 어려움
- 그렇다고 성능을 위해 API를 왜곡하면 안좋은 생각
- 잘 설계한 프로그램을 완성한 후에나 최적화를 고려해볼만
- 최적화 시도한다면 전후 성능을 측정해라
아이템 68: 일반적으로 통용되는 명명 규칙을 따르라
- 철자
- 패키지, 클래스, 인터페이스, 메서드, 필드, 타입 변수의 이름을 다룸
- 패키지, 모듈
- 각 요소를 점으로 구분해 계층적으로
- 소문자나 숫자
- 외부 공개 패키지라면 도메인 이름 역순
- 짧은 단어나 약어
- 클래스, 인터페이스
- 대문자 시작
- 널리 통용되는 줄임말(e.g. min, max…) 아니라면 줄여 쓰지 않도록 함
- 메서드, 필드
- 대문자 시작만 빼면 클래스/인터페이스와 동일
- 첫 단어가 약자라면 단어 전체가 소문자
- 단, 상수 필드는 모두 대문자+밑줄 구분 (e.g. NEGATIVE_INFINITY)
- 지역 변수
- 타입 매개변수
- 한 문자
- T (general), E (Collection), K/V (맵의 키/값), X (예외), R (메서드 반환 타입)
- 그 외에는 T, U, V 등
- 문법
- 클래스
- 단수 명사/명사구(e.g. PriorityQueue)
- 단 객체를 생성할 수 없는 클래스는 복수형 명사로
- 인터페이스
- 클래스와 비슷하거나
~able
, ~ible
의 형용사
- 메서드
- 동사구 (e.g. append)
- boolean 값 반환하면
is~
, has~
- 특별 case
- 객체 타입을 바꿔서 또다른 객체를 반환하는 인스턴스 메서드의 경우
- 객체 내용을 다른 뷰로 보여주는 경우
아이템 69: 예외는 진짜 예외 상황에만 사용하라
- 예외 기반의 프로그래밍 하지 마라…
- 예:
ArrayIndexOutOfBoundsException
뜰 때까지 순회하기(;;;;)
- 가독성 떨어지고 성능을 떨어뜨림
- 제대로 동작하지 않을 수 있음
- 잘 설계된 API는 클라가 정상적인 제어 흐름에서 예외를 사용할 일이 없어야 함
- 상태 의존적 메서드를 제공하는 클래스는 상태 검사 메서드도 함께 제공해야 함
- e.g.
Iterator
인터페이스의 next
와 hasNext
- 혹은 올바르지 않은 상태일 때 빈 옵셔널이나 null 반환
- 언제 무엇을?
- 옵셔널이나 특정 값
- 여러 스레드가 동시 접근 가능 or 외부 요인으로 상태 변화 가능한 경우
- 상태 검사와 메서드 사이에 변화 가능하기 때문
- 성능이 중요한 상황에서 상태 검사와 메서드 사이에 중복된 일을 수행하는 경우
- 상태 검사 메서드
아이템 70: 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
- 자바는 throwable로 3가지 제공: 검사 예외, 런타임 예외(비검사), 에러(비검사)
검사 예외
- 호출하는 쪽에서 복구할 것(복구 가능한 조건)이라 여겨지는 상황
- 검사 예외를 던지면 caller가 catch로 처리하거나 밖으로 전파하도록 강제
- API 사용자에게 회복을 요구하는 셈
런타임 예외
- 프로그래밍 오류
- 대부분 클라가 API 명세의 제약을 지키지 못했다는 뜻
- “복구 가능한가?”는 API 설계자의 판단에 따라
에러
아이템 71: 필요 없는 검사 예외 사용은 피하라
- 검사 예외를 제대로 사용하면 api와 프로그램의 질을 높일 수 있음
- 발생한 문제를 프로그래머가 처리해 안전성을 높이게 해줌
- 하지만 api 사용자에게 부담
- 처리하거나 전파해야 함
- 스트림 안에서 직접 사용할 수 없음
- 잘 사용될 수 없는 경우라면 비검사 예외 사용하는 게 좋음
- 잘 사용될 수 없는 경우란
- api를 제대로 사용해도 발생할 수 있거나
- 프로그래머가 의미 있는 조치를 취할 수 있는 경우
- 기존에 검사 예외가 있던 경우에 추가하는 건 별 일 아니지만 단 하나의 검사 예외는 부담이 훨씬 큼
회피 방법
- 적절한 옵셔널 반환
- 메서드를 2개로 쪼개 비검사 예외로 바꾸기
아이템 72: 표준 예외를 사용하라
- 재사용성을 프로그래밍에서 중요
- 표준 예외 재사용의 장점
- 다른 사람이 익히고 사용하기 쉬움
- 예외 클래스가 적을수록
- 메모리 사용량이 적고
- 클래스 적재 시간도 줄어듦
예시
IllegalArgumentException
- caller가 인수로 부적절한 값을 넘겼을 때
IllegalStateException
- 대상 객체의 상태가 호출된 메서드를 수행하기 적합하지 않을 때
- e.g. 제대로 초기화되지 않은 객체를 사용하려 할 때
ConcurrentModificationException
- 단일 스레드에서 사용하려 설계한 객체를 여러 스레드가 동시에 수정하려 할 때
- 문제가 생길 “가능성” 정도를 알려줌
UnsupportedOperationException
- 그 외에도
NullPointerException
, IndexOutOfBoundsException
, ArithmeticException
, NumberFormatException
등등…
주의사항
Exception
, RuntimeException
, Throwable
, Error
는 직접 재사용하지 말기
- api 문서를 참고해 어떤 상황에서 던져지는지 확인하기
- 이름 뿐만 아니라 예외가 던져지는 맥락도 부합할 때 재사용하기
- IllegalState, IllegalArgument 중 뭘 쓸지 헷갈린다면?
- 인수 값이 무엇이었든 어차피 실패했을 것 →
IllegalStateException
- 그렇지 않은 경우 →
IllegalArgumentException
아이템 73: 추상화 수준에 맞는 예외를 던지라
아이템 74: 메서드가 던지는 모든 예외를 문서화하라
- 예외는 메서드를 올바르게 사용하는 데 매우 중요한 정보
- 검사 예외는 항상 따로따로 선언하고 각 예외가 발생하는 상황을 javadoc의
@throws
태그로 정확히 문서화하기
- 공통 상위 클래스 하나로 묶어 선언하지 말기
- e.g.
Exception
, Throwable
을 던진다고 선언하지 말기
- 근데 main은 ㄱㅊ (JVM만 호출하기 때문)
- 비검사 예외도 문서화해두면 좋다
- 특히 인터페이스 메서드
- 조건이 인터페이스의 일반 규약에 속하게 되고 모든 구현체가 일관되게 동작하게 해줌
- 단 비검사 예외는 메서드 선언의 throws 목록에 넣지 말기 (
@throws
태그 문서화는 하되)
- 사용자가 할 일이 달라지기 때문
- 시각적으로 구분할 수도 있음 (”아 메서드 선언에 없고
@throws
문서에는 있으면 비검사 예외구나~”)
- 클래스의 여러 메서드가 같은 이유로 같은 예외를 던진다면 클래스 설명에 추가하는 방법도 있음
아이템 75: 예외의 상세 메시지에 실패 관련 정보를 담으라
- 예외의
toString
메서드에 실패 원인에 대한 정보를 최대한 많이 담아야 함
- 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 함
- 최종 사용자에게는 친절한 안내 메시지, 예외 메시지는 가독성보다는 담긴 내용이 더 중요
- 메시지 템플릿 만들어놓고 예외 생성자에서는 필요한 정보만 받는 것도 생각해볼만 함
- e.g.
IndexOutOfBoundsException
예외의 경우 (int lowerBound, int upperBound, int index
)
아이템 76: 가능한 한 실패 원자적으로 만들라
- 호출된 메서드가 실패하더라도 해당 객체는 호출 전 상태를 유지해야 함
- 예외 발생했을 때 객체 상태가 메서드 호출 전과 달라진다면 실패 시 객체 상태를 api 설명에 명시하기
how?
- 불변 객체로 설계
- 작업 수행 전 매개변수의 유효성 검사하기
- 실패 가능성이 있는 코드는 객체의 상태를 바꾸는 코드보다 앞에 위치시키기
- 객체의 임시 복사본에서 작업하고 성공하면 원래 객체와 교체하기
- 실패를 가로채는 복구 코드 작성, 작업 전 상태로 되돌리기 (잘 안쓰임)
아이템 77: 예외를 무시하지 말라
catch
블록을 비우지 말기
- 무시해야 할 때도 있긴 있음
FileInputStream
닫을 때
- 읽기만 했으므로 ㄱㅊ
- 로그를 남기는 정도?
- 그러나 무시하는 경우라도 그 이유를
catch
절에 주석으로 남기기
- 예외 변수의 이름을
ignored
로 하는 것도 방법
아이템 78: 공유 중인 가변 데이터는 동기화해서 사용해라
synchronized
키워드는 해당 메서드나 블록을 한 번에 한 스레드씩 수행하도록 보장
- 동기화의 역할
- 일관성이 깨진 상태를 볼 수 없게 해줌
- 같은 락의 보호 하에 수행된 이전 수정의 최종 결과를 보게 해줌
여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고 쓰는 동작은 반드시 동기화해야 함
- 언어 명세상 long, double 외 변수를 읽고 쓰는 것은 atomic
- 하지만 한 스레드가 저장한 값이 다른 스레드에 보이는지는 보장하지 않음
- 즉 동기화는 스레드 사이의 안정적 통신에 필요
- 예시: 1초 후에 스레드 멈추는 방법
- boolean 필드를 synchronized 메서드로 접근해 읽고 쓰기
- boolean 필드를 volatile로 선언
- Volatile은 주의해서 쓰자
- volatile int에
++
연산자 써도 안전하지 않을 수 있음
- 접근, 변경하는 메서드를 synchronized로
- 대신 AtomicLong을 사용하는 것도 방법
- volatile은 통신만 지원하지만 이건 atomicity까지 지원
getAndIncrement()
- 하지만 웬만하면 가변 데이터는 단일 스레드에서 쓰자
아이템 79: 과도한 동기화는 피하라
- 과도한 동기화는 단점들 불러옴
- 성능 저하
- deadlock
- 예측할 수 없는 동작
- 동기화 메서드나 블록 안에서는 제어를 클라에게 양도하면 안됨
- 안그러면 응답 불가나 안전 실패
- e.g. 재정의 가능한 메서드 호출, 클라가 넘겨준 함수 객체 호출
- 기본적으로 동기화 영역에서는 최대한 일을 적게 하자
- 가변 클래스라면 다음 2가지 방법 중 하나 쓰자
- 동기화하지 말고 클래스를 동시에 사용하는 클래스에서 외부 동기화하도록 하기
- 동기화를 내부에서 수행해 스레드 안전한 클래스로 만들기
- 단 외부에서 객체 전체에 락 거는 것보다 동시성을 월등히 개선할 수 있을 때만
아이템 80: 스레드보다는 실행자, 태스크, 스트림을 애용하라
- work queue는 다음과 같이 쉽게 생성 가능
ExecutorService exec = Executors.newSingleThreadExecutor()
exec.execute(runnable)
exec.shutdown()
- ExecutorService의 주요 기능들
- 특정 태스크 완료 대기
- 태스크 중 하나 혹은 모든 태스크가 완료되기를 기대
- 실행자 서비스 종료 대기
- 완료된 태스크 결과 차례로 받기
- 태스크를 특정 시간, 혹은 주기적으로 실행하게 함
- 둘 이상의 스레드가 처리하게 하고 싶다면 스레드 풀 생성
- 서버 종류에 따라 다른 스레드 풀 사용
- 가벼운 서버라면
Executors.newCachedThreadPool
- 하지만 이 경우 태스크들이 즉시 스레드에 위임, 가용 스레드가 없다면 새로 생성
- 고로 서버가 무겁다면 스레드가 계속 생성됨. cpu 사용량 100%!
- 무거운 서버라면
Executors.newFixedThreadPool
(스레드 개수가 고정)
- 작업 큐를 직접 만들거나 스레드를 직접 다루는 것도 일반적으로 삼가자
- 실행자 프레임웤은 작업 단위(태스크)와 실행 메커니즘이 잘 분리
- 태스크의 종류
- Runnable
- Callable
- Runnable과 비슷하나 값 반환, 예외 던지기 가능
- ExecutorService = 태스크를 수행하는 일반적인 메커니즘
- ForkJoinTask
- fork-join 풀이라는 특별한 ExecutorService가 실행해줌
- 작은 태스크들로 나뉘고 이 태스크들은 ForJoinPool을 구성하는 스레드들이 처리
- 먼저 끝낸 스레드는 다른 스레드의 남은 태스크를 가져와 처리
아이템 81: wait과 notify보다는 동시성 유틸리티를 애용하라
- wait과 notify는 제대로 사용하기 어려우니까 동시성 유틸을 쓰자
- 크게 3가지 범주
- 실행자 프레임웤
- 동시성 컬렉션
- 동기화 장치 (synchronizer)
동시성 컬렉션
- 표준 컬렉션 인터페이스에 동시성을 가미한 것
- 동기화를 내부에서 수행
- 고로 외부에서 락 쓰면 더 느려짐
- 내부의 락을 handle할 수 없기 때문에 여러 기본 동작을 atomic하게 수행해주는 메서드들도 있음
Collections.synchronizedMap
→ ConcurrentHashMap
- 일부 인터페이스는 작업 성공까지 block되도록 한 것도 있음
- e.g.
BlockingQueue
take
했는데 원소 없으면 원소 추가될 때까지 기다림
동기화 장치
- 스레드가 다른 스레드를 기다릴 수 있게 해줌
- 예시
Phaser
(가장 강려크)
CountDownLatch
, Semaphore
(많이 쓰임)
CyclicBarrier
, Exchanger
CountDownLatch
wait
과 notify
- 웬만하면 동시성 유틸리티 써야겠지만 레거시를 다뤄야 할수도 있음
- wait 쓸 땐 wait loop를 사용하자
- 조건으로 한 스레드만 혜택을 받을 수 있다면 notifyAll 말고 notify를 쓰자
아이템 82: 스레드 안전성 수준을 문서화하라
- 한 메서드를 여러 스레드가 동시에 호출할 때 그 메서드가 어떻게 동작하는지 문서화
- javadoc 기본 옵션으로는 synchronized 한정자가 포함 안됨
- 스레드 안전성의 순서
- 불변 > unconditionally 스레드 안전 > conditionally 스레드 안전 > 스레드 안전하지 않음 > 스레드 적대적
- 불변: 클래스 인스턴스는 상수와 같아 동기화 필요 없음
- e.g. String, Long, BigInteger
- unconditionally~ : 인스턴스는 수정될 수 있지만 내부에서 충실히 동기화, 외부에서 동기화 없이 동시에 사용해도 안전함
- e.g. AtomicLong, ConcurrentHashMap
- conditionally~: unconditionally와 같지만 일부 메서드는 동시에 사용하려면 외부 동기화 필요
- e.g. Collections.synchronized 래퍼 메서드가 반환한 컬렉션
- 이 경우 주의해서 문서화해야 함
- 어떤 순서로 호출할 때 외부 동기화가 필요한가?
- 그리고 어떤 락을 얻어야 하는가?
- 안전하지 않음: 인스턴스는 수정될 수 있으며 동시에 사용하려면 각 메서드 호출을 클라가 선택한 외부 동기화 메커니즘으로 감싸야 함
- e.g. ArrayList, HashMap같은 기본 컬렉션
- 적대적: 모든 메서드 호출을 외부 동기화로 감싸도 안전하지 않음
- 서비스 거부 공격
아이템 83: 지연 초기화는 신중히 사용하라
아이템 84: 프로그램의 동작을 스레드 스케줄러에 기대지 말라
- 스케줄링 정책은 OS마다 다름 → 정확성/성능이 스케줄러에 따라 달리지는 프로그램은 이식성이 낮음
- 가장 좋은 건 실행 가능한(전체 스레드 수가 아님) 평균 스레드 수를 프로세서 수보다 지나치게 많아지지 않게 하는 것
- 실행 준비가 된 스레드는 작업 완료 때까지 계속 실행되게 만들기
유의점들 (실행 가능한 스레드 수를 적게 유지하는 법)
- 스레드는 당장 처리할 작업이 없다면 실행돼서는 안됨
- busy wait 상태가 없어야 함
Thread.yield
사용 피하기
아이템 85: 자바 직렬화의 대안을 찾으라
- 직렬화는 편하긴 하지만 문제점이 있음
- 근본적으로 공격 가능 범위가 너무 넓음
readObject
는 클래스패스 내 거의 모든 타입의 객체를 만들어낼 수 있음
- 역직렬화 과정에서 타입들 안의 모든 코드를 수행할 수 있음 ⇒ 타입 코드 전체가 공격 범위에 들어가게 됨
- gadget: 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행할 수 있게 하는 메서드
- 신중히 제작한 바이트 스트림만 역직렬화 해야함
- 역직렬화에 시간이 오래 걸리는 짧은 스트림으로 서비스 거부 공격도 가능
- 고로 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화(자바 직렬화)하지 않는 것
- 대신 cross-platform structured-data representation 사용하자
- JSON, protobuf…
- 자바 직렬화보다 간단
- 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않음
- 대신 기본, 배열 타입만 지원
- JSON은 텍스트 기반이라 읽기 쉽고, protobuf는 이진 표현이라 빨라요
- protobuf는 부가적으로 문서를 위한 스키마를 제공
- 레거시로 인해 자바 직렬화를 해야 한다면?
- 신뢰할 수 없는 데이터는 역직렬화 하지 말기
- 확신도 할 수 없다면?
- 객체 역직렬화 필터링 사용 (
java.io.ObjectInputFilter
)
- 데이터 스트림이 역직렬화되기 전에 필터를 설치하는 기능
- 클래스 단위의 블랙 리스트/화이트 리스트 방식 지원
- 하지만 모든 걸 걸러주진 못함
아이템 86: Serializable을 구현할지는 신중히 결정하라
- 우선 결론: 직렬화 가능 클래스 만들 거면 설계를 제대로 해야 한다~
문제점들
- 한 번 구현 후(자바 기본 방식) 릴리즈해버리면 수정하기 어려움
- 첫 공개 당시 내부 구현에 묶임
- 이후 내부 구현을 고치면 원래 직렬화 형태와 달라지게 됨, 프로그램 깨질 수도 있음
- private, package-private 필드마저 공개하는 꼴 → 캡슐화 깨짐
- 예) 자동 생성된
serialVersionUID
를 사용하는 경우
- 버그, 보안
- 역직렬화는 숨은 생성자
- 불변식, 내부를 들여다볼 수 없다는 전제들을 싸그리 무시
- 새로운 버전 릴리즈 할 때 테스트할 것이 늘어남
- 호환성 검사 해야 함
- 새로운 버전 → 직렬화 → 역직렬화 → 옛날 버전…
- 고칠 때마다 테스트할 게 비례해서 증가
언제 무엇을 어떻게
- 객체 전송/저장용이라면 어쩔 수 없음ㅠ
- 값을 나타내거나 컬렉션 → 구현
- 동작하는 객체 → 구현 X
- 상속용으로 설계된 클래스, 인터페이스는 대부분 Serializable을 구현/확장해서는 안된다
주의할 점들
- 클래스의 인스턴스 필드가 직렬화, 확장 모두 가능하다면?
- 하위 클래스에서
finalize
메서드 재정의 못하게 해야 함
- 자신이 재정의하면서 final로 선언
- 그래야 불변식 보장 가능
- 필드가 기본값으로 초기화될 경우 불변식이 깨진다면
- Serializable 구현을 안한다면
- 상속용 클래스인데 직렬화 지원 안하면 하위 클래스에서 직렬화 하려 할 때 부담
- 이런 클래스 역직렬화 하려면 상위 클래스에서 매개변수 없는 생성자 제공해야 함
- 안그러면 직렬화 프록시 패턴 써야 함(아이템 90)
- 내부 클래스는 직렬화 구현하면 안됨
- 내부 클래스에는 바깥 인스턴스의 참조 등을 저장하기 위해 컴파일러가 자동으로 일부 필드 추가
- 얘네들이 클래스 정의에 어떻게 추가되는지는 언어 명세에 정의 x
아이템 87: 커스텀 직렬화 형태를 고려해보라
- 고민 후 괜찮다고 판단(유연성, 성능, 정확성)될 때만 기본 직렬화 형태를 사용해라
- 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태도 괜찮다
class Name implements Serializable {
private final String lastName;
private final String firstName;
}
- 그렇지 않은 경우의 예시: String linked list
public final class StringList implements Serializable {
private int size = 0;
private Entry head = null;
private static class Entry implements Serializable {
String data;
Entry next;
Entry prev;
}
}
- 문제점
- 공개 api(StringList.Entry)가 현재 내부 구현 방식에 묶임
- 나중에 linked list 사용 안해도 관련 코드는 지원해야 함
- 너무 많은 공간
- 직렬화하면 모든 엔트리와 연결 정보까지 포함해 크기가 커짐
- 디스크 저장, 네트워크 전송 느려짐
- 오래 걸림
- stack overflow
- 기본 직렬화는 객체 그래프를 재귀 순회
- 그리 큰 객체 아니어도 stack overflow 가능
- 근데 또 플랫폼마다 달라질 가능성
- 이렇게 고치자
public final class StringList implements Serializable {
private transient int size = 0;
private transient Etnry head = null;
private static class Entry {
String data;
Entry next;
Entry prev;
}
public final void add(String s) { }
private void writeObject(ObjectOutputStream s) throws IOException{
s.defaultWriteObject();
s.writeInt(size);
for (Entry e = head; e != null; e = e.next) {
s.writeObject(e.data);
}
}
private void writeObject(ObjectOutputStream s) throws IOException, ClassNotFoundException{
s.defualtReadObject();
int numElements = s.readInt();
for (int i = 0; i < numElements; i++) {
add((String) s.readObject());
}
}
}
- defaultWriteObject, defualtReadObject 해줘야 나중에 transient 아닌 필드 추가돼도 호환
- defaultWriteObject: 신 → 직렬화 → 역직렬화 → 구
- writeObject의 문서화
- 이 메서드는 private이지만 사실상 public api
- 커스텀 직렬화의 경우 대부분의 인스턴스 필드를 transient로 선언하기
- 그렇지 않으면 직렬화됨
- 객체의 논리적 상태와 무관한 필드인 게 확실할 때만 transient 생략
- transient로 선언된 필드들은 역직렬화하면 기본값으로 초기화
- 원치 않는 동작인 경우 readObject 메서드에서 defaultReadObject 호출 후 원하는 값으로 복원
- 동기화 메커니즘을 사용 중인 경우 직렬화에도 똑같이 적용해야 함
- 직렬화 가능 클래스에 모두 직렬 버전 UID를 명시적으로 부여하기
아이템 88: readObject 메서드는 방어적으로 작성하라
- readObject는 또다른 public 생성자, 그래서 다른 생성자와 마찬가지로 다음을 고려해야 함
- readObject 메서드에서 defaultReadObject를 호출한 후 역직렬화된 객체가 유효한지 검사 필요
- 유효하지 않은 경우 InvalidObjectException 던지기
- 또 역직렬화 할 때는 클라가 소유해서는 안되는 객체 참조(private 가변 요소)는 방어적으로 복사하기
- 안그러면 역직렬화 후 byte stream으로 참조, 수정 가능해짐
- 단 final 필드는 방어적 복사가 불가능, 필요하다면 final 한정자 제거
- 순서: 방어적 복사 → 유효성 검사
- readObject 내에서는 재정의 가능 메서드를 호출하면 안됨
- 하위 클래스에서 역직렬화 마치기도(유효성 검사, 방어적 복사하기) 전에 재정의된 메서드 실행됨 → 오작동
아이템 89: 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
- readResolve 사용하면 readObject가 만든 인스턴스를 다른 것으로 대체 가능
- e.g. 싱글턴 객체 반환하기
- 이 경우 모든 객체 참조 필드는 transient로 선언해야 함
- 안그러면 공격당하기 쉽다
- transient가 아닌 필드 있을 경우
- 그 필드는 readResolve 실행 전 역직렬화됨
- 그러면 그 인스턴스 참조 훔쳐와 저장할 수 있음
- 그럴 바에야 enum으로 구현하는 것이 낫다
아이템 90: 직렬화된 인스턴스 대신 직렬화된 프록시(serialization proxy pattern) 사용을 검토하라
- how? → 바깥 클래스의 논리적 상태를 정밀히 표현하는 중첩 클래스 설계
- 얘가 직렬화 프록시
- private static으로 선언
- 생성자는 단 하나, 바깥 클래스를 매개변수로
- 생성자는 단순히 매개변수 인스턴스 데이터 복사
- 바깥 클래스, 직렬화 프록시 모두 Serializable 구현
class Period implements Serializable {
private final Date start;
private final Date end;
private static class SerializationProxy implements Serializable {
private static final long serialVersionUID = 123456789L;
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private Object readResolve() {
return new Period(start, end);
}
}
private Object writeReplace() {
return new SerializationProxy(this);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다");
}
}
- 한계
- 클라가 확장할 수 있는 클래스에는 적용 불가
- 객체 그래프에 순환이 있는 클래스에 적용 불가
- 좀 느려짐