Search
📘

Effective Java 3/E - Joshua Bloch

1. 객체 생성과 파괴

가. 생성자 대안 1: 정적 팩토리 메서드

복수의 생성자가 필요한 경우 정적 팩토리 매서드 권장
장점
클래스의 인스턴스 생성 로직에 적절한 이름을 정의할 수 있음
→ 생성자의 경우, 시그니처는 다르지만 생성자의 이름은 같으므로 의도와는 다른 생성자를 호출할 수 있음
같은 인스턴스를 반환해야 하는 경우, 여러번 인스턴스를 생성하지 않아도 됨
→ 인스턴스를 미리 생성하거나 캐싱하여 불필요한 객체 생성 방지 가능
단점
생성자의 역할을 대신 수행하므로 주석을 통해 부가 설명이 필요함
정적 팩토리 메서드만 사용할 경우, 하위 클래스 생성이 불가함
→ 상속하여 하위 클래스를 정의하기 위해 public or protected 생성자가 필요
정적 팩터리 메소드 정의 Convention
from: 하나의 매개변수를 받아서 매개변수 타입의 인스턴스 반환
Date d = Date.from(instance);
of: 복수의 매개변수를 받아서 해당하는 집계 타입의 인스턴스 반환
List<Date> dates = Date.of(firstDate, secondDate);
getInstance: 매개변수로 타입의 인스턴스를 반환하지만 동일한 인스턴스로 보장 X
Date d = Date.getInstance(instance);
newInstance: 새로운 인스턴스 반환 보장
Date d = LocalDate.newInstance(instance);

나. 생성자 대안 2: 빌더

생성자 매개변수가 4개 이상 또는 추후 많아질 가능성이 높다면 빌더 권장
장점
생성자 매개변수가 많은 경우, 간략하게 인스턴스 생성이 가능
→ 점층적 생성자 패턴: 매개변수의 수만큼 따라 같은 이름의 생성자를 정의해야 함
→ 자바빈즈 패턴: 매개변수의 수만큼 setter를 정의해야 함
추상 클래스에서 생성한 빌더를 상속 받아서 구현할 경우, 더욱 간결하게 사용 가능
public abstract class Pizz { public enum Topping {H, M, O, P} final Set<Topping> toppings; }
Java
복사
Code from 19 - 20 page, Item 2, Effective Java 3/E
단점
소수의 매개변수 기반으로 생성자를 정의하는 것보다 빌더 정의에 더 많은 비용이 듬

다. 싱글톤 패턴 정의

싱글톤 패턴 단점: 싱글톤 인스턴스는 mocking하기 어려워서 테스트가 어렵다?
→ 이해가 안된다, public static final로 정의되었다면 호출하면 되는데 mocking에 어려움이 어떤 구간에서 발생하는가?

라. 의존 객체 주입 > 자원 직접 명시

마. 불필요한 객체 생성을 피할 것

바. ‘try-with-resources’ > ‘try-finally’

