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

최지환·2022년 4월 17일
14

책은 읽어야 좋다

목록 보기
4/4
post-thumbnail

정적 팩토리 메서드

정적 팩토리 메서드란 무엇인가?

내가 정적 팩토리 메서드에 대해서 잘 모르는 것 같다는 생각이 들었다.
내가 알고 있던 정적 팩토리 메서드는 of, from 등 메소드 이름을 지정하고, 생성자 호출 방식이 아닌, 메서드 호출 방식으로 객체를 생성하는 것이다.
나는 그냥 사용법만 알았고, 이것을 왜? 써야하는지 그리고 어떤 단점이 있는지에 대해서는 잘 몰랐다.

그래서 이번에 정리해 보았다.


정적 팩토리 메서드(Static Factory Method)는 무엇 일까?

정적 팩토리 메서드는 객체의 생성을 담당하는 클래스 메서드 이다. 어려운 용어를 더 어렵게 설명한 느낌이다.

일반적으로 처음 자바를 공부 할 때, 객체를 생성하기 위해서는 new 키워드를 사용한다고 알고 있다. 그렇다면 메서드를 이용해서 객체를 만들 수 있을까? 라는 생각이 들 수 있다. 반은 맞고 반은 틀리다.

new 를 직접적으로 사용하지 않을 뿐, 정적 팩토리 메서드라는 클래스 내에 선언되어있는 메서드를 내부의 new를 이용해 객체를 생성해 반환하는 것이다. 즉 정적 팩토리 메소드를 통해서 new를 간접적으로 사용한다!


직접 코드로 확인

자바의 String 클래스를 이용해 정적 팩토리 메서드가 무엇인지 알아보자.

처음 자바를 배울 때 String 객체를 생성 해주려면 다음과 같이 한다고 배운다.

String str1 = new String("hello")
//사실 new String은 생략하고 String str1 = "hello"라고 해도 되지만 new를 통한 객체 생성의 
//이해를 돕기 위해 명시하겠다.

String.java의 valueOf

위 코드의 valueOf()은 파라미터로 값을 받으면, 그 값을 기반으로 String 객체로 만들어 반환해준다.

만약 “hello”라는 문자열을 갖고 있는 String 객체를 만들려면

String str = String.valueOf(”hello”)

라는 코드를 사용하면 된다.

즉 자바에서 String 객체를 만들려면 valueOf()를 사용해서 만들 수 있다. 그렇다면 두가지 방식을 모두 사용해 객체를 생성 해보자.

예제 코드 - 생성자 방식과 정적 팩토리 메서드 방식 비교

public static void main(String[] args) {
        String str1 = String.valueOf("hello valueOf");
        System.out.println(str1);

        String str2 = new String("hello new");
        System.out.println(str2 );
    }

출력 결과

두가지 경우 모두 성공적으로 String 타입 객체를 반환해 줌을 알 수 있다.

이것이 바로 오늘 알아볼 정적 팩토리 메서드 이다.


생성자와 정적 팩토리 메서드에 대해

생성자를 통해 객체를 생성하는 방식 (new 방식) 과 정적 팩토리 메서드로 객체를 만드는 방식이 하는 일은 모두 같아 보인다. 그럼 둘 중 아무거나 사용해서 객체를 만들면 되는건가?
아마 그것은 아닐 것이다.

만약 둘 중 아무거나 사용 해도 된다면, 개발자들은 정적 팩토리 메서드라는 개념을 만들지도 않았을 것이다. 분명히 정적 팩토리 메서드가 탄생한 이유가 있을 것이다.

이펙티브 자바라는 책을 읽어보면 정적 팩토리 메서드가 생성자 방식과 다른 점 5개가 있다고 한다. 나는 이 내용을 다뤄보고자 한다.
우선 장점 5가지를 미리 알려주겠다.

  1. 이름을 가질 수 있다.
  2. 호출 될 때마다 인스턴스를 새로 생성하지 않아도 된다.
  3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
  4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
  5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이게 각각의 장점이 무엇을 말하는지 코드를 통해 알아 보자


1. 이름을 가질 수 있다.

