정적 팩토리 메서드 패턴
정적 팩토리 메서드(Static Factory Method) 패턴은 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴이다.
지금까지는 public으로 직접 생성자를 호출하여 생성하였는데, 별도의 객체 생성의 역할을 하는 클래스 메서드를 통해 간접적으로 객체 생성을 유도하는 것이다. 그리고 이 정적 메서드를 통칭적으로 정적 팩토리 메서드 패턴이라고 부른다.
class Book {
private String title;
// 생성자를 private화 하여 외부에서 생성자 호출 차단
private Book(String title, String writerName) {
this.title = title;
this.writerName = writerName;
}
// 정적 팩토리 메서드: 제목만으로 익명 작가 책 생성
static Book fromTitle(String title) {
// 메서드에서 생성자를 호출하고 리턴함
return new Book(title, "익명");
}
// 정적 팩토리 메서드: 제목과 작가 이름으로 책 생성
static Book of(String title, String writerName) {
return new Book(title, writerName);
}
}
Java
복사
public static void main(String[] args) {
// 정적 메서드 호출을 통해 인스턴스화된 객체를 얻음
Book book1 = Book.of("어린왕자", "생텍쥐페리");
}
Java
복사
정적 팩토리 메서드 특징
이펙티브 자바에서 강조하는 내용 중 가장 대표적인 부분이 “생성자 대신 정적 팩토리 메서드를 고려하라” 이다.
1. 생성 목적에 대한 이름 표현이 가능
위에서 생성자인 Book(String, String)과 Book.fromTitle(String, String) 중 어느쪽이 “책의 제목을 반환한다” 라는 뜻을 더 잘 설명할수 있을까?
프로그래밍에서 중요한 요소 중에 하나가 코드를 읽기 쉽도록 작성해야 한다는 점이다. 그런 의미에서 new 생성자 방법은 단지 매개변수의 유형과 개수를 제안할 뿐이지 어떠한 역할 표현이나 편의성을 제공하지 않는다. 즉, 생성자로 넘기는 매개변수 만으로는 반환될 객체의 특성을 제대로 표현하기 어렵다는 것이다.
→ 이렇듯 정적 팩토리 메서드는 메서드 사용자로 하여금 어떤 상태의 객체를 반환받을 수 있는지에 대해 유추할 수 있게 한다.
2. 인스턴스에 대한 통제 및 관리가 가능
메서드를 통해 한 단계 거쳐 간접적으로 객체를 생성하기 때문에, 기본적으로 전반적인 객체 생성 및 통제 관리를 할 수 있게 된다. 즉, 필요에 따라 항상 새로운 객체를 생성해서 반환할 수도 있고, 아니면 객체 하나만 만들어두고 이를 공유하여 재사용하게 하여 불필요한 객체를 생성하는 것을 방지 할 수 있다.
대표적인 예로 Singleton 패턴을 들 수 있다. getInstance()라는 정적 팩토리 메서드를 사용해서 오로지 하나의 객체만 반환하도록 하여 객체를 재사용해 메모리를 아끼도록 유도할 수 있다.
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
// 정적 팩토리 메서드
public static Singleton getInstance() {
return instance;
}
}
Java
복사
또 다른 예로는 인스턴스에 대한 캐싱(Caching) 절차 구조를 정적 팩토리 메서드로 구현할 수 있다.
인스턴스에 대해 캐싱을 하면서 필요한 인스턴스만 뽑아 재사용하여 메모리를 절약할 수 있다.
class Day {
private String day;
public Day(String day) { this.day = day; }
public String getDay() { return day; }
}
// Day 객체를 생성하고 관리하는 Flyweight 팩토리 클래스
class DayFactory {
// Day 객체를 저장하는 캐싱 저장소 역할
private static final Map<String, Day> cache = new HashMap<>();
// 자주 사용될것 같은 Day 객체 몇가지를 미리 등록한다
static {
cache.put("Monday", new Day("Monday"));
cache.put("Tuesday", new Day("Tuesday"));
cache.put("Wednesday", new Day("Wednesday"));
}
// 정적 팩토리 메서드 (인스턴스에 대해 철저한 관리)
public static Day from(String day) {
if(cache.containsKey(day)) {
// 캐시 되어있으면 그대로 가져와 반환
System.out.println("해당 요일은 캐싱되어 있습니다.");
return cache.get(day);
} else {
// 캐시 되어 있지 않으면 새로 생성하고 캐싱하고 반환
System.out.println("해당 요일은 캐싱되어 있지 않아 새로 생성하였습니다.");
Day d = new Day(day);
cache.put(day, d);
return d;
}
}
}
Java
복사
public static void main(String[] args) {
// 이미 등록된 요일 가져오기
Day day = DayFactory.from("Monday");
System.out.println(day.getDay());
// 등록되지 않은 요일 가져오기
day = DayFactory.from("Friday");
System.out.println(day.getDay());
}
Java
복사
3. 객체 생성을 캡슐화
생성자를 사용하는 경우 외부에 내부 구현을 드러내야 하는데, 정적 팩토리 메서드는 구현부를 외부로 부터 숨길 수 있어 캡슐화(encapsulation) 및 정보 은닉(information hiding)을 할수 있다는 특징이 있다. 또한 노출하지 않는다는 특징은 정보 은닉성을 가지기도 하지만 동시에 사용하고 있는 구현체를 숨겨 의존성을 제거해주는 장점도 지니고 있다.
정적 팩토리 메서드 네이밍 규칙
정적 팩토리 메서드 와 다른 정적 메서드와 역할을 구분짓기 위해 독자적인 네이밍 컨벤션(Convention)이 존재한다. 단, 정적 팩토리 메서드에서의 네이밍은 단순히 선호도 의미를 넘어서 거의 법칙 정도로 사용되는 것이니, 각 네이밍의 역할에 대해 알아두는 것은 개념을 아는 것만큼 중요하다.
•
from : 하나의 매개 변수를 받아서 객체를 생성. 기존 데이터에서 객체를 변환할 때
•
of : 여러개의 매개 변수를 받아서 객체를 생성. 데이터 집계만 할 때
•
getInstance | instance : 인스턴스를 생성. 싱글턴 반환 or 캐시 재활용
•
newInstance | create : 항상 새로운 인스턴스를 생성
•
valueOf : 기본 타입 → 객체 변환 시 (ex. String.valueOf(123))
Lombok으로 정적 팩토리 메서드 만들기
@RequiredArgsConstructor(staticName = "of")
class Product {
private Long id;
private String name;
}
public class Main {
public static void main(String[] args) {
Product p = Product.of();
}
}
Java
복사
개인적으로 나는 record를 활용해서 dto를 생성하고, 이에 정적 팩토리 메서드를 활용하는걸 좋아한다. record의 특성상 getter나 생성자를 자동으로 만들어주기에 코드가 깔끔해지고, 그만큼 정적 팩토리 메서드 생성에 좀 더 집중할 수 있기 때문이다.
public class CoordinateRes {
private final String roomId;
private final int roundNum;
//... 생략
public CoordinateRes(String roomId, int roundNum, ...) {
this.roomId = roomId;
this.roundNum = roundNum;
}
public String getRoomId() { return roomId; }
public int getRoundNum() { return roundNum; }
}
Java
복사
public record CoordinateRes(String roomId, int roundNum, ...) {}
Java
복사
이렇게 class로 정의하는 코드가 간단해지고, 아래와 같이 정적 팩토리 메서드도 간단히 만들 수 있다
public static CoordinateRes from(Question question, Coordinate answerCoordinate, List<SubmitCoordinate> submitCoordinates) {
return new CoordinateRes(
question.getRoomId(),
question.getRoundNum()
//...
);
}
Java
복사
정적 팩토리 메서드 문제점
1. private 생성자일 경우 상속 불가능
정적 팩토리 메서드로 클래스를 설계하면 생성자를 private로 설정하게 된다. 따라서 정적 팩토리 메서드를 적용하는 경우 상속을 이용한 확장이 불가능해진다.
2. API 문서에서의 불편함
생성자는 하나의 자바 프로그래밍 언어의 스펙이기 때문에 JavaDoc 같은 문서에서 상단에 정의되어 그에 대한 스펙 검색을 할 수가 있다. 반면 정적 팩토리 메서드는 개발자가 임의로 만든 메서드이므로, 정적 팩토리 메서드 역할을 하는 메서드를 따로 찾아서 이해해야한다.
때문에 API 문서를 깔끔하게 작성해야하며, 네이밍 할 때 컨벤션을 정확히 지킴으로써 문제점을 극복하기도 한다.