1. 생성자 대신 정적 팩터리 메서드를 고려하라
😉 : ‘이펙티브 자바’ 공부 기록입니다.
클래스의 인스턴스를 얻는 가장 대표적인 방법은 public 생성자이다. 하지만 클래스는 생성자와 별도로 클래스의 인스턴스를 반환하는 단순한 정적 메서드인 정적 팩터리 메서드(static factory method)를 제공할 수 있다.
정적 팩터리 메서드의 장점
이름을 가질 수 있다
생성자는 이름을 가질 수 없어서 반환될 객체의 특성을 나타낼 수 없다. 또한 하나의 시그니처로는 하나의 생성자만 만들 수 있다.
하지만 정적 팩터리 메서드는 이름을 가질 수 있어 반환될 객체의 특성을 잘 나타낼 수 있고 시그니처에 제약을 받지 않는다.
호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다
불변 클래스의 경우, 인스턴스를 미리 만들어 놓거나 생성된 인스턴스를 캐싱하여 재활용하는 식으로 매번 인스턴스를 생성하지 않을 수 있다. 이를 통해, 생성 비용이 큰 같은 객체가 자주 요청되는 상황에서 성능 향상을 이룰 수 있다.
플라이웨이트 패턴도 이와 비슷한 기법이다. (추후, 포스트로 설명 추가 예정)
정적 팩터리 메서드를 통해 인스턴스 통제(instance-controlled) 클래스가 될 수 있다.
따라서 필요에 따라 클래스를 싱글톤(Singleton) 또는 인스턴스화 불가(noninstantiable)로 만들 수 있다.
반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다
1
2
3
4
5
6
7
8
9
10
11
public interface HelloService{
String hello();
static HelloService of(String lang){
if (lang.equals("ko")){
return new KoreanHelloService();
}else{
return new EnglishHelloService();
}
}
}
정적 팩터리 메서드를 통해 반환할 객체의 클래스를 자유롭게 선택할 수 있는 유연성이 생긴다. 따라서, 구현 클래스를 공개하지 않고도 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다
반환 타입의 하위 타입인 객체이면 모두 반환할 수 있다. 클라이언트는 팩터리가 반환하는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다.
정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다
해당 특징은 서비스 제공자 프레임워크를 만드는 근간이 된다.
서비스 제공자 프레임워크 (Service provider framework)
- 서비스 인터페이스 (Service Interface)
- 구현체의 동작을 정의한다. (단순하게 구현체 interface class라고 생각해도 될 것 같다.)
- 제공자 등록 API (Provicer registraction API)
- 제공자가 구현체를 등록할 때 사용한다. ex) 스프링에서 @Config 를 사용한 Configuration class에서 @Bean을 통해 구현체를 등록하는 것
- 서비스 접근 API (Service access API)
- 클라이언트가 서비스의 인스턴스를 얻을 때 사용한다. ex) 스프링에서 ApplicationContext의 getBean을 통해 서비스의 인스턴스를 얻는 것, @Autowired를 통해 서비스의 인스턴스를 얻는 것
- 서비스 제공자 인터페이스 (Service provider interface)
- 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명한다.
- 서비스 제공자 프레임워크 패턴에는 여러 변형 방식이 있다. -> 브리지 패턴, 의존 객체 주입 등
스프링과 상관없는 예시 코드
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Service Interface
public interface HelloService {
void sayHello();
}
// 제공자 등록 API
public class HelloServiceProviderRegistry {
private static Map<String, HelloService> serviceMap = new HashMap<>();
public static void registerService(String serviceName, HelloService service) {
serviceMap.put(serviceName, service);
}
public static HelloService getService(String serviceName) {
return serviceMap.get(serviceName);
}
}
// 서비스 접근 API
public class ServiceAccessor {
public static void accessService(String serviceName) {
ServiceInterface service = ServiceProviderRegistry.getService(serviceName);
if (service != null) {
service.sayHello();
} else {
System.out.println("Service '" + serviceName + "' not found.");
}
}
}
// Service Provider Interface
public interface DatabaseConnector {
Connection connect();
}
// Service Provider Implementation
public class MySQLConnector implements DatabaseConnector {
@Override
public Connection connect() {
// MySQL 연결 코드
return null;
}
}
public class PostgreSQLConnector implements DatabaseConnector {
@Override
public Connection connect() {
// PostgreSQL 연결 코드
return null;
}
}
정적 팩터리 메서드의 단점
상속이 불가능하다
상속을 하기 위해서는 public/protected 생성자가 필요하다. 따라서 정적 팩터리 메서드만 제공하는 경우에는 상속이 불가능하다. 다만 해당 특징은 상속보다는 컴포지션을 사용하도록 유도하고 불변 타입으로 만들기 위해서는 해당 제약이 강제된다는 점에서 오히려 장점으로 보일 수 있다.
상속이 필요한 경우에는 클래스 위임(class delegation)을 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
//Printer을 상속해야하는 경우
class TextProcessor {
private Printer printer;
public TextProcessor(Printer printer) {
this.printer = printer;
}
public void processText(String text) {
// TODO: 추가적인 작업을 수행
printer.print(text);
}
}
정적 팩터리 메서드는 프로그래머가 찾기 힘들다
정적 팩터리 메서드는 생성자와 같이 자바독이 알아서 처리해주지 않아서 API 문서를 잘 써놓아야 한다.
개인적인 느낀점
실제로 나는 개발할 때 정적 팩터리 메서드를 자주 쓰는 편이다. dto를 response, request로 바꾸거나 아니면 반대로 바꾸거나 할 때, 서비스 계층에서 비즈니스 로직만 보이게 하기 위해 to, from 형태의 정적 팩터리 메서드를 자주 쓰는데, 개인적으로는 장점을 많이 체감하고 있다. 위에 나온 것과 같이 ‘이름을 가질 수 있다’가 제일 와닿는 장점이고, 그 다음으로는 ‘반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다’가 다음으로 와닿는 장점이다. 실제로 예시 코드처럼 분기를 쳐서 서로 다른 하위 타입 객체를 리턴해야 하는 종종 있다. (지금 예시는 잘 생각이….)