ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라
    백수의 개발/이펙티브 자바 2019. 7. 3. 13:14

    우리가 자주 사용하는 정적 팩터리 메서드

    java개발을 하다보면 정적 팩터리 메서드를 사용하는 경우들이 있다.

    대표적으로 Arrays, Collections 클래스에서 사용하는 메서드들이다.

    String[] nameArray = {"홍길동", "홍길순", "홍길두", "홍길희"};
    List<String> nameList = Arrays.asList(nameArray);
    Collections.sort(nameList);

    이 뿐만 아니라 java의 primitive type에 대한 객체들에서도 정적 팩터리 메서드를 사용할 수 있다.

    int five = Integer.valueOf(5);
    boolean isCorrect = Boolean.valueOf(true);
    BigInteger bigIntegerTen = BigInteger.valueOf(10);

     

    정적 팩터리 메서드란?

    static으로 선언된 메서드이며, new Object()와 같이 객체생성을 하지 않고 사용할 수 있는 메서드이다.

    실제로 Boolean객체의 valueOf의 함수가 정적 팩터리 메서드인데 내부를 보면 아래와 같다.

    public final class Boolean implements java.io.Serializable,
                                          Comparable<Boolean>
    {
    	...
    	public static Boolean valueOf(boolean b) {
    		return (b ? Boolean.TRUE : Boolean.FALSE);
    	}
    	...
    }

    이와 같이 정적 팩터리 메서드로 선언하였기 때문에 객체 생성을 하지 않아도 valueOf라는 메서드를 사용할 수 있는 것이다.

    // 객체 생성
    Boolean booleanObject = new Boolean(true);
    Boolean trueObejct = booleanObject.valueOf(true);
    
    // 객체 생성 x
    Boolean trueObejct = Boolean.valueOf(true);
    

     

    정적 팩터리 메서드의 장점 Top 5

    1. 이름을 가질 수 있다.

    한 객체의 생성자 여러개로 오버로딩하면, 모든 생성자의 이름은 같지만 매개변수만 다르다.

    이러한 경우 단순히 매개변수만 보고 의미하는 바를 정확하게 알기 힘든 경우가 많다.

     

    따라서 정적 팩터리 메서드의 이름을 잘 지어 반환될 객체의 특정을 쉽게 묘사하여 사용할 수 있다. 아래 BigInteger의 예시를 보자.

     

    BigInteger의 probablePrime을 예시로 볼 수 있는데, BigInteger.probablePrime값이 소수인 BigInteger를 반환하는 정적 팩터리 메서드이다.

     

    따라서 해당 코드를 사용하는 입장에서 잘 지어진 이름의 정적 팩터리 메서드를 사용하는 것이 훨씬 편할 것이다. 아래와 같이 말이다.

    // 생성자로 객체 생성
    BigInteger probablePrimeBigInteger = new BigInteger(10, 100, new Random());
    // 정적 팩터리 메서드로 객체 생성
    BigInteger probablePrimeBigInteger = BigInteger.probablePrime(10, new Random());

     

    2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

    정적 팩터리 메서드 호출을 위해 새로운 인스턴스를 생성하지 않기 때문에, 불변 클래스(immutable class)인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.

    public final class Boolean implements java.io.Serializable,
                                          Comparable<Boolean>
    {
    	public static final Boolean TRUE = new Boolean(true);
    	public static final Boolean FALSE = new Boolean(false);
    	...
    	public static Boolean valueOf(boolean b) {
    		return (b ? Boolean.TRUE : Boolean.FALSE);
    		// 아래와 같이 객체 생성을 하지 않는다.
    		// return (b ? new Boolean(true) : new Boolean(false));
    	}
    	...
    }

    인스턴스 캐싱은 크게 두가지 경우에 활용한다.

    첫번째는, 자주 사용되는 객체가 있는 경우

    두번째는, 자주 사용되지 않지만 객체 생성하는데 비용이 큰 객체이다.

     

    이러한 캐싱방식은 Integer에서도 활용되는데 나중에 다뤄보도록 하자.

     

    3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

    이를 통해 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하여 엄청난 유연성을 제공한다.

     

    Collections의 newSetFromMap 메서드를 통해 하나의 예를 보자.

    newSetFromMap는 Set인터페이스를 반환하게 되어있지만, 실제로 newSetFromMap는 Set의 하위 타입 객체를 반환하고 있다.

    실제 소스코드를 보면 아래와 같이 반환 타입의 하위 타입 객체를 반환하는 것을 볼 수 있다.

    public static <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
    	return new SetFromMap<>(map);
    }

     

    4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

    이 부분도 위와 같이 연결되는 부분이다. 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.

     

    EnumSet의 noneOf 메서드를 통해 하나의 예를 보자.

    noneOf는 EnumSet객체를 반환하게 되어있지만, 실제로는 EnumSet의 하위 타입 객체들 RegularEnumSet과 JumboEnumSet중 하나를 반환하고 있다.

    실제 소스코드를 보면 아래와 같이 반환 타입의 하위 타입 객체 중 하나를 선택하여 반환하는 것을 볼 수 있다.

    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);
    }

     

    5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

    JDBC가 이와 같은 내용의 대표적인 예라고 할 수 있다.

    Database에는 MySql, OracleDB, MariaDB등 다양하게 존재하는데 이를 JDBC라는 프레임워크로 관리할 수 있다.

     

    JDBC는 크게 아래와 같은 역할을 하는 컴포넌트들로 구성되어있다.

    Connection : 서비스 인터페이스 역할

    DriverManager.registerDriver : 제공자 등록 API 역할

    DriverManager.getConnection : 서비스 접근 API 역할

    Driver : 서비스 제공자 인터페이스 역할

     

     

    실제 JDBC를 사용하면 아래와 같다.

    String driverName = "com.mysql.jdbc.Driver";
    String url = "jdbc:mysql://localhost:3306/test";
    String user = "root";
    String password = "root";
    
    try {
    	Class.forName(driverName);
    	Connection connection = DriverManager.getConnection(url, user, password);
    } catch (ClassNotFoundException e) {
    	e.printStackTrace();
    } catch (SQLException e) {
    	e.printStackTrace();
    }

    분명 DriverManager.registerDriver를 통해 Driver를 설정해야할 것 같지만, 실제로는 등록되지않는다.

    그렇다면 Driver를 설정하지 않았는데 Connection은 어떻게 어떤 Driver로 해야하는지 알게 되는 것일까?

     

    앞에 Class.forName에 Driver의 정보를 준 것으로 보아 해당 부분에서 어떤 일이 일어났을 것이다.

    Driver Class API(Class.forName) 에 Class.forName 설명을 보면 아래와 같다.

    The interface that every driver class must implement.
    The Java SQL framework allows for multiple database drivers.
    
    Each driver should supply a class that implements the Driver interface.
    
    The DriverManager will try to load as many drivers as it can find and then for any given connection request, it will ask each driver in turn to try to connect to the target URL.
    
    It is strongly recommended that each Driver class should be small and standalone so that the Driver class can be loaded and queried without bringing in vast quantities of supporting code.
    
    When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a driver by calling
    
       Class.forName("foo.bah.Driver")

    Driver 클래스가 클래스로더에 의해 로드가 되면 자체적으로 인스턴스를 만들어 DriverManager 클래스에 등록이 되어야 한다. 이러한 작업은 Class.forName(String name) 메소드에 의해서 작동한다고 되어있다.

    Class.forName(String name)은 파라미터로 받은 name에 해당하는 클래스를 로딩하며, 클래스가 로드 될 때 static 필드의 내용이 실행되는 것을 이용해 자기 자신을 DriverManager 클래스에 등록한다. 자바 가상머신이 동작을 시작하고, 코드가 실행되기 전까지는 어떤 JDBC 드라이버가 사용될 지 모르기 때문에, 동적으로 드라이버를 로딩하기 위해 리플렉션(java.lang.reflect)을 이용한다.(출처: https://plposer.tistory.com/61 [안JAVA먹지])

     

    정적 팩터리 메서드의 단점 Top 2

    1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

    상속을 하기 위한 생성자가 없으니, 상속을 할 수 없다는 것은 당연할 수 있다.

    그래서 만약, 정적 팩터리만 제공한 클래스를 확장해서 상속하고자 할 때, 상속이 불가능하다는 것이다.

     

    그러나 이 제약은 상속보다 컴포지션을 사용하도록 유도하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들이기도 한다.

    2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

    어떤 라이브러리를 사용하기 위해 API문서를 보면 정적 팩터리 메서드에 대해서 확인하기가 쉽지 않다.

     

    아래 Boolean의 API문서를 보면, 생성자에 대한 내용은 별도로 작성하여 구분이 잘 된다.

    그러나 정적 팩터리 메서드에 대해서는 일반 메서드와 같이 작성이 되어있어 구분하기 쉽지않다.

     

    마무리

    정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 중요하다.

    그렇다고 하더라도 정적 팩터리를 사용하는게 유리한 경우가 더 많으므로 public 생성자를 제공하던 습관이 있다면 고치자.

    댓글

Designed by Tistory.