[Effective Java] Item 1-4

yoonthegarden·2024년 4월 27일
1

Effective Java

목록 보기
1/1
post-custom-banner

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

클라이언트가 클래스의 인스턴스를 얻는 방법

  • public 생성자
  • 정적 팩터리 메서드(static factory method)
    → 클래스의 인스턴스를 반환하는 단순한 정적 메서드

정적 팩터리 메서드(static factory method)의 장점

  • 이름을 가질 수 있다.
    → 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환된 객체의 특성을 제대로 설명하지 못하는 반면, 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
  • 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
    → 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. 따라서 (특히 생성 비용이 큰) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다. (like Flyweight pattern)
    (대표 적인 예 Boolean.valueOf(boolean))
    → 언제 어느 인스턴스를 살아 있게 할지 철저히 통제할 수 있다. : 인스턴스 통제 클래스
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    → 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 가진다.
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  • 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

인스턴스를 통제하는 이유?

  • 클래스를 싱글턴(singletone)으로 만들 수 있다.
  • 인스턴스화 불가로 만들 수 잇다.
  • 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.

정적 팩터리 메서드(static factory method)의 단점

  • 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
    → 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야한다는 점에서 오히려 장점일수도..
  • 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
    → 문서를 잘 작성하고 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다. (from, of, valueOf, instance, create, getType, newType, type …)

핵심 정리

→ 인스턴스를 생성하는 두가지 방법의 상대적인 장단점을 이해하고 사용하자. 정적 팩터리를 사용하는게 유리한 경우가 더 많기에 무작정 public 생성자를 제공하지 말자.



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

정적 팩터리와 생성자에게 공통된 제약

→ 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.

점층적 생성자 패턴(telescoping constructor pattern)

  • 필수 매개 변수만 받는 생성자부터 .. 선택 매개 변수를 다 받는 생성자까지 늘려가는 방식이다.
    public class NutritionFacts {
    	private final int servingSize; // (ml, 1회 제공량) 필수
    	private final int servings; // (회, 총 n회 제공량) 필수
    	private final int calories; // (1회 제공량당) 선택
    	private final int fat; // (g/1 회 제공량) 선택
    	private final int sodium; // (mg/1 회 제공량) 선택
    	private final int carbohydrate; // (g/1 회 제공량) 선택
    	
    	public NutritionFactsint servingSize, int servings) {
    		this(servingSize, servings, 0;
    	}
    	public NutritionFactsint servingSize, int servings, int calories) {
    		this(servingSize, servings, calories, 0;
    	}
    	public NutritionFacts(int servingSize, int servings,int calories, int fat) {
    		this(servingSize, servings, calories, fat, 0);
    	}
    	public NutritionFacts(int servingSize; int servings, int calories, int fat, int sodium) {
    		this(servingSize, servings, calories, fat, sodium, 0);
    	}
    	public NutritionFactsdnt servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
    		this.servingSize = servingSize;
    		this.servings = servings;
    		this.calories = calories;
    		this.fat = fat;
    		this.sodium = sodium;
    		this.carbohydrate = carbohydrate;
    	}
    }
  • 단점
    • 사용자가 설정하길 원치 않는 매개변수까지 포함하기 쉬운데 그런 매개변수에도 값을 지정해줘야 한다.
    • 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다.

자바빈즈 패턴(JavaBeans pattern)

  • 매개변수가 없는 생성자로 객체를 만든 후, setter 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.
    public class NutritionFacts {
    	//매개변수들은 (기본값이 있다면) 기본값으로 초기화된다
    	private int servingSize = -1; // 필수; 기본값 없음
    	private int servings = -1; //필수; 기본값 없음
    	private int calories = 0;
    	private int fat = 0;
    	private int sodium = 0;
    	private int carbohydrate = 0;
    	
    	public NutritionFacts() { }
    	
    	// 세터 메서드들
    	public void setServingSize(int val) { servingSize = val; }
    	public void setServings(int val) { servings = val; }
    	public void setCalories (int val) { calories = val; }
    	public void setFat(int val) { fat = val; }
    	public void setSodium(int val) { sodium = val; }
    	public void setCarbohydrate(int val) { carbohydrate = val; }
    }
  • 단점
    • 객체 하나를 만들기 위해서 메서드 여러 개를 호출해야한다.
    • 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다.
      → 일관성이 깨진 객체가 만들어지면, 버그를 심은 코드와 그 버그 때문에 런타임에 문제를 겪는 코드가 물리적으로 멀리 떨어져 있을 것이므로 디버깅이 힘들다.

