[Effective Java] 생성자 대신 팩토리 메서드 사용하기

8 분 소요

정적 팩토리 메서드란?

우리가 새로운 인스턴스를 생성할 때, 어떤 방식을 사용하나요?

보통 new를 통해 생성자를 사용하여 새 인스턴스를 만듭니다. 하지만 이러한 방식은 여러 단점들을 포함하고 있는데요. 이러한 단점들을 극복하고 더 가독성 있는 코드를 위해 정적 팩토리 메서드를 사용하는 것이 더 좋습니다.

정적 팩토리 메서드는 말그대로 Static Factory Method를 말합니다. 이 함수는 어떠한 비지니스 로직을 포함하지 않고 오직 클래스의 인스턴스만을 반환하는 함수입니다.

먼저 예로, String 객체를 생성한다고 해봅시다.

1
2
3
4
5
6
public static void main(String[] args) {
    String str1 = new String("test");
    String str2 = String.valueOf("test");

    System.out.println(str1 + " " + str2);
}

위 코드를 보면, 똑같은 test라는 값을 가진 String 객체를 만들었음에도, 다른 방식을 사용했습니다. str1의 경우가 우리에게 익숙한 생성자를 이용하여 인스턴스를 만든 방식이고, str2의 경우가 바로 이번 게시물에서 공부하게 될 정적 패토리 메서드를 이용해서 만든 인스턴스 입니다.

이 두가지는 어떤 차이가 있을까요?

valuOf 라는 메서드를 타고들어가서 한번 어떻게 선언 되어 있는지 확인 해봅시다.

1
2
3
public static String valueOf(char data[]) {
    return new String(data);
}

위 코드를 보면, 해당 메서드는 static으로 선언되어 있으며 new String() 을 통해 우리가 넣어준 값을 매개변수로 String 객체를 생성해주고 있습니다.

사실상 생성자 방식과 크게 다른점이 없는 것이죠.

이렇게 하면 어떤 장점들이 생기길래, 생성자보다 정적 팩터리 메서드를 사용하는 것을 권장하는 걸까요?

1. 이름을 가질 수 있다

프로그래밍에서 변수명과 함수명을 잘 짓는 것은 매우 중요합니다.

함수 이름만으로도 이 함수가 어떤일을 하는 함수인지 알 수 있다면, 그 코드가 본인이 작성한 코드가 아니더라도 알아보기가 쉬울 것 입니다.

그래서 정적 팩토리 메서드를 사용해 생성자에 이름을 붙여줄 수 있는 것은 별것 아닌 것 처럼 보이지만 가독성에 있어 큰 장점이 됩니다.

예를 들어 봅시다.

카드라는 클래스를 만들어 본다고 가정해봅시다.

카드에는 신용카드와 체크카드가 있고, 신용카드의 경우 계좌에 있는 돈을 다써도 limit 만큼의 돈을 신용으로 더 쓸 수 있으며, 체크카드는 limit이 자동으로 0이되어 신용을 사용할 수 없게 한다고 해보겠습니다.

1
2
3
4
5
6
7
8
9
class Card {
    int money;
    int limit;

    public Card(int money, int limit) {
        this.money = money;
        this.limit = limit;
    }
}

대략 이렇게 코드를 작성해볼 수 있겠죠?

그럼 한번 신용카드와 체크카드 객체를 만들어 봅시다.

각각 5000원씩의 돈이 들어있고, 신용카드의 경우는 5000의 limit이 더 있어 현금외에도 5000의 돈을 신용으로 더 쓸 수 있다고 해봅시다.

1
2
Card creditCard = new Card(5000, 5000);
Card checkCard = new Card(5000, 0);

이렇게 코딩하면 우리가 원하는 기능을 구현해볼 수 있겠네요. 하지만 이렇게 하면, 체크카드의 경우는 무조건 limit이 0이 되어야 하는데, 누군가는 실수로 체크카드임에도 limit을 0으로 설정해놓지 않을 수 있습니다.

또, 이름이 현재는 creditCard와 checkCard로 되어있지만, 만약 카드의 이름이 minjaeCard라면, 생성자만 보고서는 이 카드가 체크카드인지, 신용카드인지 판별하기 어려울 것 입니다.

(물론 이 경우에는 Card를 상속받는 ChcekCard와 CreditCard라는 클래스를 만들어주는게 더 효율적인 방법일 것 같지만, 예시를 들기 위해서 이렇게 한다고 가정해보겠습니다.)

그럼 정적 팩터리 메서드를 사용하면 어떻게 될까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Card {
    int money;
    int limit;

    public static Card createCheckCard(int money) {
        return new Card(money, 0);
    }

    public static Card createCreditCard(int money, int limit) {
        return new Card(money, limit);
    }

    public Card(int money, int limit) {
        this.money = money;
        this.limit = limit;
    }
}
1
2
Card creditCard = Card.createCreditCard(5000, 5000);
Card checkCard = Card.createCheckCard(5000);

