문서를 다루는 프레임워크를 하나 짠다고 하자. Application은 애플리케이션의 뼈대다 — 새 문서를 열고, 목록에 담고, 창을 띄우고, 저장 시점을 관리한다. Document는 그 문서 하나하나다. 이 뼈대는 놀랄 만큼 일반적이다. “새 문서 만들기” 메뉴를 누르면 문서 객체를 하나 만들어 목록에 추가하고 화면에 띄운다는 흐름은, 그림판이든 워드프로세서든 스프레드시트든 똑같다.
문제는 Application이 이 흐름을 다 알면서도 정작 어떤 문서를 만들어야 하는지는 모른다는 데 있다. 그림판이라면 DrawingDocument를, 워드라면 TextDocument를 만들어야 하는데, 프레임워크를 짜는 시점에는 그게 무엇이 될지 알 수 없다. 프레임워크는 자신을 가져다 쓸 미래의 애플리케이션을 미리 알 방법이 없기 때문이다. 상위는 절차를 쥐고 있는데 구체 타입은 하위만 안다 — 팩토리 메서드(Factory Method)는 정확히 이 어긋남을 위한 패턴이다.
무엇이 문제인가 — 동기
가장 단순하게 짜면 Application의 문서 생성 자리에 new TextDocument()를 박게 된다.
public void newDocument() {
Document doc = new TextDocument(); // 여기가 문제
docs.add(doc);
doc.open();
}
이 한 줄이 프레임워크 전체를 특정 제품에 못박는다. 세 가지가 동시에 아프다.
첫째, 프레임워크가 구체 클래스에 의존한다. Application은 문서의 흐름만 관리하면 되는데, TextDocument라는 구체 타입의 이름을 자기 코드 안에 지니게 됐다. 프레임워크(일반적이어야 하는 쪽)가 응용(구체적인 쪽)을 알아 버린 것이다. 의존의 방향이 거꾸로 섰다.
둘째, 재사용이 막힌다. 그림판을 만들려는 사람은 이 Application을 그대로 쓸 수 없다. newDocument의 흐름은 완벽히 재사용하고 싶은데, 딱 한 줄 new TextDocument() 때문에 클래스 전체를 복사해 고쳐야 한다. 재사용하고 싶은 것(흐름)과 바꾸고 싶은 것(제품)이 한 메서드에 엉겨 붙어 떼어지지 않는다.
셋째, 확장점이 없다. 프레임워크를 쓰는 쪽이 “여기서는 내 문서를 만들어 달라”고 끼어들 자리가 없다. 프레임워크가 미리 뚫어 둔 구멍이 없으면, 사용자는 프레임워크의 흐름에 자기 타입을 주입할 방법이 없다.
판단 기준: 상위 코드가 전체 절차를 쥐고 있는데 그 절차 중 “객체를 만드는 한 걸음”만 미래에 달라진다면, 그 한 걸음을 비워 둘 자리로 팩토리 메서드를 의심한다. 함정: 만들 제품이 처음부터 하나로 정해져 있고 서브클래스로 갈릴 일이 없다면, new를 그냥 두는 게 정답이다 — 확장점은 미래에 실제로 갈릴 축에만 뚫는다.
패턴의 구조 — 누가 무엇을 하나
팩토리 메서드는 배역이 넷이지만, 핵심은 흐름을 쥔 쪽과 타입을 정하는 쪽을 상속으로 가른다는 한 문장이다.
Product(제품 인터페이스). 프레임워크가 다룰 객체의 공통 얼굴이다. Document가 이것 — 상위 코드는 이 인터페이스만 알고, 구체적으로 무슨 문서인지는 모른다.
ConcreteProduct(구체 제품). 인터페이스를 실제로 구현한 제품들. TextDocument, DrawingDocument. 프레임워크는 이 이름들을 몰라야 한다.
Creator(생성자 — 여기가 핵심). 제품을 사용하는 상위 클래스다. Application이 이것. 결정적인 점은, Creator가 제품을 new로 직접 만들지 않고 팩토리 메서드라는 자기 자신의 메서드를 호출해 얻는다는 것이다. 이 팩토리 메서드는 대개 추상(abstract Document createDocument())이라 Creator 자신은 무엇이 만들어질지 모른다. Creator는 “만들어라”라고 자기 자신에게 요청만 하고, 그 결과를 받아 흐름을 이어 간다.
ConcreteCreator(구체 생성자). Creator를 상속해 팩토리 메서드를 오버라이드하는 서브클래스. TextApplication이 createDocument를 재정의해 new TextDocument()를 돌려준다. “무엇을 만들지”의 결정이 오직 여기, 이 한 메서드에 모인다.
협력의 결정적 지점은 누가 팩토리 메서드를 부르는가다. 부르는 쪽은 상위 클래스(Application.newDocument)이고, 채우는 쪽은 하위 클래스(TextApplication.createDocument)다. 상위가 자기가 정의한 추상 메서드를 자기 흐름 안에서 호출하면, 런타임에는 서브클래스가 채운 구현이 실행된다 — 이것이 제어의 역전이다. 상위가 “언제 만들지”의 타이밍을 쥐고, 하위가 “무엇을 만들지”의 내용을 쥔다. 이 분업이 팩토리 메서드의 전부다.
판단 기준: 상위 클래스의 흐름 코드가 자기 자신의 추상 메서드를 호출해 제품을 얻는 모양이 보이면 팩토리 메서드가 제대로 앉은 것이다. 함정: 팩토리 메서드를 static으로 만들어 버리면 서브클래스가 오버라이드할 수 없다 — 흔히 “정적 팩토리 메서드”(생성자 대신 이름 있는 static 메서드)와 혼동하는데, 그것은 이 패턴이 아니다. 이 패턴의 핵심은 오버라이드 가능한 인스턴스 메서드라는 점이다.
코드로 — 패턴이 자라나는 과정
new를 하드코딩한 프레임워크에서 출발해, 생성을 추상 메서드로 비우고, 서브클래스가 채우게 만드는 과정을 단계로 따라간다.
public class Application { private List<Document> docs = new ArrayList<>(); // 문서를 여는 흐름: 만들고 → 목록에 담고 → 연다. // 이 절차 자체는 어떤 애플리케이션에서나 똑같다. public void newDocument() { Document doc = new TextDocument(); // 딱 이 한 줄이 프레임워크를 TextDocument에 못박는다 docs.add(doc); doc.open(); }}// 그림판을 만들려면? newDocument의 흐름을 통째로 재사용하고 싶은데// new TextDocument() 한 줄 때문에 Application을 복사해 고쳐야 한다.// 재사용할 것(흐름)과 바꿀 것(제품)이 한 메서드에 엉겨 있다.
첫 스텝의 new TextDocument()가 사라진 것이 아니라 서브클래스로 옮겨 갔다는 점이 핵심이다. 팩토리 메서드는 생성을 없애지 않는다. 생성이라는 결정을, 흐름을 쥔 상위 클래스에서 타입을 아는 하위 클래스로 미룰 뿐이다. 그 대가로 상위의 흐름이 특정 제품에서 풀려나, 서브클래싱만으로 새 제품에 재사용된다. 전략(Strategy)이 합성으로 알고리즘을 밖에서 갈아 끼웠다면, 팩토리 메서드는 상속으로 생성 결정을 아래로 미룬다 — 같은 “결정을 미루기”를 다른 축으로 판다.
판단 기준: 리팩터링 후 “새 제품 지원 = 새 서브클래스 하나 + createDocument 한 메서드, 상위 흐름 0줄 수정”이 성립하면 팩토리 메서드가 제대로 앉은 것이다. 함정: 팩토리 메서드 하나를 위해 클래스 전체를 상속해야 한다는 점을 잊으면, 제품 하나 바꾸자고 Creator 계층이 제품 계층과 나란히 두 배로 불어난다.
결과 — 무엇을 얻고 무엇을 내주나
팩토리 메서드가 사는 값은 프레임워크에 확장점을 뚫어 주는 것이다. 상위 클래스는 구체 제품을 몰라도 완결된 흐름을 제공하고, 그 흐름은 특정 제품에 묶이지 않은 채 재사용된다. 프레임워크를 쓰는 쪽은 팩토리 메서드 하나만 오버라이드해 자기 타입을 흐름 속에 주입한다. 의존의 방향이 바로 선다 — 상위(일반적인 쪽)가 하위(구체적인 쪽)를 알지 않고, 하위가 상위의 구멍을 채운다. new가 한 자리(오버라이드된 메서드)로 격리되니, 제품을 바꾸는 변경이 그 한 메서드 안에 갇힌다.
대가는 세 가지다. 첫째, 서브클래싱을 강요한다. 제품 하나를 새로 지원하려면 반드시 Creator를 상속한 새 클래스를 만들어야 한다. 다른 이유로 상속할 일이 없는데 오직 제품 타입을 바꾸려고 클래스 계층 하나를 통째로 만드는 것은, 상속이라는 무거운 도구를 생성 하나에 쓰는 셈이다. 그래서 Creator 계층이 Product 계층과 평행하게 불어나는 경향이 있다 — 제품이 N개면 서브클래스도 N개. 둘째, 간접성이 는다. new TextDocument()를 읽으면 무엇이 만들어지는지 그 자리에서 보이지만, createDocument()는 실제 타입을 보려면 어느 서브클래스가 실행되는지 런타임을 따라가야 한다. 코드를 눈으로 좇는 비용이 오른다. 셋째, 컴파일타임에 고정된다. 무엇을 만들지가 어떤 서브클래스를 인스턴스화했느냐로 정해지므로, 런타임에 제품 종류를 자유롭게 바꾸려면 다른 도구(프로토타입 등)가 필요하다.
운영비용 렌즈로 보면 순이득의 조건이 분명하다. 프레임워크를 짜서 남에게 확장을 열어 주는 자리라면, 서브클래싱의 대가보다 “구체 제품을 몰라도 흐름을 재사용시키는” 이득이 압도적이다. 반대로 만들 제품이 하나로 굳어 있고 확장될 일이 없는 응용 코드라면, 늘어난 Creator 계층은 간접성만 더하고 확장점은 쓰이지 않는다.
판단 기준: 얻는 것(확장점·재사용·의존 역전)과 내주는 것(서브클래스 강요·간접성·컴파일타임 고정)을 세어 순이득이 양수일 때만 팩토리 메서드를 앉힌다. 함정: 생성 로직 하나 감추자고 이 패턴을 쓰면 클래스 계층만 불어난다 — 서브클래싱 없이 생성만 캡슐화하려면 이름 있는 static 메서드나 별도 팩토리 객체로 족하다.
언제 쓰고, 언제 과용인가
팩토리 메서드가 정확히 맞는 자리는 이렇다. 클래스가 자신이 만들 객체를 미리 알 수 없고, 그 결정을 서브클래스에 맡기고 싶을 때. 프레임워크나 라이브러리를 만들어 사용자에게 “여기서 당신의 타입을 끼워 넣으라”는 확장점을 열어야 할 때. 상위 클래스의 흐름은 공유하되 흐름이 다루는 제품만 갈아 끼우고 싶을 때. 자바 컬렉션의 iterator()가 교과서적 사례다 — AbstractList는 순회의 골격을 제공하지만 실제로 어떤 Iterator를 만들지는 각 리스트 구현이 iterator()를 오버라이드해 정한다.
과용의 냄새는 반대편에서 난다. 만들 제품이 하나로 정해져 있는데 “유연성”을 위해 미리 팩토리 메서드를 파고 추상 Creator를 세우면, 그건 오지 않을 확장에 거는 보험이다. 생성 로직을 감추고 싶을 뿐 서브클래스로 갈릴 일이 없다면, 무거운 상속 계층 대신 정적 팩토리 메서드 한 줄이 낫다. Creator 서브클래스가 오직 createDocument 한 메서드만 오버라이드하고 다른 아무 역할도 없다면, 상속의 무게에 견주어 얻는 게 얇지 않은지 의심해야 한다.
여기서 이웃 패턴과의 경계를 그어 둘 필요가 있다. 추상팩토리(Abstract Factory)와는 상하 관계에 가깝다 — 추상 팩토리는 **관련된 제품 군(群)**을 통째로 만드는 인터페이스이고, 그 인터페이스의 각 생성 메서드는 보통 팩토리 메서드로 구현된다. 즉 추상 팩토리가 팩토리 메서드를 부품으로 자주 쓴다. 하나의 제품을 서브클래스가 정하게 하는 것이 팩토리 메서드, 한 벌의 제품군을 갈아 끼우는 것이 추상 팩토리다. 템플릿메서드(Template Method)와는 한 몸처럼 붙어 다닌다 — 팩토리 메서드는 대개 템플릿 메서드 안에서 호출된다. newDocument가 곧 템플릿 메서드(흐름의 골격)이고, 그 골격이 채워야 할 훅 중 “제품을 만드는 훅”이 팩토리 메서드다. 템플릿 메서드가 알고리즘의 빈칸을 서브클래스에 맡기는 일반형이라면, 팩토리 메서드는 그 빈칸이 객체 생성인 특수형이다.
판단 기준: 상위 흐름이 “제품 하나를 만드는 한 걸음”만 하위에 맡기면 팩토리 메서드, “제품 여러 개로 이뤄진 한 벌”을 통째로 맡기면 추상 팩토리를 고른다. 함정: 팩토리 메서드를 정적 팩토리 메서드(생성자 대신 쓰는 이름 있는 static 메서드)와 같은 것으로 여기면 패턴의 핵심인 오버라이드를 놓친다 — 이 패턴의 심장은 “서브클래스가 재정의하는 인스턴스 메서드”라는 점이다.
다음으로 프로토타입