팩토리 메소드 (Factory method) 패턴
팩토리 메소드는 객체 생성 방식을 캡슐화하여 객체 생성의 책임을 서브클래스에 위임하는 디자인 패턴이다.
이 패턴은 객체 생성을 위해 new 연산자를 직접 사용하지 않고, 객체 생성 과정을 별도의 메소드로 정의하여
객체 생성의 유연성을 높인다.
팩토리 메소드 패턴의 구조
Creator(창조자 인터페이스)
객체 생성을 위한 팩토리 메소드를 정의하는 인터페이스를 의미
실제로 객체를 생성하지 않으며, 서브클래스(구체적인 창조자 클래스)가 이 메소드를 구현하여 객체를 생성
ConcreteCreator(구체적인 창조자 클래스)
Creator 인터페이스의 서브클래스(구현 클래스), 실제 객체 생성을 담당
Product(제품)
팩토리 메소드가 생성하는 객체의 타입을 나타내는 인터페이스나 추상 클래스를 의미한다.
ConcrteProduct(구체적인 제품)
실제로 팩토리 메소드에 의해 생성되는 구체적인 객체를 의미한다.
OCP가 지켜지지 않는 예시
OCP(Open-Closed Principle) 개방-폐쇄 원칙
확장에는 열려 있고, 수정에는 닫혀 있어야 한다는 SOLID 원칙 중 하나이다.
즉, 기존 코드를 변경하지 않고도 기능을 확장할 수 있도록 설계를 해야한다.
객체 생성에 관련된 코드가 OCP를 위반하는 대표적인 케이스는 새로운 객체를 생성할 때마다
코드의 일부를 수정해야 하는 경우이다.
public class Ship {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Ship{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
public class ShipFactory {
public static Ship orderShip(String name, String email){
// validate
if(name == null || name.isBlank()){
throw new IllegalArgumentException("배 이름을 지어주세요.");
}
if(email == null || email.isBlank()){
throw new IllegalArgumentException("연락처를 남겨주세요.");
}
prepareFor(name);
Ship ship = new Ship();
ship.setName(name);
coloring(ship, name);
sendEmailTo(email, ship);
return ship;
}
private static void sendEmailTo(String email, Ship ship) {
System.out.println(email + " send message: " + ship.getName() + " 다 만들었습니다.");
}
private static void prepareFor(String name) {
System.out.println(name + " 만들 준비 중");
}
private static void coloring(Ship ship, String name){
if(name.equalsIgnoreCase("whiteShip")){
ship.setColor("white");
}
else if(name.equalsIgnoreCase("blackShip")){
ship.setColor("black");
}
}
}
public class Client {
public static void main(String[] args) {
Ship whiteShip = ShipFactory.orderShip("whiteShip", "client1@naver.com");
Ship blackShip = ShipFactory.orderShip("blackShip", "client2@naver.com");
System.out.println(whiteShip);
System.out.println(blackShip);
}
}
출력
whiteShip 만들 준비 중
client1@naver.com send message: whiteShip 다 만들었습니다.
blackShip 만들 준비 중
client2@naver.com send message: blackShip 다 만들었습니다.
Ship{name='whiteShip', color='white'}
Ship{name='blackShip', color='black'}
현재는 ShipFactory 통해서 Ship 인스턴스를 만들고 있다.
해당 코드에 어떤 문제점이 있을까?
1. Ship 클래스를 상속 받는 하위 타입의 WhiteShip / BlackShip 필요성
2. 만약 다른 종류의 배를 만든다고 하면 ShipFactory 코드의 변경이 필요
→ OCP가 지켜지지 않음
나중에 BlueShip을 추가한다고 하면
coloring() 메소드에 조건을 추가해야 한다.
3. 유연성 부족
각 배의 생성 방식이 ShipFactory에 고정되어 있어, 배의 속성이나 동작이 달라져도 쉽게 확장하거나
유지보수하기 어렵다.
팩토리 메소드 패턴 적용하기
public class Ship {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Ship{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
public class BlackShip extends Ship{
}
public class WhiteShip extends Ship{
}
public interface ShipFactory {
default Ship orderShip(String name, String email){
validate(name, email);
prepareFor(name);
Ship ship = createShip(name); // coloring & naming 특정 배를 만드는 공정
sendEmailTo(email, ship);
return ship;
}
Ship createShip(String name); // 하위 클래스에 생성을 위임
private static void sendEmailTo(String email, Ship ship) {
System.out.println(email + " send message: " + ship.getName() + " 다 만들었습니다.");
}
private void prepareFor(String name){
System.out.println(name + " 만들 준비 중");
}
private void validate(String name, String email){
if(name == null || name.isBlank()){
throw new IllegalArgumentException("배 이름을 지어주세요.");
}
if(email == null || email.isBlank()){
throw new IllegalArgumentException("연락처를 남겨주세요.");
}
}
}
public class WhiteShipFactory implements ShipFactory{
@Override
public Ship createShip(String name) {
WhiteShip ship = new WhiteShip();
ship.setName(name);
ship.setColor("white");
return ship;
}
}
public class BlackShipFactory implements ShipFactory{
@Override
public Ship createShip(String name) {
BlackShip ship = new BlackShip();
ship.setName(name);
ship.setColor("black");
return ship;
}
}
public class Client {
public static void main(String[] args) {
WhiteShipFactory whiteShipFactory = new WhiteShipFactory();
BlackShipFactory blackShipFactory = new BlackShipFactory();
WhiteShip whiteShip = (WhiteShip) whiteShipFactory.orderShip("whiteShip", "client1@naver.com");
BlackShip blackShip = (BlackShip) blackShipFactory.orderShip("blackShip", "client2@naver.com");
System.out.println(whiteShip);
System.out.println(blackShip);
}
}
출력
whiteShip 만들 준비 중
client1@naver.com send message: whiteShip 다 만들었습니다.
blackShip 만들 준비 중
client2@naver.com send message: blackShip 다 만들었습니다.
Ship{name='whiteShip', color='white'}
Ship{name='blackShip', color='black'}
개선 사항 정리
OCP (Open-Closed Principle) 준수 + 확장성 및 유연성
이전 코드
ShipFactory의 coloring 메서드에서 새로운 배의 종류가 추가될 때마다 조건문을 추가해야 했습니다. 즉, 새로운 배를 추가하면 기존 코드를 수정해야 했으므로 OCP를 위반했다.
개선된 코드
ShipFactory 인터페이스를 통해 객체 생성 로직을 createShip 메서드로 분리하고, 각 배의 종류에 맞는 팩토리 클래스(WhiteShipFactory, BlackShipFactory)에서 배의 인스턴스를 생성하도록 했다. 따라서 새로운 배 종류가 추가될 때는 기존 코드를 수정할 필요 없이 새로운 팩토리 클래스를 추가하면 된다.
객체 생성 책임 분리 (SRP 측면)
이전 코드
ShipFactory가 직접 Ship 객체를 생성하고, 배의 색깔을 지정하는 역할도 담당했었다. 이로 인해 ShipFactory는 특정 배의 생성 및 설정 과정에 강하게 결합되어 있었다.
개선된 코드
배의 생성 책임을 각 구체적인 팩토리 클래스(WhiteShipFactory, BlackShipFactory)로 위임했다. ShipFactory 인터페이스는 공통적인 로직(orderShip)만 담당하고, 구체적인 배 생성은 각 팩토리에서 관리하게 된다. 이로 인해 배의 생성 과정이 변경되어도 ShipFactory 인터페이스는 영향을 받지 않고, 각 배의 팩토리 클래스에서만 수정이 이루어지므로 책임이 분리되었다.
클라이언트 코드도 변경이 없으려면?
public class Client {
public static void main(String[] args) {
Client client1 = new Client();
Client client2 = new Client();
client1.print(new WhiteShipFactory(), "whiteShip", "client1@naver.com");
client2.print(new BlackShipFactory(), "blackShip", "client2@naver.com");
}
private void print(ShipFactory shipFactory, String name, String email){
System.out.println(shipFactory.orderShip(name, email));
}
}
// 출력
whiteShip 만들 준비 중
client1@naver.com send message: whiteShip 다 만들었습니다.
Ship{name='whiteShip', color='white'}
blackShip 만들 준비 중
client2@naver.com send message: blackShip 다 만들었습니다.
Ship{name='blackShip', color='black'}
클라이언트 코드에서 ShipFactory 인터페이스를 사용하고, 이를 DI(Dependency Injection) 방식으로 전달받으면, 클라이언트 코드 변경 없이 새로운 ShipFactory 구현체를 사용할 수 있게 된다.
이렇게 하면 클라이언트는 어떤 구체적인 팩토리(예: WhiteShipFactory, BlackShipFactory)가 들어오는지 신경 쓸 필요 없이 인터페이스 기반으로 동작하게 된다. 새로운 타입의 배를 추가할 때에도 클라이언트 코드는 전혀 변경되지 않고, 새로운 팩토리 구현만 추가하면 된다.
즉, OCP(개방-폐쇄 원칙)를 준수하고 클라이언트 코드를 변경 없이 유지할 수 있게 되는 것이다.
ShipFactory 인터페이스에 대한 의존성 주입(DI)을 통해 클라이언트가 구체적인 구현에 의존하지 않고, 유연하게 확장 가능하다.
팩토리 메소드 패턴 장단점
장점
- 확장성: 새로운 제품(서브클래스)을 추가할 때, 기존 코드를 수정하지 않고 새로운 팩토리 클래스를 생성하여 기능을 확장할 수 있다. 이로 인해 OCP(변경에 닫혀있고 확장에 열려있음) 원칙이 지켜진다.
- 유지보수성: 객체 생성 로직이 팩토리 메소드에 캡슐화되어 있어, 객체 생성 방식의 변경이 필요할 경우 팩토리 메소드만 수정하면 되므로 코드의 유지보수가 용이하다.
- 코드의 간결함: 클라이언트 코드는 구체적인 클래스에 의존하지 않고 팩토리 메소드를 통해 객체를 생성하므로 코드가 더 깔끔하고 간결해진다.
- 인터페이스와 구현의 분리: 클라이언트는 객체 생성에 대해 구체적인 클래스가 아닌 인터페이스에 의존하게 되어, 구현 세부 사항을 숨길 수 있다.
단점
- 복잡성 증가: 팩토리 메소드 패턴을 사용하면 클래스 구조가 복잡해질 수 있으며, 불필요한 인터페이스와 클래스가 증가할 수 있다. 간단한 경우에는 오히려 코드가 복잡해질 수 있다.
- 추상화 오버헤드: 객체 생성에 대한 추상화가 필요하므로, 오버헤드가 발생할 수 있다.
- 디버깅 어려움: 객체 생성 과정이 간접적이기 때문에, 디버깅 시 생성 로직을 추적하기 어려울 수 있다.
만약 확장 가능성이 높은 product라면 팩토리 메소드 패턴을 고려해 보고 아니라면 일단 적용하지 않는다.
처음부터 팩토리 메소드 패턴을 적용하는 건 개발에 부담이 많이 간다
클린 코드, SOLID가 잘 지켜지는 코드도 중요하지만 제일 중요한 건 마감일을 지키는 것
팩토리 메소드 패턴 실무에서는?
Calendar (java.util)
/**
* Calendar simple factory
*/
public class SimpleFactory {
public Ship createProduct(String name){
if(name.equals("whiteShip")){
return new WhiteShip();
}
else if(name.equals("blackShip")){
return new BlackShip();
}
throw new IllegalArgumentException();
}
}
public class CalendarExample {
public static void main(String[] args) {
System.out.println(Calendar.getInstance().getClass());
System.out.println(Calendar.getInstance(Locale.forLanguageTag("th-TH-x-lvariant-TH")).getClass());
System.out.println(Calendar.getInstance(Locale.forLanguageTag("ja-JP-x-lvariant-JP")).getClass());
}
}
출력
class java.util.GregorianCalendar
class sun.util.BuddhistCalendar
class java.util.JapaneseImperialCalendar
'
간단한 형태의 유사 팩토리 메소드 패턴이다.
getInstance() 메소드가 오버로딩 되어있고 매개변수에 따라서 반환하는 인스턴스가 달라진다.
Spring 프레임워크 BeanFactory
config.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="hello" class="java.lang.String">
<constructor-arg value="hello"/>
</bean>
</beans>
@Configuration
public class Config {
@Bean
public String hello(){
return "hello";
}
}
public class SpringBeanFactoryExample {
public static void main(String[] args) {
BeanFactory xmlFactory = new ClassPathXmlApplicationContext("config.xml");
String hello1 = xmlFactory.getBean("hello", String.class);
System.out.println(hello1);
BeanFactory beanFactory = new AnnotationConfigApplicationContext(Config.class);
String hello2 = beanFactory.getBean("hello", String.class);
System.out.println(hello2);
}
}
BeanFactory 인터페이스를 사용하여 XML 또는 애너테이션 기반의 설정에 따라 객체를 생성하고 관리
BeanFactory → Creator
AnnotationConfigApplicationContext & ClassPathXmlApplicationContext → ConcreteCreator
Object → Product
String → ConcreteProduct
참고자료
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4
'Computer Sience > Desgin Pattern' 카테고리의 다른 글
[Design Pattern] 어댑터 (Adapter) 패턴 (2) | 2024.10.22 |
---|---|
[Design Pattern] 프로토타입 (Prototype) 패턴 (2) | 2024.10.20 |
[Design Pattern] 빌더(Builder) 패턴 (6) | 2024.10.19 |
[Design Pattern] 추상 팩토리(Abstract Factory) 패턴 (0) | 2024.10.18 |
[Design Pattern] 싱글톤(Singleton) 패턴 (1) | 2024.10.16 |