이렇게 이번에는 정적 패터리 메서드를 사용해서 객체를 생성하게 해보았습니다.

어떤가요? 장점이 느껴지시나요?

우선 createCreditCard라는 메서드를 사용해서, 우리가 만들 객체가 “신용카드”임을 헷갈리지 않게 되었으며, 체크카드의 경우는 매개변수에 0이라고 해줄 필요없이 createCheckCard를 사용하면 알아서 매개변수를 money만 받고 limit은 0으로 설정하도록 도와줍니다.

이름이 생기니 한결 가독성이 나아지고, 어떤 객체가 반환되는지를 더 직관적으로 알 수 있게 되어 실수를 더 줄일 수 있게 되었습니다.

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

객체를 생성하는 것은 꽤나 비용이 드는 일입니다.

객체가 많아질 수록 힙영역에 공간이 빠르게 차고, 이 공간이 빠르게 차면 찰수록 GC도 자주 일어나 성능이 저하될 우려가 큽니다. 그렇기 때문에 같은 객체라면 굳이 여러개 생성하지 말고, 이미 생성되었던 객체를 반환해주는게 더 효과적일 것입니다.

이것을 디자인패턴에서는 싱글톤 (Singleton) 패턴이라고도 합니다.

대표적인 예로 Boolean.valueOf 메서드가 있습니다.

1
2
3
4
5
6
7
8
public final class Boolean implements java.io.Serializable, Comparable<Boolean> {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);
    
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
}

위의 코드를 보면 마찬가지로 valueOf로 값을 받아 객체를 생성해주는 정적 팩토리 메서드인데요, 위에서 본 String.valueOf와는 다른 점이 있습니다.

미리 final로 상수 객체인 TRUE와 FALSE를 만들어 놓은 뒤, 메서드를 실행할때마다 새로운 객체를 생성해주는 것이 아니라, static으로 선언하여 애플리케이션 실행 시에 미리 만들어진 객체를 반환해줍니다.

이렇게 하면 기존 방식보다 객체를 훨씬 적게 만들 수 있을것 입니다.

이 방식의 예제를 하나 더 보도록 하겠습니다. 이런 방식으로도 사용 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Day {
    private static final Map<String, Day> days = new HashMap<>();

    static {
        days.put("mon", new Day("Monday"));
        days.put("tue", new Day("Tuesday"));
        days.put("wen", new Day("Wednesday"));
        days.put("thu", new Day("Thursday"));
        days.put("fri", new Day("Friday"));
        days.put("sat", new Day("Saturday"));
        days.put("sun", new Day("Sunday"));
    }

    public static Day from(String day) {
        return days.get(day);
    }

    private final String day;

    private Day(String day) {
        this.day = day;
    }

    public String getDay() {
        return day;
    }
}

위의 예제를 보면 어차피 요일은 월요일부터 일요일까지 밖에 없음으로, 미리 월요일부터 일요일까지의 Day 객체를 생성해둔 뒤, 이를 해쉬맵에 넣어 놨다가 해당 정적 팩토리 메서드를 호출하면 새로운 객체를 생성하는 것이 아니라, 해당 요일의 값을 String으로 받아 해당 객체를 반환해주는 방식입니다.

이렇게 하면 객체를 재사용해서 효율적일 뿐만아니라, 실수로 이 7가지 요일 외의 다른 값이 들어오면 컴파일전에 미리 잡아 주어서 더 안전할 것입니다.

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

정적 팩토리 메서드를 사용하면, 하위 자료형 객체를 반환할 수 있습니다.

아래 코드를 보도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Card {
    public static Card from(int expenditure) {
        if (0 < expenditure && expenditure <= 1000) {
            return new Silver();
        }
        if (1000 < expenditure && expenditure <= 2000) {
            return new Gold();
        }
        if (2000 < expenditure && expenditure <= 3000) {
            return new Platinum();
        }
        if (3000 < expenditure && expenditure <= 4000){
            return new Diamond();
        }
        throw new IllegalArgumentException();
    }
}
class Silver extends Card {
}

class Gold extends Card {
}

class Platinum extends Card {
}

class Diamond extends Card {
}

위 코드는 아까 들었던 예시인 카드의 클래스 인데요, 이번에는 Card가 등급이 있다고 가정해 봅시다.

지출액에 따라, 1000 이하면 실버등급, 1000에서 2000사이면 골드 등급, 2000에서 3000사이면 플래티넘, 3000에서 4000사이면 다이아몬드라고 해보도록 하죠. 혹여나 이상한 음수나 조건에 맞지않는 값이 들어오면 예외를 출력하도록 하겠습니다.

정적 팩토리 메서드를 사용하면, Card하나에서 이렇게 하위 객체를 반환하게 할 수 있습니다.

1
Card creditCard = Card.from(2500);

