럿고의 개발 노트

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라. 본문

Java Note/이펙티브 자바 3판(EFFECTIVE JAVA 3E)

아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라.

KimSeYun 2020. 3. 14. 19:48

이팩티브 자바 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)

  • 공유를 통하여 대량의 객체들을 효과적으로 지원하는 방법으로 메모리를 가볍게 유지 시켜주는 것이다.
  • 플라이웨이트 패턴을 적용하기에 앞서 몇 가지 확인해야 할 것 이 있음.
    1. 어플리케이션에 의해 생성되는 객체의 수가 많아야 한다.
    2. 생성된 객체가 오래도록 메모리에 상주하며, 사용되는 횟수가 많다.
    3. 객체의 특성을 내적 속성과 외적 속성으로 나눴을때, 객체의 외적 특성이 클라이언트 프로그램으로부터 정의되어야 한다.
  • 여기서 내적 속성은 객체를 유니크하게 하는 것이고, 외적 속성은 클라이언트의 코드로부터 설정되어 다른 동작을 수행하도록 사용되는 특성입니다.
  • 예를 들어 클라이언트가 프로그램을 사용하다가 이미지가 필요하다면 이미지를 생성하는데, 이때 생성한 이미지와 동일한 이미지를 필요하다고 하면 또 새로운 이미지를 생성할 것이다. 그러다 보면 메모리는 오버플로우가 발생할 확률이 높아진다.
  • 그렇다면 이 문제점을 해결하기 위해서는 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가 있다.

  • 서비스 제공자 프레임워크에서의 제공자는 서비스의 구현체다.

  • 그리고 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해준다.

  • 서비스 제공자 프레임워크의 컴포넌트

    1. 서비스 인터페이스 : 구현체의 동작을 정의 : Connection
    2. 제공자 등록 API : 제공자가 구현체를 등록할 때 사용 : DriverManager.registerDriver
    3. 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용 : DriverManager.getConnection
    4. 서비스 제공자 인터페이스 : 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명 : 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 getTypenewType의 간결한 버전

정리

  • 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다.
  • 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.
Comments