본문 바로가기

Language & Framework/Java

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

 

 

 

* 정적 팩터리 메서드는 말 그대로 Static Factory Metod를 의미하며 Factory Method Pattern과는 무관하다 *

 

이펙티브 자바의 첫 번째 아이템, 정적 페터리 메서드다.

정적 팩터리 메서드라고 하면 어렵게 느껴질 수 있는데, 아래와 같은 것들이 정적 팩터리 메서드다.

 

1
2
3
4
5
6
7
publc class Example {
   public static void main(String[] args) {
       String str = String.valueOf(1);
       Optional<String> value = Optional.empty();
       List<Integer> numList = List.of(1,2,3,4,5,6);
   }
}
cs

 

그렇다.. 아마도 다들 밥먹듯이 사용하고 있는 것이 바로 정적 팩터리 메서드인 것이다.

말 그대로 static으로 instance를 만드는 factory 역할을 하는 method, 정적 팩터리 메서드다.

 

정적 팩터리 메서드를 고려하라고 하는 이유는 무엇일까?

 

첫 번째,

생성자와 달리 이름을 가질 수 있으며 하나의 시그니처로 여러 종류의 인스턴스를 생성할 수 있다.

이게 왜 장점이 될 수 있는지는 코드를 보면 곧바로 알 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class LoginManager {
    private String email;
    private String username;
    private String password;
 
    public LoginManager(String username, String password) {
        this.username = username;
        this.password = password;
    }
 
    public LoginManager(String email, String password) {
        this.username = username;
        this.password = password;
    }
}
cs

 

이게 불가능한 코드라는 사실은 굳이 설명하지 않아도 알 것이다.

그러면 아래는 어떨까 ?

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LoginManager {
    private String email;
    private String username;
    private String password;
    
    public static LoginManager withUserMailAndPassword(String email, String password) {
        LoginManager loginManager = new LoginManager();
        loginManager.email = email;
        loginManager.password = password;
        return loginManager;
    }
 
    public static LoginManager withUsernameAndPassword(String username, String password) {
        LoginManager loginManager = new LoginManager();
        loginManager.username = username;
        loginManager.password = password;
        return loginManager;
    }
}
cs

 

단순히 알아보기 쉽다 어렵다 같은 문제를 떠나서, 같은 시그니처를 가진 Factory를 여러 개 정의할 수 있다는 것은 해당 객체의 확장성을 더욱 증대시킨다.

 

내가 임의로 만든 클래스라서 공감이 되지 않을 수도 있다, 그렇다면 자바 소스코드에서 이런 형태를 찾아보자.

 

 

 

Optional에서도 같은 시그니처 (T value)를 가진 인스턴스를 생성할 수 없는 것을 정적 팩터리 문제를 통해서 해결하였으며, 거기에 더해 이름을 나타낼 수 있다는 장점을 이용하여 of는 단순히 해당 value를 가진 Optional을 리턴하지만, ofNullable은 내부에서 미리 null check를 통해 안정성을 보장해준다.

 

그리고 또한 개발자는 해당 메서드명을 통해 내부의 소스코드를 직접 읽어보지 않아도 해당 메서드를 통해 생성되는 인스턴스가 어떤 성질을 가지고 있을지 미리 파악할 수 있다는 큰 장점이 있다.

우리가 협업 시 개개인이 만든 메서드의 원리를 하나하나 뜯어보고 이해할 수는 없으니 말이다.

 

 

두 번째,

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

 

호출될 때마다 인스턴스를 새로 생성하지 않아도 된다는 건 어떤 경우에 장점이 될까?

자주 사용되는 (생성 비용이 크다면 더더욱) 인스턴스를 미리 하나만 생성해놓고 해당 인스턴스를 끊임없이 재사용할 수 있다.

 

이는 싱글톤 패턴에서 보편적으로 사용되는 전략이다. (Enum을 사용하는 방법이 있지만 이 쪽이 일반적이다.)

이번에는 순서를 바꿔 자바 소스코드를 먼저, 내 코드를 뒤에 보도록 하겠다.

 

 

일단 기본적으로 Optional 내부에서 반환되는 EMPTY는 단일 인스턴스이다.

미리 EMPTY가 static 멤버로서 존재하고 있으며, empty 메서드를 사용하면 매번 이를 반환한다.

그런데 의아한 점은 주석에 이것이 싱글톤을 보장하지 않으니 "=="을 사용하는 모험을 하지 말라고 적혀있다는 점이다.. 아쉽게도 이 부분에 대한 궁금증을 해소하기 위해 많은 검색을 해봤으나 명확한 답을 찾을 수 없었다.

내가 추측하기로는 해당 방식이 100% 안전한 싱글톤 구현 방법이 아니기 때문인 것 같은데, 이에 대해서는 이후 관련 글을 작성해볼 예정이다.

 

아무튼 "실제 동작이 어떻건 간에" 이것은 전형적인 싱글톤 패턴이다.

 

 

 

Boolean 내부의 valueOf 인스턴스 또한 마찬가지다. 미리 만들어진 TRUE와 FALSE라는 싱글톤 인스턴스를 반환한다.

