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

Wintering·2022년 5월 30일
0

이펙티브 자바

목록 보기
11/18

정적 팩토리 메소드

  • 객체 생성의 역할을 하는 클래스 메서드
    생성자와 별도로 객체를 생성하는 메소드를 정적으로 만들어 객체 생성을 캡슐화하여 제공할 수 있다.
public class Product{
	private String name;
    
    public Product(String name){
    	this.name = name;
    }
    
    public static Product nameOf(String name){
    	return new Product(name);
    }
    
    public static void main(String[] args){
    	Product product = new Product("book");
    }
}
  • 정적 팩토리 메소드 예시
Boolean 클래스의 ValueOf()
LocalTime의 of()
enum Class의 ValueOf()

미리 생성된 객체를 '조회'하는 메서드이기 때문에 객체를 '생성'하는 '팩토리'라 역할을 한다고 보기는 어렵지만, 외부에서 원하는 객체를 반환하고 있으므로 결과적으로는 정적팩토리라고 간주한다.


장점

1. 이름을 가질 수 있다.

그냥 생성자를 만드는 것보다 객체 생성의 의미를 파악하기 쉽다.
정적 팩토리 메소드의 이름에 표한하고자 하는 특성을 명시함으로써 객체의 특성을 한번에 유추 가능하다.

2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.

불변클래스는 인스턴스를 미리 만들어두거나, 새로 생성한 인스턴스를 캐싱하여 재사용하므로 불필요한 객체의 생성을 줄일 수 있다. 이 때 정적 팩터리 메서드와 캐싱구조를 함께 사용한다면 매번 새로운 객체를 만들 필요가 없다.

class Singletone{
	private staitc Singleton singleton = null;
    
    private Singletone(){}
    
    static Singleton getInstanceO(){
    	if(singleton == null){
        	singleton = new Singleton();
        }
        return singleton;
    }
}
public class Main(String[] args){
	Singleton s1 = singleton.getInstance();
    Singleton s2 = singleton.getInstance();
    System.out.println(s1 == s2); 				// true
}

생성자를 private으로 제한하여 새로운 객체 생성을 제한하고 getInstance( ) 메소드를 static으로 선언하여 인스턴스를 생성한다. s1과 s2는 같은 인스턴스
반복되는 요청에 같은 객체를 반환하는 형식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아있게 할 지 철저하게 통제할 수 있다. 이런 클래스를 인스턴스 통제 클래스라 부르고 인스턴스를 통제하면 위와같이 클래스를 싱글톤으로 만들수도 있고, 인스턴스화 불가로 만들 수도 있다. 인스턴스를 통제한다는 건 인스턴스가 단 하나뿐임을 보장하는 것이고, 플라이웨이트 패턴의 근간이 된다.

(+) 플라이웨이트 패턴 
데이터를 공유하여 메모리를 절약하는 패턴으로 공통으로 사용되는 객체는 한 번만 사용되고 
Pool에 의해서 관리/사용되는 디자인패턴 (ex. JVM의 String Constant Pool)`

3. 하위 자료형 객체를 반환할 수 있다.

상속을 활용할 때 나타나는 특징.
자바의 다형성의 특징을 이용해 인터페이스 자체를 하위클래스(구현체)의 노출없이 반환이 가능하다.
GoF에서 소개하는 팩토리 패턴과 유사하게 객체 생성을 조건에 따라 분기한다는 개념이다.

class order{
	public static Discount createDiscountProduct(String code) throws Exception{
    	if(!isValidCode(code)){
        	throw new Exception("잘못된 할인 코드");    
        }
        if(isUsableCoupon(code)){
        	return new Coupon(1000);
        }else if(isUsablePoint(code)){
        	return new Point(500);
        }
        throw new Exception("이미 사용한 코드");
    }
}
class Coupon extends Discount {}
class Point extends Discount {}

3번의 내용은 인터페이스를 정적 팩토리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.

ex. java.util.Collections 클래스를 굳이 만들지 않고도 인터페이스 자체에서 정척 메소드를 얻도록 구현

pbulic interface List<E> extends Collection<E>{
	static <E> List<E> of() {
    	return (List<E>) ImmutableCollections.ListN.EMPTY_LIST;
    }
}

클라이언트는 반환되는 클래스가 무엇인지 알 필요 없이 그냥 of() 메서드의 기능이 무엇인지만 알고, List.of()의 형태를 사용하기만 하면된다.

(+) 자바 8 이전의 방식
인터페이스의 유사 클래스를 만들고, 그 안에 정적메소드를 정의하는 방식으로 우회해서 사용했다.

public class Collection {
	private Collection() {}
    ```
    public static final List EMPTY_LIST = new EmptyList<>()'
    public static final<T> list<T> emptyList(){
    	retrun (List<T>) EMPTY_LIST;
    }
}

