TIL/Java

[Java] Effective Java 3/E item 1~3

JoJobum 2023. 5. 26.

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가지

  1. public static final 필드 방식의 싱글톤
public class Test {
	public static final Test INSTANCE = new Test();
	private Test() { ... }
		
}

public 이나 protected 생성자가 없기에 INSTANCE 를 초기화될 때 만들어진 인스턴스는 하나임을 보장

예외로는 권한이 있는 클라이언트가 리플렉션 API를 사용해서 private 생성자 호출할 수 있음 ⇒ 이를 방어하기 위해서는 2번째 객체가 생성되려 할 때 예외 던지게 하면 됨

장점

  • public static 필드가 final이니 절대로 다른 객체 참조 불가
  • 간결함
  1. 정적 팩토리 메소드를 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 외의 클래스를 상속해야 한다면 사용 불가한 방법)

반응형

'TIL > Java' 카테고리의 다른 글

[Java] Effective Java 3/E 4장 item 15~20  (0) 2024.01.18
[Java] Effective Java 3/E 3장 item 10~14  (1) 2023.11.14
[JAVA] Java Version 8 vs 17  (0) 2023.10.21
[Java] Effective Java 3/E item 7~9  (1) 2023.06.13
[Java] Effective Java 3/E item 4~6  (0) 2023.05.28

댓글