[디자인 패턴] 6. Static Factory Method

김주희's avatar
Jul 23, 2025
[디자인 패턴] 6. Static Factory Method

정적 팩토리 메서드

💡
목적 : 객체 생성을 쉽게 한다. 정적 = static 팩토리 = 공장처럼 찍어내는
 
앞에서 패턴은 설계할 때부터 먼저 고려하는 것이 아니라 리팩토링과 같은 때 적용해야 하는데 정적 팩토리 메서드 패턴은 이름은 패턴이지만 패턴 아닌 패턴이라서 프로젝트 시에 바로 적용해도 된다. 오히려 자주 사용하도록 추천한다고 한다. 즉 정적 팩토리 메서드는 static을 잘 쓰는 방법에 가깝다.
 
자바에서 객체를 생성할 때 보통 new 키워드를 사용한다. 하지만 이 방식은 코드 유연성이나 가독성 측면에서 한계를 가진다. 정적 팩토리 메서드는 생성자를 직접 호출하지 않고, 정적 메서드로 객체를 생성하는 방식이다.
 
정적 팩토리 메서드에는 두 가지 케이스가 있는데 각각 type1과 type2 예시를 통해 알아보자.
 

Type 2 : 하나의 클래스 안에서 다양한 형태로 객체 생성하기

정적 팩토리 메서드는 단순히 new를 감싸는 것을 넘어서, 객체 생성에 의도와 목적을 부여할 수 있다. 특히 type2에서는 하나의 클래스 안에서 create, of, from 등의 정적 메서드를 사용해 다양한 상황에 맞는 객체를 생성한다.

1. Create – 특정한 용도나 역할에 맞는 객체 만들기

