안녕하세요? 이번 시간부터 Effective Java 도서를 읽으며 학습한 내용을 코드 예제를 생각하여 함께 작성해보려고 합니다. 모든 코드는 Github에 업로드되어 있어 편하게 확인하실 수 있습니다.
(공부한 내용을 먼저 Github에 정리했으며, 블로그에도 포스팅할 예정이니 참고해주세요.)
생성자 대신 정적 팩터리 메서드를 고려하라
2장 객체 생성과 파괴의 item 1은 "생성자 대신 정적 팩터리 메서드를 고려하라"는 내용을 다루고 있다.
이번 장의 포인트는 다음과 같다.
포인트
public 생성자를 사용해서 객체를 생성하는 방법 말고,
public static 팩토리 메소드를 사용해서 해당 클래스의 인스턴스를 만드는 방법도 있다.
이제 정적 팩터리 메서드를 고려했을 때 장점과 단점을 알아보자.
장점1. 이름을 가질 수 있다.
생성자는 생성되는 객체의 특성을 직관적으로 설명하지는 않는다.
메서드 명을 지음으로서 어떤 객체가 생성되는지 더 구체적으로 알 수 있다.
또한 생성자는 똑같은 타입을 파라미터로 받는 생성자 두 개를 만들 수 없는데 이때도 정적 팩토리 메서드로 가능하다.
아래 예제 코드와 함께 알아보자.
정적 팩터리 메서드로 객체를 생성할 경우, Foo.withName("potter")과 같이 이름을 가진 메서드를 가질 수 있다.
class Foo {
String name;
String gender;
public Foo() {}
// 생성자
public Foo(String name) {
this.name = name;
}
// 메서드명을 지정하여 어떤 객체가 생성되는지 더 구체적으로 알 수 있다.
public static Foo withName(String name) {
return new Foo(name);
}
// (생성자와 다르게) 정적 팩토리 메서드는 똑같은 타입을 파라미터로 받는 메서드를 만들 수 있다.
public static Foo withGender(String gender) {
Foo foo = new Foo();
foo.gender = gender;
return foo;
}
}
public class Simulation {
public static void main(String[] args) {
Foo foo = new Foo("peanuts"); // 생성자로 객체를 생성할 경우
Foo foo2 = Foo.withName("potter"); // 정적 팩터리 메서드로 객체를 생성할 경우
}
}
장점2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
"Foo.getFoo_value()" 를 통하여, 미리 생성해둔 인스턴스를 재활용하여 불필요한 객체 생성을 피할 수 있다.
class Foo {
String name;
String gender;
public Foo() {}
private static final Foo FOO_VALUE = new Foo(); // 정적 상수 변수 선언
public Foo(String name) {
this.name = name;
}
public static Foo withName(String name) {
return new Foo(name);
}
public static Foo withGender(String gender) {
Foo foo = new Foo();
foo.gender = gender;
return foo;
}
public static Foo getFoo_value() {
return FOO_VALUE;
}
}
public class Simulation {
public static void main(String[] args) {
Foo foo = new Foo("peanuts");
Foo foo2 = Foo.withName("potter");
// 미리 생성해둔 인스턴스를 재활용하여 불필요한 객체 생성을 피할 수 있다.
Foo foo3 = Foo.getFoo_value();
Foo foo4 = Foo.getFoo_value();
System.out.println(System.identityHashCode(foo3)); //990368553
System.out.println(System.identityHashCode(foo4)); //990368553
}
}
- 불변(immutable) 클래스(아이템 17)인 경우나 매번 새로운 객체를 만들 필요가 없는 경우에 미리 만들어둔 인스턴스 또는 캐시해둔 인스턴스를 반환할 수 있다.
- Boolean.valueOf(boolean) 메소드도 그 경우에 해당한다.
장점3. 반환 타입의 하위 타입 객체 반환 가능
리턴 타입은 인터페이스로 지정해서 구현체는 노출시키지 않을 수 있다.
아래 예제 코드와 함께 정리해보자.
// 인터페이스
interface Singable {
void sing();
}
// 인스턴스 반환을 위한 팩터리 클래스 생성
class SingerFactory {
public static Singable createSinger(String name) {
if ("ADELE".equalsIgnoreCase(name))
return new Adele();
else if ("IU".equalsIgnoreCase(name))
return new IU();
return null;
}
}
// 구현 클래스 1
class IU implements Singable {
@Override
public void sing() {
System.out.println("드라마");
}
}
// 구현 클래스 2
class Adele implements Singable {
@Override
public void sing() {
System.out.println("lalala");
}
}
public class Main {
public static void main(String[] args) {
Singable singer1 = SingerFactory.createSinger("IU");
singer1.sing(); //드라마
}
}
API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
예를 들어 java.util.Collection에서 정적 팩터리 메서드를 통해 45개의 유틸리티 구현체를 제공한다.
인터페이스만 노출되므로 '개념적인 무게 줄이기' 가능. 프로그래머가 알아야하는 개수와 난이도가 줄어든다.
- 자바 8부터는 public static 메서드를 인터페이스 추가 가능하다.
- 그래서 굳이 Collecions라는 클래스를 만들지 않고도 인터페이스에 구현 가능하다.
- 하지만 private static은 자바 9부터 가능하다.
- 자바9를 쓰면 private static까지 인터페이스에 추가할 수 있으니 java.util.Collection 같은 유틸성 클래스들은 필요 없어진다.
장점4. 입력 매개변수에 따라 다른 클래스의 객체를 반환할 수 있다.
매개변수에 따라 다른 클래스 객체를 반환할 수 있다.
아래 코드와 같이, createSinger() 안에 값을 넣어 다른 객체를 생성할 수 있다.
Singable singer1 = SingerFactory.createSinger("IU");
singer1.sing(); //드라마
Singable singer1 = SingerFactory.createSinger("Adele");
singer1.sing(); //lalala
장점 5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
인터페이스나 클래스가 만들어지는 시점에서 하위 타입의 클래스가 존재하지 않아도
나중에 만들 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받아서 사용가능하다.
반환값이 인터페이스가 되며 정적 팩터리 메서드이 변경없이 구현체를 바꿔 끼울 수 있다.
public static void main(String[] args) {
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
// 첫번째 구현체를 가져옴(있을수도 없을수도 optional로 가져옴
Optional<HelloService> helloServiceOptional = loader.findFirst();
helloServiceOptional.ifPresent(h -> {
System.out.println(h.hello());
});
}
API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
- ServiceLoader<>는 자바가 기본으로 제공해주는 정적 팩토리 메소드이다.
- ServiceLoader.load(HelloService.class); (iter 반환 타입 → 여러개의 HelloService 를 구현한 모든 구현체를 가져온다.)
- 책에서는 JDBC를 예로 드는데, JDBC의 경우 Mysql oracle 어떤 데이터베이스를 사용할지 모르는 상태에서도 코드 작성이 가능하다.
단점1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 그래서 java.util.Collections는 상속할 수 없다.
- 상속보다 컴포지션을 사용(아이템18) 도록 유도하고 불변 타입(아이템17)로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점이 될 수도 있기는 하다.
단점2. 프로그래머가 찾기 어렵다.
생성자의 경우 javaDoc이 자동으로 컬럼을 두어 표시하지만 정적 팩토리 메서드의 경우 그런 표시가 없기 때문에 프로그래머가 찾기 어렵다.
'Dev Books > Effective Java' 카테고리의 다른 글
[item 27] 비 검사 경고를 제거하라 (0) | 2024.05.31 |
---|---|
[item 26] 로 타입은 사용하지 말라 (0) | 2024.05.31 |
[item 24] 멤버 클래스는 되도록 static으로 만들라. (0) | 2024.04.05 |
[item 3] private 생성자나 열거 타입으로 싱글턴임을 보증하라 (1) | 2024.01.14 |
[item 2] 생성자에 매개변수가 많다면 빌더를 고려하라. (0) | 2024.01.14 |
댓글