티스토리 뷰
🌻 ⬇️
https://refactoring.guru/design-patterns/factory-method
📝 한 줄 정리
A.k.a. Virtual Constructor
Factory 인터페이스에서 객체를 생성하는 팩토리 메소드 및 공통 비즈니스 로직 선언하고, 이걸 상속 받은 각 Concrete Factory 클래스에서 각 타입의 객체를 생성하도록 팩토리 메소드 오버라이딩 및 각 커스텀한 비즈니스 로직 구현으로
여러 타입의 product에 대해 클라이언트 코드는 그냥 추상적인 product 객체라고 생각하고 대응할 수 있도록 한다.
🤔 해결할 문제 상황
물류 관리 앱을 만든다고 생각해보자. 기존의 첫번째 버전 앱은 트럭으로만 운송할 수 있어서 대부분의 코드가 Truck 클래스 안에 담겨있다.
그런데, 앱이 유명해져서 해외 운송 요청도 들어오게 됐다. 기존 코드는 Truck 클래스랑 커플링 되어있는데 Ship을 앱에 새로 들이려고 한다면 코드 수정을 엄청 해야한다.. 거기다가 추후에 다른 타입의 운송 수단을 추가로 도입하기로 한다면?!
운송 타입에 따른 조건문 범벅이 되어있는 끔찍한 코드가 될 것이다..
🧐 해결 방법
new 연산자를 사용한 객체 직접 생성 대신 특수한 "factory" 메소드 호출 방식으로 코드를 수정하자 : 객체는 여전히 new 연산자를 통해 생성될테지만, 그 코드는 factory 메소드 내부에 있을거다.
factory 메소드에서 리턴되는 객체는 종종 product로 불리기도 한다.
그저 생성자 호출 코드를 다른 파트로 옮긴 것일 뿐이니까 이 패턴이 무의미하다고 느낄 수도 있다.
그렇지만 factory 메소드(=예를 들면 createTransport )를 subclass에서 오버라이드 해서 어떤 클래스의 product를 생성할지 바꿀 수 있다. 리턴할 product가 같은 인터페이스나 base class를 상속한 것이어야 한다는 제약 사항은 존재함.
예를들어 Truck, Ship 클래스 모두 deliver 메소드를 정의한 Transport 인터페이스를 상속받고 있는데, deliver는 각자 방식으로 알아서 구현. truck은 땅에서 화물을 운반하고, ship은 바다에서 화물 운반.
RoadLogistics 클래스의 팩토리 메소드는 트럭 객체를, SeaLogistics 클래스의 팩토리 메소드는 ship 객체를 리턴한다.
공통 인터페이스를 상속받아서 구현한 이상 클라이언트 코드는 어떤 객체든 deliver메소드에 respond_to 하는 추상적인 운송 수단으로만 취급할 거다.
🏗구조
* creator라는 클래스 이름과 다르게 product(객체) 생성이 이 클래스의 주된 responsibility가 아닐 수도 있다!
보통 creator클래스는 product와 관련 있는 코어 비지니스 로직을 담고 있을 수 있다.
팩토리 메소드 패턴은 이런 로직들을 concrete product 클래스와 decouple할 수 있게 해준다.
실무에서도 클래스 이름이 ~Factory 이런식이 아닐 수 있음. 하지만 그 내부에 product 생성 관련 메소드가 포함되어 있다면 팩토리 메소드 패턴을 적용했구나 유추해볼 수 있음.
👩💻 pseudo 코드
클라이언트 코드랑 concrete UI 클래스를 decouple하면서, cross-platform UI 객체 생성하기 예시
// creator 클래스는 product 클래스의 객체를 리턴해야 하는 factory 메소드를 선언한다.
// 이 creator 클래스의 subclass가 이 메소드 실제 구현
class Dialog is
// 이 factory 메소드의 몇가지 default 구현을 제공할 수도 있음
abstract method createButton():Button
// creator라는 이름과 다르게 이 클래스의 메인 responsibility는 products 생성이 아닐 수 있다.
// factory 메소드에서 리턴하는 product 객체에 의존하는 코어 비지니스 로직을 포함하는 경우가 많다.
// subclass들은 factory 메소드를 오버라이딩 해
// 다른 타입의 product를 리턴 하는 것으로 간접적으로 비지니스 로직을 수정한다.
method render() is
// product 객체를 생성하도록 factory 메소드 호출
Button okButton = createButton()
// 그 product를 사용
okButton.onClick(closeDialog)
okButton.render()
// creators를 상속받은 Concreate creator클래스는
// factory 메소드를 오버라이딩 해서 리턴하는 product 타입을 바꾼다
class WindowsDialog extends Dialog is
method createButton():Button is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton():Button is
return new HTMLButton()
// product 인터페이스는 모든 concrete product들이 무조건 구현해야 할 operation 들을 선언한다.
interface Button is
method render()
method onClick(f)
// concrete product들은 product 인터페이스에 대한 다양한 구현 제공
class WindowsButton implements Button is
method render(a, b) is
// window 스타일 버튼 렌더링
method onClick(f) is
// native OS 클릭 이벤트랑 바인딩 한다.
class HTMLButton implements Button is
method render(a, b) is
// HTML 버튼 리턴
method onClick(f) is
// 웹 브라우저 클릭 이벤트에 바인딩
class Application is
field dialog: Dialog
// 현재 configuration이나 환경변수 세팅에 따라서 concrete creator를 고른다.
method initialize() is
config = readApplicationConfigFile()
if (config.OS == "Windows") then
dialog = new WindowsDialog()
else if (config.OS == "Web") then
dialog = new WebDialog()
else
throw new Exception("Error! Unknown operating system.")
// 클라이언트 코드는 concrete creator 인스턴스를 가지고 작업하게 된다
// base 인터페이스에 선언된 메소드를 사용하는 한 클라이언트에 어떤
// creator 서브 클래스를 전달해도 괜찮음.
method main() is
this.initialize()
dialog.render()
팩토리 메소드 패턴을 사용하면 OS마다 다른 dialog 로직을 사용할 필요가 없다.
base dialog 클래스에서 버튼을 생성하는 팩토리 메소드를 선언해 놓으면, 나중에 팩토리 메소드로 window-스타일 버튼을 생성하는 dialog subclass를 만들 수 있다. 이 subclass는 base class로 부터 대부분의 dialog 관련 코드를 상속 받아 스크린에 window 버튼을 렌더링 할 수 있다.
🍟 적용 분야
- 미리 객체가 어떤 타입이고, 우리 코드가 해당 객체에 대해 어떤 의존성을 가지게 될지 모를때 팩토리 메소드 패턴을 도입하라.
=> product 생성 코드와 실제 product를 사용하는 코드를 분리할 수 있도록 해준다. 예를 들어 당신의 앱에 새로운 product 타입을 도입하려고 한다면 그저 팩토리 메소드를 오버라이딩 하는 새로운 creator 서브클래스를 추가하면 된다.
- 당신의 라이브러리나 프레임워크 유저에게 내부적인 컴포넌트를 추가할 수 있게 만들고 싶을 때
=> 라이브러리나 프레임워크 확장의 가장 쉬운 방법이 상속이다.
오픈소스 UI 프레임워크를 사용해서 앱을 작성한다고 가정해보자. 라운드 버튼이 필요한데 프레임워크는 네모 버튼만 제공한다. Button 클래스를 RoundButton subclass로 확장했다. 그런데 메인 UIFramework에 기본 버튼 대신에 당신의 새로운 subclass 버튼을 사용한다고 알려줘야 한다.
그럴려면 base framework 클래스로부터 createButton 메소드를 오버라이딩 하는(네모가 아닌 라운드 버튼 생성함) UIWithRoundButtons 서브클래스를 생성하고 앞으로 UIFramework 대신 UIWithRoundButtons 클래스를 사용한다.
- 매번 새로운 객체를 다시 만드는 대신 이미 있는 객체를 재사용해서 시스템 자원을 절약하고 싶을 때
=> db 연결, 파일시스템, 네트워크 자원 같은 resource-intensive 객체를 사용해야 할 때 이런 경험이 있었을 것이다.
기존 객체를 재사용하려면..
1. 생성된 객체를 트래킹할 수 있는 저장소가 필요
2. 누가 객체를 요청하면 pool안에서 free한 객체를 찾아야함
3. 그리고 그것을 클라이언트 코드에 리턴해주기
4. free 객체가 없다면 새로 생성해서 pool에 추가해줘야함
코드 중복을 막으려면 위의 저런 코드들을 한 곳에 모아둬야 한다. 가장 적합한 장소는 재사용할 객체의 클래스 생성자. 그러나 생성자는 정의에 따라 항상 "새로운" 객체를 리턴해야 함.
그렇다면 팩토리 메소드가 적합할 듯 하다 : 새로운 객체를 생성할 수도, 이미 있는 것을 리턴할 수도 있는 생성자가 아닌 일반 메소드
🔮 장점
- creator와 concrete product가 타이트 하게 커플링 되는 것을 막을 수 있다.
- 단일책임원칙. product 생성 코드를 한 곳에 몰아 놓을 수 있어서 관리가 쉬워짐.
- open-closed : 기존 클라이언트 코드가 계속 동작하게 하면서 새로운 타입의 product를 들여올 수 있음.
🧸 단점
- 계속 새로운 subclass를 도입해야 해서 코드가 좀 더 복잡해질 수 있다.
🦄 다른 참고하면 좋을 자료
'시리즈 > 디자인패턴' 카테고리의 다른 글
[Creational pattern] Abstract Factory 패턴 (0) | 2022.03.09 |
---|---|
OOP 패러다임 : class-based vs prototype-based (0) | 2022.02.12 |
[Creational pattern] Prototype 패턴 (0) | 2022.02.12 |
[Creational pattern] Singleton 패턴 (0) | 2022.01.15 |