티스토리 뷰

일반적으로 인스턴스의 생성은 public 생성자를 통해 이루어질 수 있습니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Users user = new Users(26, "jo", "busan", "ds4ouj@naver.com");
    }

    public static class Users {

        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }
    }
}

위의 코드는 아주 일반적이기 때문에 큰 어려움 없이 이해할 수 있습니다.

이때 'Users의 email이 들어오지 않을 수 있다'는 요구 사항이 추가된다면, 생성자를 하나 더 추가하면서 문제를 해결할 수 있습니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Users user = new Users(26, "jo", "busan");

    }

    public static class Users {

        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }

        public Users(int age, String name, String address) {
            this.age = age;
            this.name = name;
            this.address = address;
        }
    }
}

그런데 여기서 추가로 'Users의 address가 들어오지 않을 수 있다'는 요구 사항이 추가되면, 문제가 복잡해집니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Users user = new Users(26, "jo", "busan");

    }

    public static class Users {

        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }

        public Users(int age, String name, String address) {
            this.age = age;
            this.name = name;
            this.address = address;
        }

        public Users(int age, String name, String email) {
            this.age = age;
            this.name = name;
            this.email = email;
        }
    }
}

위와같이 생성자를 추가하게 되면 Users(int , String, String) 형식의 생성자가 2개 존재하기 때문에 컴파일 에러가 발생합니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Users user = new Users(26, "jo", "busan");

    }

    public static class Users {

        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }

        public Users(int age, String name, String address) {
            this.age = age;
            this.name = name;
            this.address = address;
        }

        public Users(String name, int age, String email) {
            this.age = age;
            this.name = name;
            this.email = email;
        }
    }
}

그래서 public 생성자의 매개변수 위치를 바꿔 중복되는 생성자를 만들지 않으면서 요구사항도 만족할 수 있도록 만들었습니다.

하지만, 매우 안좋은 코드입니다.
인스턴스를 만들기 위해 넣는 인자가 어떤 필드로 들어갈지 명확하지 않기에 결국 Class 문서나 코드를 직접 열어봐야 합니다.

이러한 문제를 좀더 명확하게 해결할 수 있는 방법이 있는데, 바로 '정적 팩토리 메서드'를 이용하는 방법입니다.
위에서 주어진 요구사항을 만족하기 위해 public 생성자가 아닌 정적 팩토리 메서드를 사용한다면 아래와 같은 코드를 얻을 수 있습니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Users user1 = Users.userWithAddress(26, "jo", "Busan");
        Users user2 = Users.userWithEmail(26, "jo", "ds4ouj@naver.com");

    }

    public static class Users {

        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }

        public static Users userWithAddress(int age, String name, String address) {
            return new Users(age, name, address, "Null");
        }

        public static Users userWithEmail(int age, String name, String email) {
            return new Users(age, name, "Null", email);
        }
    }
}

팩토리 메서드 덕분에 인스턴스 생성이 좀 더 직관적으로 변한 것을 볼 수 있습니다.
이렇게 팩토리 메서드를 사용하면 여러 장단점을 얻을 수 있는데, 이제부터 팩토리 메서드를 사용했을 경우 어떤 장단점을 얻을 수 있는지 알아보겠습니다.

1. 이름을 가질 수 있습니다.
public 생성자의 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 나타내지 못합니다. 반면, 정적 팩토리 메서드는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있습니다.

위에서 살펴본 예시만 봐도 매개변수의 위치가 다른 생성자 vs 메서드 이름으로 구분된 생성자에서 이 차이를 알 수 있습니다.
따라서 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩토리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주면 됩니다.

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 됩니다.
객체의 생성에는 비용이 들어갑니다. 만약 생성할 수 있는 인스턴스가 한정적이라면, 미리 인스턴스를 만들어 놓고, 재활용하여 객체 생성에 소모되는 비용을 절감할 수 있습니다. 실제로 Java에서 이 전략을 사용하고 있는 Boolean class를 살펴보겠습니다.

public final class Boolean implements java.io.Serializable,
                                      Comparable<Boolean>, Constable{

    public static final Boolean TRUE = new Boolean(true);

    public static final Boolean FALSE = new Boolean(false);


    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
}

Boolean.class의 valueOf 메서드는 객체를 미리 생성해놓고 재활용하는 방식으로 동작합니다. 따라서 같은 객체가 자주 요청되는 상황이라면 성능을 끌어올려 줄 수 있는 장점을 얻게 됩니다.

이렇게 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있습니다. 그리고 이런 클래스를 '인스턴스 통제 (instance-controlled) 클래스'라고 부릅니다.

인스턴스 통제를 한다면, 클래스를 싱글톤으로 만들 수도 있고, 동치인 인스턴스가 단 하나뿐임을 보장할 수도 있습니다. 또한 후에 배울 '플라이웨이트 패턴'의 근간이 되는 기법입니다.

3. 반환 타입의 하위 타입 객체를 반환할 수 있습니다.
이 장점 덕분에 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 유연성을 얻을 수 있습니다. 예를들어 아래의 코드를 보겠습니다.

public class ExampleStaticFactoryMethod {

    public static void main(String[] args) {
        Member member = Users.member(26, "jo", "Busan", "ds4ouj@naver.com");
    }

    public static class Users {
        private int age;
        private String name;
        private String address;
        private String email;

        public Users(int age, String name, String address, String email) {
            this.age = age;
            this.name = name;
            this.address = address;
            this.email = email;
        }

        public static Member member(int age, String name, String address, String email) {
            return new Member(age, name, address, email);
        }
    }

    public static class Member extends Users{

