정적 팩토리 메서드란 무엇인가?
내가 정적 팩토리 메서드에 대해서 잘 모르는 것 같다는 생각이 들었다.
내가 알고 있던 정적 팩토리 메서드는 of, from 등 메소드 이름을 지정하고, 생성자 호출 방식이 아닌, 메서드 호출 방식으로 객체를 생성하는 것이다.
나는 그냥 사용법만 알았고, 이것을 왜? 써야하는지 그리고 어떤 단점이 있는지에 대해서는 잘 몰랐다.
그래서 이번에 정리해 보았다.
정적 팩토리 메서드는 객체의 생성을 담당하는 클래스 메서드 이다. 어려운 용어를 더 어렵게 설명한 느낌이다.
일반적으로 처음 자바를 공부 할 때, 객체를 생성하기 위해서는 new
키워드를 사용한다고 알고 있다. 그렇다면 메서드를 이용해서 객체를 만들 수 있을까? 라는 생각이 들 수 있다. 반은 맞고 반은 틀리다.
new
를 직접적으로 사용하지 않을 뿐, 정적 팩토리 메서드라는 클래스 내에 선언되어있는 메서드를 내부의 new
를 이용해 객체를 생성해 반환하는 것이다. 즉 정적 팩토리 메소드를 통해서 new
를 간접적으로 사용한다!
자바의 String 클래스를 이용해 정적 팩토리 메서드가 무엇인지 알아보자.
처음 자바를 배울 때 String 객체를 생성 해주려면 다음과 같이 한다고 배운다.
String str1 = new String("hello")
//사실 new String은 생략하고 String str1 = "hello"라고 해도 되지만 new를 통한 객체 생성의
//이해를 돕기 위해 명시하겠다.
위 코드의 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가지를 미리 알려주겠다.
- 이름을 가질 수 있다.
- 호출 될 때마다 인스턴스를 새로 생성하지 않아도 된다.
- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
이게 각각의 장점이 무엇을 말하는지 코드를 통해 알아 보자
객체 지향 프로그래밍에서의 객체는 자신에게 주어진 역할과 책임
이 있다. 따라서 이런 역할을 수행하기 위해 객체 생성시, 역할 수행이라는 생성 목적
에 따라 생성자를 구별해서 사용하는 경우가 있다.
이때 new
를 이용해 객체를 생성 하면, 프로그래머는 해당 생성자의 내부 구조를 알고 있어야, 목적에 맞게 객체를 생성할 수 있다. 반면에 정적 팩터리 메서드를 사용하면 메서드 네이밍에 따라 반환될 객체의 특성을 묘사 할 수가 있다. 이말은 메서드 네이밍에 따라 코드의 가독성이 상승 할 수 있다는 장점이 있다.
이제 예제 코드를 통해 확인해보자.
public class Car {
private final String name;
private final int oil;
}
위의 코드를 보자. Car 클래스는 String
타입의 name 필드와, int 타입의 oil 필드를 가지고 있다. 이 Car 클래스 틀을 가지고 각각 생성자 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인 자동차를 생성해준다.
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;
}
}
public static void main(String[] args) {
Car fullOilCar = createCar("car1", 10);
Car noOilCar = createNoOilCar("car2");
}
정적 팩터리 메서드 방식의 Main을 살펴보자.
아마 이전 생성자 방식에 비해 어떤 자동차 객체가 반환 되는지 유추 할 수 있음을 알 수가 있다.
메서드 네임을 살펴보면, createCar는 자동차 객체를 반환하고, creteNoOilCar는 기름이 없는 자동차 객체를 반환함을 알 수 있다.
즉 생성자 방식에 비해, 상대적으로 어떠한 객체가 반환 되는지 유추 할 수 있다는 장점이 있다.
이는 코드의 가독성을 높혀주고 생산성을 높혀준다!
이에 대한 예시는 enum이 대표적이다.
사용되는 값들의 개수가 정해져 있으면 해당 값을 미리 생성해놓고 조회(캐싱) 할 수 있는 구조로 만들 수 있다. 즉 정적 팩터리 메서드와 캐싱구조를 함께 사용하면 객체를 매번 새롭게 만들어 줄 필요가 없다.
코드로 확인해보자.
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;
}
}
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 로 객체를 매번 만들어 반환하는 것)을 없앨 수 있다.
이 장점은 상속을 사용 할 때, 확인 할 수 있다. 이때 정적 팩터리 메서드가 반환값 반환할 때, 상황에 따라 하위 클래스 타입의 객체를 반환 해줄 수 있다. 이것도 역시 코드로 확인해보자.
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 () 라는 정적 팩토리 메서드를 사용하여 객체 생성 시, 수강 학기에 따라 해당하는 학년 객체를 반환해준다.
이는 정적 팩터리 메서드를 이용하면 객체 생성시, 분기처리를 통해 하위 타입의 객체를 반환할 수 있다는 것을 알 수 있다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환 할 수 있다는 말은, 유연성이 높아진다는 것처럼 느껴진다.
이말은 반환 타입의 하위 타입이기만 하면 어느 타입이든 객체를 반환해도 상관 없다는 말이다.
→ 만약 특정 인터페이스들을 상속 받은 구현체들이 있을 때, 객체 생성 시 상황에 따라서 유동적으로 해당하는 구현체 타입으로 반환 한다고 생각 됨.
이에 대한 예제는 따로 구현하지 않고, 자바의 EnumSet을 확인해보자
다음 코드는 자바의 EnumSet의 정적 팩터리 메서드이다.
코드를 확인해 보면 파라미터로 부터 받은 값의 길이에 따라 RegularEnumSet()이나 JumboEnumSet을 반환 한다.
입력 매개변수에 따라 다른 클래스의 객체를 반환 할 수 있다.
이 장점은 서비스 제공자 프레임워크를 만드는 근간이 된다고 한다. 사실 이 부분이 제일 이해가 되지 않았다.. 이는 더 공부해보고 나중에 보충 하겠다.
그렇다면 장점만 있을까? 그렇지 않다. 정적 팩터리 메서드는 단점도 있다.
개발자가 임의로 만든 정적 팩토리 메서드 특성 상, 다른 개발자들이 사용시에 정퍽 팩토리 메서드를 찾기가 어렵다고 생각 할 수 있다.
하지만 이는 암묵적으로 사용하는 정적 팩터리 메서드 컨벤션과, API 문서 작성을 잘 하면 해결 할 수 있다고 생각한다.