본문 바로가기

CS/JAVA

정적 팩토리 메서드(Static Factory Method)

패스트 캠퍼스의 강의를 듣던 중 정적 팩토리 메서드를 사용하는 상황을 보았다. 처음 보는 객체 생성 방식이길래 찾아보던 중 이것이 정적 팩토리 메서드라는 것을 알게 되었고, 관련 내용을 찾아보던 중 이펙티브 자바라는 책을 알게 되었다. 

조슈아 블로크의 Effective Java에서 첫 번째 아이템으로 '생성자 대신 정적 팩토리 메서드를 고려하라'는 주제를 내놓았다.  정적 팩토리 메서드가 과연 무엇이고, 왜 써야 할까?

Static Factory Method에 관해 다양한 블로그와 강의 영상이 있지만, 인프런에서 백기선 강사님의 <이펙티브 자바 완벽 공략 1부>를 참고하였습니다.


Static Factory Method

정적 팩토리 메서드는 객체 생성 역할을 하는 클래스 메서드로 생성자(Contstructor)를 통해서가 아닌 Static Method를 통해서 객체를 생성하는 역할을 한다. 여기서 팩토리는 객체를 생성하는 역할을 분리한 것을 의미. 정적 팩토리 메서드는  디자인 패턴 중 팩토리 패턴과는 관련이 없다.

Lombok을 통해서도 생성 가능

ex) @RequiredArgsConstructor(staticName = "of")

 

public class UserAccount {
   
    private Long id;

    private String userId;
    private String userPassword;

    private String email;
    private String nickname;
    private String memo;

    protected UserAccount() {}

    private UserAccount(String userId, String userPassword, String email, String nickname, String memo) {
        this.userId = userId;
        this.userPassword = userPassword;
        this.email = email;
        this.nickname = nickname;
        this.memo = memo;
    }

    public static UserAccount of(String userId, String userPassword, String email, String nickname, String memo) {
        return new UserAccount(userId, userPassword, email, nickname, memo);
    }
}

위 코드는 패스트 캠퍼스 강의에서 나온 코드다. 이전에는 생성자를 public으로 선언하여 클래스 밖에서 생성자를 호출할 수 있도록 하였는데 여기서는 private로 생성자를 만들고 외부에서는 Static 메서드를 통해서만 생성자에 접근할 수 있게끔 하였다. 알다시피 static 메서드는 클래스명. 메서드 이름으로 호출할 수 있다. 따라서 위 같은 경우는 UserAccount.of(parameter1, parameter2, ···)으로 호출 가능하다. 처음에는 왜 사용하는지 의아했지만 정적 팩토리 메서드의 장점이 눈에 들어오기 시작했다.

Effective Java에서 저자인 조슈아는 정적 팩토리 메서드가 생성자를 대체할 만큼의 장점을 가지고 있으나 단점도 있으니 생성자를 만들 때 고려해보라고 했다.

장점

1. 이름을 가질 수 있어 가독성이 좋다.

 

생성자를 만들 때를 생각해보자

public class Order {
   
    private boolean prime;
    private boolean urgent;
    private Product product;    

    public Order(Product product, boolean prime) {
    	this.product = product;
        this.prime = prime;
    }
    
    public Order(boolean urgent, Product product) {
    	this.product = product;
        this.urgent = urgent;
    }

	/**에러 발생
	public Order(Product product, boolean urgent) {
    	this.product = product;
        this.urgent = urgent;
    }
	*/
}

 

생성자(Constructor)는 인자로 받는 파라미터의 개수에 따라 생성자를 여러 개 생성할 수 있다. 하지만 생성자는 클래스 명을 동일하게 사용해야 하는 규칙 때문에 각각의 생성자는 이름을 가질 수 없다. 하지만 생성자를 사용하면 객체를 생성하려는 것이 Prime Order인지 Urgent Order인지 알 수 있는 방법이 없다. 이 점을 해결하기 위해 Static Factory Method를 사용한다. 생성자의 파라미터 타입이 중복되는 경우에 사용하면 좋다.

 

다음의 예를 보자

public class Order {
   
    private boolean prime;
    private boolean urgent;
    private Product product;    

    public static Order primeOrder(Product product) {
    	Order order = new Order();  // 아무런 생성자를 작성하지 않았다면 암묵적으로 기본생성자가 생성된다.
    	order.product = product;
        order.prime = true;
        return order;
    }
    
    public static Order urgentOrder(Product product) {
    	Order order = new Order();
    	order.product = product;
        order.urgent = true;
        return order;
    }
    
}

 

2. 호출할 때마다 인스턴스를 새로 생성하지 않아도 된다.

- 캐싱해 재활용하자

 

생성자를 통해서 객체를 생성하면 매번 객체가 생성된다. 하지만 인스턴스의 개수를 제한할 때에도 적합하다. 설정 정보를 담은 클래스라던가, 싱글톤 패턴처럼 하나의 객체만을 가지고 새로 생성하지 않고 전에 만들어 둔 객체를 전달해줄 필요가 있는 경우가 있다.

