들어가면서
이번 부트캠프를 하면서 팀별 스터디를 진행하게 되었다. 팀별 스터디 주제로 정해진 게 이펙티브 자바였는데, 이 책이 이해하기 쉽지 않을 것이라는 것을 알고 있었다.
가장 우려되는 점이 경험 부족이었다. 보통 이런 책들은 현업에서 코드를 많이 짜보고, 그만큼 많은 경험을 쌓고 난 뒤에 보는 것이 가장 좋다고 생각한다. 선배 개발자 형이 해준 말이 “책을 읽으면서 코드가 떠올라야 한다”라고 했었는데 매우 공감한다. 그래서 잘못 읽으면 오히려 독이 될 이 책을 어떻게 읽고 정리를 할지 고민을 많이 하게 되었다. 그러던 중, 인프런에서 백기선님의 이펙티브 자바 강의가 있다는 것을 알게 되어 바로 결제하였다. 현업자분이 실제 코드와 함께 설명을 해주어 내가 걱정했던 부분을 해결해 주셨다.
그래서 앞으로 이펙티브 자바, 강의와 함께 블로그에 정리해두려고 한다.
정적 팩터리 메서드란?
정적 팩토리 메서드(Static Factory Method)는 객체 지향 프로그래밍에서 생성자(constructor) 대신 객체를 생성하는 정적(static) 메서드를 말한다.
이번 포스트에서는 정적 팩토리 메서드를 사용하였을 때의 장점과 단점, 어떤 상황에서 사용하는 지를 알아보려 한다.
정적 팩터리 메서드를 사용했을 때의 장점
이름을 가질 수 있다.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
private OrderStatus orderStatus;
public Order(Product product, boolean prime){
this.product = product;
this.prime = prime;
}
}
다음과 같은 클래스가 있다. 여기서, 긴급한 주문의 객체를 새로 생성해야 한다고 가정해보자.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
private OrderStatus orderStatus;
public Order(Product product, boolean prime){
this.product = product;
this.prime = prime;
}
public Order(Product product, boolean urgent){
this.product = product;
this.urgent = urgent;
}
}
그렇다고 위 코드처럼 작성을 하게 되면 타입이 같은 매개변수가 두개 들어왔기 때문에 컴파일 오류가 발생한다. 그러면 이걸 해결할 수 있는 방법은 없을까?
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
private OrderStatus orderStatus;
public Order(Product product, boolean prime){
this.product = product;
this.prime = prime;
}
public Order(boolean urgent, Product product){
this.product = product;
this.urgent = urgent;
}
}
다음과 같이 작성하게 되면 컴파일 오류는 면할 수 있다. 하지만 그렇게 바람직한 방법은 아닌 것 같다. 이게 긴급한 객체인지 외부에서 알 수 있는 방법이 없기 때문이다. 이 경우, 정적 팩토리 메서드 패턴을 사용하면 다음과 같이 작성할 수 있다.
public class Order {
private boolean prime;
private boolean urgent;
private Product product;
private OrderStatus orderStatus;
public static Order primeOrder(Product product) {
Order order = new Order();
order.prime = true;
order.product = product;
return order;
}
public static Order urgentOrder(Product product) {
Order order = new Order();
order.urgent = true;
order.product = product;
return order;
}
}
이렇게 작성하게 되면 이 주문이 긴급한지 아닌지 확실히 알 수 있다.
호출될때마다 인스턴스를 새로 생성하지는 않아도 된다.
다음과 같은 제품 객체가 있다.
public class Product {
public static void main(String[] args) {
TestSettings testSettings1 = new TestSettings();
TestSettings testSettings2 = new TestSettings();
System.out.println(testSettings1);
System.out.println(testSettings2);
}
}
여기서 설정 객체를 두개 생성해서 비교해보자.
위 그림처럼 객체의 해시코드 값이 다른 것을 알 수 있다. 이런 설정 파일들은 생성자 통제가 불가능하다. 그래서 정적 팩터리 메서드를 사용하면 다음과 같이 설정해 줄수 있다.
public class Settings {
private boolean useAutoSteering;
private boolean useABS;
private Difficulty difficulty;
private Settings() {}
private static final Settings SETTINGS = new Settings();
public static Settings getInstance() {
return SETTINGS;
}
}
다음 클래스와 같이 객체 생성을 외부에서 할 수 없도록 private으로 제한한 후, 정적 팩토리 메서드 getInstance()
를 이용하여 미리 만들어놓은 Setting 객체를 가지고 올 수 있게 된다. 이펙티브 자바에서는 플라이웨이트 패턴과 비슷하다고 한다.
NOTE플라이웨이트 패턴(flyweight)
- 객체를 가볍게 만들어 메모리 사용을 줄이는 패턴(많은 수의 유사한 객체를 효율적으로 관리 가능)
- 자주 변하는 속성(또는 외적인 속성, extrinsit)과 변하지 않는 속성(또는 내적인 속성, intrinsit)을 분리하고 재사용하여 메모리 사용을 줄일 수 있다.
- 내적(intrinsic) 상태: 여러 객체 간에 공유될 수 있는 불변 데이터
- 외적(extrinsic) 상태: 컨텍스트에 따라 변하는 데이터로, 클라이언트가 관리하거나 전달
예를 들어, 클래스에서 메모리 용량이 많은 불변 데이터(내적 상태)가 있고 이와 유사한 여러 객체를 사용해야 할 때, 이 불변 데이터를 별도의 공유 가능한 클래스로 분리한다. 이후 플라이웨이트 팩토리를 통해 객체를 요청할 때, 팩토리는 먼저 이미 생성된 동일한 내적 상태를 가진 객체가 있는지 확인하고, 있다면 그 객체를 반환하고 없다면 새로 생성하여 캐시에 저장한 후 반환한다. 클라이언트는 필요한 외적 상태를 직접 관리하거나 메서드 호출 시 매개변수로 전달한다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
코드로 보자.
public interface HelloService {
String hello();
static HelloService of(String lang){
if(lang.equals("ko")){
return new KoreanLanguageService();
} else{
return new EnglishLanguageService();
}
}
}
위 코드처럼 매개변수를 받고, 매개변수에 따라 다른 클래스를 반환하게 할 수 있다.
public static void main(String[] args){
HelloService eng = HelloService.of("eng");
System.out.println(eng.hello());
// 출력: hello
}
정적 팩터리 메서드를 반환하는 시점에서는 반환할 객체의 클래스가 존재하지 않아도 된다.
public class HelloServiceFactory {
public static void main(String[] args) {
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println(h.hello());
});
}
}
위 코드를 보면 정적 팩토리 메서드만 존재하고, 구현체는 존재하지 않는다.(HelloService
도 인터페이스이다.) 하지만 실행하면 다음과 같은 결과가 나온다.
그래서 어떻게 Ni Hao가 나왔는지, 디버깅을 해보면
Optional 내부에 ChineseHelloService 구현체가 들어가 있는 것을 볼 수 있다.
이 ChineseHelloService는 다른 프로젝트에서 구현체를 구현한 것으로, 다음과 같이 META-INF/services 하위 폴더(두개의 디렉토리의 하위 디렉토리이다.)에 클래스가 존재하는 위치의 경로를 등록시켜준 것이다.
NOTEServiceLoader
ServiceLoader는 Java의 핵심 API로, 서비스 제공자(Service Provider) 프레임워크를 구현하기 위한 클래스이다. 이 클래스는 Java 6(Java SE 6)부터 도입되었으며,
java.util
패키지에 포함되어 있다.ServiceLoader는 느슨한 결합(loose coupling)을 가능하게 하는 메커니즘으로, 인터페이스나 추상 클래스의 구현체를 동적으로 발견하고 로드할 수 있게 해준다. 이를 통해 모듈화된 애플리케이션 개발이 가능해진다.
굳이 그렇게 해야 되나요?
public static void main(String[] args) {
// 1번
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println(h.hello());
});
// 2번
HelloService helloService = new ChineseHelloService();
System.out.println(helloService.hello());
}
dependency에 정상적으로 등록해줬다면 2번처럼 작성해도 똑같이 작동할 것이다. 하지만, 1번과 2번 코드는 확연히 다른 차이가 있다. 1번은 ChineseHelloService와 의존성이 없지만, 2번은 존재한다.
예를 들어, JDBC 드라이버가 비슷한 예이다. 우리는 어떤 데이터베이스 코드를 사용할 지 모른다. Mysql에서 동작하게 할 수 도 있고, 다른 데이터베이스를 사용해야 할 수도 있다. 그래서 JDBC도 ServiceLoader를 사용하여 드라이버를 동적으로 로드한다.(JDBC 4.0 이후에 해당된다. 3.0은 명시적으로 드라이버 클래스를 로드해야 했다.)
단점
정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다. 상속불가능하다.
상속을 하려면 public이나 protected 생성자가 필요하니 그럴 수 있다. 굳이 만든다고 한다면 새로운 클래스를 만들고, 정적 팩토리 메서드로 만든 클래스를 참조하여 수정하는 방법이 있을 수 있다.
정적 팩터리 메서드는 개발자들이 찾기 어렵다.
Javadoc을 작성하는 경우 생성자는 javadoc에 잘 보이지만, 정적 팩토리 메소드를 사용하면 다른 함수들도 많기 때문에, 잘 보이지 않는 경우가 많다. 그래서 주석 처리를 잘 해주고, 보편적으로 사용하는 함수 이름들을 사용하는 것이 좋다.
- 정적 메서드 패턴에 자주 사용되는 이름
Method | Description | Example |
---|---|---|
of | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 정적 메서드 | Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING); |
valueOf | from과 of의 더 자세한 버전 | BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); |
instance 혹은 getInstance | (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다. | StackWalker luke = StackWalker.getInstance(options); |
create 혹은 newInstance | instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다. | Object newArray = Array.newInstance(classObject, arrayLen); |
getType | getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. “Type”은 팩터리 메서드가 반환할 객체의 타입이다. | FileStore fs = Files.getFileStore(path) |
newType | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. “Type”은 팩터리 메서드가 반환할 객체의 타입이다. | BufferedReader br = Files.newBufferedReader(path); |
type | getType과 newType의 간결한 버전 | List<Complaint> litany = Collections.list(legacyLitany); |
from | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드 | Date d = Date.from(instant); |
출처
- 이펙티브 자바 아이템 1: 생성자보다 정적 팩터리 메서드를 고려하라
- 백기선의 이펙티브 자바(인프런)