정적 팩토리 메서드에 대해서(Effective Java)

Choizz·2023년 7월 6일
0

이펙티브 자바

목록 보기
1/13

이번 포스팅은 이펙티브 자바에 나오는 토픽 중 "생성자 대신 정적 팩터리 메서드를 고려하라."라는 내용에 대해 포스팅하려고 합니다.


정적 팩토리 메서드란?

우선 정적 팩토리 메서드라는 것이 무엇일까요?

  • 정적이라는 말이 붙은 것으로 봐서 static을 사용한다는 것을 의미할 겁니다.
  • 팩토리라는 표현은 객체를 생성하는 역할을 붙리하겠다는 의미가 있을 것 입니다.

이러한 네이밍을 고려했을 때,
정적 팩토리 메서드는 객체 생성의 역할을 하는 메서드라고 볼 수 있습니다.

List<Integer> lists = List.of(1,2,3,4);


정적 팩토리 메서드는 어떤 경우에 필요할까?

그렇다면 정적 팩토리 메서드가 왜 필요할까요? 그냥 생성자를 사용하면 되지 않을까요??

1. 생성자의 시그니쳐가 중복되거나 생성자가 여러 개일 경우

  • 밑의 코드의 1번 생성자와 2번 생성자는 동시에 사용할 수 없습니다.
  • 시그니쳐가 같기 때문입니다. 하지만 생성자를 통해 생성되는 객체의 특성이 달라집니다.
    • 하나는 order 객체가 urgent를 갖는 경우 사용하고, 나머지는 prime일 경우에 사용합니다.
  • 3번과 4번처럼 정적 팩토리 메서드를 사용한다면 하나의 시그니쳐를 가진 생성자를 가지고 다른 특성을 가진 객체를 만들 수 있습니다.
  • 또한 메서드 네이밍 덕분에 어떤 특징을 갖는 객체인지 알아보기 쉽게 됩니다.
public class Order {

    private boolean prime;

    private boolean urgent;

    private Product product;

    private OrderStatus orderStatus;
    
    //1과 2 생성자는 컴파일 오류가 난다.
    //1
    public Order(Product product, boolean urgent){
    	this.product = product;
        this.urgent = urgent;
    }
    //2
    public Order(Product product, boolean prime){
    	this.product = product;
        this.prime = prime;
    }
    
	//정적 팩토리 메서드 사용	
	//3
    public static Order primeOrder(Product product) {
        Order order = new Order();
        order.prime = true;
        order.product = product;

        return order;
    }

	//4
    public static Order urgentOrder(Product product) {
        Order order = new Order();
        order.urgent = true;
        order.product = product;
        return order;
    }
}

2. 객체를 호출할 때마다 새로운 객체 생성이 필요 없는 경우

  • 밑의 코드처럼 기본 생성자에 private 메서드를 넣어서 클라이언트에서 생성자를 통해 객체 생성을 하지 못하게 막습니다.
  • 그리고 정적 팩토리 메서드 #getInstance()를 사용해서 미리 생성해 놓은 객체를 가지고 오게 합니다.
  • 이렇게 되면, 자주 사용하는 객체를 미리 만들어 놓고 하나의 객체만으로 사용가능 합니다.
  • 싱글톤 패턴이나 플라이웨이트 패턴에 사용됩니다.
public class Settings {

    private boolean useAutoSteering;

    private boolean useABS;

    private Difficulty difficulty;

    private Settings() {}

    private static final Settings SETTINGS = new Settings();

    public static Settings getInstance() {
        return SETTINGS;
    }

}

3. 그 외의 장점

(1) 하위 타입의 객체를 반환할 수 있고, 따라서 매개변수에 따라 다른 객체를 반환할 수 있다.

  • 정적 팩토리 메서드는 리턴 타입을 가질 수 있기 때문에 Java의 다형성을 이용해서 하위 타입의 객체를 반환할 수 있습니다.
  • 동일한 타입의 다른 구현체를 추가할 수 있게 됩니다.
public interface HelloService {
    String hello();
}
public class EnglishHelloService implements HelloService{

    @Override
    public String hello() {
        return "hello";
    }
}
public class KoreaHelloService implements HelloService{

    @Override
    public String hello() {
        return "안녕";
    }
}
  • 정적 팩토리 메서드에서 매개 변수에 따라 HelloService를 구현한 객체를 리턴할 수 있습니다.
public class HelloServiceFactory {

