정적 팩토리 메서드 패턴
2024.05.08
정적 팩토리 메서드(Static Factory Method)란
Static Method를 사용하여 인스턴스를 생성하는 방식을 의미한다.
-
Static Method?
Static Method는 인스턴스를 생성하지 않고 호출할 수 있는 메소드이다
정적 팩토리 메서드를 사용하는 이유
1. 생성 목적에 대한 메서드 이름 표현
new 키워드를 사용하여 객체를 생성하는 경우 다음과 같은 불편을 겪을 수 있다.
- 이 객체를 왜 생성하는지 한눈에 알기 어렵다.
- 생성자에 어떤 인자가 들어가는지, 인자의 순서는 어떻게 되는 지 모른다.
- 위의 정보를 알기 위해선 우리가 사용하고자 하는 클래스의 내부 구조를 뜯어봐야 한다.
- 우리가 사용하고자 하는 클래스가 바이트로 컴파일된 상태면 이를 다시 디컴파일 해야한다.
위 같은 문제점을 정적 메소드의 적절한 네이밍을 통해 반환될 객체의 특성과, 어떤 인자를 넣어야 하는지를 쉽게 유추할 수 있고 그렇기 때문에 내부 구조를 뜯어볼 필요도, 다시 디컴파일 할 필요도 없다. 후술하겠지만 정적 팩토리 메서드의 경우 정해진 네이밍 규칙이 있기 때문에 이 규칙을 알고 있다면 어떤 인자를 사용하는지와 반한 객체의 특성을 유추할 수 있다.
new 생성자 사용시 객체 생성 코드
class Developer{
private String name;
private String part = "Backend";
public Developer(String name, String part) {
this.name = name;
this.part = part;
}
public Developer(String name) {
this.name = name;
}
}
public static void main(String[] args) {
// Mingyu라는 이름의 백엔드 개발자
Developer developer = new Developer("Mingyu");
// Sungjae라는 이름의 프론트엔드 개발자
Developer frontendDeveloper= new Developer("Sunjae", "Frontend");
}
정적 팩토리 메소드 사용시 객체 생성 코드
class Developer{
private String name ;
private String part ;
// private 생성자
private Developer(String name , String part ) {
this.name = name ;
this.part = part ;
}
// 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
public static Developer partBackendFrom(String name ) {
return new Developer(name , "Backend");
}
// 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
public static Developer namePartOf(String name , String part) {
return new Developer(name , part);
}
}
public static void main(String[] args) {
// Mingyu라는 이름의 백엔드 개발자
Developer developer = Developer.nameBackendFrom("Mingyu");
//Sungjae라는 이름의 프론트엔드 개발자
Developer forntendDeveloper = Developer.namePartOf("Sungjae", "Frontend");
}
생성자 방식과 정적 팩토리 메서드 방식을 비교해 보면 정적 팩토리 메서드 방식이 메서드 이름을 통해 생성될 객체의 특징을 쉽게 알 수 있다. 이렇게 코드의 가독성이 증가하면서 객체 생성의 이유를 빠르게 파악하기 좋다.
-
private 생성자?
외부에서 new 키워드를 통한 객체생성이 불가, 따라서 인스턴스를 갖지 않는 객체를 만들어 주기 위해 사용.
2. 객체 생성에 대한 통제및 관리 용이
생성자를 통해 객체를 생성하면 오직 “생성”의 역할만 하지만, 정적 팩토리 메서드를 사용하면 객체 생성에 대한 개수 제어나 생성 여부에 대한 여부를 확인할 수 있는 등 생성에 대한 추가적인 제어가 가능하다.
크게 두가지 예를 들어보면
- Singlteton 패턴
- 캐싱 구조
이 두가지 방법으로 사용된다.
Singleton 패턴
-
Singleton 패턴?
메모리를 절약하기 위해 하나의 유일한 객체를 만들어 이 객체를 재사용하는 코드 패턴. 스프링에서 핵심 원리로 사용된다.
class Singleton {
private static Singleton instance;
private Singleton() {}
// 정적 팩토리 메서드
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
위 코드와 같은 방식으로 이미 객체를 생성한 적이 있는지 확인 후 생성할 수 있다.
-
synchronized?
synchronized, 동기화를 의미한다. 자바에서 멀티쓰레드를 통해 동시에 접근하는 것을 막아주는 역할, 동시에 접근하여 2개의 객체를 생성하여 싱글톤 패턴을 파괴하는 것을 방지.
캐싱구조
-
캐싱?
임시 저장 위치에 저장하여 보다 빠르게 액세스할 수 있도록 하는 프로세스, 간단하게 조회를 의미한다고 생각하면 된다.
정적 메소드 팩토리를 활용하여 인스턴스에 대해 캐싱 구조를 적용시킬 수 있다. 마찬가 메모리 절약의 효과가 있다.
Hashmap 자료구조를 이용하여 key값을 정적 팩토리 메서드의 인자로 받고 해당 key값이 존재 여부를 확인한다. 존재한다면 기존의 인스턴스를 반환하여 재사용하고 없다면 새로 생성하여 캐싱해 두는 방식으로 작동한다.
-
예제 코드
class Day { private String day; public Day(String day) { this.day = day; } public String getDay() { return day; } } 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; } } }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()); }
3. 객체 생성을 캡슐화 할 수 있다.
-
캡슐화?
캡슐화(Encapsulization)란?
데이터의 은닉을 말한다. 여기서는 생성자를 클래스의 메서드 안으로 숨기면서 내부 상태를 외부에 드러낼 필요없이 객체 생성 인터페이스 단순화 시킬 수 있다.
생성자를 사용하는 경우 객체 생성의 로직 같은 내부 구현을 드러내야 하는데, 정적 팩토리 메서드를 활용할 경우 외부로 숨길 수 있어 캡슐화 및 정보 은닉에 유용하다. 동시에 구현체를 숨겨 의존성을 제거해 주는 장점또한 존재한다.
-
예제 코드
interface Grade { String toText(); } class A implements Grade { @Override public String toText() {return "A";} } class B implements Grade { @Override public String toText() {return "B";} } class C implements Grade { @Override public String toText() {return "C";} } class D implements Grade { @Override public String toText() {return "D";} } class F implements Grade { @Override public String toText() {return "F";} }class GradeCalculator { // 정적 팩토리 메서드 public static Grade of(int score) { if (score >= 90) { return new A(); } else if (score >= 80) { return new B(); } else if (score >= 70) { return new C(); } else if (score >= 60) { return new D(); } else { return new F(); } } }public static void main(String[] args) { String jeff_score = GradeCalculator.of(36).toText(); String herryPorter_score = GradeCalculator.of(99).toText(); System.out.println(jeff_score); // F System.out.println(herryPorter_score); // A }
4. 반환 타입의 하위 타입 객체를 반환할 수 있다.
다향성의 특징을 활용한 정적 메소드 팩토리 패턴의 특징이다.
정적 팩토리 메서드를 사용할 경우 자기 자신만 생성할 수 있는 생성자에 비해, 객체의 클래스 타입을 선택함에 있어서 유연성을 지닌다.
public interface User {
static User of(String level){
if (level === 'admin') {
return new Admin(level);
}
return new NormalUser (level);
}
}
class Admin implements User{ }
class NormalUser implements User{ }
위 코드를 보면 인자로 받는 레벨 값에따라 동일한 인터페이스를 상속받은 객체들이 조건에 따라 다르게 반환되고 있는 것을 볼 수 있다.
5. 파라미터에 따라 다른 객체를 반환할 수 있다.
4번의 내용과 유사하다. 메서드 호출의 파라미터 값을 통해 얻을 객체의 인스턴스를 자유롭게 선택할 수 있는 유연성을 갖는다.
public interface HelloService {
String hello(); //인터페이스는 접근제어자가 없으면 public으로 지정
// 자바8 이후부터 인터페이스에 static 메소드 선언 가능
static HelloService from(String lang) {
if(lang.equals("ko")) {
return new KoreaHelloService();
} else {
return new EnglishHelloService();
}
}
}
public class KoreaHelloService implements HelloService {
@Override
public String hello() {
return "안녕하세요";
}
}
public class EnglishHelloService implements HelloService {
@Override
public String hello() {
return "Hello";
}
}
public class HelloServiceFactory {
public static void main(String[] args) {
HelloService eng = HelloService.from("eng"); //인터페이스 타입 기반을 사용할 수 있도록 강제
System.out.println(eng.hello()); //hello 출력
}
}
6.정적 팩토리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다.
public class CarFactory {
private CarFactory() {
}
public static Car createCar(String type) {
if (type.equals("sports")) {
return new SportsCar();
}
if (type.equals("suv")) {
return new SuvCar();
}
throw new IllegalArgumentException("해당 타입의 자동차가 존재하지 않습니다 : " + type);
}
}
기존에 위의 코드가 있다고 가정하자. 그후 전기자동차(ElectricCar)가 추가 되었다고 생각해보자.
public class CarFactory {
private CarFactory() {
}
public static Car createCar(String type) {
if (type.equals("sports")) {
return new SportsCar();
}
if (type.equals("suv")) {
return new SuvCar();
}
if (type.equals("electric")) {
return new ElectricCar();
}
throw new IllegalArgumentException("해당 타입의 자동차가 존재하지 않습니다 : " + type);
}
}
그러면 이렇게 ElectricCar에 대한 클래스를 만들고 ElecticeCar를 생성하는 메서드만 추가해 주면 된다.
새로운 타입의 객체가 필요하다면, 메서드만 추가해주면 되기에 확장에 용이하다.
정적 팩토리 메서드의 단점
당연하게도 장점만 있는 것은 아니다. 장단점이 있으니 상황에 맞게 패턴을 적용시키는 것이 중요하다.
1. 상속 불가능
정적 팩토리 메서드의 경우 생성자의 접근 제어자를 private로 설정한다. 그 이유는 생성자로 인스턴스를 생성하는 것을 막기 위함이다. 이때, 생성자의 접근제어자를 private로 설정할 경우 해당 생성자에 접근할 수 없기 때문에 상속이 불가능하다.
다만 이점은 단점이라고만 말할 수 없고, 정적 팩토리 메서드의 스펙이라고 말할 수 있다.
객체지향적 설계에서는 상속보다는 합성을 지향한다. 상속에 많은 단점과 한계가 있어서, 상속보다는 합성의 사용을 권한다. “상속 보단 합성”원칙에 대해서는 추가적으로 찾아보는 것을 권한다.
코드로 예를 들어보면 다음과 같다.
생성자 방식(상속이 불가하지만 예시를 위해 추가)
public class Car {
private Car() {
}
public static Car createCar() {
return new Car();
}
}
public class SportsCar extends Car {
}
상속대신 합성 사용
public class Car {
private Car() {
}
public static Car createCar() {
return new Car();
}
}
public class SportsCar {
private final Car car;
public SportsCar(Car car) {
this.car = car;
}
// ...
}
이렇게 SportsCar 객체가 더 이상 Car를 상속받지 않고, Car를 인스턴스 변수로 갖고 있다. 이렇게 합성을 사용하면 상속을 사용한 것 보다 객체간 결합도가 줄어든다. 객체 지향 프로그래밍은 낮은 결합도를 추구한다.
따라서 상속을 사용했을때 보다 결합도가 낮아짐으로써 유연성이 증가한다.
그렇기에 단점이라고 뽑기는 애매하고 스펙이라고 표현할 수 있을 것 같다.
2. 프로그래머의 추가적인 확인 작업 필요
생성자의 경우 javadoc을 보면 문서에서 상단에 정리되어 있지만 정적 팩토리 메서드는 따소 정리되어 있지 않다. 그래서 많은 메서드들 사이에서 정적 팩토리 역할을 하는 메서드를 추가적으로 찾아줘야 한다.
그래서 클래스를 설계할 때 API문서를 깔끔하게 작성하고, 아래에서 이야기할 네이밍 컨벤션을 잘 지킴으로써 문제점을 극복할 수 있다.
이런 문제점들이 있지만, 대부분 해결할 수 있고 미미한 것 이기 때문에 “생성자 대신 정적 팩토리 메서드를 고려하라”라는 말이 존재하는 것 같다.
정적 팩토리 메서드 네이밍 규칙
정적 팩토리 메서드에서는 다른 정적 메소드와 구분하기 위해 지켜야할 네이밍 컨벤션이 존재한다. 이 부분은 선호도가 아닌 법칙 수준으로 작용하고 있고 네이밍 컨벤션을 제대로 지켜야 정적 팩토리 메서드의 단점을 최소화 하고 장점을 최대화 할 수 있다.
- from : 하나의 매개 변수를 받아서 객체를 생성
- of : 여러개의 매개 변수를 받아서 객체를 생성
- getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
- newInstance | create : 항상 새로운 인스턴스를 생성
- get[Type] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음
- new[Type] : 항상 다른 타입의 새로운 인스턴스를 생성
정적 팩토리 메서드를 언제 사용하면 좋은가?
정적 팩토리 메서드 방식에 큰 단점은 없기에 특별한 목적없이 사용해도 괜찮지만, 많은 사람들이 이미 생성자 방식이 더 익숙하기 때문에 직관적이고 읽기 쉬운 것은 생성자 방식이다.
따라서 복잡한 초기화 로직이 없다면 생성자 방식을 선택하고 그외 정적 팩토리 메서드 방식의 장점을 활용할 수 있는 상황에선 정적 팩토리 메서드를 사용하는 것이 나을거라 생각한다.
1. 객체를 여러번 생성할 필요가 없을 때
메서드를 정의할 때 싱글톤 방식으로 지정해 줄 수 있기 때문에 여러번 생성할 필요가 없는 객체는 캐시하여 재사용하는 방식으로 리소를 효율적으로 사용할 수 있다.
2. 반환 타입의 하위 객체or 다른 클래스의 객체를 반환할 때
입력 매개변수에 따라 다양한 타입의 객체를 반환하는 유연성이 필요할 때 사용할 수 있다.
3. 초기화에 복잡한 로직이 필요할 때
객체 초기화 과정이 복잡하고 많은 단계를 거칠 때 정적 팩토리 메서드를 사용하면 클라이언트는 객채 생성이 필요한 내부 로직을 파악하지 않아도 정적 팩토리 메서드를 통해 간편하게 객체를 생성할 수 있다.
4. 유연한 확장성이 필요할 때
프레임워크나 라이브러리를 만들때 특히 유용하다고 한다.
JDBC와 같은 DB Connection 라이브러리를 예를 들어보자.
각각의 관계형 데이터베이스는 커넥션 연결 방법, SQL 전달 방식 그리고 결과를 응답 받는 방법이 모두 다르다. 이는 데이터베이스를 변경할 때마다 애플리케이션 서버에 개발된 데이터베이스 사용 코드도 같이 변경해야 하고, 개발자가 각 데이터베이스마다 커넥션 연결, SQL 전달 및 응답 결과를 받는 방법을 새로 배워야 하는 불편함을 초래한다.이러한 문제를 해결하기 위해 등장한 것이 바로 JDBC이다.
이 JDBC의 핵심적인 기능 중 하나가 적절한 데이터베이스 드라이버를 찾아 연결 객체를 생성하고 반환해주는데정적 팩토리 메서드의 특징 중 하나인 "정적 팩토리 메서드를 작성하는 시점에 반환할 객체의 클래스가 존재하지 않아도 된다"는 장점이 잘 드러나는 부분이다.
실제로 메서드를 작성하는 시점에는 어떤 데이터베이스 드라이버가 사용될지, 어떤 연결 객체가 생성될지 알 수 없다.이는실제 메서드를 호출하는 시점에 결정되는 것이며, 따라서개발자는 메서드를 호출하기만 하면 되고, 데이터베이스에 대한 구체적인 지식 없이도 데이터베이스 접근이 가능해 진다
-
JDBC란?
JDBC(Java Database Connectivity)는 Java 기반 애플리케이션의 데이터를 데이터베이스에 저장 및 업데이트하거나, 데이터베이스에 저장된 데이터를 Java에서 사용할 수 있도록 하는 자바 API이다.
정적 팩토리 메서드 패턴 vs 빌더 패턴
SOPT에서 세미나를 들을때 두 패턴에 대해 같이 듣게 되었어서 이 패턴들을 비교하여 언제 정적 팩토리 메서드 패턴을 사용하고 언제 빌더 패턴을 사용하면 좋을 지 추가적으로 알아봤다.
정적 팩토리 메서드 패턴의 경우 많은 장점들이 있지만, 인자가 많아질 경우 역시 한번에 인자들에 대해 파악하기 어렵다. 생성자를 사용하는 것 보다는 낫지만 이런 부분에서는 빌더 패턴이 더 우세한 경향을 갖는다.
또한 정적 팩토리 매서드나 생성자나 설정할 필요가 없는 필드에도 항상 인자를 전달해야 한다. 필요한 객체를 직접 생성하는 클라이언트는 먼저 필수 인자들을 생성자에 전부 전달하고 빌더 객체를 만들어 빌더 객체에 정의된 설정 메소드를 통해 선택적 인자들을 추가해 나간다.
간단하게 이야기해서 인자가 많이 필요한 클래스를 설계할 때 , 대부분의 인자가 선택적 인자인 경우 빌더 패턴을 사용하는게 더 좋다고 생각한다.
참고문헌
💠 정적 팩토리 메서드 패턴 (Static Factory Method)
정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?
정적 팩토리 메서드(Static Factory Method)
최근 글
- LLM 결과물 생성시 비용 절감 과정2025.09.11
- 서버 내 에러 발생 시 에러 알람 구현 과정2025.01.12
- 이메일 초대를 위한 내부 로직 개선2024.09.24
- @Transactional의 진실과 오해2024.09.07
- [Security] 오류 발생 시점에 따른 필터처리 및 체인구조 파악2024.08.02