객체 지향 프로그래밍에서의 객체는 자신에게 주어진 역할과 책임이 있다. 따라서 이런 역할을 수행하기 위해 객체 생성시, 역할 수행이라는 생성 목적에 따라 생성자를 구별해서 사용하는 경우가 있다.
이때 new 를 이용해 객체를 생성 하면, 프로그래머는 해당 생성자의 내부 구조를 알고 있어야, 목적에 맞게 객체를 생성할 수 있다. 반면에 정적 팩터리 메서드를 사용하면 메서드 네이밍에 따라 반환될 객체의 특성을 묘사 할 수가 있다. 이말은 메서드 네이밍에 따라 코드의 가독성이 상승 할 수 있다는 장점이 있다.
이제 예제 코드를 통해 확인해보자.

Car 클래스 예제

public class Car {

    private final String name;
    private final int oil;

   }

위의 코드를 보자. Car 클래스는 String 타입의 name 필드와, int 타입의 oil 필드를 가지고 있다. 이 Car 클래스 틀을 가지고 각각 생성자 new 방식과 정적 팩토리 메소드 방식을 구현해보자

예제 코드 - 생성자 new 방식

public class Car {

    private final String name;
    private final int oil;

    public Car(String name, int oil) {
        this.name = name;
        this.oil = oil;
    }
    
    public Car(String name){
        this.name = name;
        this.oil = 0;
    }
}

우선 String name, int oil 을 파라미터로 받는 생성자와, String name만을 받는 생성자를 구현하였다.

첫번째 생성자 String name, int oil 방식은 생성자에서 입력받은 자동차 이름과, 기름 용량을 기반으로 자동차 객체를 생성해 준다.

두번째 생성자 String name은 생성자에서 입력받은 자동차 이름으로 기름 용량이 0인 자동차를 생성해준다.

예제 코드 - 생성자 new 방식의 main

public static void main(String[] args) {
        Car fullOilCar = new Car("fullOilCar", 10);
        Car noOilCar = new Car("noOilCar");
    }

위 코드를 살펴보자. 우리가 기름을 가진 자동차 객체를 생성 할 때, 이름과 기름값을 넘겨주면 된다. 하지만 기름이 없는 자동차를 생성 할때, new Car(”noOilCar”) 만으로는 Car 클래스 생성자 내부에서 어떤 식으로 기름을 할당하는지가 명확하지 않다.

이런 문제를 방지 하는 것이 바로 정적 팩토리 메서드 이다.

정적 팩토리 메서드를 사용하면 클래스내에 여러 생성자가 있는 경우 각 생성자를 사용 할 때 보다, 어떤 객체가 반환되는지 쉽게 유추할 수 있다.

그렇다면 정적 팩토리 메서드 방식으로 리팩터링 해보자

예제 코드 - 정적 팩토리 메서드 방식

public class Car {

    private final String name;
    private final int oil;

    public static Car createCar(String name, int oil) {
        return new Car(name, oil);
    }

    public static Car createNoOilCar(String name) {
        return new Car(name, 0);
    }

    private Car(String name, int oil) {
        this.name = name;
        this.oil = oil;
    }
}

예제 코드 - 정적 팩토리 메서드 방식의 main

public static void main(String[] args) {
        Car fullOilCar = createCar("car1", 10);
        Car noOilCar = createNoOilCar("car2");
    }

정적 팩터리 메서드 방식의 Main을 살펴보자.

아마 이전 생성자 방식에 비해 어떤 자동차 객체가 반환 되는지 유추 할 수 있음을 알 수가 있다.

메서드 네임을 살펴보면, createCar는 자동차 객체를 반환하고, creteNoOilCar는 기름이 없는 자동차 객체를 반환함을 알 수 있다.

즉 생성자 방식에 비해, 상대적으로 어떠한 객체가 반환 되는지 유추 할 수 있다는 장점이 있다.

이는 코드의 가독성을 높혀주고 생산성을 높혀준다!


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

이에 대한 예시는 enum이 대표적이다.
사용되는 값들의 개수가 정해져 있으면 해당 값을 미리 생성해놓고 조회(캐싱) 할 수 있는 구조로 만들 수 있다. 즉 정적 팩터리 메서드와 캐싱구조를 함께 사용하면 객체를 매번 새롭게 만들어 줄 필요가 없다.