Singleton 패턴의 예)

public class Singleton {
    
    private static Singleton singleton = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if(singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

Boolean 클래스를 살펴보자.

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>, Constable
{
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);
    
    ......
    
    public static Boolean valueOf(boolean b) {
        return b ? Boolean.TRUE : Boolean.FALSE;
	}
    
    .......
}

  Boolean 클래스는 static final로 Boolean 객체를 미리 생성해 놓았다.

그리고 valueOf의 메서드를 호출하면 새로 값을 생성해서 리턴해주는 것이 아닌 Boolean 클래스가 가지고 있는 객체를 리턴해준다. 이런 식으로 인스턴스를 통제할 수 있다. 이것은 Fly Weight 패턴과 유사하다.

Flyweight pattern  

플라이 웨이트 패턴(Flyweight pattern)은 동일하거나 유사한 객체들 사이에 가능한 많은 데이터를 서로 공유하여 사용하도록 하여 메모리 사용량을 최소화하는 소프트웨어 디자인 패턴이다. 종종 오브젝트의 일부 상태 정보는 공유될 수 있는데, 플라이웨이트 패턴에서는 이와 같은 상태 정보를 외부 자료 구조에 저장하여 플라이웨이트 오브젝트가 잠깐 동안 사용할 수 있도록 전달한다.

위키백과 中

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. (4번과 동시설명)

- 인터페이스 기반의 프레임워크를 사용할 수 있게 해준다.

- 다형성도 이것에 해당한다.

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

- DTO와 Entity 사이의 형 변환

- 캡슐화(Encapsulation)

 

다음과 같은 구조를 가진 인터페이스와 구현체들이 있다고 보자.

 

 

public interface HelloService {
	String hello();  //인터페이스는 접근제어자가 없으면 public으로 지정
    
    // 자바8 이후부터 인터페이스에 static 메소드 선언 가능
    static HelloService of(String lang) {
        if(lang.equals("ko")) {
            return new KoreaHelloService();
        } else {
            return new EnglishHelloService();
        }
    }
}

public class KoreaHelloService implements HelloService {

    @Override
    public String hello() {
        return "안녕하세요";
    }
}

public class EnglishHelloService implements HelloService {

    @Override
    public String hello() {
        return "Hello";
    }
}


public class HelloServiceFactory {
	
    public static void main(String[] args) {
    
    	HelloService eng = HelloService.of("eng");  //인터페이스 타입 기반을 사용할 수 있도록 강제
        System.out.println(eng.hello());    //hello 출력
    }
    
}

HelloServiceFactory 클래스를 보자. eng 인스턴스는 HelloService 인터페이스를 따르고 `eng`라는 문자열 값만 넘긴다. 그럼 인터페이스 내의 정적 팩토리 메서드는  매개 변수만 보고 EnglishHelloService의 구현체를 리턴해준다. 따라서 정적 팩토리 메서드는 하위 타입의 객체를 넘겨줄 수 있고, 매개변수에 따라 각각 다른 하위 타입의 객체를 리턴하는 것이 가능하다.

 

Entity와 DTO 간의 형 변환도 살펴보자

public record ArticleDto(
        Long id,
        UserAccountDto userAccountDto,
        String title,
        String content,
        String hashtag,
        LocalDateTime createdAt,
        String createdBy,
        LocalDateTime modifiedAt,
        String modifiedBy
) {
    public static ArticleDto of(Long id, UserAccountDto userAccountDto, String title, String content, String hashtag, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy) {
        return new ArticleDto(id, userAccountDto, title, content, hashtag, createdAt, createdBy, modifiedAt, modifiedBy);
    }

    // Article 객체를 받아 ArticleDto로 변환
    public static ArticleDto from(Article entity) {
        return new ArticleDto(
                entity.getId(),
                UserAccountDto.from(entity.getUserAccount()),
                entity.getTitle(),
                entity.getContent(),
                entity.getHashtag(),
                entity.getCreatedAt(),
                entity.getCreatedBy(),
                entity.getModifiedAt(),
                entity.getModifiedBy()
        );
    }

    //ArticleDto -> Article로 형변환
    // 이렇게하면 Article은 DTO의 존재를 몰라도 된다.
    public Article toEntity() {
        return Article.of(
                userAccountDto.toEntity(),
                title,
                content,
                hashtag
        );
    }
}

정적 팩토리 메서드 from 부분을 살펴보자 

정적 팩토리 메서드가 없었다면 from의 메서드는 생성자로 구현해야 했을 것이다.

그럼 ArticleDto dto = new ArticleDto(entity.getId(), entity.getTitle() , ······)로 Article의 내부 모듈들, 필드들이 들어나게 된다. 하지만 from 정적 팩토리 메서드를 통해 Article의 모듈들을 숨기고 인스턴스만 던져줌으로써 쉽게 형 변환할 수 있다.

 

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

→ 인터페이스만 있고 구현체는 없어도 가능하다.

이 부분은 백기선 강사님의 이펙티브 자바 완벽 공략을 참고해주세요... 원리는 알겠으나 어떻게 사용하면 좋은지 잘 감이 안 와요,,,

 

자바가 제공하는 정적 팩토리 메서드인 ServiceLoader가 있어서 load 메서드를 통해 다른 구현체를 불러올 수 있다.

해당 파일은 xml을 통해 jar 파일로 불러와 참조할 수 있다. 이렇게 사용하면 의존성을 줄일 수 있다.

즉, ServiceLoader로 호출하게 되면 어떠한 구현체가 참조될지 모르지만 그 구현체가 따르고 있는 인터페이스 기반을 따르기에 의존적이지 않다.

 

단점

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

public class Settings {

