-
Notifications
You must be signed in to change notification settings - Fork 1
item 1 Jung inchul
Effective Java 3e 아이템 1을 요약한 내용 입니다.
클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 클래스는 생성자와 별도로 정적 팩터리 메서드를 제공할 수 있다.
정적 팩터리 메서드란?
객체 생성을 캡슐화하는 기법이다. 좀 더 구체적으로는 객체를 생성하는 메소드를 만들고, static으로 선언하는 기법이다. 클래스는 클라이언트에 public 생성자 대신 (혹은 생성자와 함께) 정적 팩터리 메서드를 제공할 수 있다.
먼저 정적 팩터리 메서드가 생성자보다 좋은 장점 다섯 가지를 알아보자
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
// 값이 소수인 BigInteger를 생성한다.
// 일반 생성자로 생성한 객체
BigInteger num = new BigInteger(0, 0, new Random());
// 정적 팩터리 메서드로 생성한 객체
BigInteger num2 = BigInteger.probablePrime();
하나의 시그니처로는 생성자를 하나만 만들 수 있다. 입력 매개변수들의 순서를 다르게 한 생성자를 새로 추가하는 식으로 이 제한을 피해볼 수도 있지만 좋지 않은 발상이다.
이름을 가질 수 있는 정적 팩터리 메서드에는 이런 제약이 없다. 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자
대표적인 예로 Boolean.valueOf(boolean) 메서드는 객체를 아예 생성하지 않는다. 따라서 (특히 생성 비용이 큰) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 준다. 플라이웨이트 패턴도 이와 비슷한 기법이라 할 수 있다.
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. 이런 클래스를 인스턴스 통제 클래스라 한다.
플라이웨이트 패턴이란?
데이터를 공유 사용하여 메모리를 절약할 수 있는 패턴이다. 일반적으로 공통으로 사용되는 객체는 새로 생성해서 사용하지 않고 공유를 통해 효율적으로 자원을 활용한다. 한번 생성된 객체는 두번 생성되지 않고, 풀(Pool)에 의해서 관리 및 사용된다.
// Flyweight 패턴 적용 사례
public class Test{
public static void main(String[] args){
String s = "hello";
String s1 = new String("hello");
String s2 = "hello";
System.out.println("s == s1 ? " + (s == s1)); // false
System.out.println("s == s2 ? " + (s == s2)); // true
}
}
인스턴스를 통제하는 이유는 무엇일까?
- 인스턴스를 통제하면 클래스를 싱글턴으로 만들 수도 인스턴스화 불가로 만들수도 있다.
- 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다.
이 특징은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 '엄청난 유연성'을 제공한다. 이는 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이기도 하다.
컬렉션 프레임워크는 이 45개 클래스를 공개하지 않기 때문에 API 외견을 훨씬 작게 만들 수 있었다. API가 작아진 것은 물론 개념적인 무게, 즉 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도도 낮췄다.
자바 8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 별로 없다.
자바 9에서는 private 정적 메서드까지 허락하지만 정적 필드와 정적 멤버 클래스는 여전히 public이어야 한다.
public interface Pizza {
void cook();
public static BulgogiPizza(String topping){
return new BulgogiPizza(topping);
}
}
public class BulgogiPizza implements Pizza{ ... }
EnumSet 클래스는 public 생성자 없이 오직 정적 팩터리만 제공하는데, OpenJDK에서는 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다.
원소가 64개 이하면 원소들을 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스, 65개 이상이면 JumboEnumSet의 인스턴스를 반환한다.
클라이언트는 팩터리가 건네주는 객체가 어느 클래스의 인스턴스인지 알 수도 없고 알 필요도 없다. EnumSet의 하위 클래스이기만 하면 되는 것이다.
public interface Pizza {
void cook();
public static BakedPizza(Boolean isVegiterian, String topping){
return isVegiterian ? new VegitablePizza(topping); : new BulgogiPizza(topping);
}
}
public class BulgogiPizza implements Pizza{ ... }
public class VegitablePizza implements Pizza{ ... }
대표적인 서비스 제공자 프레임워크로는 JDBC가 있다.
서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 이뤄진다. 구현체의 동작을 정의하는 서비스 인터페이스, 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API, 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API가 그 주인공이다.
클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있다. 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다. 이 서비스 접근 API가 바로 서비스 제공자 프레임워크의 근간이라고 한 '유연한 정적 팩터리'의 실체다.
JDBC에서는 Connection이 서비스 인터페이스 역할을, DriverManager.registerDriver가 제공자 등록 API 역할을, Driver가 서비스 제공자 인터페이스 역할을 수행한다.
이 제약은 상속보다는 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.
생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
Method | Example |
---|---|
from | Date d = Date.from(instance); |
of | EnumSet.of(JACK | QUEEN | KING) |
valueof | BigInteger.valueOf(Integer.MAX_VALUE) |
instance | getInstance |
create | newInstance |
getType | Files.getFileStore(path) |
newType | Files.newBufferedReader(path) |
type | Collections.list(legacyLitany); |
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자!