Factory Method
Factory Method 패턴이란?
Factory Method 패턴은 객체 생성을 서브클래스에 위임하는 디자인 패턴입니다. 이 패턴을 통해 객체 생성의 복잡성을 서브클래스에 맡기고, 객체 생성에 대한 로직을 캡슐화 하여 코드의 유연성을 높일 수 있습니다. 이를 통해 클라이언트는 어떤 객체가 생성되는지 알 필요 없이 객체를 사용할 수 있습니다.
Factory Method 패턴 구조
- Client → Creator
- 클라이언트는
Creator
를 통해 객체 생성 요청(CreateProduct()
)을 보냅니다.
- 클라이언트는
- ConcreteCreator → Creator
Creator
는 객체 생성 책임을ConcreteCreatorA
또는ConcreteCreatorB
로 위임합니다.
- ConcreteCreator → Product
ConcreteCreator
는 구체적인Product
(ConcreteProductA
,ConcreteProductB
)를 생성하여 반환합니다.
- Client → Product
- 클라이언트는 반환된
Product
를 사용하며, 구체적인 구현에는 의존하지 않습니다.
- 클라이언트는 반환된
Factory Method 패턴 적용
Factory Method 패턴의 필요성
도서 관리 시스템에서 여러 종류의 도서 객체가 존재한다고 가정해보겠습니다. 예를 들어, 전자책, 종이책, 오디오북 등의 다양한 도서 유형이 있을 때, 각 유형마다 다른 객체를 생성해야 할 수 있습니다. 이때 Factory Method 패턴을 사용하면 객체 생성 로직을 캡슐화하여 코드의 확장성을 높이고, 객체 생성 과정을 서브클래스에 위임할 수 있습니다.
잘못된 처리
일반적으로 객체를 직접 생성하는 경우, 객체 타입이 추가되거나 변경될 때마다 클라이언트 코드를 수정해야 하는 문제가 발생할 수 있습니다.
public class BookFactory
{
public Book CreateBook(string type)
{
if (type == "eBook")
{
return new EBook();
}
else if (type == "paperBook")
{
return new PaperBook();
}
else if (type == "audioBook")
{
return new AudioBook();
}
else
{
throw new ArgumentException("Invalid book type");
}
}
}
- 확장성의 문제 :새로운 도서 유형이 추가될 때마다 클라이언트 코드에 수정이 필요하며, 코드가 비대해질 수 있습니다.
- 단일 책임 원칙 위반 : 객체 생성과 관련된 로직이 클라이언트 코드에 포함되므로, 객체 생성을 관리하는 코드와 비즈니스 로직이 뒤섞여 단일 책임 원칙을 위반할 수 있습니다.
일반적으로 객체를 생성할 때는
new
키워드를 사용하여 직접 인스턴스를 생성합니다. 그러나 이렇게 하면 코드가 특정 클래스에 의존하게 되어, 새로운 객체를 추가하거나 생성 로직을 변경할 때 기존 코드의 많은 부분을 수정해야 할 수 있습니다. 이는 유지보수가 어렵고, 확장성이 떨어지는 문제를 야기합니다.
Factory Method 패턴 적용 예시
Factory Method 패턴을 적용하면 객체 생성 로직을 서브클래스에서 담당하게 하고, 클라이언트는 그 생성 과정을 알 필요 없이 객체를 사용할 수 있습니다.
// Product 인터페이스
public interface IBook
{
void Read();
}
// Concrete Product: EBook
public class EBook : IBook
{
public void Read() => Console.WriteLine("Reading an eBook.");
}
// Concrete Product: PaperBook
public class PaperBook : IBook
{
public void Read() => Console.WriteLine("Reading a paper book.");
}
// Concrete Product: AudioBook
public class AudioBook : IBook
{
public void Read() => Console.WriteLine("Listening to an audiobook.");
}
// Creator 클래스
public abstract class BookFactory
{
public abstract IBook CreateBook();
public void ReadBook()
{
IBook book = CreateBook();
book.Read();
}
}
// Concrete Creator: EBookFactory
public class EBookFactory : BookFactory
{
public override IBook CreateBook() => new EBook();
}
// Concrete Creator: PaperBookFactory
public class PaperBookFactory : BookFactory
{
public override IBook CreateBook() => new PaperBook();
}
// Concrete Creator: AudioBookFactory
public class AudioBookFactory : BookFactory
{
public override IBook CreateBook() => new AudioBook();
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
BookFactory factory = new EBookFactory();
factory.ReadBook(); // Output: Reading an eBook.
factory = new PaperBookFactory();
factory.ReadBook(); // Output: Reading a paper book.
}
}
Factory Method 패턴 구성 요소
Product
객체 생성을 위한 인터페이스를 정의합니다.
IBook
인터페이스가 이에 해당합니다.
Concrete Product
Product
인터페이스를 구현하여 구체적인 객체를 생성합니다.
예제에서는 EBook
, PaperBook
, AudioBook
이 해당합니다.
Creator
Product
객체를 생성하는 Factory Method
를 정의합니다. 객체 생성의 책임을 서브클래스에 위임하며, 생성된 객체를 사용하는 메서드를 제공합니다.
예제에서는 BookFactory
가 이에 해당합니다.
Concrete Creator
Creator
클래스를 상속받아 구체적인 Factory Method
를 구현합니다. 이 클래스는 특정 제품 객체를 생성하는 역할을 합니다.
예제에서는 EBookFactory
, PaperBookFactory
, AudioBookFactory
가 해당합니다.
개선 사항
코드의 유연성 증가
새로운 유형의 도서 객체가 추가될 때마다 Concrete Product
클래스와 Concrete Creator
클래스를 추가하기만 하면 됩니다.
예를 들어, Comix 를 추가 한다고 할 때, 추가되는 코드는 다음과 같습니다.
// Concrete Product: Comix
public class Comix : IBook
{
public void Read() => Console.WriteLine("Reading a comix.");
}
// Concrete Creator: ComixFactory
public class ComixFactory : BookFactory
{
public override IBook CreateBook() => new Comix();
}
기존 코드를 수정할 필요 없이 확장할 수 있어, 개방-폐쇄 원칙을 잘 준수합니다.
객체 생성 로직의 분리
객체 생성 로직이 클라이언트 코드에서 분리되므로, 단일 책임 원칙을 준수하게 됩니다.
적용 시 유의 사항
클래스의 증가
Factory Method
패턴을 사용하면, Product
클래스와 Creator
클래스 외에도 구체적인 제품을 생성하는 Concrete Product
와 Concrete Creator
클래스들이 계속해서 증가할 수 있습니다. 이로 인해 클래스가 너무 많아져 복잡해질 수 있습니다.
작은 프로젝트나 간단한 객체 생성에서는 굳이 Factory Method 패턴을 적용하지 않고, 간단한 객체 생성 방식을 사용하는 것이 더 적합할 수 있습니다.
객체 생성의 책임 분리
객체 생성의 책임이 분리되지만, 잘못된 경우에는 객체 생성 로직이 복잡해질 수 있습니다. 객체가 복잡해지지 않도록 각 클래스가 맡는 책임을 명확히 할 필요가 있습니다.
객체지향 원칙과의 관계
Factory Method와 캡슐화
Factory Method 패턴은 객체 생성 로직을 캡슐화하여 클라이언트 코드에서 숨깁니다. 클라이언트는 구체적인 객체 생성 과정에 대해 알 필요 없이, 객체를 생성할 수 있습니다. 이는 시스템의 복잡도를 줄이고, 클라이언트 코드의 간결성을 유지하는 데 도움이 됩니다.
Factory Method와 다형성
Factory Method 패턴은 다형성을 적극적으로 활용합니다. Creator
는 Product
객체를 생성하는 인터페이스만 정의하고, 구체적인 생성은 서브클래스에서 처리합니다. 다형성을 통해 Creator
클래스는 Concrete Product
클래스에 의존하지 않고, 상위 Product
인터페이스에 의존합니다. 이를 통해 객체 생성 방식의 확장성을 높일 수 있습니다. BookFactory
는 다양한 도서 객체들을 생성할 수 있지만, ReadBook()
메서드를 통해 상위 인터페이스에 의존하므로 확장성이 증가합니다.
Factory Method와 단일 책임 원칙
Factory Method 패턴은 객체 생성의 책임을 Creator
클래스에 위임하여, 클라이언트 코드에서 객체 생성과 관련된 책임을 분리합니다. 이를 통해 객체 생성과 관련된 로직은 모두 Creator
클래스에 캡슐화되며, 클라이언트 코드는 객체 생성에 신경 쓸 필요 없이 비즈니스 로직에 집중할 수 있습니다. 각 Concrete Creator
는 자신이 생성하는 제품 객체에 대한 책임만 가지며, 새로운 제품이 추가될 때도 각 제품에 맞는 팩토리 클래스가 추가되므로 단일 책임 원칙SRP를 준수합니다.
Factory Method와 개방_폐쇄 원칙
Factory Method 패턴은 새로운 Product
객체를 생성하는 방식이 추가되더라도 기존 코드를 수정할 필요 없이 확장할 수 있습니다. 새로운 유형의 책이 필요할 경우, 새로운 Concrete Product
와 Concrete Creator
클래스를 추가하기만 하면 됩니다. 기존의 BookFactory
와 클라이언트 코드는 수정할 필요가 없습니다.
Factory Method와 리스코프 치환 원칙
Factory Method 패턴에서 Creator
클래스는 항상 Product
인터페이스를 반환하므로, 구체적인 Product
클래스가 바뀌더라도 클라이언트는 동일한 방식으로 메서드를 호출할 수 있습니다. 각 Concrete Product
클래스는 Product
인터페이스를 준수하므로, 어떤 구체적인 클래스가 생성되더라도 상위 클래스에서 정의된 인터페이스만 사용할 수 있습니다. 클라이언트가 EBookFactory
에서 AudioBookFactory
로 변경하더라도 IBook
인터페이스를 통해 동일한 방식으로 책을 읽을 수 있습니다.
맺음말말
Factory Method 패턴은 객체 생성 로직을 서브클래스에 위임하여, 클라이언트 코드와 객체 생성 로직을 분리하는 유용한 패턴입니다. 객체 생성의 확장성을 높이고, 객체 생성 로직의 복잡성을 줄이는 데 유용하며, 객체지향 설계 원칙을 잘 준수합니다.
심화 학습
Factory Method와 Abstract Factory의 차이
- Abstract Factory 패턴 심화 참조
Factory Method와 Strategy
Factory Method 패턴과 전략 패턴Strategy은 서로 조합하여 객체의 생성 방식 뿐만 아니라, 생성된 객체의 동작 방식도 유연하게 제어할 수 있습니다. 예를 들어, 책을 생성한 후, 그 책이 어떤 포맷으로 제공될 지를 전략 패턴을 통해 결정할 수 있습니다.
// ICreationStrategy 인터페이스
public interface ICreationStrategy
{
IBook CreateBook();
}
// EBook 생성 전략
public class EBookCreationStrategy : ICreationStrategy
{
public IBook CreateBook() => new EBook();
}
// PaperBook 생성 전략
public class PaperBookCreationStrategy : ICreationStrategy
{
public IBook CreateBook() => new PaperBook();
}
// BookWithStrategy 클래스
public class BookWithStrategy
{
private ICreationStrategy _strategy;
public BookWithStrategy(ICreationStrategy strategy)
{
_strategy = strategy;
}
public void CreateAndReadBook()
{
IBook book = _strategy.CreateBook();
book.Read();
}
}
// 클라이언트 코드
public class Client
{
public static void Main(string[] args)
{
// 전자책 생성 전략 사용
ICreationStrategy ebookStrategy = new EBookCreationStrategy();
BookWithStrategy ebookWithStrategy = new BookWithStrategy(ebookStrategy);
ebookWithStrategy.CreateAndReadBook();
// Output: Reading an eBook.
// 종이책 생성 전략 사용
ICreationStrategy paperBookStrategy = new PaperBookCreationStrategy();
BookWithStrategy paperBookWithStrategy = new BookWithStrategy(paperBookStrategy);
paperBookWithStrategy.CreateAndReadBook();
// Output: Reading a paper book.
}
}
클라이언트는 전략 패턴을 사용하여 생성할 책의 유형을 런타임에 결정합니다. 전략 객체를 변경하면 책의 생성 방식이 달라지며, 생성된 객체는 팩토리 메서드를 통해 처리됩니다.
Factory Method와 Singleton
Factory Method 패턴을 통해 객체를 생성하되, 생성된 객체가 애플리케이션 내에서 하나만 존재하도록 싱글턴 패턴을 적용합니다.
public class SingletonBookFactory : IBookFactory
{
private static SingletonBookFactory _instance = new SingletonBookFactory();
private SingletonBookFactory() {}
public static SingletonBookFactory Instance => _instance
public IBook CreateEBook() => new EBook();
public IBook CreatePaperBook() => new PaperBook();
}
// 클라이언트 코드
public class Client
{
public static void Main(string[] args)
{
SingletonBookFactory factory = SingletonBookFactory.Instance;
// 전자책 생성
IBook ebook = factory.CreateEBook();
ebook.Read();
// 종이책 생성
IBook paperBook = factory.CreatePaperBook();
paperBook.Read();
}
}
싱글턴 팩토리 인스턴스는 전역에서 동일한 인스턴스를 유지하며, 클라이언트는 이 팩토리를 사용해 책을 생성합니다. 이를 통해 애플리케이션 전역에서 하나의 팩토리만 사용하도록 보장할 수 있습니다.
Factory Method와 의존성 주입
의존성 주입은 객체의 의존성을 외부에서 주입받아 객체 간 결합도를 낮추는 방법입니다. 팩토리 객체를 의존성 주입Defendancy Injection, DI을 통해 주입받아, 객체 생성을 유연하게 제어할 수 있으며, 이를 통해 객체의 생명 주기를 주입 받은 팩토리에서 관리할 수 있습니다.
// BookService 클래스
public class BookService
{
private readonly IBookFactory _bookFactory;
public BookService(IBookFactory bookFactory)
{
_bookFactory = bookFactory;
}
public void ReadBook()
{
IBook ebook = _bookFactory.CreateEBook();
ebook.Read();
}
}
// 클라이언트 코드 (DI 컨테이너와 함께)
public class Client
{
public static void Main(string[] args)
{
// 의존성 주입 시뮬레이션 (수동으로 DI 처리)
IBookFactory bookFactory = new BookFactory();
BookService bookService = new BookService(bookFactory);
// 전자책 읽기
bookService.ReadBook();
}
}
팩토리 객체를 의존성 주입을 통해 주입받아, 클라이언트는 팩토리의 구체적인 구현을 알 필요 없이 책을 생성하고 사용할 수 있습니다. 이를 통해 객체 간 결합도를 낮추고, 테스트가 용이한 구조를 만들 수 있습니다.