    private boolean useAutoSteering;
    private boolean useABS;
    private Difficulty difficulty;
    
    private Settings(){}
    
    private static final Settings SETTINGS = new Settings();
    
    public static Settings newInstance() {
        return SETTINGS;
    }
}

다음과 같은 Settings 클래스는 생성자가 private로 되어있기 때문에 다른 곳에서 사용할 수 없다. 즉 다른 클래스에서는 Settings 클래스를 상속받을 수 없다는 이야기가 된다. 하지만 이런 경우에는 상속 대신 Settings 인스턴스를 통해 기능을 위임하면 되기도 한다. 

그리고 굳이 생성자를 private로 놓지 않고, public으로 놓아 정적 팩토리 메서드와, 생성자를 모두 사용할 수 있도록 제공해도 된다.

 

2. 정적 팩토리 메서드는 프로그래머가 확인 작업이 필요하다.

javadoc의 내용을 보면 생성자는 문서로 정리되어 있다. 하지만 정적 팩토리 메서드는 따로 정리되지 않기 때문에 이 부분에서 확인 작업이 필요하다. 클래스 내에 메서드가 많을 경우 정적 팩토리 메서드 또한 문서에서 찾기 힘들다. 이 부분에 대한 해결책으로는 정적 팩토리 메서드의 네이밍을 신경 써서 지어야 쉽게 알 수 있다.

 

Naming Pattern

반드시 이 내용을 따르라는 것은 아니지만, 다른 개발자가 봤을 때 이해할 수 있도록 작성을 하자.

  • from
    • 하나의 매개변수를 사용하여 해당 유형의 인스턴스를 반환(생성)하자.
    • ex) Date d = Date.from(instant);
  • of
    • 여러 매개변수를 사용하여 이러한 매개변수를 가진 인스턴스를 반환하자.
    • ex) Set<Rank> faceCards = EnumSet.of("Jack", "Queen", "King");
  • valueOf
    • from과 of보다 더 구체적인 버전
    • ex) BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance or getInstance
    • 인스턴스를 반환하나, 동일한 인스턴스를 반환하는 것을 보장하지 않는다.
    • 싱글톤 패턴에서 주로 사용
    • ex) StackWalker luke = StackWalker.getInstance(options);
  • create or newInstance -
    • 매번 새로운 인스턴스를 보장, getInstance와 유사
    • ex) Object newArray = Array.newInstance(classObject, arrayLen);
  • get[Type]
    • 다른 타입의 인스턴스 생성
    • ex) FileStore fs = Files.getFileStore();
  • new[Type]
    • 다른 타입의 새로운 인스턴스를 생성
    • ex) BufferedReader br = Files.newBufferedReader(path);
  • type
    • getType(), newType()보다 간결한 버전
    • ex) List<Complaint> litany = Collections.list(legacyLitany)

 

 


참고하면 좋은 사이트

 

[ Java ] 28. 정적 팩토리 메서드?!

정적 팩토리 메서드 우리가 어떤 인스턴스를 새로 생성할 때는 보통 생성자를 이용한다. 보통 실제 개발에 가면 public 생성자(혹은 빌더패턴)를 주로 이용해서 사용하는데, 이보다 좀 더 나은 방

coder-in-war.tistory.com

 

 

정적 팩토리 메서드(static factory method)

static 메서드로 객체 생성을 캡슐화한다

johngrib.github.io

 

 

Java - 정적 팩토리 메서드의 정의와 네이밍 컨벤션

정적 팩토리 메서드(static factory method)는 실무에서도 활용하기 쉬운 프로그래밍 기법입니다. 정확히는 GoF 디자인 패턴 중 팩토리 패턴에서 용어를 가져와 정의한 기법으로 "객체 생성 메서드"라

7942yongdae.tistory.com

 

 

정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?

tecoble.techcourse.co.kr

 

'CS > JAVA' 카테고리의 다른 글

JAVA의 리플렉션 API  (0) 2023.01.08
[JAVA] String 그리고 StringBuffer와 StringBuilder  (0) 2022.08.30
Abstract Class와 Interface  (0) 2022.08.23
Java에서의 Hash  (0) 2022.08.16
JVM 구조  (0) 2022.07.25