[생성 패턴] 팩토리 메서드 패턴

joyful·2024년 12월 4일
0

디자인패턴

목록 보기
1/1

들어가기 앞서

이 글은 디자인 패턴들 문서를 공부한 내용을 정리한 글입니다. 모든 출처는 해당 문서에 있습니다.


1. 정의

  • 부모 클래스에서 객체들을 생성할 수 있는 인터페이스를 제공하고, 자식 클래스들이 생성될 객체들의 유형변경할 수 있도록 하는 생성 패턴
  • 가상 생성자라고도 불린다.

2. 등장 배경

2-1. 문제점

  • 기존 클래스들에 결합된 코드가 많을수록 프로그램에 새 클래스를 추가하는 데 어려움이 생긴다.
    → 새 클래스를 추가하기 위해서는 전체 코드 베이스 변경이 필요하기 때문

2-2. 해결방안

  • 객체 생성을 직접적으로 호출(new 연산자 사용)하는 대신 팩토리 메서드에 대한 호출로 대체한다.
    • 객체는 팩토리 메서드 내에서 생성된다.
  • 자식 클래스에서 팩토리 메서드를 오버라이딩하여 메서드에 의해 생성되는 제품(객체)들의 클래스를 변경할 수 있다.
  • 이 방법은 다음과 같은 제한 사항이 있다.
    • 자식 클래스들이 서로 다른 구체적인 객체를 생성하고 싶다면, 공통적인 기초 클래스 또는 인터페이스를 공유하는 경우에만 가능하다.
    • 팩토리 메서드의 반환 타입이 공통된 기초 클래스나 인터페이스로 선언되어야 한다.
  • 클라이언트 코드(팩토리 메서드를 사용하는 코드)는 모든 객체를 추상적으로 간주한다.
    • 공통 메서드가 있다는 사실만 알고, 어떻게 구현되었는지는 알지 못한다.
    • 구현 방식은 중요한 부분이 아니다.

3. 구조

3-1. 제품(객체)

  • 인터페이스를 선언한다.
  • 인터페이스는 생성자와 자식 클래스들이 생성 가능한 모든 객체의 공통 사항이다.

3-2. 구상 제품(구현 객체)

  • 제품 인터페이스의 다양한 구현들

3-3. 크리에이터(Creator) 클래스

  • 새로운 제품 객체들을 반환하는 팩토리 메서드를 선언한다.

    • 팩토리 메서드의 반환 유형은 제품 인터페이스와 일치해야 한다.
  • 구현 방식

    팩토리 메서드추상 메서드기본값 제공
    하위 클래스 구현 여부반드시 구현해야 함구현하지 않아도 기본값이 동작함
    객체 생성 로직각 하위 클래스에서 자신만의 객체 생성 로직을 정의• 기본값 로직이 제공됨
    • 필요한 경우에만 오버라이드 가능
  • 제품과 관련된 핵심 비즈니스 로직이 존재한다.

  • 팩토리 메서드는 이 로직과 구체적인 구현을 분리하는 역할을 한다.
    → 즉, 팩토리 메서드가 어떤 객체를 생성할지 결정하는 로직만 처리한다.

3-4. 구상 크리에이터(구현 크리에이터)

  • 기초 팩토리 메서드를 오버라이드(재정의)하여 다른 유형의 제품을 반환하게 해주는 역할을 수행한다.

💡 참고사항

  • 팩토리 메서드는 항상 새로운 인스턴스들을 생성할 필요가 없다.
  • 팩토리 메서드는 기존 객체들을 캐시, 객체 풀, 다른 소스로부터 반환할 수 있다.

4. 사용 상황

✅ 객체들의 정확한 유형과 의존관계를 아직 모르는 경우

앱에 새로운 객체를 추가하고 싶은 경우, 새로운 크리에이터 자식 클래스를 생성한 후 해당 클래스 내부의 팩토리 메서드를 오버라이딩 하면 된다.

✅ 라이브러리 또는 프레임워크의 사용자들에게 내부 컴포넌트들을 확장하는 방법을 제공하고 싶은 경우

프레임워크에서 표준 컴포넌트 대신 커스텀 자식 클래스를 사용하고 싶은 경우, 프레임워크 전체에서 컴포넌트들을 생성하는 코드를 단일 팩토리 메서드로 줄이고, 누구나 해당 팩토리 메서드를 오버라이드 할 수 있도록 한다.

🔍 예시

