반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. (인터페이스 기반 프레임워크, 인터페이스에 정적 메소드)
입력 매개변수가 따라 매번 다른 클래스의 객체를 반환할 수 있다. (EnumSet)
정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. (서비스 제공자 프레임워크)
단점
상속을 하려면 public이나 protected 생성하기 필요하니 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
유의
장점과 단점을 잘 파악하여 정적 팩토리 메소드의 사용을 "고려"하라.
장점 1. 이름을 가질 수 있다. (원하는 객체의 특징을 이름으로 더 잘 표현할 수 있다.)
Before
동일한 시그니처의 생성자를 두 개 이상 가질 수 없다.
다음와 같이 굳이 만들 수는 있겠지만, 생성자는 별도의 이름을 가질 수도 없기에 의미가 퇴색된다.
디자인 패턴인 팩토리 패턴, 추상 팩토리 패턴과 달리, 정적 팩토리 메소드는 단지 방법이다.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
public Order(Product product, boolean prime) {
this.product = product;
this.prime = true;
}
public Order(boolean urgent, Product product) {
this.product = product;
this.urgent = true;
}
After
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
public static Order primeOrder(Product product) {
Order order = new Order();
order.prime = true;
order.product = product;
return order;
}
public static Order urgentOrder(Product product) {
Order order = new Order();
order.urgent = true;
order.product = product;
return order;
}
장점 2. 호출될 때마다 인스턴트를 새로 생성하지 않아도 된다.
생성자가 있다면 어디서든 생성자를 호출하여 인스턴스를 새로 만들 수 있기 때문에 인스턴스를 한 개로 제한하는 것이 불가능하다.
생성자를 잠궈두고 정적 팩토리 메소드를 제공하는 방식으로 인스턴스를 제한할 수 있다.
생성자를 public하게 제공하는 순간부터 객체 생성에 대한 권한도 위임하는 것이다.
Boolean.valueOf()와 같이, 메소드에서 매개변수에 따라 각기 다른 인스턴스를 리턴하는 것도 정적 팩토리 메소드를 통해 가능해진다.
FlightWeight Pattern
자주 사용하는 인스턴스, 값들을 캐싱해서 넣어두고 꺼내 쓰는 패턴으로, 정적 팩토리 메소드는 인스턴스를 통제하는 방법이기 때문에 책에서도 이 패턴이 언급된다.
예시
생성자를 잠궈두고 정적 팩토리 메소드를 제공하는 방식으로 인스턴스를 제한할 수 있다.
public class Fonts {
private String name;
private Fonts() {}
private static final Fonts FONTS = new Fonts();
public static Fonts getInstance() {
return FONTS;
}
}
장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
장점 4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
장점 5. 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
생성자를 사용할 때처럼 고정적이지 않고 유연함이 생긴다.
장점 3. 은 인터페이스 기반의 프레임워크를 사용할 수 있게 해준다
클라이언트 코드로부터 인터페이스 기반의 프레임워크 사용을 강제할 수 있다.
정적 팩토리 클래스를 별도로 많이 만들지 않아도 된다. (자바 8 이후에서만 가능하다. 자바 8 이전에는 static한 메소드를 인터페이스에 둘 수 없었다.)
예시
정적 팩토리 메소드를 작성하는 시점에는 굳이 구현할 필요는 없다. (구현체 없는 상태의 HelloService)
ServiceLoader : 자바에서 지원하는 정적 팩토리 로더 (Iterator) 여러 개의 서비스가 있을 수 있기 때문이다.
지금 참조할 수 있는 클래스 path 의 helloservice 구현체를 가져온다. (여러 개 있으면 다 가져온다.)
JDBC는 ServiceLoader가 등장하기 전에 만들어져서, ServiceLoader를 사용하지는 않는다.
public interface HelloService {
String hello();
}
``` java
import me.tony.hello.KoreanHelloService;
public class HelloServiceFactory {
public static void main(String[] args) {
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println(h.hello());
});
// 동일 동작을 위해 구현체를 호출할 수도 있지만, 유연성이 그만큼 떨어지는 것이다.
HelloService helloService = new KoreanHelloService();
System.out.println(helloService.hello());
}
<br>
## 단점 1. 상속을 하려면 public이나 protected 생성하기 필요하기 때문에 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
## 단점 2. 정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
- 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 의미이다.
- Javadoc : Constructor는 별도의 컬럼이 있어서 눈에 쉽게 띈다. 정적 팩토리 메소드도 물론 Method의 컬럼에서 확인할 수 있지만, 메소드가 많아지면 많아질수록 가시성이 떨어지는데, 이 또한 단점이다.
* 그마나 네이밍으로 (getInstance, newInstance, of, valueOf 등) 분류할 수 있기는 하다.
* Javadoc 으로 잘 문서화 해둔다면 그래도 이 단점을 어느정도 극복할 수 있다.
<br>
## 아이템 1. 생성자 대신 정적 팩토리 메소드를 고려하라.
## 완전 공략
- **열거 타입**은 인스턴트가 하나만 만들어짐을 보장한다.
- 같은 객체가 자주 요청되는 상황이라면 **플라이웨이트 패턴**을 사용할 수 있다.
- 자바 8부터는 **인터페이스가 정적 메소드**를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 필요가 없다.
- **서비스 제공자 프레임워크**를 만드는 근간이 된다.
- **서비스 제공자 인터페이스**가 없다면 각 구현체를 인스턴스로 만들 때 리플렉션을 사용해야 한다.
- 브리지 패턴
- 의존 객체 주입 프레임워크 (E.g. Spring framework)
<br>
## 완전 공략 1. 열거 타입 (Enumeration)
- 상수 목록을 담을 수 있는 데이터 타입.
- 특정한 변수가 가질 수 있는 값을 제한할 수 있다. **타입-세이프티 (Type-Safety)**를 보장할 수 있다.
* Enum으로 표현할 값을 String, char, int 등으로 표현했을 경우, 의도하거나 정의하지 않은 값이 주입될 가능성이 있다.
* 이를 방지하거나 검증하기 위한 코드를 추가적으로 작성해야 한다.
* Enum 값들은 JVM 내에서 오로지 딱 1 개만 만들어진다.
* (Enum을 사용하는 방법이 싱글톤 패턴을 안전하게 구현할 수 있는 방법 중 하나인 이유이다.)
- 질문1) 특정 enum 타입이 가질 수 있는 모든 값을 순회하며 출력하라.
- 질문2) enum은 자바의 클래스처럼 생성자, 메소드, 필드를 가질 수 있는가?
- 질문3) enum의 값은 == 연산자로 동일성을 비교할 수 있는가?
- 과제) enum을 key로 사용하는 Map을 정의하세요. 또는 enum을 담고 있는 Set을 만들어 보세요.
<br>
- 답1) Enum.values() 에 Enum의 값들이 다 들어있다.
* (Arrays.stream(OrderStatus.values()).forEach(System.out::println);
- 답2) 된다.
- 답3) 된다. ('만약 00 상태라면 00 해라'의 코드에서 .equals를 쓰는 것보다 낫다. (null safe하기 때문이다.)
- 과제 답) EnumMap, EnumSet을 사용하는 것이 다른 Map, Set을 사용하는 것보다 훨씬 빠르다.
* EnumMap, EnumSet도 Java Collections Framework의 멤버이다.
* Hash는 물론, 다른 어떤 방식보다도 검색에 특화되어 있지만 해싱이라는 과정이 필요하기 마련이다.
* 그러나 enum은 단일 객체를 보장하기 때문에 이러한 과정이 필요하지 않다.
* key 값으로 null을 받지 않고 NullPointerException을 던진다.
* EnumMap은 (Enum에 명시되어 있는) 순서 또한 보장된다. (탐색도 hashmap보다 더 빠르고 treemap의 장점도 가졌다.)
<br>
## 완전 공략 2. 플라이웨이트 패턴
- 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴이다.
* '같은 객체가 자주 사용된다' - 매번 새로운 객체를 만드는 것이 아니라 같은 객체를 캐싱해두었다가 사용하는게 좋지 않을까.
- 자주 변하는 속성(또는 외적인 속성, extrinsit)과 변하지 않는 속성(또는 내적인 속 성, intrinsit)을 분리하고 재사용하여 메모리 사용을 줄일 수 있다.
<br>
## 완전 공략 3. 인터페이스와 정적 메소드 (자바8과 9에서 주요 인터페이스 변화)
- 기본 메소드 (default method)와 정적 메소드를 가질 수 있다.
- 기본 메소드
* 인터페이스에서 메소드 선언 뿐 아니라, 기본적인 구현체까지 제공할 수 있다.
* 기존의 인터페이스를 구현하는 클래스에 새로운 기능을 추가할 수 있다.
- 정적 메소드
* 자바 9부터 private static 메소드도 가질 수 있다. (내부의 다른 public static한 메소드들에서 사용할)
* 단, private 필드는 아직도 선언할 수 없다.
- 질문1) 내림차순으로 정렬하는 Comparator를 만들고 List<Integer>를 정렬하라.
- 질문2) 질문1에서 만든 Comparator를 사용해서 오름차순으로 정렬하라.
<br>
- 답1) `Comparator<Integer> descendingOrder = (o1, o2) -> o2 - o1;`
- 답2) `descendingOrder.reversed()`
* Comparator의 default 메소드로 추가되었다.
<br>
## 완전 공략 4. 서비스 제공자 프레임워크 (확장 가능한 애플리케이션을 만드는 방법)
- 구현 형태보다 개념(목적)이 중요하다.
* 개념(목적) - 확장 가능한 애플리케이션을 제공하는 것
* 확장 가능한 애플리케이션 - 코드를 변경하지 않고 외적인 것을 변경했을 때 애플리케이션이 다르게 동작할 수 있게 하는 것.
- 주요 구성 요소
* 서비스 제공자 인터페이스(SPI)와 서비스 제공자(서비스 구현체)
* 인터페이스와 구현체가 따로 있어도 된다.
* 서비스 제공자 등록 API(서비스 인터페이스의 구현체를 등록하는 방법)
* `META-INF.services` 아래의 항목
* 서비스 접근 API(서비스의 클라이언트가 서비스 인터페이스의 인스턴스를 가져올 때 사용하는 API)
* `HelloService helloservice = applicationContext.getBean(HelloService.class);`
* `ServiceLoader.load()` - 스프링을 쓰지 않는다면 좋은 대안이다.
- 다양한 변형
* 브릿지 패턴
* 목적 - 구체적인 것과 추상적인 것을 분리. (그 사이에 다리를 두는 형식의 패턴)
* 서로 영향은 주지 않으면서 각각이 계층 구조로 발전할 수 있게 해준다.
* e.g. 챔피온 - 스킨 (브릿지 패턴을 사용하지 않으면, 스킨이 하나 생기면 그에 상응하는 챔피온이 그 만큼 늘어나게 된다.)
* PSA(Portable Service Abstraction) 까지도 연결이 된다.
* 의존 객체 주입 프레임워크
* java.util.ServiceLoader
* https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
* https://docs.oracle.com/javase/tutorial/ext/basics/spi.html
<br>
## 완전 공략 5. 리플렉션 (Reflection)
- 클래스로더를 통해 읽어온 클래스 정보(=거울에 반사된 정보)를 사용하는 기술
- 리플렉션을 사용해 클래스를 읽어오거나, 인스턴스를 만들거나, 메소드를 실행하거나, 필드의 값을 가져오거나 변경하는 것이 가능하다.
- 언제 사용할까?
* 특정 애노테이션이 붙어있는 필드 또는 메소드 읽어오기 (JUnit, Spring)
* 특정 이름 패턴에 해당하는 메소드 목록 가져와 호출하기 (getter, setter)
* Class.forName(), getConstructor(), newInstance(), getDeclaredMethod(), getDeclaredField() 등으로 참조 및 변경할 수 있다. (private 하더라도 가능하다.)
* ...
- https://docs.oracle.com/javase/tutorial/reflect/
아이템 1. 생성자 대신 정적 팩토리 메소드를 고려하라.
핵심 정리
장점 1. 이름을 가질 수 있다. (원하는 객체의 특징을 이름으로 더 잘 표현할 수 있다.)
Before
디자인 패턴인 팩토리 패턴, 추상 팩토리 패턴과 달리, 정적 팩토리 메소드는 단지 방법이다.
After
장점 2. 호출될 때마다 인스턴트를 새로 생성하지 않아도 된다.
Boolean.valueOf()
와 같이, 메소드에서 매개변수에 따라 각기 다른 인스턴스를 리턴하는 것도 정적 팩토리 메소드를 통해 가능해진다.예시
생성자를 잠궈두고 정적 팩토리 메소드를 제공하는 방식으로 인스턴스를 제한할 수 있다.
장점 3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
장점 4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
장점 5. 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
예시
ServiceLoader : 자바에서 지원하는 정적 팩토리 로더 (Iterator) 여러 개의 서비스가 있을 수 있기 때문이다.
String hello(); }
public class HelloServiceFactory {
}