만약 static 필드가 미리 메모리를 점유하고 있는 게 싫다면 호출하는 시점에 만들 수 있도록 아래와 같은 구조로 만들 수도 있을 것이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
class Singleton5 {
    Singleton5() {}
 
    private static class SingletonBox {
        private static final Singleton5 INSTANCE = new Singleton5();
    }
 
    public static Singleton5 getInstance() {
        return SingletonBox.INSTANCE;
    }
}
cs

 

 

해당 코드는 아래의 싱글톤 패턴에 관련된 게시글에서 다뤘다.

 

https://7357.tistory.com/195?category=1077082 

 

디자인 패턴 (1) Singleton Pattern : 싱글톤 패턴

소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생

7357.tistory.com

 

 

 

 

세 번째,

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

 

이는 Interface가 static method를 가질 수 있게 된 JAVA8+ 부터 빛을 발하는 특성이다.

이러한 특성은 대표적으로 List 인터페이스에서 찾아볼 수 있는데, 우리가 자주 사용하는 of()가 바로 그것이다.

 

 

우리는 List.of()라는 정적 팩터리 메서드를 통해 ArrayList, LinkedList 등 다양한 하위 타입의 객체를 반환할 수 있다.

정적 팩터리 메서드의 이러한 특성 덕분에 우리는 자바 소스코드에서 45가지의 컬렉션 타입마다 각각 반환 메서드가 작성되어 있는 끔찍한 광경을 보지 않을 수 있으며, 오직 해당 인터페이스 하나만으로 api 설명서를 일일히 읽어보지 않고도 모든 하위 클래스를 다룰 수 있게 된다.

 

 

 

 

네 번째,

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

 

위에서 연결되는 특성이라고 볼 수 있는데, 어떤 하위 타입이든 반환할 수 있으니 당연히 애초부터 메서드에 어떤 경우에 어떤 하위 타입을 반환할 것인지 지정할 수 있다.

 

 

EnumSet 클래스의 경우 noneOf로 인스턴스를 생성할 때, element가 64개 이하이면 RegularEnumSet을 반환하고 초과라면 JumboEnumSet을 반환한다.

사실 EnumSet이 자주 쓰는 인터페이스는 아니라서 보다 친숙한 인터페이스를 찾아오고 싶었으나 마땅히 예시를 찾지 못했다.

추후 EnumSet, EnumMap에 대해서도 시간이 있으면(..) 한 번 정리해보고 싶다.

 

 

 

 

 

 

다섯 번째,

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

 

 

이는 사실 API보다는 SPI를 위한 기능이므로 나에게 와닿는 장점은 아니지만.. 일단 책에 적혀있으니 나름대로 알아봤다.

이를 이해하기 위해서는 Java6에서 제공되는 Service Loader에 대해 알아야 한다.

 

1
2
3
4
5
6
7
public class KingbatnunClass implements BotongInterface {
 
  public String getData() {
    return "어쩔TV";
  }
 
}
cs

 

이런 인터페이스를 만들고 META-INF 파일에 아래와 같이 저장한다.

 

example.impl.KingbatnunInterface

 

이제 해당 인터페이스에 대한 정보가 jar에 저장되었다.

serviceLoader를 사용하면 jar에서 해당 인터페이스에 대한 정보를 "Import 없이" 가져올 수 있다.

 

1
2
3
ServiceLoader<BotongService> loader = ServiceLoader.load(BotongService.class)
BotongService service = loader.next();
String tvtv = service.getData();
cs

 

이 방법은 Spring의 Annotation Processor나 Configurence 등에 사용된다고 하는데 이 부분은 내가 설명할 수 있을 정도로 이해하지 못했으므로 이 쯤에서 넘어가겠다.

 

 

 

 

 

장점은 여기까지, 별로 중요한 단점들은 아니라고 생각되지만 책에서 언급하는 단점 두 가지에 대해 알고 넘어가자.

 

첫 번째,

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

 

이렇게 크게 작성했지만, 큰 의미는 없는 단점이다.

그냥 생성자도 만들고 정적 팩터리 메서드도 제공해도 아무런 문제가 없으며, 상속 대신 하위 클래스(?) 내부에 해당 인스턴스를 멤버로 넣어도 되고.. 다양한 해결책들이 존재한다.

 

두 번째,

정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

 

마찬가지로 큰 의미는 없다.. 이는 java document를 보면 무슨 말인지 이해할 수 있는데

 

 

Java Document에서 Constructor는 항목을 따로 만들어주지만 정적 팩터리 메서드는 그런 거 없다.

당연한 일이다.. 뭘 기준으로 정적 팩터리 메서드인지 알아서 구별해주겠는가.

 

그리고 소스코드 내부에서도 생성자가 아닌 메서드가 팩토리 역할을 하고 있을 경우 직관적으로 알아보기 힘든 것은 사실이다.

 

따라서 저자는 "from, of, valueOf, instance, getInstance, create, newInstance, getType, newType, type" 등 주로 사용되는 정적 팩터리 메서드 작명 방식을 사용하는 것을 추천하고 있으며, 혹은 API 문서를 더 신경써서 작성하는 것도 하나의 방법이다.

 

음.. 적고보니 정말 단점은 뭐 없다. 저자도 장점만 나열하긴 뭐해서 언급한 수준의 단점인 것 같다.

 

이번 글은 여기서 끝.