    public static HelloService of(String language){
       if(language.equals("kor")){
           return new KoreaHelloService();
       }
       return new EnglishHelloService();
    }
}
---------------------------------------------------------

	public static void main(String[] args) {
        HelloService kor = HelloServiceFactory.of("kor");
        System.out.println(kor instanceof KoreaHelloService);//true
        System.out.println(kor.hello());//안녕
        
        HelloService eng = HelloServiceFactory.of("eng");
        System.out.println(eng instanceof EnglishHelloService); //true
        System.out.println(eng.hello()); //hello
    }

(2) 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

  • 정적 팩토리 메서드를 사용하면, 컴파일 타임 때, 반환할 객체의 클래스가 존재하지 않아도 됩니다. 런타임 시점에 정적 팩토리 메서드에서 반환할 수 있으면 됩니다.
  • 서비스 제공자 프레임워크를 만드는 근간이 됩니다.

서비스 제공자 프레임 워크란?

서비스 제공자 프레임워크(service provider framework)에서 서비스를 제공하는 제공자(provider)는 서비스의 구현체다. 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

  • 여기서 중요한 것은 클라이언트를 구현체로부터 분리해준다.라는 말이 핵심인 것 같습니다.
  • 확장 가능한 애플리케이션을 만들도록 해주는 것이 목적이라고 생각합니다.
  • 이를 위해서는 4가지 요소가 필요합니다.
    • 서비스 제공자 인터페이스 : 어떤 서비스를 확장가능하도록 만들 것이냐에 초점을 맞춤니다. 여러 구현체를 가질 수 있겠죠?
    • 서비스 제공자 : 서비스를 구현한 것을 의미합니다.
    • 서비스 제공자 등록 api : 서비스 구현체를 등록하는 방법을 제공합니다. (스프링에서 @Configuration, @Bean 등등)
    • 서비스 접근 api : 클라이언트에서 서비스를 가지고 오는 방법을 제공합니다. (@Autowired 등등)

  • 그렇다면 왜 정적 팩토리 메서드가 서비스 제공자 프레임워크를 만드는 근간이 될까요?
    • 앞에서 설명드린 것처럼 정적 팩토리 메서드는 리턴 타입으로 상위 타입을 지정할 수 있습니다.
    • 이것은 정적 팩토리 메서드를 쓰면 클라이언트에서는 코드 변경이 없고 구현체만 바꿀 수 있다는 것을 의미합니다.
    • 확장성이 생기는 것이죠!

  • ServiceLoader 클래스의 load라는 정적 팩토리 메서드를 사용한다고 했을 때,
    • 밑의 코드에서 HelloService 인터페이스를 구현한 클래스가 프로젝트 내에 없다고 하더라도 출력이됩니다.
    • 프로젝트 내에서 외부의 jar 파일을 참조해 외부에 구현한 클래스를 참조하여 출력할 수 있습니다.
    • HelloService라는 상위 타입을 리턴할 수 있기 때문입니다.
  ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
  Optional<HelloService> helloServiceOptional = loader.findFirst();
  helloServiceOptional.ifPresent(h -> {
     System.out.println(h.hello()); //ni hao
  });
  • 결국, 인터페이스에 의존하게 함으로써 한층 더 유연한 코드를 구현할 수 있게 됩니다.

단점이 있다면?

  1. 객체 생성에 정적 팩토리 메서드만 사용하는 경우 기본 생성자에 private을 사용하기 때문에 상속을 할 수 없습니다.
  2. 클래스에서 사용하는 메서드가 많아질 경우 정적 팩토리 메서드를 찾기 힘들수 있습니다.
    • 그래서, 정적 팩토리 메서드임을 확인할 수 있는 네이밍 컨벤션을 사용합니다.

정적 팩토리 메서드 네이밍 컨벤

  • from : 하나의 매개 변수를 받아서 객체를 생성
  • of : 여러개의 매개 변수를 받아서 객체를 생성
  • getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • newInstance | create : 새로운 인스턴스를 생성
  • get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.

정리하면

  • 무조건적으로 정적 팩토리 메서드를 사용하라는 것은 아닙니다.
  • 그리고 생성자와 정적 팩토리 메서드 모두를 사용할 수도 있습니다.
  • 한 클래스를 생성하기 위한 매개변수가 많아질 경우 고려해볼 수 있을 듯 합니다.

reference

profile
집중

0개의 댓글