4. 객체 생성을 캡슐화 할 수 있다.

생성자를 사용하는 경우 외부에 내부 구현이 드러는 것과 달리, 정적 팩토리 메소드를 사용하면 내부 구현을 캡슐화하여 사용할 수 있다. DTO와 Entity간의 형변환이 가장 자주 보이는 예시이다.

@Builder
public class ProductDto{
	private String name;
    private String date; 
    
    public static ProductDto from(Product product){
    	return new ProductDto(product.getName(), product.getDate());
    }
}
public class Main(){
	public static void main(String[] args){
    	Product product = repository.getById(id);
        
        ProductDto productDto = new ProductDto(product.getName(), product.getDate()); //생성자
        ProductDto productDto = ProductDto.from(product);	//정적메소드
    }
}

정적 팩토리 메서드를 사용하면 단순히 생성자의 역할을 대신하는 것 뿐 아니라, 좀더 가독성 좋은 코드를 작성할 수 있고 객체지향적으로 프로그램이 할 수 있도록 도와준다.

(+) 롬복을 활용한 경우

@RequiredArgsConstructor(staticName = "of")
public class Product{
	private final Long id;
    private final String name;
}

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

반환값이 인스턴스여도 된다.
정적 팩토리 메서드의 변경 없이 구현체를 갈아 끼울 수 있다. -> 반환값의 구현체이기만 하다면 가능

import java.util.ArrayList;
import java.util.List;

public class TicketStore {
  /** TicketSeller는 인터페이스이고 구현체가 없음에도 아래와 같은 메서드 작성이 가능하다.**/
    public static List<TicketSeller> getSellers(){
        return new ArrayList<>();
    }
}

정적팩터리메서드 활용 예시

서비스 제공자 프레임워크

  • 서비스 인터페이스 : 구현체의 동작 정의
  • 제공자 등록 API : 제공자가 구현체 등록
  • 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 -> 원하는 구현체의 조건을 입력하고 그에 따라 기본 구현체를 반환하거나 조건에 부합하는 구현채를 반환할 수 있음. 이를 정적 팩터리메소드로 작성할 수 있다.
  • 서비스 제공자 인터페이스 (SPI) : 서비스 인터페이스 인스턴스를 생성하는 팩터리 객체 설명

"보통의 API들은 구현체의 interface를 외부로 공개하여 구현체를 사용하는 주체가 자신의 환경에 맞게 사용한다. 반면 SPI는 사용자가 구현해야 할 interface를 정의한다. SPI 사용자 (보통 driver vendor)가 자신의 환경에 맞는 구현체를 직접 정의하여 제공하면 SPI를 제공해준 service에서는 제공 받은 구현체를 불러다 사용하는 형태로 동작한다."

public void jdbcExample() throws SQLException{

  Driver drvier = new Driver();
  DriverManager.registerDriver(driver);	// 제공자 등록 API

  Connection connection = DrvierManager.getConnection(
  jdbc:myysql://127.0.0.1:3306/jdbc?severTimezome=UTC", "root", "password")//서비스 인터페이스

  Statement statement = connection.createStatement();
  ResultSet resultSet = statement.executeQuery("SELECT*FROM test");

  while(resultSet.next()){
      System.out.println(resultSet.getString(1));
      System.out.println(resultSet.getString(1));	
	}
}

Driver driver = new Driver()는 jdbc 정의 인터페이스가 아닌 mysql에서 제공하는 구현체임을 주의! Class.forName으로도 등록이 가능하다.


단점

1. 상속을 하려면 public / private 생성자가 필요하니 정적 팩토리 메소드만 제공한다면 하위 클래스를 만들 수 없다.

  • 하지만 상속보다는 Composition을 활용하는 걸 지향하고, 불변타입을 만들기 위해서는 이 단점이 오히려 장점으로 작용할 수 있다.

2. 정적 팩터리 메서드는 프로그래머가 찾기 힘들다.

  • 정적 팩토리 메서드와 정적 메서드의 구분을 위해, 구분을 쉽게 하기 위한 네이밍이 존재한다.

    from : 하나의 매개 변수를 받아서 객체를 생성
    of : 여러개의 매개 변수를 받아서 객체 생성
    getInstance | instance : 인스턴스 생성 (이전 것 과 같을 수도 있다.)
    newInstance | instance : 새로운 인스턴스 생성
    get(OrderType) : 다른 타입의 인스턴스를 생성.
    new(OrderType) : 다른 타입의 새로운 인스턴스를 생성

참고

0개의 댓글