이렇게 하면, 코드를 작성하는 사람이 카드가 어떤 하위 객체가 있는지, 금액에 따라 어떤 하위 객체로 만들어야 할 지 알 필요없이, 지출액만 입력하면 알아서 등급에 맞는 하위 객체 자료형으로 객체가 생성됩니다.

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

아까 체크카드와 신용카드의 예시를 다시 가져와 설명하도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Card {
    int money;
    int limit;

    public static Card createCheckCard(int money) {
        return new Card(money, 0);
    }

    public static Card createCreditCard(int money, int limit) {
        return new Card(money, limit);
    }

    public Card(int money, int limit) {
        this.money = money;
        this.limit = limit;
    }
}

아까 코드에서는 createCheckCard와 createCreditCard를 사용해서 객체를 생성했습니다.

이 두개의 정적 팩토리 메서드를, 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다는 정적 팩토리 메서드의 장점을 살려서 하나의 메서드로 통합해주도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Card {
    int money;
    int limit;

    public static Card of(int money) {
        return new CheckCard(money, 0);
    }

    public static Card of(int money, int limit) {
        return new CreditCard(money, limit);
    }

    public Card(int money, int limit) {
        this.money = money;
        this.limit = limit;
    }


}

class CreditCard extends Card {
    public CreditCard(int money, int limit) {
        super(money, limit);
    }
}

class CheckCard extends Card {
    public CheckCard(int money, int limit) {
        super(money, limit);
    }
}

이렇게 하면 어떤 결과가 나올거라고 예상되시나요?

전에는 createCheckCard로 체크카드를 발급하고, createCreditCard로 신용카드를 발급했다면, 이제는 of 메서드 하나만으로 money와 limit을 모두 기입해주면 신용카드, money만 입력해주면 체크카드, 이런식으로 매개변수의 개수에 따라 다른 객체를 반환하도록 합니다.

이렇게 정적 팩토리 메서드를 사용하면, 매개변수 갯수에 따라 다른 객체를 반환하게 할 수 있습니다.

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

아마 이 부분이 가장 이해하기 어려운 부분이라고 생각합니다.

인터페이스만 존재하는 객체의 구현체가 없어도 정적 팩토리 메소드를 사용하면 그 구현체를 사용할 수 있다는 뜻인데요.

이 부분은 글로 설명하는 것보다 아래 영상을 보시는 것이 빠를 것 같아서 링크 첨부합니다.

백기선 유튜브 - 자바, 생성자 대신 정적팩토리의 장점 마무리

아마 스프링과 같은 프레임워크의 구조에 대해 공부를 해보신 분들은 이 기능이 왜 프레임워크의 근간이 되는 기능인지 이해가 되실 것 같습니다.

간단하게 설명하자면 결국은 구현체에 대한 의존성없이 인턴페이스 기반으로 코딩할 수 있도록 도와주는 역할을 한다는 것입니다.

정적 팩토리 메서드의 단점

모든 것이 그렇듯 장점만 있는 방식은 없을 것입니다.

정적 팩토리 메서드도 마찬가지로 단점들을 갖고 있는데요. 다음과 같습니다.

  1. 상속에는 pulic 혹은 protected 생성자가 필요하므로 정적 팩토리 메서드만 제공할 경우, 상속이 불가능하다
  2. 정적 팩토리 메서드를 다른 개발자들이 찾기 어렵다.
    개발자가 임의로 만든 정적 팩토리 메서드 특성 상, 다른 개발자들이 사용시에 정퍽 팩토리 메서드를 찾기가 어렵다고 생각 할 수 있다.

따라서 이러한 단점을 해결하기 위해서 보편적으로 사용하는 네이밍 컨벤션을 지켜주어 다른 개발자가 편하게 사용할 수 있도록 해주어야 합니다.

정적 팩토리 메서드의 네이밍 컨벤션은 다음과 같습니다.

  • from : 하나의 매개변수를 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • valueOf : from과 Of의 더 자세한 버전
  • instance 혹은 getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장 하지 않음
  • create 혹은 newInstance :instance 혹은 getInstance 와 같으나 매번 새로운 인스턴스를 생성해 반환 함을 보장.

위에서도 from, of, valueOf, create등을 제가 이미 사용하고 있었습니다.

getInstance()와 newInstance()의 차이

여기서 getInstance와 newInstance의 설명이 조금 헷갈리신다면, 간단하게 getInstance()의 경우는 아까 배웠던 요일의 예시처럼 이름만 같다면 동일한 객체를 반환해주는 싱글톤객체를 반환하는 메서드일때는 getInstance()로 네이밍을 해주는 것이 좋고, 매번 새로운 객체를 반환하는 메서드일때는 newInstance()으로 이름을 지어주는것이 바람직하다는 것을 뜻합니다.

이렇게 이펙티브 자바의 첫번째 아이템인 생성자 대신 정적 팩토리 메서드를 고려하라의 이유를 자세하게 알아보았습니다.

댓글남기기