Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라.
정적 팩터리 메서드란?
정적(static) 팩토리(factory) 메서드 (method)
객체 생성을 캡슐화하는 기법
좀 더 구체적으로는 객체를 생성하는 메서드를 만들고, static으로 선언하는 기법이다.
생성자 호출 방식이 아닌, 메서드 호출 방식으로 객체를 생성하는 것이다.
정적 팩토리 메서드는 객체의 생성을 담당하는 클래스 메서드이다.
일반적으로, Java를 공부할 때 객체의 생성을 위해 `new` 키워드를 사용한다.
그렇다면 어떻게 메서드를 이용해서 객체를 만들 수 있을까?
`new`를 직접적으로 사용하지 않을 뿐, 정적 팩토리 메서드라는 클래스 내에 선언되어 있는 메서드를 내부의 `new`를 이용해 객체를 생성하여 반환하는 것이다.
정적 팩토리 메서드를 통해 `new`를 간접적으로 사용한다.
코드를 통해 형태를 비교해보자!
생성자를 이용해 인스턴스를 생성하는 방법
class Book {
private String title;
private long isbn;
// 생성자를 private화 하여, 외부에서 생성자 호출 차단
public Book(String title) {
this.title = title;
}
}
public static void main(String[] args) {
Book book = new Book("이펙티브 자바");
}
정적 팩터리 메서드를 이용해 인스턴스를 생성하는 방법
class Book {
private String title;
private long isbn;
// 생성자를 private화 하여, 외부에서 생성자 호출 차단
private Book(String title) {
this.title = title;
}
//정적 팩토리 메서드
public static Book titleOf(String title) {
return new Book(title);
}
}
public static void main(String[] args) {
Book book = Book.titleOf("이펙티브 자바");
}
생성자로 인스턴스를 생성하는 것을 방지하기 위해 생성자의 접근 제어자를 private으로 설정한다.
다음과 같은 구조라고 생각하면된다.
개발을 하면서, 알게모르게 정적 팩토리 메서드 유형들을 종종 보았을 텐데, 예시를 알아보자.
다음 코드는 boolean 기본 타입의 박싱 클래스인 Boolean 객체 참조로 변환해주는 예시이다.
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
Boolean.valueOf(String) 메서드는 문자열의 내용에 따라 `Boolean.TRUE` 또는 `Boolean.FALSE` 를 반환하여, 불필요한 Boolean 객체 생성을 방지한다. 이는 메모리 사용을 최적화하는 효과를 갖는다.
다음 코드는 java.util 패키지에 포함되어 있는 LocalTime 클래스의 정적 팩토리 메서드이다.
public static LocalTime of(int hour, int minute) {
HOUR_OF_DAY.checkValidValue(hour);
if (minute == 0) {
return HOURS[hour]; // for performance
}
MINUTE_OF_HOUR.checkValidValue(minute);
return new LocalTime(hour, minute, 0, 0);
}
LocalTime time = LocalTime.of(9,30);
of 메서드를 사용해 직접 생성자를 통해 객체를 생성하는 것이 아니라 메서드를 통해 객체를 생성하고 있다.
`getInstance`, `of`, `getType`, `from` 과 같이 정적 팩토리 메서드는 이름을 가지고 있어, 객체 생성의 의도를 명확하게 전달한다.
정적 팩터리 메서드가 생성자보다 좋은 장점 5가지
1. 이름을 가질 수 있다.
정적 팩터리 메서드는 생성자에 비해 가독성이 높다.
public class Book {
private String title;
private long isbn;
public Book(String title) {
if (title.equals("이펙티브 자바")) {
this.title = title;
}
}
public Book(long isbn) {
if (isbn == 1) {
this.isbn = isbn;
}
}
}
public static void main(String[] args) {
Book book1 = new Book("이펙티브 자바");
Book book2 = new Book(1);
}
생성자를 이용해 객체를 생성할 때, '코드 작성자'는 이미 내부 구조를 알기 때문에 생성자의 몇 번째 인자에 어느 값이 들어야 하는 지 알고 있지만, 객체의 내부 구조를 모르는 사람은 인스턴스를 생성할 때 몇 번째 인자에 어떤 값이 들어가야 하는 지 모른다.
자바의 특성상 생성자는 클래스 이름으로 고정되기 때문에 발생하는 문제이다.
public class Book {
private String title;
private long isbn;
private Book(String title, long isbn) {
this.title = title;
this.isbn = isbn;
}
public static Book createByIsbn(long isbn) {
if (isbn == 1) {
return new Book("이펙티브 자바", 1);
}
throw new IllegalArgumentException("일치하는 책이 없습니다.");
}
public static Book createByTitle(String title) {
if (title.equals("이펙티브 자바")){
return new Book(title, 1);
}
throw new IllegalArgumentException("일치하는 책이 없습니다.");
}
}
public static void main(String[] args) {
Book book1 = Book.createByTitle("이펙티브 자바");
Book book2 = Book.createByIsbn(1);
}
혼자 개발을 한다면 상관없겠지만, 협업하는 환경에서 다른 사람이 코드만으로 의도를 파악하기 어렵다.
하지만, 이름을 가질 수 있는 정적 팩터리 메서드에는 제약이 없다.
2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
인스턴스의 생성을 제어할 수 있다.
`new` 키워드를 사용하면, 객체는 무조건 새로 생성된다.
만약 자주 사용 될 것 같은 인스턴스는 클래스 내부에 미리 생성해 놓은 다음 반환하면 코드를 최적화 할 수 있다.
정적 팩터리 메서드는 한단계 거쳐 간접적으로 객체의 생성 조건을 체크하고, 필요에 따라 기존 인스턴스를 반환하거나 새로운 인스턴스를 생성할 수 있기 때문에 객체 생성의 유연성을 높이고, 객체 재사용으로 메모리를 아낄 수 있도록 유도한다.
대표적으로 싱글톤 디자인 패턴과 인스턴스 캐싱하는 코드를 살펴보자
싱글톤 패턴
public class Singleton {
// 클래스 로딩 시점에 인스턴스를 생성
private static final Singleton instance = new Singleton();
// private 생성자로 외부에서 인스턴스 생성을 막음
private Singleton() {}
// 유일한 인스턴스를 반환하는 메서드
public static Singleton getInstance() {
return instance;
}
}
인스턴스 캐싱
public enum Color {
RED, GREEN, BLUE;
// 정적 팩토리 메서드
public static Color fromString(String colorName) {
switch (colorName.toUpperCase()) {
case "RED":
return RED;
case "GREEN":
return GREEN;
case "BLUE":
return BLUE;
default:
throw new IllegalArgumentException("Unknown color: " + colorName);
}
}
public static void main(String[] args) {
Color c1 = Color.fromString("red");
Color c2 = Color.fromString("RED");
// c1과 c2는 동일한 인스턴스임
System.out.println(c1 == c2); // true
Color c3 = Color.fromString("blue");
Color c4 = Color.fromString("BLUE");
// c3과 c4도 동일한 인스턴스임
System.out.println(c3 == c4); // true
}
}
import java.util.HashMap;
import java.util.Map;
public class Person {
private final String name;
private Person(String name) {
this.name = name;
}
private static final Map<String, Person> personCache = new HashMap<>();
// 정적 팩토리 메서드
public static Person getInstance(String name) {
return personCache.computeIfAbsent(name, Person::new);
}
public String getName() {
return name;
}
public static void main(String[] args) {
Person p1 = Person.getInstance("Alice");
Person p2 = Person.getInstance("Alice");
// p1과 p2는 동일한 인스턴스임
System.out.println(p1 == p2); // true
Person p3 = Person.getInstance("Bob");
Person p4 = Person.getInstance("Bob");
// p3과 p4도 동일한 인스턴스임
System.out.println(p3 == p4); // true
// p1과 p3는 다른 인스턴스임
System.out.println(p1 == p3); // false
}
}
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
반환 타입의 하위 타입 객체를 반환할 수 있다. -> 인터페이스 기반 프로그래밍에서 유용하게 사용 가능
정적팩토리 메서드는 구현 클래스를 숨기고 인터페이스 타입으로 객체를 반환할 수 있어, 클라이언트 코드와의 결합도를 낮춘다.
따라서 객체 생성 로직을 캡슐화하고 시스템의 확장성과 유지보수성을 향상시킨다.
// 인터페이스 정의
interface Animal {
void speak();
}
// 하위 타입 1
class Dog implements Animal {
@Override
public void speak() {
System.out.println("Woof Woof");
}
}
// 하위 타입 2
class Cat implements Animal {
@Override
public void speak() {
System.out.println("Meow Meow");
}
}
// 하위 타입 3
class Bird implements Animal {
@Override
public void speak() {
System.out.println("Tweet Tweet");
}
}
// 정적 팩토리 메서드를 포함하는 클래스
class AnimalFactory {
// 정적 팩토리 메서드
public static Animal createAnimal(String animalType) {
if ("DOG".equalsIgnoreCase(animalType)) {
return new Dog();
} else if ("CAT".equalsIgnoreCase(animalType)) {
return new Cat();
} else if ("BIRD".equalsIgnoreCase(animalType)) {
return new Bird();
}
throw new IllegalArgumentException("Unknown animal type");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// 정적 팩토리 메서드를 사용하여 객체 생성
Animal dog = AnimalFactory.createAnimal("DOG");
Animal cat = AnimalFactory.createAnimal("CAT");
Animal bird = AnimalFactory.createAnimal("BIRD");
// 생성된 객체 사용
dog.speak(); // 출력: Woof Woof
cat.speak(); // 출력: Meow Meow
bird.speak(); // 출력: Tweet Tweet
}
}
1. 인터페이스 정의
- Animal 인터페이스는 speak 메서드를 정의
2. 하위 타입
- Dog, Cat, Bird 클래스는 Animal 인터페이스를 구현한다.
- 각 클래스는 speak 메서드를 자신만의 방식으로 구현한다.
3. 정적 팩토리 메서드
- AnimalFactory 클래스에는 createAnimal이라는 정적 팩토리 메서드가 존재한다.
- 이 메서드는 입력 문자열 AnimalType에 따라 Dog, Cat, Bird 객체를 반환한다.
- 반환타입은 Animal 인터페이스
4. 메인 클래스
- AnimalFactory.createAnimal 메서드를 사용해 Dog, Cat, Bird 객체를 생성하고 반환받는다.
- 생성된 객체는 Animal 타입으로 반환되지만, 실제로는 Dog, Cat, Bird 타입의 인스턴스이다.
- speak 메서드를 호출하면 각 클래스의 구현에 따라 출력이 달라진다.
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
입력 매개 변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
내용은 3번 장점과 동일하다.
메서드이기 때문에 파라미터를 받을 수 있고, 그렇기 때문에 파라미터 별로 분기처리하여 다른 객체를 반환할 수 있다는 의미이다.
하단 내용처럼 분기처리가 가능하다는 내용!
// 정적 팩토리 메서드를 포함하는 클래스
class AnimalFactory {
// 정적 팩토리 메서드
public static Animal createAnimal(String animalType) {
if ("DOG".equalsIgnoreCase(animalType)) {
return new Dog();
} else if ("CAT".equalsIgnoreCase(animalType)) {
return new Cat();
} else if ("BIRD".equalsIgnoreCase(animalType)) {
return new Bird();
}
throw new IllegalArgumentException("Unknown animal type");
}
}
이펙티브 자바 책에서는 EnumSet 클래스가 생성자 없이 public static 메서드로 `allOf()`, `of()` 등을 제공하는데, 리턴하는 객체의 enum 타입의 개수에 따라 `ReqularEnumSet` 또는 `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. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
정적 팩터리 메서드의 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다.
이 내용도 장점 3, 4가 이어지는 내용이다.
반환할 객체의 클래스가 존재하지 않아도 된다.
- 메서드를 작성할 때, 반환할 객체의 구체적인 클래스가 반드시 존재하지 않아도 된다.
- 정적 팩토리 메서드는 인터페이스나 상위 클래스를 반환 타입으로 지정하고, 구체적인 클래스는 나중에 정의 될 수 있다.
- 인터페이스와 다형성을 활용한 설계의 유연성을 강조
- 어떤 타입의 객체를 반환할지 결정하는 로직이 팩토리 메서드 내부에 있기 때문에, 클라이언트는 팩토리 메서드를 호출하기만 하면 되고, 클라이언트는 어떤 클래스의 객체가 생성되는 지 알 필요가 없다.
// 인터페이스 정의
interface Animal {
void speak();
}
// 정적 팩토리 메서드를 포함하는 클래스
class AnimalFactory {
// 정적 팩토리 메서드
public static Animal createAnimal(String animalType) {
if ("DOG".equalsIgnoreCase(animalType)) {
return new Dog(); // Dog 클래스가 나중에 정의됨
} else if ("CAT".equalsIgnoreCase(animalType)) {
return new Cat(); // Cat 클래스가 나중에 정의됨
}
throw new IllegalArgumentException("Unknown animal type");
}
}
// 메인 클래스
public class Main {
public static void main(String[] args) {
// 정적 팩토리 메서드를 사용하여 객체 생성
Animal dog = AnimalFactory.createAnimal("DOG");
Animal cat = AnimalFactory.createAnimal("CAT");
// 생성된 객체 사용
dog.speak(); // 출력: Woof Woof
cat.speak(); // 출력: Meow Meow
}
}
// 나중에 구체적인 클래스 정의
class Dog implements Animal {
@Override
public void speak() {
System.out.println("Woof Woof");
}
}
class Cat implements Animal {
@Override
public void speak() {
System.out.println("Meow Meow");
}
}
그래서 ?
정적 팩터리 메서드를 활용하면, 반환할 객체의 클래스가 컴파일 타임에는 존재하지 않아도 된다. 특정한 하위 타입이 필요한 런타임 시점 때, 정적 팩토리 메서드 내에서 찾을 수 있으면 된다.
이게 무엇을 의미하는데?
이런 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다.
서비스 제공자 프레임워크 ?
다양한 서비스 제공자들이 하나의 서비스를 구성하는 시스템
클라이언트가 실제 구현된 서비스를 이용할 수 있도록 하는데, 클라이언트는 세부적인 구현 내용을 몰라도 서비스를 이용할 수 있다.
JDBC는 MySQL, ORACLE 등의 서비스 제공자들이 JDBC라는 하나의 서비스를 구성한다.
서비스 제공자 프레임워크의 구성 (핵심 컴포넌트 3종과 종종 함께 사용되는 네번째 컴포넌트)
- 구현체의 동작을 정의하는 서비스 인터페이스
- 제공자가 구현체를 등록할 때 사용하는 제공자 등록 API
- 클라이언트가 서비스의 인스턴스를 얻을 때 사용하는 서비스 접근 API
- (선택) 서비스 인터페이스의 인스턴스를 생성하는 팩터리 객체를 설명하는 서비스 제공자 인터페이스
대표적인 서비스 제공자 프레임워크 JDBC
서비스 제공자 프레임워크에서의 제공자(provider)는 서비스의 구현체다. 그리고, 이 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트로부터 구현체로부터 분리해준다.
- 서비스 인터페이스 : Connection
- 제공자 등록 API : DriverManager.registerDrive()
- 서비스 접근 API : DriverManager.getConnection()
- 서비스 제공자 인터페이스 : Driver
// 1. JDBC 드라이버 로드
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 데이터베이스 연결 설정
String url = "jdbc:mysql://localhost:3306/mydatabase";
String user = "root";
String password = "password";
// getConnection : 서비스 접근 API
connection = DriverManager.getConnection(url, user, password);
하단의 블로그 글을 보면, JDBC가 어떻게 정적 팩토리 메서드 를 이용해 서비스 제공자 프레임워크를 구현했는지 알 수 있다.
https://devyongsik.tistory.com/294
하단의 블로그글을 참고하면, 서비스 제공자 프레임워크를 가공한 실제 사용사례가 있다. 5번 장점이 어려웠는데, 하단 블로그 글을 보고 이해했다 :)
https://sihyung92.oopy.io/java/service-provider-framework
정적 팩터리 메서드의 단점
1. 상속을 하려면 public 이나 protected 생성자가 필요하니, 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
생성자가 private 이기 때문에 상속이 불가능하다.
정적 팩터리 메서드는 인스턴스를 생성하는 방법을 캡슐화한다. 이를 통해 객체 생성로직을 자유롭게 제어 할 수 있다.그러나, 정적 팩터리 메서드만 제공하고 생성자를 private으로 선언하면, 외부 클래스나 하위 클래스는 생성자를 호출할 수 없다.
하단의 코드는 상속받은 클래스가 정적팩터리 메서드 선언으로 생성자를 private으로 선언했기에
상속받는 하위 클래스에서 컴파일 에러가 발생하는 예제이다.
public class Parent {
private int value;
// private 생성자
private Parent(int value) {
this.value = value;
}
// 정적 팩터리 메서드
public static Parent create(int value) {
return new Parent(value);
}
public int getValue() {
return value;
}
}
public class Child extends Parent {
private int additionalValue;
public Child(int value, int additionalValue) {
// Parent 생성자를 호출할 수 없기 때문에 컴파일 에러 발생
super(value);
this.additionalValue = additionalValue;
}
public int getAdditionalValue() {
return additionalValue;
}
}
상속을 가능하게 하려면 부모 클래스의 생성자를 public 혹은 protected로 선언해야하는데,
이 경우, 정적 팩터리 메서드의 장점 중 하나인 생성자 호출의 통제력이 감소한다.
따라서 이 제약은 컴포지션을 사용하도록 유도하고, 불변 타입으로 만들려면 이 제약을 지켜야한다는 점에서 장점으로 받아들일 수 있다.
으악, 컴포지션은 차차 정리해보도록 한다 ..
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
API 문서에서의 불편함
생성자처럼 Javadoc API 문서에 명확히 드러나지 않는다.
따라서, 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
API 문서 잘 써두고, 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화한다.
정적 팩터리 메서드 네이밍 컨벤션
- `from` : 하나의 매개 변수를 받아서 객체를 생성 `Date d = Date.from(instant);`
- `of` : 여러개의 매개 변수를 받아서 객체를 생성 `Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);`
- `valueOf` : from과 of의 더 자세한 버전 `BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)`
- `instance` || `getInstance` : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
- `newInstance` || `create` : 새로운 인스턴스 생성
- `getType` : 다른 타입의 인스턴스 생성 `FileStore fs = Files.getFileStore(path)`
- `newType` : 다른 타입의 새로운 인스턴스 생성 `BufferedReader br = Files.newBufferedReader(path);`
- `type` : `List<Complaint> litany = Collections.list(legacyLitany);`
그래서 결론은?
이펙티브 자바에서는 정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니, 상대적인 장단점을 이해하고 사용하는 것이 좋다고 한다. 그렇다고 하더라도, 정적팩터리를 사용하는 게 유리한 경우가 더 많으므로, 무작정 public 생성자를 제공하던 습관이 있으면 고치자고 마지막으로 정리했다.
정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것 뿐아니라, 좀 더 가독성 좋은 코드를 작성하고, 객체지향적으로 프로그래밍 할 수 있도록 도와준다. 도메인에서 '객체 생성'의 역할 자체가 중요한 경우라면, 정적 팩토리 클래스를 따로 분리하는 것을 고려해보자.
그리고, 알게 된 사실 🧐
디자인 패턴에 대해 빌더 패턴,, 정도 알고 있었고 오늘로부터 정적 팩터리 메서드가 있구나 알게되었다.
내가 개발할 때 Entity를 Dto를 변환하는 과정에서 중복 코드를 줄이고자 static을 선언하고 빌더패턴으로 반환해주는 방식을 자주 사용해왔다. 오늘부로 내가 응용해왔던 패턴이 "정적 팩터리 메서드" + "빌더 패턴" 이구나 알게되었다. ㅎㅎ
@Builder
public record MissionApprovalDto(
MemberProfileDto approver,
String comment
) {
public static MissionApprovalDto of(Member parent, String comment) {
if (parent == null) {
return null;
}
return MissionApprovalDto.builder()
.approver(MemberProfileDto.of(parent))
.comment(comment)
.build();
}
}
Dto를 Entity 변환할 때 역시 `public static toEntity()` 메서드를 이용하고 Entity를 Dto 변환할 때 역시 `public static from()` 방식을 이용해 유용하게 사용할 수 있다!
다른 사람들의 코드를 많이 참고하면서 쓰다보니 어떤 패턴인지도 모르고 사용해왔구나 ㅎㅎㅎ ... ㅎㅎ
그리고 정적 메서드 팩토리 기반 Dto 생성 방식 관련해서 인프런에 질문을 보았는데, 나 역시 궁금해왔던 사항이라서 기록해본다.
dto에서 toEntity VS entity 안에 정적 팩토리 메서드 - 인프런
@Service public class MemberService { private final MemberRepository memberRepository; public void save(MemberDto memberDto) { Me...
www.inflearn.com
정적 팩토리 메서드를 알아봤다.
오늘부터 이펙티브 자바 스터디를 시작하고 정리 1일차인데, 벌써 흥미롭다.
여러 코드를 참고하면서 나름 내 방식을 만들어서 유지보수를 고려해 개발에 적용하고 있었는데,
스스로 이게 맞아..? 이렇게 해도 되나? 했었다.
이펙티브 자바를 읽고 나서 왜 이런 패턴이 생겨났는지 알게 되었고, 파생되는 모르는 개념을 살펴 볼 수 있어 좋다!
근데 양이 생각보다 많아서 7월 이내로 다 끝낼 수.. 있겠지?
참고 블로그
https://tecoble.techcourse.co.kr/post/2020-05-26-static-factory-method/
https://f-lab.kr/insight/java-static-factory-methods
https://incheol-jung.gitbook.io/docs/study/effective-java/undefined/2020-03-20-effective-1item
'Programming > Java' 카테고리의 다른 글
[Java] 비교를 위한 인터페이스 : Comparable과 Comparator (0) | 2024.07.01 |
---|