본문 바로가기

OOP, FP/이펙티브 자바

이펙티브 자바 01. 정적 팩토리 메소드와 서비스 제공자 인터페이스 (JDBC 예제)

객체 생성

자바에 처음 입문하면 클래스와 객체에 대해 배운다.

이후 실습을 통해 생성자를 정의하고 new 키워드로 생성자를 호출하여 객체를 만들어 사용하는 연습을 하고,

시그니처와 오버로딩을 이용해 여러 개의 생성자를 정의하여 제법 객체지향적인 코드를 만든다.


생성자

생성자는 객체를 생성하기 위해 기본적으로 제공되는 기능이다.

하지만 생성자의 시그니처만을 보고 개발자의 의도를 완전히 파악하기는 어렵다.

때문에 우리는 API 문서를 보고, 우리에게 필요한 생성자를 찾아야 한다.

정적 팩터리 메소드를 사용하면 메소드 이름, 반환 자료형만으로 의도를 전달할 수 있다.


정적 팩토리 메소드

클래스 자신의 타입을 반환하는 public static 메소드.

클래스를 정의할 때 생성자와는 별도로 정적 팩토리 메소드를 제공할 수 있다.

정적 팩토리 메소드의 장점을 살펴보자.


1. 생성자와는 달리 정적 팩토리 메소드에는 이름이 있다.

이전에 설명한 생성자의 단점을 보완한다.

생성자를 이용할 경우 파라미터 정보만 제공되기 때문에, 어떤 객체가 생성되는 지를 전달하기 어렵다.

정적 팩토리 메소드는 이름을 잘 짓기만 하면 사용하기 쉽고, 사용자가 작성한 코드의 가독성도 높아진다.


public BigInteger probablePrime(int bitLength, Random rnd);


예를 살펴보자.

위 메소드는 JDK 1.4 버전 이후로 제공되는 정적 팩토리 메소드이다.

소수일 가능성이 높은 BigInteger 객체를 생성한 뒤 반환한다.


생성자의 또 다른 문제점은 생성자는 시그니처별로 하나의 생성자만 넣을 수 있다는 것이다.

이러한 제약을 벗어나기 위해서는 인자의 순서를 바꿔야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Foo {
    public Foo() {
        // do something
    }
    
    public Foo(int a) {
        // do something
    }
    
    public Foo(String a) {
        // do something
    }
    
    public Foo(int a, String b) {
        // do something
    }
    
    public Foo(String b, int a) {
        // do something
    }
}
cs


이 클래스를 사용하는 API 사용자는 각각의 생성자 용도를 절대 기억하지 못헐 것이다.

사용자는 매번 API 문서를 참조해야한다.

반면 정적 팩토리 메소드에는 이름이 있으므로 그런 문제가 생기지 않는다.

같은 시그니처를 갖는 생성자를 여러 개 정의할 필요가 있을 때는 정적 팩토리 메소드를 이용하자.


2. 생성자와는 달리 호출할 때마다 새로운 객체를 생성할 필요는 없다.

생성자를 호출할 경우 무조건 새로운 객체를 생성하여 반환한다.

객체의 기본 상태를 제어할 수는 있지만, 객체가 생성되는 것만은 피할 수 없다.

반면 정적 팩토리 메소드는 매번 새로운 객체를 생성하지 않아도 된다


변경이 불가능한 클래스라면 미리 만들어 둔 객체를 반환하도록 구현한다.

또는 만든 객체를 캐싱해놓고 재사용하여 같은 객체가 불필요하게 생성되는 일을 피할 수 있다.

아래는 Boolean 래퍼 클래스에서 제공하는 정적 팩토리 메소드이며, 이 기법을 적절히 활용한 사례이다.


1
2
3
public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}
cs


이러한 기법은 동일한 객체가 요청되는 일이 잦거나, 객체를 만드는 비용이 클 때 적용하면 성능을 개선할 수 있다.


개체 통제 클래스

정적 팩토리 메소드를 이용하면 같은 객체를 빈복해서 반환할 수 있으므로 객체가 존재하는 시점을 정밀하게 제어할 수 있다.

이러한 기능을 갖춘 클래스를 개체 통제 클래스라고 부른다.

개체 수를 제어하면 싱글턴 패턴을 따르도록 할 수 있고, 객체 생성이 불가능한 클래스를 만들 수도 있다.

