[Java] Effective Java 3/E item 1~3
Item1: 생성자 대신 정적 팩토리 메소드를 고려하라
클래스 인스턴스를 얻는 전통적인 수단는 public 생성자
이와 별도의 수단으로 클래스는 정적 팩토리 메소드를 제공할 수 있음
정적 팩토리 메소드가 public 생성자에 비해 가지는 장단점
장점
- 이름을 가질 수 있음
- Ex) BigInteger(int, int, Random) vs BigInteger.probablePrime
- 호출될 때마다 인스턴스를 새로 생성하지 않아도 됨
- 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능적으로 좋음
- 반복되는 요청에 같은 객체를 반환하는 식의 정적 팩토리 방식의 클래스는 인스턴스 통제할 수 있음. 이를 통해 클래스를 싱글톤으로 만들 수도, 인스턴스화 불가로 만들 수도 있음
- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있음
- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음
- 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨
단점
- 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없음
- 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
Item2: 생성자에 매개변수가 많다면 빌더를 고려해라
정적 펙토리와 생성자의 공통 문제, 선택적 매개변수가 많을 때 적절히 대응하는 것이 어렵다.
점층적 생성자 패턴으로 해결할 수도 있지만 선택적 매개변수가 많아질수록 생성자 오버로딩이 많아지고 코드 가독성이 저하됨
public class Pizza {
private String crust;
private String sauce;
private List<String> toppings;
public Pizza(String crust) {
this.crust = crust;
this.toppings = new ArrayList<>();
}
public Pizza(String crust, String sauce) {
this(crust);
this.sauce = sauce;
}
public Pizza(String crust, String sauce, List<String> toppings) {
this(crust, sauce);
this.toppings = toppings;
}
// Getter and setter methods omitted for brevity
}
// Usage
Pizza pizza1 = new Pizza("thin", "tomato", Arrays.asList("pepperoni", "mushrooms"));
Pizza pizza2 = new Pizza("thick", "pesto");
Pizza pizza3 = new Pizza("deep dish");
자바빈즈 패턴의 경우 빈 생성자를 만들고 setter 메소드들을 통해 값을 설정하는 방식이다.
public class Pizza {
private String crust;
private String sauce;
private List<String> toppings;
public Pizza() {
this.toppings = new ArrayList<>();
}
public String getCrust() {
return crust;
}
public void setCrust(String crust) {
this.crust = crust;
}
public String getSauce() {
return sauce;
}
public void setSauce(String sauce) {
this.sauce = sauce;
}
public List<String> getToppings() {
return toppings;
}
public void setToppings(List<String> toppings) {
this.toppings = toppings;
}
}
// Usage
Pizza pizza = new Pizza();
pizza.setCrust("thin");
pizza.setSauce("tomato");
pizza.setToppings(Arrays.asList("pepperoni", "mushrooms"));
인스턴스를 만들기 쉽고, 가독성이 좋음
하지만, 객체 하나를 만들기 위해 여러 메소드들을 호출해야 하고 객체가 완전히 생성되기 전까지 일관성이 무너진 상태에 놓이게 된다. 이러한 경우 다중 스레드 환경에서 안전하지 않을 수 있음
빌더 패턴의 경우 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비함
public class Pizza {
private String crust;
private String sauce;
private List<String> toppings;
private Pizza(PizzaBuilder builder) {
this.crust = builder.crust;
this.sauce = builder.sauce;
this.toppings = builder.toppings;
}
public static class PizzaBuilder {
private String crust;
private String sauce;
private List<String> toppings;
public PizzaBuilder() {
this.toppings = new ArrayList<>();
}
public PizzaBuilder crust(String crust) {
this.crust = crust;
return this;
}
public PizzaBuilder sauce(String sauce) {
this.sauce = sauce;
return this;
}
public PizzaBuilder toppings(List<String> toppings) {
this.toppings = toppings;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
// Getter/Setter methods omitted for brevity
}
// Usage
Pizza pizza1 = new Pizza.PizzaBuilder()
.crust("thin")
.sauce("tomato")
.toppings(Arrays.asList("pepperoni", "mushrooms"))
.build();
Pizza pizza2 = new Pizza.PizzaBuilder()
.crust("thick")
.sauce("pesto")
.build();
Pizza pizza3 = new Pizza.PizzaBuilder()
.crust("deep dish")
.build();
매개변수가 많다면 빌더패턴이 좋다. 코드 양이 많아지는 단점 같은 경우 lombok의 어노테이션 등을 활용하면 줄일 수 있음
Item3: private 생성자나 열거 타입으로 싱글톤임을 보증하라
클래스를 싱글톤으로 만들면 이를 사용하는 클라이언트는 테스트하기 어려울 수 있음
왜냐하면 타입을 인터페이스로 정의하고 이를 구현해서 만든 싱글톤이 아니라면 싱글톤 인스턴스를 Mock 구현으로 대체할 수 없기 때문
싱글톤을 만드는 방식 2가지
- public static final 필드 방식의 싱글톤
public class Test {
public static final Test INSTANCE = new Test();
private Test() { ... }
}
public 이나 protected 생성자가 없기에 INSTANCE 를 초기화될 때 만들어진 인스턴스는 하나임을 보장
예외로는 권한이 있는 클라이언트가 리플렉션 API를 사용해서 private 생성자 호출할 수 있음 ⇒ 이를 방어하기 위해서는 2번째 객체가 생성되려 할 때 예외 던지게 하면 됨
장점
- public static 필드가 final이니 절대로 다른 객체 참조 불가
- 간결함
- 정적 팩토리 메소드를 public static 멤버로 제공
public class Test {
private static final Test INSTANCE = new Test();
private Test() { ... }
public static Test getInstance() { return INSTANCE; }
}
getInstance 는 항상 같은 객체의 참조를 반환하므로 제 2의 Test 인스턴스는 만들어지지 않음
(리플렉션을 통한 예외는 1번과 동일하게 적용된다)
장점
- API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있음
- 원한다면 정적 팩토리를 제너릭 싱글톤 팩토리로 만들 수 있음
- 정적 팩토리의 메소드 참조를 공급자로 사용할 수 있음
위의 방식들로 만든 싱글톤 클래스를 직렬화하려면 Serializable 구현으로 부족
모든 인스턴스 필드를 일시적(transient)로 선언하고 readsolve 메소드를 제공해야 함 ⇒ 만약 없다면 직렬화된 인스턴스를 역직렬화할 때 새로운 인스턴스 생성됨
3번 방법
public enum Test {
INSTNACE;
}
public 방식과 비슷, 더 간결, 직렬화도 추가 없이 가능
리플렉션 공격도 막음
⇒ 대부분의 상황에서 원소가 하나뿐인 열거 타입의 싱글톤을 만드는 것이 가장 좋은 방법
(Enum 외의 클래스를 상속해야 한다면 사용 불가한 방법)