빌더 패턴(Builder pattern)

  • 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(혹은 정적 팩터리)를 호출해 빌더 객체를 얻는다.
  • 빌더는 보통 생성할 클래스 안에 정적 멤버 클래스로 만들어 둔다.
    public class NutritionFacts {
    	private final int servingSize;
    	private final int servings;
    	private final int calories;
    	private final int fat;
    	private final int sodium;
    	private final int carbohydrate;
    	
    	public static class Builder {
    		// 필수 매개변수
    		private final int servingSize;
    		private final int servings;
    		// 선택 매개변수 - 기본값으로 초기화한다
    		private int calories = 0;
    		private int fat = 0;
    		private int sodium = 0;
    		private int carbohydrate = 0;
    		
    		public Builder(int servingSize, int servings) {
    			this.servingSize = servingSize;
    			this.servings = servings;
    		}
    		public Builder calories(int val) { 
    			calories = val; return this; 
    		}
    		public Builder fat(int val) { 
    			fat = val; return this; 
    		}
    		public Builder sodium(int val) {
    			sodium = val; return this; 
    		}
    		public Builder carbohydrate(int val) {
    			carbohydrate = val; return this; 
    		}
    		public NutritionFacts build() {
    			return new NutritionFacts(this);
    		}
    	}
    	
    	private NutritionFacts(Builder builder) {
    		servingSize = builder.servingSize;
    		servings = builder.servings;
    		calories = builder.calories;
    		fat = builder.fat;
    		sodium = builder.sodium;
    		carbohydrate = builder.carbohydrate;
    	}
    }
  • 빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출 될 수 있다. 이런 방식을 메서드 호출이 흐르듯 연결된다 라는 뜻으로 플루언트 API(fluent API), 메서드 연쇄(method chaining) 라 한다.
  • 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.
  • 빌더 패턴은 유연하다
    • 빌더 하나로 여러 객체를 순회하면서 만들 수 잇다
    • 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수 있다.
    • 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수 있다.
  • 단점
    • 객체를 만들기 위해서는 그 전에 빌더부터 만들어야 한다. 빌더 생성 비용이 크진 않지만 성능에 민감할 경우 문제가 될 수 있다.
    • 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다. but API는 시간이 지날수록 매개변수가 많아지는 경향이 있다.
  • 안정적인 빌더패턴을 위해
    • 잘못된 매개변수를 일찍 발견하기 위해선 빌더의 생성자와 메서드에서 입력된 매개변수를 검사하고, build 메서드가 호출하는 생성자에서 여러 매개변수에 걸친 불변식(invariant)을 검사하자.
    • 불변식을 보장하려면 빌더로부터 매개변수를 복사한 후 해당 객체 필드들도 검사해야 한다.

핵심 정리

→ 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 더 그렇다. 빌드는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.



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

싱글턴(singleton)

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 예로는 함수와 같은 무상태 객체나 설계상 유일해야하는 시스템 컴포넌트가 있다.
  • 단점
    • 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱클턴이 아니면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없으므로 테스트하기가 어려워질 수 있다.