개체 수를 엄격하게 통제하는 enum 열거 자료형의 경우  a == b 일 경우에만 a.equlas(b)가 참이 되도록 만들어졌다.

이런 경우 equals대신 == 연산자를 사용하여 비교할 수 있으므로 성능이 향상된다.


3. 생성자와는 달리 반환값 자료형의 하위 자료형 객체를 반환할 수 있다.

정적 팩토리 메소드를 이용하면 반환되는 객체의 클래스를 훨씬 유연하게 결정할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Foo { 
    
    public Foo() {
        
    }
    
    public static Foo fooBar() {
        return new Bar();
    }
    
    public static Foo foo() {
        return new Foo();
    }
    
    private static class Bar extends Foo {
 
        public Bar() {
            
        }
    };
};
cs


위와 같은 유연성을 활용하면 public으로 선언되지 않은 클래스의 객체를 반환하는 API를 만들 수 있다.

구현 세부사항을 감출 수 있으므로 간결한 API가 되며, 인터페이스 기반 프레임워크 구현에 적합하다.

인터페이스는 정적 팩토리 메소드의 반환값 자료형으로 이용된다.


자바의 컬렉션 프레임워크에는 32개의 컬렉션 인터페이스 구현체가 들어있다.

이 구현체들 전부는 java.util.Collections 라는 객체 생성 불가능 클래스의 정적 팩토리 메소드를 통해 이용하는데,

반환되는 객체의 실제 클래스는 public이 아니다.

이를 이용한 클라이언트 코드는 반환된 객체의 실제 구현 세부사항이 아니라 인터페이스만 보고 작성하게 된다.

구현체별로 32개의 public 클래스를 만들었다면 API 규모는 더 커졌을 것이다.


다른 예를 살펴보자.

JDK 1.5 부터 도입된 java.util.EnumSet 에는 public으로 선언된 생성자가 없으며, 정적 팩토리 메소드뿐이다.

이 메소드들은 enum 상수 개수에 따라 두 개 구현체 가운데 하나를 골라 해당 클래스의 객체를 만들어 반환한다.

enum 상수들이 64개 이하일 경우 long 변수를 사용하는 RegularEnumSet의 객체를,

enum 상수들이 64개를 초과할 경우 long 배열을 사용하는 JumboEnumSet의 객체를 반환한다.
API를 이용하는 클라이언트는 위와 같이 내부적으로 어떤 클래스가 이용되는지 알 수 없다.

따라서 RegularEnumSet 클래스가 enum 상수들이 적은 상황에 맞는 성능을 제공하지 못할 경우

다음번 릴리스에는 안전하게 다른 클래스로 바꿀 수 있으며, 단지 EnumSet의 하위 클래스라는 사실만 중요하다.


서비스 제공자 프레임워크

JDBC(Java Database Connectivity API)와 같은 서비스 제공자 프레임워크의 근간을 이루는 것이 바로 유연한 성격을 지닌 정적 팩토리 메소드들이다. 서비스 제공자 프레임워크는 다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템으로, 클라이언트가 실제 구현된 서비스를 이용할 수 있도록 하는데, 클라이언트는 세부적인 구현 내용을 몰라도 서비스를 이용할 수 있다.

자바의 JDBC는 MySQL, Oracle, SqlServer 등 다양한 서비스 제공자들이 JDBC라는 하나의 서비스를 구성한다.


서비스 제공자 프레임워크는 세 가지의 핵심 컴포넌트로 구성된다.

(1) 서비스 제공자가 구현하는 서비스 인터페이스

(2) 구현체를 시스템에 등록하여 클라이언트가 쓸 수 있도록 하는 서비스 등록 API

(3) 클라이언트에게 실제 서비스 구현체를 제공하는 서비스 접근 API


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String driverName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/board";
String user = "root";
String password = "1234@";
 