User에서 Role이 TEACHER와 STUDENT로 나뉠때 User, Teacher, Student 테이블로 정규화를 다 지켜서 나뉠 수도 있지만, User 테이블 하나만 있는, 그래서 User 클래스 안에 학생 용도의 필드와 선생님 용도의 필드가 함께 들어있다고 가정해보자.
public class User { private Integer id; private String username; private String password; private String role; // Teacher, Student // 학생 private Integer score; private Integer level; // 선생 private Integer sal; private String position; private User(Integer id, String username, String password, String role, Integer score, Integer level, Integer sal, String position) { this.id = id; this.username = username; this.password = password; this.role = role; this.score = score; this.level = level; this.sal = sal; this.position = position; } }
 
이전에는 선생님이든 학생이든 간에 전부 생성자를 통해 new 키워드로 직접 하나하나 값을 넣어서 만들었다. 그러나 정적 팩토리 메서드를 활용하여 CreateStudent, CreateTeacher 메서드를 만들면 학생과 선생님이라는 용도에 맞게 User를 만들 수 있게 된다.
 
실제로는 입력받는 JoinDTO를 가지고 만들게 되겠지만 지금은 우선 값이 들어왔다고 가정하고 만들면 다음과 같다.
public static User createStudent() { return new User(null, "ssar", "1234", "STUDENT", 100, 1, null, null); } public static User createTeacher() { return new User(null, "ssar", "1234", "TEACHER", null, null, 3000, "MANAGER"); }
 
최종적으로 다음과 같이 사용할 수 있다.
User teacher = User.createTeacher(); User student = User.createStudent();
 

전체 소스 코드

package ex96.type2; public class User { private Integer id; private String username; private String password; private String role; // Teacher, Student // 학생 private Integer score; private Integer level; // 선생 private Integer sal; private String position; public static User createStudent() { return new User(null, "ssar", "1234", "STUDENT", 100, 1, null, null); } public static User createTeacher() { return new User(null, "ssar", "1234", "TEACHER", null, null, 3000, "MANAGER"); } private User(Integer id, String username, String password, String role, Integer score, Integer level, Integer sal, String position) { this.id = id; this.username = username; this.password = password; this.role = role; this.score = score; this.level = level; this.sal = sal; this.position = position; } }
 

2. Of - 여러 개의 필드 중 일부 필드들을 선택해서 객체 생성

Rectangle 클래스는 x, y, width 필드를 가지고 있다. width은 넓이로 x와 y를 곱하면 되는 값이기 때문에 x와 y는 입력받지만 width는 입력받을 필요가 없다. 따라서 모든 필드를 가지는 생성자는 private로 전환하고 of라는 정적 메서드를 만들어서 x와 y를 입력받고 x, y, 그리고 x * y를 속성으로 가지는 Rectangle 객체를 생성할 수 있다.
public class Rectangle { private double x; private double y; private double width; private Rectangle(double x, double y, double width) { this.x = x; this.y = y; this.width = width; } public static Rectangle of(double x, double y) { return new Rectangle(x, y, x * y); } }
 
of를 통해 내가 원하는 사이즈로 커스텀한 Rectangle 객체를 만들 수 있다.
Rectangle mySizeR = Rectangle.of(500, 500);
 

3. From - 외부 객체를 변환 혹은 외부 입력 값을 받아 객체 생성

from 메서드는 외부 객체를 변환하거나 혹은 외부 입력 값을 받아 객체를 생성하는 메서드로 예를 들면 User 객체를 만들때 JoinDTO를 입력받아서 User 객체로 변환하게 되는데 이때 from을 쓴다.
또는 아래와 같이 Rectangle 예제의 from 메서드는 어떤 크기인지를 외부에서 value로 입력받아서 일치하는 value 값에 따라 다른 Rectangle 객체를 만든다.
public static Rectangle from(String value) { if (value.equals("small")) { return new Rectangle(50, 50, 50 * 50); } else if (value.equals("medium")) { return new Rectangle(100, 100, 100 * 100); } else if (value.equals("large")) { return new Rectangle(200, 200, 200 * 200); } else { throw new IllegalArgumentException("Invalid rectangle value"); } }
 
from 메서드에 어떤 크기인지 인자로 전달하면 그 크기에 해당하는 Rectangle 객체를 만들 수 있다.
Rectangle smallR = Rectangle.from("small"); Rectangle mediumR = Rectangle.from("medium"); Rectangle largeR = Rectangle.from("large");
 

4. 결론

위와 같이 정적 팩토리 메서드를 사용하게 될 경우 두 가지 이점이 있다.
  1. 의도와 목적에 맞는 객체를 생성할 수 있다.
  1. 캡슐화를 통해 불필요하거나 잘못된 객체 생성을 방지할 수 있다.
 

전체 소스 코드

package ex96.type2; public class Rectangle { private double x; private double y; private double width; private Rectangle(double x, double y, double width) { this.x = x; this.y = y; this.width = width; } public static Rectangle from(String value) { if (value.equals("small")) { return new Rectangle(50, 50, 50 * 50); } else if (value.equals("medium")) { return new Rectangle(100, 100, 100 * 100); } else if (value.equals("large")) { return new Rectangle(200, 200, 200 * 200); } else { throw new IllegalArgumentException("Invalid rectangle value"); } } public static Rectangle of(double x, double y) { return new Rectangle(x, y, x * y); } }
 
 

+ 도메인의 침범

type 2의 예제를 통해 from 메서드에 대해 배우고 나니까 기존에 사용하던 toEntity와 from 메서드가 크게 다른 게 없다고 느껴지는데 무엇이 다른걸까?
 
 
 
 
 
 
 

Type 1 : 다양한 구현체를 선택적으로 생성하기

lib 디렉토리

type1의 디렉토리 구조를 보면 lib는 외부 라이브러리, 즉 내가 건들 수 없는 파일이다. 각각의 MariaDB와 OracleDB는 DB 인터페이스의 추상 메서드 setUrl과 execute를 각각 재정의하고 있다. 이때 MariaDB와 OracleDB가 가지는 필드는 각각 path와 url로 다르다. 그러나 인터페이스를 재정의 함으로써 setUrl, setPath가 아니라 setUrl 하나로 통일하여 사용 가능하다.
notion image
 
 

DBFactory 클래스

DBFactory 클래스를 만들고 싱글톤 패턴을 적용한다. 찍어내는게 다양한 것일 뿐이지 공장은 하나면 되니까. (아래의 코드에서는 instance 객체 또한 private으로 만들어서 getter를 통해서 접근 가능하다.)
 
그리고 createDB라는 메서드를 통해 내가 원하는 db의 종류를 입력하기만 하면 그에 맞는 DB 객체를 생성해서 반환해준다.
App에서 직접 생성자를 통해 MariaDB나 OracleDB를 생성할 필요 없고 createDB 메서드가 선택한 종류를 기반을 적절한 DB 구현체(인터페이스를 구현한 클래스)를 선택하고 생성한다. 따라서 클라이언트는 어떤 DB 구현체가 선택되는지 몰라도 된다.
또한 객체 생성 뿐만 아니라 db 종류에 맞는 DB 연결 설정까지 처리해주기 때문에 클라이언트는 url 설정도 몰라도 된다.
 
즉 db를 만들 때 하나의 클래스에서 조합해서 여러가지 나오는게 아니라 createDB 팩토리 메서드를 통해 어떤 객체를 만들어 낼건지 뽑아낼 수 있다.
package ex96.type1; import ex96.type1.lib.DB; import ex96.type1.lib.Driver; import ex96.type1.lib.MariaDB; import ex96.type1.lib.OracleDB; public class DBFactory { private static DBFactory instance = new DBFactory(); private DBFactory() { } public static DBFactory getInstance() { return instance; } // 단점: OCP 위배 // 책임 : new를 대신해준다. public DB createDB(Driver driver) { // maria, oracle, mysql, mssql if (driver.getProtocol().equals("maria")) { MariaDB mariaDB = new MariaDB(); mariaDB.setUrl("jdbc:mariadb://127.0.0.1:3306"); return mariaDB; } else if (driver.getProtocol().equals("oracle")) { OracleDB oracleDB = new OracleDB(); oracleDB.setUrl("jdbc:oracle:thin://127.0.0.1:8080"); return oracleDB; } else { throw new NullPointerException("db driver not found exception"); } } }
 
package ex96.type1; import ex96.type1.lib.DB; import ex96.type1.lib.Driver; public class App { public static void main(String[] args) { DBFactory factory = DBFactory.getInstance(); DB db = factory.createDB(Driver.MARIA); // DB, MaraiDB d.execute("select"); } }
 
 

+ Adapter 패턴에서 로그인 예제의 Authentication도 적용 가능하다!

아래는 어댑터 패턴의 예제인데 이때 main 함수 내부의 Authentication도 DBFactory와 똑같은 기능을 하기 때문에 AuthenticationFactory를 만들어서 객체 생성 전담 클래스로 처리 가능하다.
package ex99; import java.util.Scanner; // FacebookProfile -> uId, uName (이게 추가될때 기존 코드를 손되지 않을 수 있게 하는게 목표) public class App { static void mainPage(UserProfile profile) { System.out.println("로그인 하신 아이디는 " + profile.getUsername() + " 입니다."); } public static void main(String[] args) { Scanner sc = new Scanner(System.in); String provider = sc.nextLine(); // 객체 생성 전담 클래스를 만들어서 처리해주면 좋다. Authentication authentication; if (provider.equals("google")) { authentication = new GoogleAuthentication(); } else if (provider.equals("kakao")) { authentication = new KakaoAuthentication(); } else if (provider.equals("facebook")) { authentication = new FacebookAuthentication(); } else { System.out.println("지원하지 않는 Provider 입니다."); return; } UserProfile userProfile = authentication.login(); mainPage(userProfile); } }
 
정적 팩토리 메서드 적용 예시
package ex99; public class AuthenticationFactory { public static Authentication create(String provider) { if (provider.equals("google")) { return new GoogleAuthentication(); } else if (provider.equals("kakao")) { return new KakaoAuthentication(); } else if (provider.equals("facebook")) { return new FacebookAuthentication(); } else { throw new IllegalArgumentException("지원하지 않는 Provider입니다."); } } } public class App { static void mainPage(UserProfile profile) { System.out.println("로그인 하신 아이디는 " + profile.getUsername() + " 입니다."); } public static void main(String[] args) { Scanner sc = new Scanner(System.in); String provider = sc.nextLine(); Authentication authentication; try { authentication = AuthenticationFactory.create(provider); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); return; } UserProfile userProfile = authentication.login(); mainPage(userProfile); } }
 
 
 
 
 
 
 
 
 
 
코드는 우변이 먼저 읽히니까 new Doorman() 쫓아내는 static 아니니까 아직 안만들어지고 heap이 만들어지고 Doorman 타입이고 heap에 쫓아내 들어옴 -> new는 끝남 인스턴스 변수는 static 공간에 저장되는데 heap의 주소를 가진다. (heap 안의 값이 아니라) static에 뜬 거는 instance라는 변수고 이 변수는 주소를 가짐 정적 팩토리 메서드 -> static으로 만든다. create = 선생님, 학생 처럼 특정한 거를 만들때는 create를 쓴다. of = 매개변수가 여러개 of로 만드는 거는 여러 필드 중 필요한 여러개의 필드를 골라서 만드는것? from(JoinDTO로부터 Member를 만들어낼때 사용) from은 하나를 받아서 하나로부터 만들어내겟다는의미? VS toEntity toEntity로 만들어도 되고 from을 만들어도 되는데 중요한건 도메인의 침범임 도메인의 침범? A, B객체가 있고 A가 먼저 만들어져있고 B로 변환 = A가 B를 알아야하는지/B가 A를 알아야하는지의 관점으로 만들어야 됨 아이스크림 A 만드는데 A에 내가 하나하나씩 넣어서 만드는게X lib = 외부에서 걍 들고 온 라이브러리라고 생각하기 dbFactory가 구현안된상태에서 app에서 써보기 instance private으로 만들고 getter 쓰기 하나의 객체로 만들어서 뽑아쓰기..? 엥 다시 녹음 찾아보자 -> db를 만들 때 하나의 클래스에서 조합해서 뭐가 여러가지 나오는게 아니라 어떤 객체를 만들어 낼건지 그걸 뽑아내는 팩토리 메서드 Authentication을 AuthenticationFactory로 만들면 되는거임
Share article

jay0628