Java

이펙티브 자바 - Item(1) 생성자 대신 정적 팩터리 메소드를 고려하라

뽀루피 2024. 7. 15. 18:43

*정적 팩토리 메소드의 예시

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
} 

 

장점 1. 이름을 가질 수 있다.

 

자바 class는 파라미터마다 다른 생성자를 가질 수 있는 특성(오버로딩)을 가지고 있는데 생성자 이름만으로는 각 생성자의 특성을 대표하기 어려운 경우가 많다. 그럴 때 위의 예시처럼 public static 팩터리 메소드를 사용하면 각 생성자의 특성을 잘 설명할 수 있다.

 

 

장점 2. 같은 타입의 파라미터를 받는 다수의 생성자를 만들 수 있다.

public Food(String name) {
...
}

public Food(String ingredient) {
...
}    

 

위의 예시는 불가하다

 

public static Food withName(String name) {
    Food food = new Food();
    food.name = name;
    return food;
}

public static Food withIngredient(String name) {
    Food food = new Food();
    food.ingredient = ingredient;
    return food;
}

 

정적 팩터리 메소드는 같은 타입의 파라미터를 받는 생성자를 만들어줄 수 있다.

 

 

장점 3. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

불변(immutable) 클래스인 경우나 매번 새로운 객체를 만들 필요가 없는 경우에 미리 만들어둔 인스턴스를 반환할 수 있다. Boolean.valueOf(boolean) 메소드도 그 경우에 해당한다.

 

*불변 클래스(모든 필드를 final로 나타내고 생성자에 파라미터로 초기화하는 클래스)

public final class Person {
    private final String name;
    private final int age;

    // 생성자에서 모든 필드를 초기화
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

 

장점 4. 반환 타입의 하위 타입 객체를 반환할 수 있다.

정적 팩토리 메서드를 사용하면 반환 타입을 특정 구현 클래스가 아닌 인터페이스 또는 상위 클래스로 지정할 수 있다.

이는 클라이언트 코드가 특정 구현에 의존하지 않도록 하여, 이후에 구현 클래스를 변경할 때 유연하게 해준다.

 

  • 예시
public interface Shape {
    void draw();
}

public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}

public class Square implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a Square");
    }
}

public class ShapeFactory {
    public static Shape createShape(String shapeType) {
        if ("circle".equalsIgnoreCase(shapeType)) {
            return new Circle();
        } else if ("square".equalsIgnoreCase(shapeType)) {
            return new Square();
        }
        throw new IllegalArgumentException("Unknown shape type");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = ShapeFactory.createShape("circle");
        shape1.draw(); // 출력: Drawing a Circle

        Shape shape2 = ShapeFactory.createShape("square");
        shape2.draw(); // 출력: Drawing a Square
    }
}

 

 

장점
구현 세부 사항 숨김: 클라이언트 코드에서는 인터페이스만을 사용하며, 실제 구현 클래스에 대해 알 필요가 없다. 책에서는 Collections 프레임워크를 예시로 들고 있다. 이는 코드의 캡슐화를 높인다.
유연성: 구현체를 수정할 때 클라이언트 코드를 변경할 필요 없이 클래스만 수정하면 된다.
확장성: 인터페이스를 통해 구현체를 유연하게 추가할 수 있다.

 

 

장점 5. 리턴하는 객체의 클래스가 입력 매개변수에 따라 매번 다를 수 있다.

장점 4의 연장선이다. 반환 타입이 Shape라고 해서 반드시 Shape만 반환할 필요는 없다.
아래의 예시를 보자.

public static Shape getShape(boolean flag) {  
    return flag ? new Shape() : new Circle();  
}

 

반환 타입은 Shape이지만 매개변수에 따라 반환 타입이 변경된다.

 

 

장점 6. 정적 팩터리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

어떤 특정 약속 되어 있는 텍스트 파일에서 정적 팩터리 메소드의 구현체의 정보를 읽어오고, 정보에 해당하는 인스턴스를 생성한다. 그리고 해당 인스턴스를 반환하면 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 동작 가능하다.

 

 

단점 1. public 또는 protected 생성자 없이 정적 팩터리 메소드만 제공하는 클래스는 상속할 수 없다.

앞서 말한 Collections 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 말이다. 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만드려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 볼 수 있다.

 

여기서 말하는 컴포지션은 객체가 다른 객체를 포함하고 있는 구조로, 상속(inheritance)과 대비되는 개념이다.
코드의 재사용성과 유연성을 높이는 데 도움된다.

 

아래는 컴포지션의 예시이다.

public class Engine {  
  public void start() {  
      System.out.println("Engine started");  
  }  
}

public class Car {  
    private Engine engine;


  public Car() {
      this.engine = new Engine();
  }

  public void startCar() {
      engine.start();
      System.out.println("Car started");
  }
}

 

 

아래는 상속의 예시이다. 메소드를 오버라이딩하기 위해서 코드가 반복되고 있음을 확인할 수 있다.

public class Animal {  
  public void makeSound() {  
      System.out.println("Animal sound");  
  }  
}

public class Dog extends Animal {  
  @Override  
  public void makeSound() {  
      System.out.println("Bark");  
  }  
}

 

불변 타입은 수정 불가능하고 인스턴스 상태가 생성 후 변경되지 않는다. 이런 경우, 상속을 통해 기능을 추가하려는 시도는 복잡성을 증가시킬 수 있고, 불변성을 해칠 가능성이 있다. 따라서 불변 타입을 설계할 때는 컴포지션을 사용하여 다른 클래스의 인스턴스를 포함하고, 그 인스턴스의 메소드를 호출하는 방식을 권장한다.

 

 

단점 2. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.

생성자는 Javadoc 상단에 모아서 보여주지만 정적 팩토리 메소드는 API 문서에 다뤄주지 않는다. 따라서 클래스나 인터페이스 문서 상단에 팩토리 메소드에 대한 문서를 제공하는 것이 좋다.


메소드 이름을 알려진 규약에 따라 짓는 형식으로 문제를 완화해보자.

  • from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드
  • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드
  • valueOf: from과 of의 자세한 버전
  • instance 혹은 getInstance: 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지 않는다.
  • create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
  • getType: getInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메소드를 정의할 때 쓴다. Type은 팩터리 메소드가 반환할 객체의 타입이다.
  • newType: newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩터리 메소드를 정의할 때 쓴다. Type은 팩터리 메소드가 반환할 객체의 타입이다.
  • type: getType과 newType의 간결한 버전