생산성, 간결성면에서 모두 try-with-resource가 낫다.
장점
코드가 간결해지고 예외 처리도 명확해짐
단점
찾아볼 수 없음
Don’t Do This
InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutStream(dst); try{ // do something } finally { out.close(); } finally { in.close(); }
Python
복사
Do this
AutoCloseable 인터페이스에 따라 try 구문 안에 자원을 할당하는 인스턴스가 자동으로 닫힘
→ 자원 관련 클래스 정의 시, AutoCloseable 인터페이스 정의 필요
try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutStream(dst)) { // do something }
Python
복사
catch 문과도 함께 사용 가능
try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutStream(dst)) { // do something } catch (IOException e) { // do something }
Java
복사

2. 모든 객체의 공통 메서드

가. equals 재정의, 규칙에 따라

재정의가 필요한 경우, 핵심 필드를 모두 포함하고 다섯가지 규약을 지켜야 한다
재정의가 필요한 경우
논리적 동치성 확인 필요 && 상위 클래스의 equals가 논리적 동치성을 비교하도록 정의되지 않았을 경우
→ 논리적 동치성(logical equality): 객체 자체(Or 객체 식별성)는 다르더라도 객체를 표현하는 필수 속성의 동일여부
→ 객체 식별성(object identity): 객체 그 자체의 동일여부, 주로 참조값 등을 통해 구분 가능
주로 값을 표현하는 값 클래스에 해당됨
재정의가 불필요한 경우
객체 인스턴스가 그 자체로 고유하여 논리적 동치성과 객체 식별성이 구분되지 않는 경우
→ ex) 정적 팩토리 방식으로 객체가 정의된 경우
객체 인스턴스의 논리적 동치성을 검사할 일이 없는 경우
상위 클래스에서 재정의된 equals가 하위 클래스에서도 동일하게 사용 가능하여 상속받아 사용할 수 있는 경우
클래스가 private으로 정의되어 외부에서 사용할 일이 없는 경우
→ 혹여나 내부적으로 실수로 호출되는 것을 방지하기 위해 AssertionError를 던지도록 equals 재정의할 수도 있음
재정의 방법
네가지 단계에 따라 재정의하는 것을 권장
→ 주의사항: Object.equals()에 대한 재정의이므로 메소드 시그니처를 바꿔서 다중정의하는 실수를 하지 말것 ex) public boolean equals(PhoneNumber o)
public final class PhoneNumber { // 생략 @Override public boolean equals(Object o) { // 1. 자기 자신에 대한 참조여부 확인, 성능 최적화 용도 if (o == this) return true; // 2. 매개변수로 전달 받은 객체의 올바른 타입 여부 확인 if (!(o instanceof PhoneNumber)) return false; // 3. 입력 객체에 대한 타입 변경 PhoneNumber pn = (PhoneNumber) o; // 4. 핵심 필드 모두에 대해 전원 일치여부 확인 return pn.lineNum = lineNum && pn.prefix == prefix && pn.areaCode == areaCode } }
Java
복사
재정의 다섯가지 규약
대칭성: x.equals(y) == y.equals(x)
추이성: if x.equals(y) == true && y.equals(z) == true, then x.equals(z) == true
일관성: 몇번을 호출하던 결과는 같아야한다
not null: x.equals(null) == false
반사성: x.equals(x) == true

나. hashCode 재정의, equals와 함께

equals가 재정의된 경우, 모든 필수 필드를 포함하여 hashCode를 재정의해야 한다
재정의가 필요한 경우
equals가 재정의 된 경우
→ 논리적으로 같은 객체는 같은 해시코드를 반환하는 것이 원칙임
→ 같은 해시코드를 반환해야 HashMap, HashSet과 같은 콜렉션에서 같은 원소로 인식됨
재정의가 불필요한 경우
equals가 재정의되지 않은 경우
재정의 방법
정석
→ equals의 핵심 필드에 대해 Short::hashCode 활용하여 해쉬화 후 더한다
31을 곱하는 이유: 31이 홀수이자 소수이기 때문?
public final class PhoneNumber { // 생략 @Override public int hashCode() { int result = Short.hashCode(areaCode); result = 31 * result + Short.hashCode(prefix); result = 31 * result + Short.hashCode(lineNum); } }
Java
복사
간편한 방법
→ 성능에 민감하지 않은 경우 사용 권장
public final class PhoneNumber { // 생략 @Override public int hashCode() { return Objects.hash(lineNum, prefix, areaCode); } }
Java
복사

다. toString 재정의, 대부분

원활한 디버깅과 사용성을 위해 재정의할 것, 상위 클래스에서 알맞게 재정의된 경우만 제외
재정의가 필요한 경우
모든 클래스
→ 단, 상위 클래스에서 알맞게 재정의된 경우 제외
재정의가 불필요한 경우
모든 주요 필드를 반환하는 toString을 상위 클래스에서 재정의한 경우
→ 문자열로 표현하기에 부적절한 필드는 제외

라. clone 재정의, 주의

객체 복제는 생성자와 팩터리를 활용할 것
재정의가 필요한 경우
오직 배열
재정의가 불필요한 경우
대부분의 경우
대안으로 복사 생성자와 복사 팩터리 사용 권장
// 복사 생성자 public Yum(Yum y){}; // 복사 팩터리 public static Yum newInstance(Yum y){};
Java
복사

마. Comparable 구현, 고려

인스턴스 간 정렬, 검색, 비교하는 기능을 컬렉션과 함께 쓴다면 Comparable 인터페이스 구현 필요
재정의가 필요한 경우
순서가 명확한 값 클래시 작성 시 Comparable 인터페이스 구현 필요
→ ex) 숫자, 연대, 알파벳 등
→ 순서 생기면 인스턴스 간 정렬, 검색, 비교하는 기능을 쉽게 구현할 수 있음
재정의가 불필요한 경우
순서의 보장이 필요 없는 클래스 작성 시

3. 클래스와 인터페이스

가. 접근 권한 최소화

public 접근제어자를 지정했다는 것은 평생 관리해야 한다는 것
public 클래스 필드 주의사항
public 필드: public 클래스에는 상수용 public static final 외에 다른 public 필드 허용 불가
→ 외부에서 수정이 가능하므로 쓰레드 안전 및 보안면에서 취약함
참조 객체: public 클래스 내부에 참조 객체는 불변객체로 정의
→ final로 참조객체를 지정해도 내부 요소는 변경이 가능하므로 반드시 불변객체로 정의해야 함

나. 접근자 메서드 활용

public 클래스에서는 필드를 직접 노출 X, package-private 또는 private 클래스에서는 무관

다. 변경 가능성 최소화

합당한 이유가 없다면 클래스는 불변으로, 필드는 private final로 정의할 것
불변 클래스로 정의하는 방법
상태 변경 메서드 제공 X
→ 특정 필드에 Getter가 있다고 반드시 Setter를 만들지 말 것
클래스에 final을 붙여서 클래스 확장 금지
모든 필드를 final 선언
모든 필드 private 선언
불변 클래스 장단점
장점: 쓰레드 안전, 유지보수 용이, 보안 안전
단점: 잠재적 성능 저하(지속적으로 값이 변경되어야 한다면 새로운 클래스를 생성해야 함)

라. 컴포지션 > 상속

컴포지션... 이해가 안된다 둘의 차이 다시 학습

마. 상속 금지

확장 가능하도록 설계하고 문서화하지 않았다면 final로 선언하여 상속을 금지시킬 것

바. 인터페이스 > 추상 클래스

인터페이스를 구현한 클래스는 타입제약이 없으므로 다중 상속으로 유용하게 사용 가능함

4. 열거 타입과 애너테이션

5. 람다와 스트림

6. 메서드

7. 일반적인 프로그래밍 원칙

8. 예외

가. 진짜 예외상황에서만 예외 사용

흐름제어(반복문 등) 상황에서 예외가 아닌 대안을 사용할 것
흐름제어에서 예외 대신 사용 가능한 옵션
상태검사 메소드, 상태 의존적 메소드 활용
→ 가장 일반적인 방법으로, 가독성이 좋고 상태 의
존적 메소드의 예외 발생에 따라 디버깅이 편리함
→ 상태검사 메소드: Iterator::iterator
→ 상태 의존적 메소드: Iterator::next
for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) { Foo foo = i.next(); }
Java
복사
Optional 또는 특정값 활용
→ case 1: 상태검사 메소드가 상태 의존적 메소드의 작업 일부를 중복 수행하는 경우, 성능 향상을 위해 Optional이나 특정값 활용
중복 수행하는 예를 알 수 잇나?
→ case 2: 멀티 쓰레드 환경이나 외부에 의해 상태가 변경 가능한 설계라면 Optional이나 특정값 활용 권장
왜냐하면 상태 검사 메소드와 상태 의존적 메소드 호출 사이에 상태 변화가 가능함

나. 복구 가능한 상황 → 검사 예외, 프로그래밍 오류 → 런타임 예외

검사 예외, 런타임 예외, 에러를 목적에 맞게 사용할 것
검사 예외
API 호출자가 충분히 복구할 수 있는 경우일 때 던지는 예외
API 호출자는 검사 예외에 대해 예외처리 의무가 있다
복구할 수 있는 방법(or 예외 상황을 벗어나는 방법)을 알려주는 메소드를 함께 전달 필요
단, 현실적으로 복구상황이 명확한 상황은 흔치 않으므로 검사 예외를 많이 사용하지 않음
RuntimeException을 상속 받지 않고, Exception을 상속 받는다
런타임 예외(or 비검사 예외)
API 호출자가 복구할 수 없거나 프로그래밍 오류일 때 던지는 예외
API 호출자는 비검사 예외에 대해 예외처리의 의무가 없다
복구 가능 및 불가능은 설계자의 판단에 달렸으나, 확신이 없다면 런타임 예외를 던질 것
RuntimeException을 상속 받는다
에러
상속하지도 말고 직접 던지는 일도 없어야 함
단, AssertionError를 던지는건 예외로 함
→ AssertionError ?

다. 꼭 필요한 상황에서만 검사예외 사용

검사 예외의 비용을 고려하여 꼭 필요한 상황에만 검사 예외를 사용할 것
검사 예외 단점
검사 예외를 던지는 메소드를 스트림에서 사용할 수 없음
→ API 호출자는 검사 예외에 대한 명확한 예외처리를 강제 받기 때문임
검사 예외를 던지면 호출하는 쪽에서 try-catch로 받아야하는 부담이 있음
→ 비검사 예외는 API 호출자가 예외처리에 대한 의무가 없음
검사 예외 대체 방법
상태 검사 메서드와 비검사 예외 활용
→ 단점: 멀티 쓰레딩 환경에서 canCreateFile(상태 검사 메서드)와 create(상태변경 메서드) 사이에 상태변화 발생 가능
public static Optional<Database> create() { // ... if (database.canCreateFile()) { // 예외가 던져질 지 여부를 boolean값으로 반환 database.create(); } else { throw new CustomDatabaseIOException(); } }
Java
복사
Optional 활용
→ 단점: 예외 원인에 대한 정보를 확인할 수 없음
public static Optional<Database> create() { // ... try { database.create(); // create() 메서드 안에선 throws IOException으로 컴파일 에러 } catch (IOException e) { return Optional.empty(); // 빈 옵셔널 반환 } return Optional.of(Database); }
Java
복사
검사 예외 사용 판단 기준
오직 하나의 검사 예외만 존재하는 경우, 호출하는 쪽에서 try-catch를 사용하는 등 비용이 많이 발생하므로 Optional로 대체 가능한지 판단 필요
단, Optional로는 사용자에게 본 예외에 대해 메시지를 통해 충분한 정보를 제공하지 못한다면 검사 예외 활용

라. 표준 예외 사용

표준 예외 사용 장점: 사용성 향상, 메모리 사용량 감소, 클래스 적재시간 감소
자주 사용하는 표준 예외 정리
IllegalStateException: 변경하려는 개체의 상태가 호출하려는 메소드의 작업을 수행하기에 부적절한 상태 알림
public class MainRunner { List<Integer> list = new ArrayList<>(); public void evenIndexAdd(Integer number){ if(list.size()%2 != 0){ throw new IllegalStateException("짝수 인덱스에 들어갈 준비가 되지 않음"); } list.add(number); } public static void main(String[] args) { MainRunner mainRunner = new MainRunner(); mainRunner.evenIndexAdd(0); mainRunner.evenIndexAdd(1); } }
Java
복사
IllegalArgumentException: 부적절한 매개변수를 전달 받은 상태 알림
public class MainRunner { List<Integer> list = new ArrayList<>(); public void positiveAdd(Integer number) { if (number < 0) { throw new IllegalArgumentException("음수값은 허용하지 않음"); } list.add(number); } public static void main(String[] args) { MainRunner mainRunner = new MainRunner(); mainRunner.positiveAdd(-1); } }
Java
복사
NullPointerException: null 값을 인자로 받지 않는 메소드에 null을 매개변수로 받은 상태 알림
public class MainRunner { List<Integer> list = new ArrayList<>(); public void add(Integer number){ if(Objects.isNull(number)){ throw new NullPointerException("값이 null 입니다."); } list.add(number); } public static void main(String[] args) { MainRunner mainRunner = new MainRunner(); mainRunner.add(null); } }
Java
복사
IllegalStateException vs IllegalArgumentException
매개변수가 무엇이든 어차피 실패한다면 IllegalStateException
→ 왜?
반대로 성공한다면 IllegalArgumentException
→ 왜?
Exception, RuntimeException, Throwable, Error 직접 사용 X
너무 포괄적인 성격의 예외이므로, 모호함은 안정적인 테스트 작성에 방해가 된다

마. 추상화 수준에 맞는 예외 던지기

현재 계층에서 예외를 처리할 수 없다면, 예외 번역과 예외 연쇄를 활용하여 하위 계층의 근본원인을 효과적으로 전달할 것
예외 처리 원칙
해당 계층에서 발생한 예외는 최대한 해당 계층에서 처리하는 것이 바람직함
단, 아래에서 처리할 수 없다면 예외 번역과 예외 연쇄를 활용하여 위로 넘길 것
예외 번역
예외 번역이 필요한 이유
→ 하위계층의 예외를 상위계층으로 전달하면 예외를 전달 받는 입장에서 원인 파악에 어려움이 있음(심하면 예외 메시지의 연관성을 찾지 못해서 무시할 수도 있음)
→ 복구 불가능한 검사 예외를 비검사 예외로 번역하여 전달함으로써 무분별한 try-catch 또는 throws를 방지할 수 있음
예시
→ SQLException은 핸들링하기 어려운 검사 예외이므로 비검사 예외로 바꿔서 상위계층으로 전달
try { } catch (SQLException e) { if(e.getErrorCode() 등을 이용해서 처리할 수 있는 경우){ // 처리 코드 } else{ // 로깅, 에러 내용 메일 전송 등의 로직 throw new RuntimeException(e); } }
Java
복사
code from https://joont.tistory.com/157 [Toward the Developer]
예외 연쇄
상위 계층에서 예외를 처리할 때 근본원인에 대한 정보를 함께 넘길 때 활용
예시
Throwable.getCause() 활용하여 하위 계층 예외를 확인할 수 있음
try { ... } catch (LowerLevelException cause) { throw new HigherLevelException(cause); } // 상위 계층에 전달할 예외 정의 class HigherLevelException extends Exception { HigherLevelException(Throwable cause) { super(cause); } }
Java
복사

바. 메서드가 던지는 예외는 모두 문서화

검사예외와 비검사예외를 모두 문서화하되 예외 종류를 구분하고 명시성을 높일 것
검사예외는 항상 따로 선언하고 @throws 태그 활용하여 문서화할 것
예시
public void examMethod() throws Exception { } // to /** * @throws SQLException SQL 이 잘못된 경우 * @throws ClassNotFoundException 지정한 경로에 클래스파일이 존재하지 않는경우. */ public void examMethod() throws SQLException,ClassNotFoundException{ Class.forName("/test/path/test.class"); // TODO 무언가 했음. }
Java
복사
code from https://github.com/Meet-Coder-Study/book-effective-java/commit/a8ac810ff5291ec2b8b37b6f7a1f4d90422796a3
비검사 예외도 @throws 태그 활용하여 문서화하되, 메서드 선언의 throws 목록에는 넣지 말것
비검사 예외와 검사 예외의 구분에 따라 API 사용자가 처리할 일이 달라지므로 명확히 구분할 것 권장
예시
/** * @throws SQLException SQL 이 잘못된 경우 * @throws NullPointerException 지정한 요소에 null 이 들어오는 경우 */ public void examMethod() throws SQLException,NullPointerException{ Class.forName("asd"); // TODO 무언가 했음. } // to /** * @throws SQLException SQL 이 잘못된 경우 * @throws NullPointerException 지정한 요소에 null 이 들어오는 경우 */ public void examMethod() throws SQLException{ Class.forName("asd"); // TODO 무언가 했음. }
Java
복사
code from https://github.com/Meet-Coder-Study/book-effective-java/commit/a8ac810ff5291ec2b8b37b6f7a1f4d90422796a3
복수의 메서드에서 공통으로 던지는 예외는 클래스 단위에서 예외를 문서화할 것
예시
public class Exam { /** * @throws NullPointerException 지정한 요소에 null 이 들어오는 경우 */ public void examMethod(){ Class.forName("asd"); // TODO 무언가 했음. } /** * @throws NullPointerException 지정한 요소에 null 이 들어오는 경우 */ public void examMethod2(){ //TODO 일하는 중 } } // to /** * <p> {@code NullPointerException} 지정한 요소에 null 이 들어오는 경우 발생합니다.</p> */ public class Exam { public void examMethod(){ Class.forName("asd"); // TODO 무언가 했음. } public void examMethod2(){ //TODO 일하는 중 } }
Java
복사
code from https://github.com/Meet-Coder-Study/book-effective-java/commit/a8ac810ff5291ec2b8b37b6f7a1f4d90422796a3

9. 제네릭

10. 동시성

11. 직렬화

Reference

Effective Java 3/E - Joshua Bloch