클래스의 인스턴스를 얻는 가장 기본적이고 쉬운 방법은 Pulic 생성자다.
class Study {
private Long id;
private String name;
private int limit;
private StudyStatus studyStatus;
public Study() {}
public Study(String name, int limit){
this.name = name;
this.limit = limit;
}
}
→ 정통적인 Public 생성자 사용법
그런데, 이런 public 생성자만 사용해서 인스턴스를 생성할 때 불편함을 느낀적은 없을까?
위 예제에서는 Study의 이름과 수용인원만 있기에 생성자도 두 개의 파라미터만 넣으면 되고 두 파라미터는 타입도 다르기에 알아보기도 어렵지 않다.
하지만, 필드값이 3~4개가 넘어가고 변수 타입이 동일한것들도 많아진다면 어떨까?
해당 클래스의 인스턴스를 생성하는것의 난이도는 확 올라간다.
그렇기에 우리는 정적 팩토리 메소드(static factory method)를 만들어 사용할 수 있다.
(디자인 패턴의 팩토리 메소드 패턴(Factory Method)와는 다르다.)
클래스의 인스턴스를 반환하는 단순한 정적 메서드인 이 정적 팩토리 메소드는 이미 많이 쓰이고 있는데, 대표적으로 기본형 변수의 래퍼클래스(Wrapper Class)을 보면 알 수 있다.
다음은 boolean 타입 의 래퍼 클래스인 Boolean의 valueOf 정적 팩토리 메서드이다.
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
→ valueOf 메서드는 boolean 기본형 변수를 Boolean 객체 참조로 변환해서 반환한다.
변수가 많은 클래스의 public 생성자의 가장 큰 문제중 하나는 내가 입력할 파라미터를 하나하나
구분하는게 쉽지 않을 뿐더러 파라미터와 생성자 만으로는 내가 반환받을 객체의 특성을 한번에 이해하기 어렵다.
예를들어 Study 클래스의 생성자 Study(String, int, StudyStatus) 를 보고 단 번에 어떤 특성의 Study 인스턴스인지 알 수 있을까?
반면 정적 팩토리 메소드를 사용하면 Study.newStudy를 사용한다면 과연 전자와 후자중 어느것이 '새로운 스터디' 를 생성한다는 점을 알기 쉬울까?
내가 전달 할 파라미터의 타입, 갯수에 따라서 public 생성자를 여러개 만들어서 다양한 매개변수에 대응할 수 있다. 하지만, 모두 결국 public 생성자이고 매개변수의 종류나 타입만 달라지기에 가독성이 떨어지는건 동일하다.
아니 오히려 생성자의 종류가 많아짐에따라 임의의 생황에서 어느 생성자를 호출해야 할지 혼동하기 쉽다.
반면, 이름을 가질 수 있는 정적 팩토리 메서드에는 이런 문제가 발생하지 않는다. 한 클래스에서 시그니처는 같지만 기대되는 특성이 다른 인스턴스가 필요하다면 생성자를 정적 팩토리 메소드로 바꾸고 네이밍을 통해 그러한 특성의 차이를 드러내는게 좋다.
public static Study newStudy(String name, int limit) {
return new Study(name, limit, DRAFT);
}
public static Study endedStudy(String name, int limit) {
return new Study(name, limit, ENDED);
}
→ 동일한 시그니처이지만 메소드명을 통해 반환될 스터디의 특성을 추측할 수 있다.
정적 팩토리 메소드를 사용하면 매번 인스턴스를 새로 생성하지 않고 기존에 만들어두거나
생성한 인스턴스를 캐싱해서 재활용 함으로써 불필요한 객체 생성을 피할수 있다.
위에서 설명한 Boolean.valueOf(boolean)이 이런 장점을 사용한 대표적인 예이다.
특히나, 규모가 커서 생성 비용이 큰 객체의 경우 요청될 때마다 생성하게되면 비용소모가 상당히 커짐으로써 성능이 떨어질 수밖에 없는데, 정적 팩토리 메소드를 사용해 이런 성능저하를 막을 수 있다.
디자인 패턴의 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷하다고 할 수 있다.
이처럼 같은 요청에는 같은 인스턴스를 반환하는 방식으로 인스턴스의 라이프 사이클을 통제할 수 있는데,
이처럼 같은 요청에는 같은 인스턴스를 반환하는 방식으로 인스턴스의 라이프 사이클을 통제할 수 있는데, 이러한 클래스를 인스턴스 통제(instance-controlled) 클래스라 부른다.
이렇게 인스턴스를 통제하면 싱글톤 패턴을 적용할수도 있고, 인스턴스화 불가(noninstantiable)로 만들수도 있다.
이와같이 인스턴스를 통제해서 동일한 값에 동일한 인스턴스(a==b && a.equals(b))는 플라이웨이트 패턴의 핵심이고 열거형(enum)은 인스턴스가 하나만 만들어짐을 보장한다.
간단한 플라이웨이트 패턴을 사용해 정적 팩토리 메소드를 사용해보자.
package me.catsbi.effectivejavastudy.item1;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class StudyFactory {
private static final Map<String, Study> store = new HashMap<>();
private static StudyFactory instance = new StudyFactory();
private StudyFactory() { }
public static StudyFactory getInstance() {
if (Objects.isNull(instance)) {
instance = new StudyFactory();
}
return instance;
}
public synchronized Study getStudy(String name, int limit) {
Study study = store.get(name);
if (Objects.isNull(study)) {
study = Study.newStudy(name, limit);
store.put(name, study);
}
return study;
}
}
Arrays 유틸 클래스에서 asList를 통해 List Collection을 만들어 사용해본적이 있다면, 이 세 번째 장점을 이해할 수 있다. 반환객체의 클래스를 자유롭게 선택할 수 있다는 유연성은 내가 구현체를 공개하지 않고 구현체를 반환할 수 있기에 API를 작게 유지할 수 있다.
이 장점이 인터페이스 기반 프레임워크의 핵심 기술이기도 하다.
예를들어 자바에서 제공하는 유틸 클래스중 java.util.Collections에서는 수정 불가, 동기화 기능 등이 들어간 컬렉션 구현체를 제공해주는데 모두 정적 팩토리 메서드를 통해 얻도록 한다.
이때, 이 컬렉션 프레임워크는 구현체를 따로 공개하지 않기에 API의 외견에서 구현체가지 신경쓸 필요가 없어 컴팩트한 개발이 가능해진다. 그렇기에 우리는 인터페이스에 정의된 메서드만 인지하면 되기에 사용법 학습에 대한 비용도 최소한으로 사용이 가능해진다.
다음은 java.util.Arrays 유틸클래스의 정적 팩토리 메서드인 asList()이다.
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
→ List의 하위 구현체인 ArrayList로 값을 래핑해 반환한다. 하지만 사용자는 이러한 구현체까지 알 필요가 없다.
Java 8 부터는 인터페이스에서도 static 키워드를 통해 정적 메서드를 가질 수 있다. 그렇기에 public 정적 멤버들도 공통된 부분들은 인터페이스에 위치시켜도 상관이 없어졌다. 다만 private 정적 메서드는 아직 java 9 이상에서만 허락되기에 정적 필드와 정적 멤버 클래스는 여전히 public이어야 한다.
단순히 하위타입을 반환한다는 점을 넘어 파라미터의 상태(값, 크기등) 에 따라 다른 하위타입을 반환 할 수도 있다.
예를 들어, 강의를 위해 강의실 객체 인스턴스를 만들어야 하는 상황에서 항상 같은 인원을 수용하는 동일한 강의실이 아니라 인원수에 따라 다른 타입의 강의실(small, medium, big)을 반환받고 싶을때, 정적 팩토리 메소드를 사용하면 호출 할때 전달하는 수강인원 파라미터로 매번 적절한 구현체를 생성해 반환해줄 수 있다.
또한, 호출하는 입장에서는 그런 내부 구현체까진 알 필요도 없기에 의존관계가 생기지 않는다.
import java.rmi.NoSuchObjectException;
import java.util.Objects;
public class ClassRoomFactory {
public static final String NOT_FOUND_CLASS_ROOM_FROM_LIMIT_COUNT = "Not Found ClassRoom from limitCount:";
private static ClassRoomFactory instance = new ClassRoomFactory();
private ClassRoomFactory() {
}
public static ClassRoomFactory getInstance() {
if (Objects.isNull(instance)) {
instance = new ClassRoomFactory();
}
return instance;
}
public static ClassRoom getClassRoom(int limitCount) throws NoSuchObjectException {
if (SmallClassRoom.supported(limitCount)) {
return new SmallClassRoom();
}
if (MediumClassRoom.supported(limitCount)) {
return new MediumClassRoom();
}
if (BigClassRoom.supported(limitCount)) {
return new BigClassRoom();
}
throw new NoSuchObjectException(NOT_FOUND_CLASS_ROOM_FROM_LIMIT_COUNT + limitCount);
}
}
→ 수강인원에 따라 Small, Medium, Big 강의실중 적절한 강의실 인스턴스가 생성되어 반환된다.
위에서 작성한 코드들에서는 다 이미 구현되어있는 구현체를 기준으로 유연함을 제공해줬다.
하지만, 이를 넘어서 정적 팩토리 메서드를 작성하는 시점에 구현되있지 않은 객체의 클래스를 반환할수도 있다.
이 부분이 서비스 제공자 프레임워크(Service Provider Framework)의 근간이 되는 개념으로 제공자(provider)가 서비스의 구현체이다.
그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제해서 클라이언트를 구현체로부터 분리해준다. (DIP)
서비스 제공자 프레임워크는 다음 3개의 핵심 컴포넌트로 이뤄진다.
클라이언트는 서비스 접근 API를 이용해서 원하는 구현체를 가져올 수 있는데, 조건을 명시하지 않을 경우 기본 구현체 혹은 지원하는 구현체들을 돌아가며 반환한다.
이러한 서비스 접근 API가 서비스 제공자 프레임워크의 근간인 유여한 정적 팩토리 메소드의 실체다.
그 밖에 서비스 제공자 인터페이스(Service Provider Interface)라는 컴포넌트가 쓰이기도하는데 이는 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다. 이러한 서비스 제공자 인터페이스가 없다면
리플렉션을 이용해서 구현체를 인스턴스화 해준다.
구현 예 : JDBC
⇒ Connection: 서비스 인터페이스 역할
⇒ DriverManager.registerDriver: 제공자 등록 API 역할
⇒ DriverManager.getConnection : 서비스 접근 API 역할
⇒ Driver: 서비스 제공자 인터페이스 역할
단점
첫 번째, 상속을 할 땐 public or protected 생성자가 필요하기에 정적 팩토리 메서드만 사용하면 하위 클래스를 만들 수 없다.
컬렉션 프레임워크의 유틸리티 구현 클래스들을 상속할 수 없다.
(interface에 정의한 정적 메소드를 상속해서 오버라이딩 할 수 없다는 의미)
이러한 제약은 상속보다는 컴포지션(위임)을 유도하고 불변 타입으로 만들기7 위해서 이 제약을 지켜야한다는 점에서 오히려 장점이 될 수도 있다.
두 번째, 정적 팩토리 메소드는 프로그래머가 찾기 힘들다.
public 생성자는 API 설명에도 나와있기 때문에 사용자가 쓰기가 명확하지만, 정적 팩토리 메서드는 인스턴스화 하기위한 방법을 직접 찾아야한다.
그러기위해 API 문서 작성을 잘 작성해놓고, 정적 팩토리 메소드명을 관례를 최대한 따라서 짓는 방식으로 사용자가 찾기 쉽도록 해야 한다. 다음은 정적 패토리 메소드에서 사용하는 명명 방식이다.
from: 매개변수를 하나를 받아 해당 타입의 인스턴스를 반환하는 형변환 메서드
⇒ Date d = Date.from(instant);
of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
⇒ Set faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf: from과 of의 더 자세한 버전
⇒ BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
instance or getInstance: (매개변수가 있다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
⇒ StackWalker luke = StackWalker.getInstance(options);
create or newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.
⇒ Object newArray = Array.newInstance(classObject, arrayLen);
getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.
⇒ FileStore fs = Files.getFileStore(path)
newType: newInstance와 같으나 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다.
⇒ BufferedReader br = Files.newBufferedReader(path)
type: getType과 newType의 간결한 버전
⇒ List litany = Collections.list(legacyLitany);