코드로 확인해보자.

예제 - Day class

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 클래스 사용 main

public static void main(String[] args) {
        Day day = Day.from("mon");
        System.out.println(day.getDay());
    }

위 코드를 살펴보면 Day.from 호출 시 정적 팩터리 메서드인 from를 이용해 mon에 해당하는 Monday 를 가진 Day 객체를 반환한다.

이때 각 요일을 stacic 을 통해 미리 생성하였기에, Day.from를 사용하면 스태틱에 미리 생성된 days에 있는 Day객체을 반환 해준다.

따라서 정적 팩터리 메서드를 사용한다면 시 미리 생성된 Day 객체를 찾아 반환만 하면 된다.

이는 객체를 생성 할 때마다, 중복되는 과정을 줄일 수 있고 로직 상에서의 중복(new 로 객체를 매번 만들어 반환하는 것)을 없앨 수 있다.

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

이 장점은 상속을 사용 할 때, 확인 할 수 있다. 이때 정적 팩터리 메서드가 반환값 반환할 때, 상황에 따라 하위 클래스 타입의 객체를 반환 해줄 수 있다. 이것도 역시 코드로 확인해보자.

코드 예제 - 하위 자료형 반환

public class Grade {
...
    private static Grade of(int takenSemester) {
        if (0 < takenSemester && takenSemester <= 2) {
            return new Freshman();
        }
        if (2 < takenSemester && takenSemester <= 4) {
            return new Sophomore();
        }
        if (4 < takenSemester && takenSemester <= 6) {
            return new Junior();
        }
        if (6 < takenSemester &&<takenSemester <= 8){
            return new Senior();
        }
        ...
    }
...
}

위 코드를 확인해보자. of () 라는 정적 팩토리 메서드를 사용하여 객체 생성 시, 수강 학기에 따라 해당하는 학년 객체를 반환해준다.

이는 정적 팩터리 메서드를 이용하면 객체 생성시, 분기처리를 통해 하위 타입의 객체를 반환할 수 있다는 것을 알 수 있다.


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

입력 매개변수에 따라 매번 다른 클래스의 객체를 반환 할 수 있다는 말은, 유연성이 높아진다는 것처럼 느껴진다.

이말은 반환 타입의 하위 타입이기만 하면 어느 타입이든 객체를 반환해도 상관 없다는 말이다.

→ 만약 특정 인터페이스들을 상속 받은 구현체들이 있을 때, 객체 생성 시 상황에 따라서 유동적으로 해당하는 구현체 타입으로 반환 한다고 생각 됨.

이에 대한 예제는 따로 구현하지 않고, 자바의 EnumSet을 확인해보자

예제 - EnumSet

다음 코드는 자바의 EnumSet의 정적 팩터리 메서드이다.

코드를 확인해 보면 파라미터로 부터 받은 값의 길이에 따라 RegularEnumSet()이나 JumboEnumSet을 반환 한다.

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


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

이 장점은 서비스 제공자 프레임워크를 만드는 근간이 된다고 한다. 사실 이 부분이 제일 이해가 되지 않았다.. 이는 더 공부해보고 나중에 보충 하겠다.


정적 팩토리 메서드의 단점

그렇다면 장점만 있을까? 그렇지 않다. 정적 팩터리 메서드는 단점도 있다.

1. 상속에는 pulic 혹은 protected 생성자가 필요하므로 정적 팩토리 메서드만 제공할 경우, 상속이 불가능하다

2. 정적 팩토리 메서드를 다른 개발자들이 찾기 어렵다.

개발자가 임의로 만든 정적 팩토리 메서드 특성 상, 다른 개발자들이 사용시에 정퍽 팩토리 메서드를 찾기가 어렵다고 생각 할 수 있다.

하지만 이는 암묵적으로 사용하는 정적 팩터리 메서드 컨벤션과, API 문서 작성을 잘 하면 해결 할 수 있다고 생각한다.


정적 팩토리 메서드 명명방식

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

0개의 댓글