✅ 기존 객체들을 재사용하여 시스템 리소스를 절약하고 싶은 경우

  • 해당 요구사항은 데이터베이스 연결, 파일 시스템 및 네트워크 등 시스템 자원을 많이 사용하는 대규모 객체들을 처리하는 경우 자주 발생한다.
  • 기존 객체를 재사용하기 위한 로직은 다음과 같다.
    1. 생성된 모든 객체를 추적하기 위한 일부 스토리지를 생성한다.
    2. 객체를 요청하는 경우, 프로그램이 해당 풀 내에서 유휴 객체를 찾는다.
      2-1. 유휴 객체가 존재하는 경우, 해당 객체를 클라이언트 코드에 반환한다.
      2-2. 유휴 객체가 존재하지 않는 경우, 프로그램이 새로운 객체를 생성하여 풀에 새로 생성한 객체를 추가한다.
  • 위 로직을 작성하게 되면 코드 양이 많고, 코드 중복을 막기 위해 한 곳에 넣어야 한다.
  • 재사용하려는 객체들의 클래스의 생성자에 배치하는 것이 합리적으로 보이나, 생성자의 특성상 기존 인스턴스를 반환할 수 없다.
  • 따라서, 새 객체들을 생성하고 기존 객체를 재사용할 수 있는 일반적인 메서드가 필요하게 되는데, 이 때 팩토리 메서드 패턴으로 구현하면 좋다.

5. 구현방법

1️⃣ 모든 제품의 공통 인터페이스를 선언한다. 인터페이스 내에는 의미 있는 메서드들을 선언해야 한다.

interface Product {
	...
}

2️⃣ 크리에이터 클래스 내부에 빈 팩토리 메서드를 추가한다. 메서드의 반환 유형은 공통 제품 인터페이스와 일치해야 한다.

abstract class Factory {
	public abstract Product createProduct();
}

3️⃣ 객체 생성 로직을 팩토리 메서드로 이동하여 중앙에서 관리하도록 한다.

  • 크리에이터 코드에서 제품 생성자들에 대한 모든 참조를 찾는다. 즉, 코드 전반에서 new 키워드를 사용해 객체를 생성하는 부분을 찾는다.
Product productA = new ProductA();
Product productB = new ProductB();
  • 이 참조들을 하나씩 팩토리 메서드에 대한 호출로 교체한다. 즉, new 키워드 대신 팩토리 메서드를 사용하여 객체를 생성하도록 수정한다.
// 수정 전
Product productA = new ProductA();
// 수정 후
Product productA = createProduct("A");
  • 제품 생성 코드를 팩토리 메서드로 추출한다. 즉, 제품을 생성하는 모든 코드를 하나의 팩토리 메서드로 옮긴다.
public Product createProduct(String type) {
	switch(type) {
    	case "A":
        	return new ProductA();
        case "B":
        	return new ProductB();
        default:
        	throw new IllegalArgumentException("Unknown product type: " + type);
    }
}

4️⃣ 다형성을 활용해 제품 생성 로직을 자식 클래스들로 분리한다.

  • 팩토리 메서드에 나열된 각 제품 유형에 대한 크리에이터 자식 클래스들의 집합을 생성한 후(각 제품 유형에 대해 별도의 자식 클래스를 만든 후), 자식 클래스들에서 팩토리 메서드를 오버라이딩 한다.
class ProductAFactory extends Factory {
	@Override
    public Product createProduct() {
    	return new ProductA();
    }
}

class ProductBFactory extends Factory {
	@Override
    public Product createProduct() {
    	return new ProductB();
    }
}
  • 기초 메서드에서 생성자 코드의 적절한 부분들을 추출한다. 즉, 팩토리 메서드에서 제품을 생성할 때 공통적인 로직은 부모 클래스에 남기고, 개별적인 로직만 자식 클래스에서 오버라이드 한다.
abstract class Factory {
	public Product create() {
    	// 공통 로직
        Product product = createProduct();
        initializeProduct(product);
        return Product;
    }
    
    // 자식 클래스에서 오버라이딩
    protected abstract Product createProduct();
    
    private void initializeProduct(Product product) {
    	// 공통적인 추가 초기화 로직
        ...
    }
}
// 사용 예시
Factory factory = new ProductAFactory();
Product product = factory.create();

5️⃣ 제품 유형이 너무 많아 모든 제품에 대해 자식 클래스들을 만드는 것이 비효율적일 때, 제어 매개변수를 활용하여 팩토리 메서드에 필요한 객체를 생성하도록 설계할 수 있다.

// Transport(운송수단) 기초 클래스
abstract class Transport {
	public abstract void deliver();
}

