[이펙티브 자바] 1. 생성자 대신 정적 팩터리 메서드를 고려하라

노을·2023년 1월 9일
0

이펙티브 자바

목록 보기
1/14
post-thumbnail

⭐ 정적 팩터리 메서드가 생성자보다 좋은 이유


1. 이름을 가질 수 있다.

생성자의 경우 동일한 시그니처 생성자 두 개를 가질 수 없다.

class A {
	
    private String chip;
    private String dale;
    
    // 불가능
    public A(String chip){
    	this.chip = chip;
    }
    
    public A(String dale){ 
    	this.dale = dale;
    }
}

반면 정적 팩터리 메서드는 이름을 가질 수 있다. 정적 팩터리 메서드 사용 시 매개변수가 같아도 이름을 다르게 하게 가능하다.

class A {
	
    private String chip;
    private String dale;
    
    public static A getChip(String chip)  {
    	A a = new A();
        a.chip = chip;
        
        return a;
    }
    
    public static A getDale(String dale)  {
    	A a = new A();
        a.dale = dale;
        
        return a;
    }
}



2. 항상 인스턴스를 새로 생성하지는 않아도 된다.

class A {

    private static final String chip = "chip";
    public static A getChip(String chip)  {
        return chip;
    }
}

이렇게 객체 하나를 만들어두고 정적 팩터리 메서드를 사용할 때 만들어 둔 객체를 반환한다. 객체 생성 비용이 큰 경우에 유용하다.


이 부분에서 플라이웨이트 패턴이 나오는데,

플라이웨이트 패턴 : 인스턴스를 가능하면 공유해서 사용함으로써 메모리를 절약하는 패턴


이 사진이 플라이웨이트 패턴을 잘 설명하고 있는 것 같아 가져왔다.
나무 색깔은 거기서 거기다. (초록이라던지,, 연두색, 갈색 정도?)
그러니까 고객이 연두색 나무가 필요하다고 하면 그걸 만들어두고, 보관하다가 다른 사람이 요청하면 이미 만들어진 연두색 나무를 주면 된다는 내용이다.
만약 빨간 색 나무를 원한 경우, 없다면 새로 만들고 있다면 그대로 준다.

만약 생성자를 사용했다면, 무한으로 나무 객체를 만들어야 하지만 정적팩터리 메서드를 사용하면 상황에 맞게 객체를 생성할 수 있다.



3. 반환타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

class A {
    public static B checkInput(String discountCode)  {
    	return new B();    
    }
}

class B extends A { }

class A 를 사용하는 클라이언트 입장에서는 구현체를 알 수 없으며, 상위 타입 코드만 알고도 구현체를 조작할 수 있다.



4. 입력 매개 변수에 따라 다른 클래스의 객체를 반환할 수 있다.

public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
        throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
        return new RegularEnumSet<>(elementType, universe);
    else
        return new JumboEnumSet<>(elementType, universe);
}

예시로 EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리 메서드만 제공한다. 만약 원소가 64개 이하이면 원소들은 long 변수 하나로 관리하는RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JunboEnumSet의 인스턴스를 반환한다.

클라이언트는 이 두 구현클래스의 존재를 몰라도 된다.



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

이 부분이 아이템1에서 가장 이해하기 어려운 부분인 것 같다.

11p. 이 특징은 서비스 제공자 프레임워크의 근간이 된다.

이런 문장이 나오는데 서비스 제공자 프레임워크의 예시인 ServiceLoader에 대해 알아보자.


만약 내 프로젝트A 인터페이스 Animal이 있고, 프로젝트B에 구현체 Dog가 잇다고 가정하자.

그런데 구현체를 Dog 말고 Cat으로 변경하고 싶어졌다. 이때 ServiceLoader라는 java9부터 제공하는 클래스를 이용하면 코드 수정 없이 구현체를 갈아끼울 수 있다!

프로젝트 A에서 프로젝트 B를 dependency 받고, 프로젝트B에서 META-INF/services 에 구현체 정보를 적어주기만 하면 된다.
Dog에서 Cat으로 변경하고 싶다면 META-INF/servicesCat 구현체를 등록하면 된다.


ServiceLoader<Animal> loader = ServiceLoader.load(Animal.class);
Optional<Animal> animalOptional = loader.findFirst();
animalOptional.ifPresent(h -> {
System.out.println(h.cry()); 
});

그러면 ServiceLoaderload()를 사용해서 Animal 타입 객체를 가져올 수 있다.
이 코드에서는 Dog, Cat과 같은 구현체 코드가 전혀 없다.

이러한 서비스 제공자 프레임워크의 패턴은 어노테이션 프로세서와 Springboot 의 autoConfiguration 에서도 비슷하게 사용된다.


반면에 구현체 객체를 생성자로 직접 만드는 경우는 어떨까?

Animal animal = new Dog();
System.out.println(helloService.cry()); 

프로젝트A는 구현체 코드에 의존적이기 때문에
Dog 에서 Cat으로 구현체를 변경하고 싶을 때 코드 수정이 발생한다.



그리고 책에서

12p. 서비스 제공자 인터페이스가 없다면 각 구현체를 인스턴스로 만들 때 리플렉션을 사용해야 한다.

이런 문장이 나온다. 이건 무슨 얘기나면,,,

일단 자바에서 객체를 생성하는 방법은 다음과 같다.

  1. new 연산자 이용 A a = new A();
  2. Class 클래스 이용
    • Class clazz = Class.forName("me.jackjack.A"); 처럼 클래스 이름으로 객체 받고 생성자 얻어서 생성
    • A.class 로 객체 받고 생성자 얻어서 생성
    • A a = new A(); a.getClass(); 객체 받고 생성자 얻어서 생성

Class를 이용하는 건 리플렉션을 이용하는 것이다. 리플랙션은 클래스로더가 읽어온 클래스 정보로 객체를 생성하는 것이다.
리플랙션을 사용해 클래스를 읽어오거나, 인스턴스를 만들거나, 메서드를 실행하거나, 필드의 값을 가져오거나 변경하는 것이 가능하다는 것이다. (private 생성자로 객체를 만들 수도 있다.)

//리플랙션 이용해서 객체 만들기
Class<?> aClass = Class.forName("패키지명+클래스명");
Constructor<?> constructor = aClass.getConstructor();
Aniaml animal = (Aniaml) constructor.newInstance();
System.out.println(animal.cry());






⭐ 정적 팩터리 메서드 단점


1. private 생성자를 만들면 상속이 불가능하다.


2. 정적 팩터리 메서드는 개발자가 찾기 어렵다.

javadoc으로 만든 자바 문서에서 개발자가 정적 팩터리 메서드를 찾기 어렵다. (수많은 메서드들 중에서 정적 팩터리 메서드가 숨어있다!)

<해결 방법>

  • 네이밍 컨벤션을 잘 지키자. (from, of, valueOf, instanse, getInstance, create, getType, newType)
  • 주석을 이용해 문서를 잘 만들자.

0개의 댓글