싱글턴 만드는 방식

  • public static final 필드 방식
    public class Elvis {
    	public static final Elvis INSTANCE = new Elvis();
    	private Elvis() { ... }
    	
    	public void leaveTheBuilding() { ... }
    }
    • 생성자는 private으로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 마련한다.
    • private 생성자는 public static final 필드를 초기화 할 때 한 번만 호출된다.
    • public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
    • 클라이언트는 리플렉션 API를 통해 private 생성자를 호출할 수 있다. 이를 막기위해 생성자를 수정해 두번째 객체 생성에 예외를 던지면 된다.
      → 클래스가 싱글턴임이 API에 명백하게 드러난다.
      → 간결하다.
  • 정적 팩터리 방식
    public class Elvis {
    	private static final Elvis INSTANCE = new ElvisO;
    	private Elvis() { ... }
    	public static Elvis getlnstance() { return INSTANCE; }
    	
    	public void leaveTheBuildingO { ... }
    }
    • Elvis.getlnstance는 항상 같은 객체의 참조를 반환하므로 제2의 Elvis 인스턴스란 결코 만들어지지 않는다.
      → API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.(호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수 있다.)
      → 정적 팩터리를 제네릭 싱글턴 팩터 리로 만들 수 있다.
      → 정적 팩터 리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
      ⇒ 이러한 장점이 굳이 필요하지 않다면 public 필드 방식이 좋다.
  • 열거 타입 방식
    • 위 두 방식으로 만든 싱글턴 클래스를 직렬화하려면 인스턴스 필드를 일시적이라고 선언하고 readResolve 메서드를 제공해야한다. 이렇게 하지 않으면 역직렬화할 때마다 새로운 인스턴스가 만들어진다.

      // 싱글턴임울 보장해주는 readResolve 메서드
      private Object readResolve() {
      	// '진짜‘ Elvis를 반환하고,가짜 Elvis는 가비지 컬렉터에 맡긴다.
      	return INSTANCE;
      }
      public enum Elvis {
      	INSTANCE;
      	public void leaveTheBuilding() { ... }
      }

      → 더 간결하고 추가 노력없이 직렬화 할 수 있다.
      → 아주 복잡한 직렬화 상황이나 리플렉션 공격에서도 제 2의 인스턴스가 생기는 일을 완벽히 막아준다.

핵심 정리

→ 대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다.



Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라

정적 메서드와 정적 필드만을 담을 클래스

  • 객체 지향적이지 않으나 나름의 쓰임새가 있다.
  • java.lang.Math와 java.util.Arrays처럼 기본 타입 값이나 배열 관련 메서드들을 모아 놓을 수 있다.
  • java.util.Collections처럼 특정 인터페이스를 구현하는 객체를 생성해주는 정적 메서드(혹은 팩터리)를 모아놓을 수도 있다.
  • final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다.
    → 정적 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 게 아니다. 하지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어준다.

private 생성자를 추가

  • 컴파일러가 기본 생성자를 만드는 경우는 오직 명시된 생성자가 없을 때뿐이니 private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
    public class Utilityclass {
    	// 기본 생성자가 만들어지는 것을 막는다(인스턴스화 방지용)
    	private UtilityClass() {
    		throw new AssertionError();
    	}
    	... // 나머지 코드는 생략
    }
  • 명시적 생성자가 private이니 클래스 바깥에서는 접근할 수 없다.
  • 꼭 Assertion Error를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 해준다.
  • 상속을 불가능하게 한다. 모든 생성자는 상위 클래스의 생성자를 호출하는데 private으로 선언했기에 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막힌다.
  • 이 코드는 어떤 환경에서도 클래스가 인스턴스화 되는 것을 막아준다.
  • 하지만 생성자가 존재하는데 호출 할 수 없는 것이 직관적이지는 않으니 적절한 주석을 달아주자.
profile
https://garden-ying.tistory.com/
post-custom-banner

4개의 댓글

comment-user-thumbnail
2024년 4월 27일

이펙티브 자바를 친절하게 정리해주셔서 감사함니다..

답글 달기
comment-user-thumbnail
2024년 5월 3일

자바의 개념을 모르는 감자는 자바공부를 더해야겠다고 느꼈습니다 ㅠㅅㅠ

답글 달기
comment-user-thumbnail
2024년 5월 30일

다음은 어딨나요?

1개의 답글