        public Member(int age, String name, String address, String email) {
            super(age, name, address, email);
        }
    }
}

위와같이 간단하게 하위 객체를 반환할 수 있고, 이 유연성을 API를 만들때 응용하면, 구현 클래스를 공개하지 않고도 객체를 반환할 수 있기에 API를 작게 유지할 수 있는 장점을 얻을 수 있습니다.

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있습니다.
3에서 얻은 장점을 활용하면 얻을 수 있는 장점입니다. 예시로 Java의 EnumSet의 내부는 어떻게 구현되어 있는지 살펴보겠습니다.

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable {
    @java.io.Serial
    private static final long serialVersionUID = 1009687484059888093L;

    final transient Class<E> elementType;

    final transient Enum<?>[] universe;

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }
}

OpenJDK에서는 원소의 수에 따라 하위 클래스 중 하나의 인스턴스를 반환합니다.
원소의 수가 64개 이하면 원소들을 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet의 인스턴스를 반환하는 것을 볼 수 있습니다.

클라이언트의 입장에서는 EnumSet의 하위 클래스이면 사용할 수 있기 때문에 이 두 클래스의 존재를 모르며 알 필요도 없습니다. 예시 코드에 이를 적용해보면 아래와 같은 코드를 얻을 수 있습니다.

public static class Users {
    private int age;
    private String name;
    private String address;
    private String email;

    public Users(int age, String name, String address, String email) {
        this.age = age;
        this.name = name;
        this.address = address;
        this.email = email;
    }

    public static Users getUserClass(int age, String name, String address, String email) {
        if (name.equals("jo")) return new Admin(age, name, address, email);
        return new Member(age, name, address, email);
    }
}

public static class Member extends Users{

    public Member(int age, String name, String address, String email) {
        super(age, name, address, email);
    }
}

public static class Admin extends Users {
    public Admin(int age, String name, String address, String email) {
        super(age, name, address, email);
    }
}

5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됩니다.
이러한 유연함은 '서비스 제공자 프레임워크(Service Provider Framework)'를 만드는 근간이 됩니다.
여기서 등장한 서비스 제공자 프레임워크는 아래와 같은 3개의 핵심 컴포넌트로 구성됩니다.

1) 서비스 인터페이스 (Service Interface)
구현체의 동작을 정의합니다.

2) 제공자 등록 API (Provider Registration API)
제공자가 구현체를 등록할 때 사용합니다.

3) 서비스 접근 API (Service Access API)
클라이언트가 서비스의 인스턴스를 얻을 때 사용하며, 정적 팩토리 메서드가 사용됩니다.

클라이언트는 Service Access API를 사용할 때 원하는 구현체의 조건을 명시할 수 있습니다. 만약 조건을 명시하지 않으면 기본 구현체를 반환하거나 지원하는 구현체들을 하나씩 돌아가며 반환합니다.

Java의 대표적인 서비스 제공자 프레임워크인 JDBC를 기준으로 살펴보면,
JDBC에서 Connection이 서비스 인터페이스의 역할을 수행하고, DriverManager.registerDriver가 제공자 등록 API 역할을, DriverManager.getConnection이 서비스 접근 API 역할을 수행합니다.

JDBC를 사용하는 코드와 구조로 조금 더 깊은 이해를 해보겠습니다. 먼저 JDBC의 Connection을 열어보겠습니다.

public interface Connection  extends Wrapper, AutoCloseable {
    //...
}

JDBC의 Connection이 Service Interface이기에, 구현체의 동작을 정의할 것이라고 기대했으나 Connection은 Interface입니다. 그렇다면 Connection의 구현체는 어디에 있는걸까요?

JDBC의 아키텍처는 위와 같습니다.
Java로 서버 애플리케이션을 구축할 때, JDBC API로 Database에 접근하고 사용할 수 있습니다. 이때 JDBC API와 DB 사이에는 JDBC Driver라는 것이 존재하는데요, 바로 여기에 Connection 구현체가 들어있습니다.

Connection conn = null;

    try{

        String url = "jdbc:mysql://localhost/dev";

        // @param  getConnection(url, userName, password);
        // @return Connection
        conn = DriverManager.getConnection(url, "dev", "dev");
        System.out.println("연결 성공");

    }
    catch(ClassNotFoundException e){
        System.out.println("드라이버 로딩 실패");
    }
    catch(SQLException e){
        System.out.println("에러: " + e);
    }
    finally{
        try{
            if( conn != null && !conn.isClosed()){
                conn.close();
            }
        }
        catch( SQLException e){
            e.printStackTrace();
        }
    }

JDBC API를 사용하는 템플릿은 위와 같은데요, 여기서 Service Access API인 DriverManager.getConnection() 은 Connection의 구현체를 return 합니다. 그리고 이 Connection 구현체 덕분에 우리는 서로 다른 DB를 하나의 JDBC API로 접근할 수 있는 것입니다.

이제 장점이 아닌, 단점에 대해 알아보겠습니다.

1. 상속을 할 수 없습니다.
상속을 하려면, public이나 protected 생성자가 필요합니다. 따라서 정적 팩토리 메서드만 제공할 경우 하위 클래스를 만들 수 없습니다.

2. 정적 팩토리 메서드는 사용하기가 어렵습니다.
생성자에 비해 정적 팩토리 메서드는 명확하지 않아 개발자가 사용하기 어렵습니다. 이 문제를 완화하기 위해 메서드 이름을 널리 알려진 규약에 따라 짓는 방식을 사용하곤 합니다. 


Ref : 이펙티브 자바 Effective Java 3/E

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함