정적 팩터리 매서드(Static Factory Method)는 Java 서적에 바이블이라고 할 수 있는 effective java를 읽은 개발자라면
생성자 대신 정적 팩토리 메서드를 고려하라 라는 첫 장에서 많이 보았을 겁니다. 정적 팩토리 메서드에 장단점에 대해서 알아봅시다.
인스턴스 전달 수단 과 정적 팩토리 메서드
클라이언트가 클래스의 인스턴스를 얻는 일반적인 수단은 Public 생성자이다.
클래스의 생성자와 별도로 static factory method를 제공할 수 있다.
static factory method는 인스턴스를 반환하는 정적 메서드를 말한다.
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
정적 팩토리 메서드의 장점
1. 이름을 가질 수 있다.
생성자에 제공하는 파라미터가 반환하는 객체를 잘 설명하지 못하는 경우에는 잘 만든 이름을 가진 static factory method를 사용하는 것이 사용하기 쉽고 가독성도 좋다.
public class Foo{
String username;
public Foo(String username){
this.username = username;
}
public static Foo withName(String username){
return new Foo(username);
}
public static void main(String[] args){
Foo foo = Foo.withName("자기개발자");
}
}
생성자는 똑같은 타입의 파라미터로 받는 생성자 두 개를 만들 수 없다.
생성자에 매개변수들의 순서를 다르게 줘서 추가하는 식으로 생성자의 시그니처 제약을 피할 수는 있지만,
각 생성자가 어떤 역할을 하는지 정확하게 파악하기 힘들며, 엉뚱한 것을 호출할 가능성이 있기 때문에
생성자를 정적 팩토리로 바꾸고 이름을 잘 지어주는 편이 좋다.
public class Foo{
String username;
String country;
public Foo(String username){
this.username = username;
}
public Foo(String country){
this.country = country;
}
//시그니처가 같기때문에 위 생성자는 사용이 불가능하다.
public static Foo withName(String username){
return new Foo(username);
}
public static Foo withCountry(String country){
Foo foo = new Foo();
foo.country = country;
return Foo;
}
public static void main(String[] args){
Foo foo = Foo.withName("자기개발자");
Foo foo = Foo.withAddress("대한민국");
}
}
2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
반드시 새로운 객체를 만들 필요가 없다.
불변 클래스인 경우나 매번 새로운 객체를 만들 필요가 없는 경우에 미리 만들어둔 인스턴스 또는
캐시 해둔 인스턴스를 반환할 수 있다.
생성 비용이 큰 상황, 객체가 자주 요청되는 상황인 경우에 성능이 매우 좋아진다.
public class Foo{
String username;
String country;
public Foo(){
}
private static final Foo Star_Foo = new Foo();
public static Foo withName(String username){
return new Foo(username);
}
public static Foo withCountry(String Country){
Foo foo = new Foo();
foo.country = country;
return Foo;
}
public static Foo(){
return Star_Foo;
}
public static void main(String[] args){
Foo foo = Foo.withName("자기개발자");
Foo foo1 = Foo.withCountry("대한민국");
Foo foo2 = Foo.getFoo(); // 객체 생성시마다 매번 새로운 객체가 나오지 않고
//동일한 Star_Foo 라는 객체가 반환됨.
}
}
불변 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.
public class Foo {
String name;
public Foo(){
}
public Foo(String name){
this.name = name;
}
private static final Foo Levelup = new Foo("자기개발자");
public static Foo getFoo(){
return Levelup;
}
public String getName(){
return this.name;
}
public static void main(String[] args){
Foo ad0 = Foo.getFoo();
Foo ad1 = Foo.getFoo();
Foo ad2 = new Foo("자기개발자");
Foo ad3 = new Foo("자기개발자");
System.out.println(ad0.name.equals(ad1.name));//true
System.out.println(ad1.name.equals(ad2.name));//true
System.out.println(ad2.name.equals(ad3.name));//true
System.out.println(ad0 == ad1);//true
System.out.println(ad1 == ad2);//false
System.out.println(ad2 == ad3);//false
}
}
- 생성자를 사용하지 않고 static final 필드에 캐시 해둔 인스턴스를 싱글턴으로 반환하기 때문에 동치인 인스턴스가 단 하나뿐임을 보장할 수 있게 된다.
3. 리턴하는 객체의 클래스가 입력 매개변수에 따라 매번 다를 수 있다.
EnumSet클래스 가 예시가 되어준다. 생성자 없이 public static 메서드 allOf(), of()등을 제공한다.
그 안에서 리턴하는 객체의 타입은 enum 타입의 개수에 따라 RegularEnumSet 또는 JumboEnumSet으로 달라짐.
객체 타입을 노출하지 않고 감춰져 있기 때문에 jdk 변화에 따라 새로운 타입을 만들거나 기존 타입을 없애도 문제가 되지 않음.
public static Foo withName(String Name){
// 리턴 타입이 Foo 라고해서 무조건 Foo를 줄필요는 없음
// 하위타입이 있으면 하위타입도 가능
...
}
-------------------------------
EnumSet<Color> colors = EnumSet.allOf(Color.class);
// 장점: 예시 Enum 안에 있는걸 Enumset안에 다 넣어줌
EnumSet<Color> blueAndWhite = EnumSet.of(BLUE,WHITE);
//장점: 예시 EnumSet<Color> interface type 이고
//실제 구현체 인스턴스는 Enum의 갯수에 따라서 달라짐
enum Color{
RED, BLUE, WHITE
}
4. 리턴하는 객체의 클래스가 public static factory method를 작성할 시점에 반드시 존재하지 않아도 된다.
public class Myfoo extends Foo{
}
//FQCN은 클래스가 속한 패키지명을 모두 포함한 이름을 말함.
public static Foo getFoo(boolean flag){
Foo foo = new Foo();
// 어떤 특정 약속되어 있는 텍스트 파일에서 Foo의 구현체에 FQCN를 읽어온다
// FQCN에 해당하는 인스턴스를 생성한다.
// foo 변수를 해당 인스턴스를 가르키도록 할 수 있다.
return foo;
}
text 파일을 myfoo에 전체 패키지 경로로 주고 위 코드를 구현한다면
getFoo가 호출되는 시점에 text 파일에 어떤 것이 적혀있는지에 따라서 다른 객체를 반환하게 된다.
jdbc도 Connection을 하고 나서 driver들이 각각 다 다르다.
다양한 패턴으로 존재하는데 브리지 패턴, DI 역시 이와 같은 서비스 제공자라고 생각할 수 있다.
정적 팩토리의 단점
1. public 또는 protected 생성자 없이 static public 메서드만 제공하는 클래스는 상속할 수 없다.
Collenctions 프레임워크에서 제공하는 편의성 구현체(java.util.Collection)는 상속할 수 없다.
오히려 불변 타입인 경우나 상속 대신 컴포지션을 권장한다.
https://colevelup.tistory.com/2
상속과 컴포지션에 대해서는 이전 포스팅에서 확인할 수 있다.
2. 프로그래머가 static factory method를 찾는 게 어렵다.
생성자는 javadoc 상단에 모아서 보여주지만 static factory 메서드는 api 문서에서 잘 다뤄주지 않는다.
클래스나 인터페이스 문서 상단에서 잘 다룰 필요가 있다.
정적 팩토리 네이밍 컨벤션 or 패턴
- . 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 : (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만,
같은 인스턴스임을 보장하지는 않는다.
ex) ****StackWalker luke = StackWalker.getInstance(options);
- .create : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
ex) ****Object newArray = Array.newInstance(classObject, arrayLen);
- .getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. “Type”은 팩토리 메서드가 반환할 객체의 타입이다.
ex) ****FileStore fs = Files.getFileStore(path)
- .newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. “Type”은 팩토리 메서드가 반환할 객체의 타입이다.
ex) BufferedReader br = Files.newBufferedReader(path);
- .type : getType과 newType의 간결한 버전.
ex) List<Complaint> litany = Collections.list(legacyLitany);
정적 팩토리 메서드와 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
무작정 생성자로 인스턴스를 제공하던 습관이었다면 고치는 계기가 되자!
'Programming Language > Java' 카테고리의 다른 글
[Java] 람다(Lambda)를 소화시켜 보자 (0) | 2023.01.08 |
---|---|
[Java] Functional Interface(함수형 인터페이스) (0) | 2023.01.08 |
[Java] Equals vs Hashcode 그리고 재정의 (0) | 2023.01.08 |
[Java] Interface(인터페이스)와 Abstract Class(추상클래스)를 비교해보자 (0) | 2022.11.19 |
[Java] 상속 vs 컴포지션 (1) | 2022.11.12 |
댓글