럿고의 개발 노트
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. 본문
이팩티브 자바 3판 - 2장. 객체 생성과 파괴
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.
public class StaticFactoryMethodEx1 {
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
}
정적 팩터리 메서드가 생성자보다 좋은 장점
1. 이름을 가질 수 있다.
- 생성자에 넘기는 매개변수와 생성자 자체만으로 반환될 객체의 특성을 제대로 설명을 못한다면, 정적 팩터리 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
- 예를들어
BigInteger(int, int, Random)
과BigInteger.probablePrime(int, Random)
둘 중 "값이 소수인BigInteger
를 반환한다."는 의미를 더 잘 설명하는지를 생각해보면 좋다. - 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자.
예제
- 이메일이나 전화번호로 연락하는 정보를 저장하는 객체가 있다고 해보자. 아래와 같이 클래스를 구현해서 인스턴스화를 할때, 이 객체가 전화번호로 연락하는 방법인지 이메일로 연락하는 방법인지 잘 알아 채기가 힘들다.
- 이럴때, 이름을 구분 해 놓는다면 쉽게 알아 차릴 수 있을 것이다.
public class Contact {
private String name;
private int phoneNumber;
private String email;
public Contact(String name, int phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
public Contact(String name, String email) {
this.name = name;
this.email = email;
}
}
package Item01;
public class Contact {
private String name;
private int phoneNumber;
private String email;
private Contact(String name, int phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
private Contact(String name, String email) {
this.name = name;
this.email = email;
}
public static Contact newInstanceByPhoneNumber(String name, int phoneNumber){
return new Contact(name, phoneNumber);
}
public static Contact newInstanceByEmail(String name, String email){
return new Contact(name, email);
}
}
2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
immutable class
는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.- 대표적인 예로
Boolean.valueOf(boolean)
메서드는 객체를 생성하지 않고 값을 반환한다. - 따라서 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어 올려 준다.
- 이와 비슷한 디자인 패턴은
플라이웨이트 패턴(Flyweight pattern)
이다. - 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있으며 이런 클래스를 인스턴스 통제 클래스라고 한다.
- 아울러 인스턴스를 통제하면 클래스를 싱글턴이나 인스턴스화 불가로 만들수 있으며 불변 값 클래스에서 같은 인스턴스가 단 하나임을 보장할 수 있다.
- 인스턴스 통제는 플라이웨이트 패턴의 근간이 되며 열거 타입은 인스턴스가 하나만 만들어짐을 보장한다.
cf) 플라이웨이트 패턴(Flyweight Pattern)
- 공유를 통하여 대량의 객체들을 효과적으로 지원하는 방법으로 메모리를 가볍게 유지 시켜주는 것이다.
- 플라이웨이트 패턴을 적용하기에 앞서 몇 가지 확인해야 할 것 이 있음.
- 어플리케이션에 의해 생성되는 객체의 수가 많아야 한다.
- 생성된 객체가 오래도록 메모리에 상주하며, 사용되는 횟수가 많다.
- 객체의 특성을 내적 속성과 외적 속성으로 나눴을때, 객체의 외적 특성이 클라이언트 프로그램으로부터 정의되어야 한다.
- 여기서 내적 속성은 객체를 유니크하게 하는 것이고, 외적 속성은 클라이언트의 코드로부터 설정되어 다른 동작을 수행하도록 사용되는 특성입니다.
- 예를 들어 클라이언트가 프로그램을 사용하다가 이미지가 필요하다면 이미지를 생성하는데, 이때 생성한 이미지와 동일한 이미지를 필요하다고 하면 또 새로운 이미지를 생성할 것이다. 그러다 보면 메모리는 오버플로우가 발생할 확률이 높아진다.
- 그렇다면 이 문제점을 해결하기 위해서는
image
를 관리하는image manager
를 만들어서image manager
가 필요한 이미지를 찾아서 넘겨주거나 없다면 새로운 이미지를 생성해서 건네 주는 역할을 하는 것을 만들어 주면 된다.
public class Flyweight {
private String data;
public Flyweight(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
import java.util.Map;
import java.util.TreeMap;
public class FlyweightFactory {
Map<String, Flyweight> pool;
public FlyweightFactory() {
this.pool = new TreeMap<>();
}
public Flyweight getFlyweight(String key) {
Flyweight flyweight = pool.get(key);
if (flyweight == null) {
flyweight = new Flyweight(key);
pool.put(key, flyweight);
System.out.println("새로 생성" + key);
}else{
System.out.println("재사용" + key);
}
return flyweight;
}
}
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight flyweight = factory.getFlyweight("A");
System.out.println(flyweight);
flyweight = factory.getFlyweight("A");
System.out.println(flyweight);
flyweight = factory.getFlyweight("B");
System.out.println(flyweight);
}
}
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 선물한다.
- API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
- 이는 인터페이스를 정적 팩터리 메서드의 반환 타입으로 사용하는 인터페이스 기반 프레임워크를 만드는 핵심 기술이다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
- 예를 들어
EnumSet
클래스는 생성자 없이 오직 정적 팩터리만 제공하는데 OpenJDK에서 원소의 수에 따라 두 가지 하위 클래스 중 하나의 인스턴스를 반환한다. - 64개 이하면 long 변수 하나로 관리하는
RegularEnumSet
의 인스턴스를, 65개 이상이면 long 배열로 관리하는JumboEnumSet
의 인스턴스를 반환한다.
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
이러한 유연함은
서비스 제공자 프레임워크
를 만드는 근간이 된다.대표적인 서비스 제공자 프레임워크로는
JDBC
가 있다.서비스 제공자 프레임워크에서의 제공자는 서비스의 구현체다.
그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.
서비스 제공자 프레임워크의 컴포넌트
- 서비스 인터페이스 : 구현체의 동작을 정의 :
Connection
- 제공자 등록 API : 제공자가 구현체를 등록할 때 사용 :
DriverManager.registerDriver
- 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용 :
DriverManager.getConnection
- 서비스 제공자 인터페이스 : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명 :
Driver
- 서비스 인터페이스 : 구현체의 동작을 정의 :
클라이언트는 서비스 접근 API를 사용할 때 원하는 구현체의 조건을 명시할 수 있다.
조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환한다.
이 서비스 접근 API가 바로 서비스 제공자 프레임워크의 근간이라고 한
유연한 정적 팩터리
이다.서비스 제공자 인터페이스가 없다면 각 구현체를 인스턴스로 만들 때 리플렉션을 사용해야 한다.
cf) 리플랙션(Reflection)
- 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법
import java.lang.reflect.Method;
public class ReflectionEx {
public static void main(String[] args) {
try{
Class c = Class.forName("java.util.Stack");
Method[] m = c.getDeclaredMethods();
for(Method method: m){
System.out.println(method.toString());
}
}catch (Throwable e){
System.out.println(e);
}
}
}
- 서비스 제공자 프레임워크 패턴에는 여러 변형이 있다.
cf) 브리지 패턴(Bridge pattern)
- 구현부에서 추상층을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하도록 합니다. 즉, 기능과 구현에 대해서 두 개를 별도의 클래스로 구현합니다.
- 즉, 기능은 추상클래스로 구현은 인터페이스로 분리시키는 것이다.
- 추상 클래스는 추상 클래스 대로, 구현은 구현 대로 변경해도 서로 영향을 주지 않도록 하는게 중요하다.
- 참고자료
public abstract class Shape {
private Drawing drawing;
public Shape(Drawing drawing) {
this.drawing = drawing;
}
public abstract void draw();
public void drawLine(int x, int y){
drawing.drawLine(x,y);
}
public void fill() {
drawing.fill();
}
}
public interface Drawing {
void drawLine(int x, int y);
void fill();
}
public class RectDrawing implements Drawing {
@Override
public void drawLine(int x, int y) {
System.out.println("Rect Draw line from" + x + " to " + y);
}
@Override
public void fill() {
System.out.println("Rect Fill!");
}
}
public class CircleDrawing implements Drawing {
@Override
public void drawLine(int x, int y) {
System.out.println("Circle Draw line from " + x + " to " + y);
}
@Override
public void fill() {
System.out.println("Circle Fill!");
}
}
public class Rectangle extends Shape {
public Rectangle(Drawing drawing) {
super(drawing);
}
@Override
public void draw() {
System.out.println("Rect draw extend");
}
}
public class Circle extends Shape{
public Circle(Drawing drawing) {
super(drawing);
}
@Override
public void draw() {
System.out.println("Rect draw extend");
}
}
package Item01.BridgePattern;
public class BridgeMain {
public static void main(String[] args) {
Shape rectangle = new Rectangle(new RectDrawing());
Shape circle = new Circle(new CircleDrawing());
rectangle.drawLine(1, 2);
rectangle.fill();
rectangle.draw();
System.out.println();
circle.drawLine(3, 4);
circle.fill();
circle.draw();
}
}
- 의존 객체 주입 프레임워크도 강력한 서비스 제공자이다.
java.util.ServiceLoader
라는 범용 서비스 제공자 프레임워크로 프레임워크를 직접 만들 필요가 거의 없어졌다. 그러나 JDBC는ServiceLoader
를 사용하지 않는다.
정적 팩터리 메서드 단점
1. 상속을 하려면 public
이나 protected
생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 의미이다.
- 그러나 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- API 설명에 명확히 들어나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 찾아야 한다.
- API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.
정적 팩터리 메서드에 흔히 사용하는 명명 방식
명명 | 설명 |
---|---|
from | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드 |
of | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드 |
valueOf | 자신의 매개변수와 같은 값을 가지는 인스턴스 반환 |
instance 또는 getInstance | (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. 싱글톤 패턴을 따를 경우, 이 메소드는 매개변수 없이 항상 같은 객체를 반환 |
create 또는 newInstance | instance 또는 getInstance 와 같지만, 매번 새로운 인스턴스를 생성해 반환 |
getType | getInstance 와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. Type 은 팩터리 메서드가 반환할 객체의 타입 |
newType | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메서드를 정의할 때 쓴다. Type 은 팩터리 메서드가 반환할 객체의 타입 |
type | getType 과 newType 의 간결한 버전 |
정리
- 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
- 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
'Java Note > 이펙티브 자바 3판(EFFECTIVE JAVA 3E)' 카테고리의 다른 글
아이템 6. 불필요한 객체 생성을 피하라. (0) | 2020.03.23 |
---|---|
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라. (0) | 2020.03.23 |
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2020.03.17 |
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라. (0) | 2020.03.16 |
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2020.03.15 |
Comments