class Truck extends Transport {
	@Override
    public void deliver() {
    	System.out.println("Deliver product by truck");
    }
}

class Train extends Transport() {
	@Override
    public void deliver() {
    	System.out.println("Deliver product by train");
    }
}

class Plane extends Transport() {
	@Override
    public void deliver() {
    	System.out.println("Deliver product by plane");
    }
}
// Mail(우편) 기초 클래스
abstract class Mail {
	//팩토리 메서드
    public abstract Transport createTransport(String type);
}

// TruckMail과 TrainMail 자식 클래스 만드는 대신 매개변수를 받아 처리
class GroundMail extends Mail {
	@Override
    public Transport createTransport(String type) {
    	if(type.equalsIgnoreCase("truck") {
        	return new Truck();
        } else if(type.equalsIgnoreCase("train") {
        	return new Train();
        } else {
        	throw new IllegalArgumentException("Unknown transport type: " + type);
        }
    }
}

class AirMail extends Mail {
	@Override
    public Transport createTransport(String type) {
    	if(type.equalsIgnoreCase("plane") {
        	return new Plane();
        } else {
        	throw new IllegalArgumentException("Unknwon transport type: " + type);
        }
    }
}
// 클라이언트 코드
public class UseExample {
	public static void main(String[] args) {
    	// GroundMail 사용
        Mail groundMail = new GroundMail();
        Transport truck = groundMail.createTransport("truck");
        truck.deliver();
        
        Transport train = groundMail.createTransport("train");
        train.deliver();
        
        // AirMail 사용
        Mail airMail = new AirMail();
        Transport plane = airMail.createTransport("plane");
        plane.deliver();
    }
}
# 결과
Deliver product by truck
Deliver product by train
Deliver product by plane

6️⃣ 추출 후, 다음 두 가지 경우로 나누어 자식 클래스에서 기초 팩토리 메서드를 구현할 수 있다.

  • 기초 팩토리 메서드가 비어 있는 경우(기초 클래스에서 아무 동작도 정의하지 않은 경우), 팩토리 메서드를 추상 메서드로 선언하여 반드시 자식 클래스에서 구현하도록 강제할 수 있다.
abstract class Mail {
	public abstract Transport createTransport();
}

class AirMail extends Mail {
	@Override
    public Transport createTransport() {
    	return new Plane();
    }
}

class GroundMail extends Mail {
	@Override
    public Transport createTransport() {
    	return new Truck();
    }
}
  • 기초 팩토리 메서드가 비어 있지 않은 경우(기초 클래스에서 기본 동작을 정의한 경우), 기초 클래스에 정의된 디폴트 행동을 유지하고, 자식 클래스에서 필요할 경우 이를 오버라이드(재정의)할 수 있다.
abstract class Mail {
	public Transport createTransport() {
    	return new Truck();
    }
}

class AirMail extends Mail {
	@Override
    public Transport createTransport() {
    	return new Plane();
    }
}

// 기초 클래스에서 제공하는 기본 동작 사용
class GroundMail extend Mail {
}

6. 장단점

6-1. 장점

  • 크리에이터와 구현 제품들의 결합도를 낮출 수 있다.
  • SRP(단일 책임 원칙)
    • 제품 생성 코드를 프로그램의 한 위치로 이동하여 코드를 더 쉽게 유지관리 할 수 있다.
  • OCP(개방/폐쇄 원칙)
    • 기존의 클라이언트 코드를 훼손하지 않고 새로운 유형의 제품들을 프로그램에 도입할 수 있다.

6-2. 단점

  • 패턴을 구현하기 위해 많은 새로운 자식 클래스들을 도입해야 하므로 코드가 더 복잡해질 수 있다.
    → 크리에이터 클래스들의 기존 계층구조에 패턴을 도입하도록 한다.

7. 다른 패턴과의 관계

패턴분석비고
추상 팩토리▪ 팩토리 메서드들의 집합을 기반으로 함
▪ 프로토타입으로 생성 메서드 구현 가능
확장성 높은 구상 클래스 생성
반복자팩토리 메서드와 함께 사용하여 호환되는 반복자 반환 가능컬렉션과 반복자 간 유연성 제공
프로토타입▪ 상속 기반 아님
▪ 복제된 객체 초기화 필요
특징이 팩토리 메서드와 반대임
템플릿 메서드▪ 팩토리 메서드는 템플릿 메서드의 특수화
▪ 팩토리 메서드는 대규모 템플릿 메서드의 단계 담당
구조적 유사성 제공

📖 참고

profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글