try {
    Class.forName(driverName);
    
    // 서비스 접근 API인 DriverManager.getConnection
    // 서비스 구현체(서비스 인터페이스)인 Connection를 반환한다.
    Connection conn = DriverManager.getConnection(url, user, password);
    
    
    
catch (ClassNotFoundException e) {
    e.printStackTrace();
catch (SQLException e) {
    e.printStackTrace();
}
cs


여기서 한가지 의문이 생긴다.

바로 사용할 DBMS에 맞는 드라이버의 이름을 넘겨주는 부분이다.

서비스 등록 API(DrivrManager.registerDriver) 가 있으니, 분명 우리가 서비스 구현체를 생성하여 등록해주어야 할 것 만 같은데,

실제로는 그냥 Calss.forName만 사용하여 드라이버의 이름을 호출하기만 했는데 서비스를 사용할 수 있게 된다.

어떻게 저렇게 동작하는 것일까?

우선 JDBC의 서비스 제공자 인터페이스(SPI) 의 구성을 살펴보자.

코드는 http://devyongsik.tistory.com/294 블로그를 참고했다.


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
31
32
33
34
interface Connection { }
 
interface Driver {
    Connection getConnection();
}
 
class DriverManager {
    // 객체 생성 불가 클래스
    private DriverManager() { };
    
    private static final Map<String, Driver> drivers = new ConcurrentHashMap<>();
    public static final String DEFAULT_DRIVER_NAME = "default";
    
    public static void registerDefaultProvider(Driver d) {
        System.out.println("Driver 등록");
        registerDefaultProvider(DEFAULT_DRIVER_NAME, d);
    }
    
    public static void registerDefaultProvider(String name, Driver d) {
        drivers.put(name, d);
    }
    
    public static Connection getConnection() {
        return getConnection(DEFAULT_DRIVER_NAME);
    }
    
    public static Connection getConnection(String name) { 
        Driver d = drivers.get(name);
        if (d == null)
            throw new IllegalArgumentException();
        
        return d.getConnection();
    }
}
cs


자바에서는 위와 같이 Connection, Driver 라는 두개의 인터페이스를 제공하며 DriverManager 클래스를 통해 Connection을 얻을 수 있다.

API를 이용하는 클라이언트 입장에선 DriverManager의 getConnectino() 혹은 getConnection(String name) 메소드를 통해 원하는 Connection을 얻을 수 있다. Connection을 얻으면 이후에는 이 서비스 구현체를 이용해 Statement를 생성하여 SQL을 실행하고 결과를 얻을 수 있다.


이제 다시 의문점으로 돌아가서 class.forName() 메소드가 어떻게 동작하는지 알아보자.

Driver 클래스의 API를 살펴보면 아래와 같은 설명이 존재한다.

.

1
2
3
4
5
6
7
8
9
10
11
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")
cs


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

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


그럼 한번 Driver 인터페이스의 구현 클래스를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyDriver implements Driver {
    private Log logger = LogFactory.getLog(MyDriver.class);
 
    private static Driver defaultDriver;
    
    static {
        defaultDriver = new MyDriver();
        DriverManager.registerDefaultProvider(defaultDriver);
    }
    
    @Override
    public Connection getConnection() {
        System.out.println("MyDriver`s connection return");
        return null;
    }
}
cs

누군가가 DB를 만들었고 거기에 맞는 Driver를 개발자들에게 제공하기 위해 만든 Driver 객체이다.

이제 JDBC 에서 사용하는 것처럼 드라이버를 로드하고 커넥션을 얻어올 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
 
    public static void main(String[] args) {
        try {
            Class.forName("MyDriver");
            Connection conn = DriverManager.getConnection();
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
    }
}
cs


결과는 다음과 같다.

1
2
Driver 등록
MyDriver`s connection return
cs


지금까지 살펴본 서비스 제공자 프레임워크의 요점은 Driver, Connection 인터페이스와 실제 그 인터페이스를 구현하는

구현체 클래스가 완전히 분리되어 제공된다는 것이다. 인터페이스를 사용해 틀을 만들어 놓고 그 틀에 맞춰 각각의 서비스 제공자들이 자신의 서비스에 맞는 구현 클래스를 제공하도록 하는 것이다.


그렇다면 JDBC에서는 무엇이 서비스 인터페이스, 접근 API, 등록 API일까?

Connection 객체로 서비스를 이용하므로 Connection이 서비스 인터페이스,

리플렉션을 이용해 클래스가 로드될 때 드라이버를 등록하는 DriverManager.registerDriver가 제공자 등록 API,

서비스 인터페이스인 Connection에 대한 접근을 제공하는 DriverManager.getConnection이 서비스 접근 API 역할을 하게 된다.


다시 정적 팩토리 메소드의 장점으로 돌아가보자.

4. 형인자 자료형 객체를 만들 때 편리하다.

1
Map<String, List<String>> m = new HashMap<String, List<String>>();
cs

형인자 자료형 클래스의 생성자를 호출 할 때는 설사 문맥상 형인자가 명백하더라도 반드시 인자로 형인자를 전달해야 한다.

그래서 위와 같이 연달아 두 번 사용하게 된다.

이처럼 자료형 명세를 중복하면 형인자가 늘어남에 따라 길고 복잡한 코드가 만들어진다.


정적 팩토리 메소드를 이용하면 컴파일러가 형인자를 스스로 알아내도록 할 수 있다.

이러한 기법을 자료형 유추(Type Interface)라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TypeUtils {
 
    public static void main(String[] args) {
        Map<String, List<String>> m = new HashMap<String, List<String>>();
        
        Map<String, List<String>> m2 = TypeUtils.newHashMapInstance();        
    }
    
    public static <K, V> HashMap<K, V> newHashMapInstance() {
        return new HashMap<K, V>();
    }
}
 
cs

이처럼 형인자 유틸리티 클래스안에 정적 팩터리 메소드를 넣으면, 위와 같이 간결하게 작성할 수 있다.

JDK 1.7 이후 Map<String, Object> map = new HashMap<>(); 와 같은 자료형 유추 기능이 추가되었다.

<> 라는 다이아몬드 기호 연산자를 이용하여 생성자에서 자료형 유추가 가능해졌으므로, 표준 컬렉션 메소드에 팩토리 메소드를 추가할 필요가 없어졌다. 하지만, SI 분야의 경우, JDK 1.7 이전 버전을 사용하는 경우가 많으므로 형인자 유틸리티 클래스는 여전히 유용할 것이다.


이번에는 정적 팩토리 메소드만 있는 클래스를 만들면 생기는 문제에 대해 살펴보자.

1. 정적 팩토리 메소드만 있는 클래스를 만들면 생기는 가장 큰 문제는, public이나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다는 것이다.

예를 들어, 자바의 컬렉션 프레임워크에 포함된 기본 구현 클래스들의 하위 클래스는 만들 수 없다.

이는 단점으로만 볼 수 없는 것이, 상속 보다는 구성을 활용하라는 OOP의 원칙을 장려한다는 이유에서 장점으로도 볼 수 있다.


2. 정적 팩터리 메소드가 다른 정적 메소드와 확연히 구분되지 않는다.


위의 그림처럼, 생성자는 다른 메소드와 뚜렷이 구별되지만, 정적 팩토리 메소드는 그렇지 않다.

그러니 생성자 대신 정적 팩토리 메소드를 통해 객체를 만들어야 하는 클래스는 사용법을 파악하기가 쉽지 않다.

클래스나 인터페이스 주석을 통해 정적 팩토리 메소드임을 널리 알리거나, 이름을 지을 때 조심하는 수 밖에 없다.

보통 정적 팩토리 메소드의 이름으로는 다음과 같은 것들을 사용한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
* valueOf
인자로 주어진 값과 같은 값을 갖는 객체를 반환한다.
이러한 정적 팩토리 메소드는 형변환 메소드이다.
 
* of
valueOf를 더 간단하게 쓴 것이다.
EnumSet 덕분에 인기를 모은 이름이다.
 
* getinstance
인자에 기술된 객체를 반환하지만 인자와 같은 값을 갖지 않을 수도 있다.
싱글턴 패턴에 자주 등장하는데, 싱글턴 패턴에서 이 메소드는 인자 없이 항상 같은 객체를 반환한다.
 
* newInstance
getInstance와 같지만 호출할 때마다 다른 객체를 반환한다.
 
* getType
getInstance와 같지만 반환될 객체의 클래스와 다른 클래스에 팩토리 메소드가 있을 때 사용한다.
Type은 팩토리 메소드가 반환할 객체의 자료형이다.
 
* newType
newInstance와 같지만 반환된 객체의 클래스와 다른 클래스에 팩토리 메소드가 있을 때 사용한다.
Type은 팩토리 메소드가 반환할 객체의 자료형이다.
 
cs


요약

정적 팩토리 메소드와 public 생성자는 그 용도가 서로 다르며, 차이점과 장단점을 이해하는 것이 중요하다.

정적 팩토리 메소드가 효과적인 경우가 많으니, 정적 팩토리 메소드를 고려해보지도 않고 무조건 public 생성자를 만드는 것은 삼가해야 한다.