본문 바로가기
Concepts/Design Pattern

객체 생성 패턴- 팩토리 메소드

by ocwokocw 2021. 2. 21.

- 참조: GoF의 디자인 패턴

- Template method: https://en.wikipedia.org/wiki/Template_method_pattern

- 팩토리 메소드

객체를 생성하기 위한 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브클래스가 내린다.

 

팩토리 메소드의 예제를 검색하면 흔히 메소드의 매개변수의 타입에 따라 반환하는 객체가 다른 예제가 많이 나온다. 그래서 흔히 객체를 매개변수타입에 따라 다르게 반환하는것이 팩토리 메소드라고 생각한다. 물론 나도 예전에는 그렇게 생각했다.

 

하지만 위의 정의를 다시한번 생각해보면 팩토리 메소드의 핵심은 매개변수에 따라 다른 인스턴스를 반환하는것이 아님을 알 수 있다. 이 패턴의 주요핵심은 어떤 인스턴스를 생성할지에 대한 결정을 서브 클래스로 미루는것이며 템플릿 메소드 패턴과 깊은 연관이 있다.

 

팩토리 메소드의 다른 이름은 가상 생성자(Virtual Constructor) 이다. 개인적으로 팩토리 메소드라는 본래 이름보다 가상 생성자라는 별칭이 더 의도를 잘 나타내는 것 같다. 하나의 예제 시나리오를 구현해보면서 팩토리 메소드 패턴에 대해 알아보자.

 

우리는 Excel이나 Word 같은 문서관리 프로그램을 사용한다. 이때 만약 해당 프로그램이 Excel 프로그램이라면 Excel 문서를 다루며, Word 프로그램이라 면 Word 문서를 다룬다. 여기까지의 사실로 2개의 추상화를 해야한다는걸 알 수 있다. 하나는 어떤 프로그램이냐에 대한 Application, 또 하나는 어떤 문서냐에 대한 Document 를 추상화해야 한다. 어떤 응용프로그램을 만드느냐에 따라 어떤 문서를 다룰지가 달라지기 때문에 미리 어떤 문서를 다룰 지 예측할 수 없다.

 

UML로 나타내면 아래와 같다. UML을 보기전 디자인패턴을 먼저 보는 사람을 위해 간단하게 설명하면, 이탤릭체(기울임체)로 되어있는것은 추상 클래스를 나타낸다. docs 라고 써져있는 표시는 집합(Aggregation)을 나타낸다.

 

ExcelDocument와 ExcelApplication은 각각 일반화(Generalization)을 하고 있다. 자세한 사항은 본 블로그의 UML 카테고리의 클래스 다이어그램을 참조하라.

위 다이어그램에서 집중해서 봐야 할 부분은 Application 부분이다. 여기서 createDocument 만 추상 메소드(기울임체)로 선언되어 있는데 이 연산이 팩토리 메소드이다. ExcelApplication 은 이 팩토리 메소드를 재정의하여 ExcelDocument를 반환한다.

 

팩토리 메소드 패턴은 4 참여자가 존재한다.

  • Product(Document): 팩토리 메소드가 생성하는 객체의 인터페이스 정의
  • ConcreteProduct(ExcelDocument): Product 클래스 구현
  • Creator(Application): Product 타입을 반환하는 팩토리 메소드(createDocument)를 선언한다. 또한 Product 객체의 생성을 위해 팩토리 메소드를 호출한다.
  • ConcreteCreator(ExcelApplication): 팩토리 메소드를 재정의하여 ConcreteProduct 의 인스턴스를 반환한다.

단순하게 생성자로 생성하지 않고 팩토리 메소드로 생성하게 되면 서브 클래스에 대해서 hook 메소드를 가지게 된다. 이는 후에 기술하겠지만 템플릿 메소드 패턴과도 연관이 되는 얘기이다. 위에 UML 에서 createDocument() 로 객체를 생성할 때 공통적인 행동이나 hook 기능을 작성할 수 있게 된다.


- Java 예제

우선 Document와 ExcelDocument 부터 정의해보자. Document는 이름 속성을 가지고 있다. 또한 열기, 닫기, 저장, 되돌리기와 같은 기본적인 오퍼레이션을 수행한다. ExcelDocument는 excel에 대한 열기, 닫기, 저장, 되돌리기 메소드를 가지고 있다.

 

public abstract class Document {

	private String documentName;
	
	public Document() {
		super();
		this.documentName = "New Doc1";
	}

	public String getDocumentName() {
		return documentName;
	}
	
	public void setDocumentName(String documentName) {
		this.documentName = documentName;
	}

	abstract void open();
	abstract void close();
	abstract void save();
	abstract void revert();
}

public class ExcelDocument extends Document {

	public ExcelDocument() {
		super();
		System.out.println("ExcelDocument has been created.");
	}

	@Override
	public void open() {
		System.out.println("excel open()");
	}

	@Override
	public void close() {
		System.out.println("excel close()");
	}

	@Override
	public void save() {
		System.out.println("excel save()");
	}

	@Override
	public void revert() {
		System.out.println("excel revert()");
	}
}

 

이제 팩토리 메소드의 중요 부분인 Application과 ExcelApplication을 정의해보자. 응용프로그램이 여러개의 Document를 다룰 수 있다고 가정하였다. 어떤 Document를 다룰지는 알 수 없지만, 새 문서 오퍼레이션(newDocument)를 수행하면 새로운 문서가 만들어 지고 해당 문서가 열린다. 

 

ExcelApplication은 이 Application을 상속받는다. 어떤 문서를 생성할지에 대한 팩토리 메소드(createDocument)를 정의하여 ExcelDocument를 반환한다.

 

public abstract class Application {

	private List<Document> docs;
	
	public Application() {
		super();
		this.docs = new ArrayList<Document>();
	}

	abstract protected Document createDocument();
	
	public void newDocument() {
		
		Document document = createDocument();
		docs.add(document);
		document.open();
	}
	
	public void openDocument(Document document) {
		document.open();
	}
}

public class ExcelApplication extends Application {

	@Override
	protected Document createDocument() {
		return new ExcelDocument();
	}
}

 

사용자는 ExcelApplication을 켜고 새 문서를 수행해본다.

 

public static void main(String[] args){

	Application excelApplication = new ExcelApplication();
	excelApplication.newDocument();
}

- 팩토리 메소드 매개변수

Excel 프로그램에서 다른 포맷으로 변환할 때에는 일단 작업 후 저장시에 타입을 정하지만, 우리는 문서를 생성할 때 Text나 CSV 파일생성을 지원한다고 가정해보자. 현재 팩토리 메소드는 createDocument()와 같은 형태이다. 다른 문서에 대한 지원을할 때, createTextDocument()와 같은 팩토리 메소드를 추가해야할까?

 

위와 같이 메소드 자체를 추가하는 방식은 Excel 프로그램만 있다면 문제가 되지 않지만, 만약 Application을 상속받는 Word 프로그램이 있는데 해당 Word 프로그램에서는 Text 문서를 지원하지 않는 경우에 대해 구현이 애매해진다. 이때에는 메소드 매개변수를 이용하면 팩토리 매개변수의 유연성을 더해줄 수 있다.

 

일단 Application을 좀 손봐야한다. DocType와 같이 지원할 문서 포맷을 추가한다. createDocument 에는 해당 DocType 매개변수를 추가하고, newDocument() 메소드에도 타입을 추가한다. 만약 매개변수 없이 default 새 문서 기능 지원을 위해 newDocument() 추상 메소드도 추가한다. (상속받는 Application에 따라 Default 새 문서 기능이 변할 수 있으므로 추상으로 전환한다.)

 

public abstract class Application {

	public enum DocType {EXCEL, TEXT, CSV, WORD};
	
	private List<Document> docs;
	
	public Application() {
		super();
		this.docs = new ArrayList<Document>();
	}

	abstract protected Document createDocument(DocType type);
	
	abstract public void newDocument();
	
	public void newDocument(DocType type) {
		
		Document document = createDocument(type);
		docs.add(document);
		document.open();
	}
	
	public void openDocument(Document document) {
		document.open();
	}
}

 

TextDocument와 CsvDocument도 ExcelDocument와 같이 구현하여 Sysout 내용만 바꾼다. 그리고 ExcelApplication을 손봐야 한다. 매개변수를 체크하여 해당 문서 타입을 반환한다. Word 문서 포맷도 있지만 이는 Excel 에서 지원하지 않는다.

 

public class ExcelApplication extends Application {

	@Override
	public void newDocument() {
		newDocument(DocType.EXCEL);
	}

	@Override
	public Document createDocument(DocType type) {
		
		switch (type) {
		case EXCEL:
			return new ExcelDocument();
		case TEXT:
			return new TextDocument();
		case CSV:
			return new CsvDocument();
		default:
			throw new RuntimeException("UnSupportedType: " + type);
		}
	}
}

 

createDocument 는 우리가 흔히 팩토리 메소드에서 많이 보는 타입이다. 글의 서두에서도 언급했듯이 이는 팩토리 메소드를 매개변수화한 방식으로 구현했을 뿐 이 자체가 팩토리 메소드 패턴이 아니다. 팩토리 메소드 패턴은 인스턴스의 생성을 서브 클래스에서 했다는것이 중요하다.

 

public static void main(String[] args){

	Application excelApplication = new ExcelApplication();
	excelApplication.newDocument();
	excelApplication.newDocument(DocType.CSV);
	excelApplication.newDocument(DocType.EXCEL);
	excelApplication.newDocument(DocType.TEXT);
	excelApplication.newDocument(DocType.WORD);
}

- 템플릿 메소드와 팩토리 메소드

글 서두에서 팩토리 메소드 패턴은 템플릿 메소드 패턴과 연관이 있다고 했다. 팩토리 메소드 패턴은 "템플릿 메소드 패턴의 생성자 버전"이라고 할 수 있다. 이게 무슨 의미일까?

 

transactionTemplate.executeWithoutResult(status -> {
	System.out.println("transaction");
});

 

웹 개발자라면 Spring Transaction을 어노테이션이 아닌 TransactionTemplate 으로 제어할 때 위와 같이 사용해본적이 있을것이다. 위에서 우리는 비즈니스 코드에 대한 부분만을 기술하지 해당 로직이 트랜잭션을 보장해야하는 부분을 코드로 작성하지는 않는다. 이처럼 템플릿 메소드는 알고리즘의 프로그램 뼈대를 제공한다.

 

아래는 우리가 작성한 newDocument 코드이다. 팩토리 메소드인 createDocument 에 대한 구체 행동을 서브 클래스에 위임하여 객체를 생성하였다. 인스턴스 생성을 담당하는 팩토리 메소드 외에는 Template처럼 동일한 행동을 보장한다.

 

public void newDocument(DocType type) {
		
	Document document = createDocument(type);
	docs.add(document);
	document.open();
}

댓글