추상 팩토리 패턴, 관련 객체를 한 벌로 만드는 원리
추상 팩토리 패턴은 서로 관련된 객체를 한 벌로 생성해 구체 클래스를 감추는 GoF 생성 패턴이다. 팩토리 메서드와의 차이, 코드 구조, 자바 표준 라이브러리 사례까지 실무 관점에서 정리했다.
추상 팩토리 패턴은 서로 어울리는 객체 여러 개를 한 벌로 찍어 내는 생성 패턴이다. 클라이언트 코드는 팩토리에 '버튼 하나, 체크박스 하나 만들어 달라'고 요청할 뿐, 그 결과물이 맥 스타일인지 윈도 스타일인지는 팩토리가 정한다. 덕분에 화면을 그리는 코드에서 구체 클래스 이름이 사라진다. GoF(Gang of Four)의 책 『디자인 패턴』(1994)은 이 패턴을 '구체 클래스를 지정하지 않고 서로 관련되거나 의존적인 객체들의 집합을 생성하는 인터페이스를 제공한다'고 정의한다.
이 글은 객체지향 언어에서 여러 제품이 한 벌로 함께 움직여야 하는 상황을 기준으로 패턴의 개념과 구조, 그리고 언제 쓰고 언제 피할지를 다룬다. 특정 프레임워크의 팩토리 API 사용법이나 의존성 주입 컨테이너 설정은 범위 밖이다.
추상 팩토리 패턴이란 무엇인가
핵심 단어는 '제품군(product family)'이다. 제품 하나가 아니라, 함께 쓰여야 자연스러운 제품들의 묶음을 통째로 만든다. 다크 테마 화면을 그린다고 해 보자. 버튼도, 입력창도, 스크롤바도 전부 어두운 계열이어야 한다. 여기서 버튼만 라이트 테마가 섞이면 화면이 깨진다. 추상 팩토리는 '다크 테마 한 벌'을 만드는 공장과 '라이트 테마 한 벌'을 만드는 공장을 따로 두고, 어느 공장을 쓸지만 바꿔 끼우면 화면 전체의 톤이 함께 넘어가게 한다.
패턴은 보통 네 가지 역할로 짜인다. 무엇을 만들지 선언하는 추상 팩토리(AbstractFactory) 인터페이스, 실제로 한 계열을 만들어 내는 구체 팩토리(ConcreteFactory), 만들어질 제품이 공유하는 추상 제품(AbstractProduct), 그리고 그 구현체인 구체 제품(ConcreteProduct)이다. 클라이언트는 추상 팩토리와 추상 제품 타입에만 기대고, 구체 클래스는 직접 손대지 않는다.
GoF는 이 패턴을 자신들이 정리한 23개 디자인 패턴 가운데 객체 생성을 다루는 '생성 패턴(creational pattern)' 다섯 개 중 하나로 분류한다. 생성 패턴은 객체를 만드는 방법과 그 객체를 쓰는 코드를 떼어 놓는 데 목적이 있는데, 그중 추상 팩토리는 '여러 개를 한 세트로' 만든다는 점에서 나머지와 갈린다.
팩토리 메서드와는 무엇이 다른가
한 줄로 답하면, 팩토리 메서드는 제품 한 종류를 서브클래스가 고르게 하고, 추상 팩토리는 서로 관련된 제품 여러 종류를 팩토리 객체 하나가 한꺼번에 만든다. 이름이 비슷해 자주 헷갈리지만 만드는 단위가 다르다. 팩토리 메서드는 '이 자리에 들어갈 객체 하나'를 상속으로 결정하고, 추상 팩토리는 '함께 쓰일 객체 한 벌'을 객체 구성으로 결정한다.
| 구분 | 팩토리 메서드 | 추상 팩토리 |
|---|---|---|
| 만드는 단위 | 제품 한 종류 | 관련 제품 여러 종류(한 벌) |
| 주된 수단 | 상속(서브클래스가 결정) | 객체 구성(팩토리 주입) |
| 생성 메서드 수 | 보통 1개 | 제품 종류만큼 여러 개 |
| 확장이 쉬운 쪽 | 새 제품 종류 추가 | 새 제품군(변형) 추가 |
디자인 패턴 참조 사이트 refactoring.guru는 많은 설계가 단순한 팩토리 메서드로 시작했다가 유연성이 더 필요해지면 추상 팩토리로 발전하는 경우가 많다고 설명한다. 처음부터 추상 팩토리로 짜면 팩토리와 제품 인터페이스가 잔뜩 생기므로, 제품군이 실제로 둘 이상 갈릴 조짐이 보일 때 옮겨 가는 편이 낫다.

코드로 보는 구조
말로만 보면 추상적이니 최소한의 코드로 확인하자. 버튼과 체크박스라는 두 제품을 한 벌로 만드는 GUI 팩토리를 예로 든다. 아래에서 클라이언트인 buildUI는 어떤 팩토리를 받았는지 전혀 모른 채 돌아간다.
// 제품군: 버튼과 체크박스
interface Button { void render(); }
interface Checkbox { void render(); }
// 추상 팩토리: 한 벌을 만드는 인터페이스
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// 구체 팩토리 1: 맥 스타일 한 벌
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// 구체 팩토리 2: 윈도 스타일 한 벌
class WinFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Checkbox createCheckbox() { return new WinCheckbox(); }
}
// 클라이언트는 어떤 팩토리인지 몰라도 된다
void buildUI(GUIFactory factory) {
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
button.render();
checkbox.render();
}MacFactory를 넘기면 맥 스타일 버튼과 체크박스가, WinFactory를 넘기면 윈도 스타일이 한 세트로 나온다. 새 플랫폼을 지원하고 싶으면 GUIFactory를 구현한 팩토리 클래스 하나와 제품 구현체만 더하면 되고, buildUI는 한 줄도 고치지 않는다. 이렇게 계열을 통째로 갈아 끼우는 것이 추상 팩토리가 노리는 지점이다.
언제 쓰고 언제 피해야 하나
쓰기 좋은 조건부터 짚으면, 제품들이 반드시 같은 계열끼리 짝을 이뤄야 하고 그 계열이 통째로 바뀔 수 있을 때다. 테마, 플랫폼, 국가별 규격처럼 '한 벌'이 여러 변형으로 갈리는 상황이 대표적이다. 이런 자리에서 추상 팩토리는 계열이 뒤섞이는 실수를 타입 수준에서 막아 준다. 맥 버튼과 윈도 체크박스가 섞인 조합은 애초에 만들어질 길이 없다.
반대로 조심할 지점은 제품 '종류'가 자주 늘어나는 경우다. 약점은 제품 종류를 하나 추가할 때 드러난다. 위 예제에 라디오 버튼을 넣으려면 GUIFactory 인터페이스에 createRadioButton()을 추가하고, 그 인터페이스를 구현한 MacFactory와 WinFactory를 전부 고쳐야 한다. 팩토리가 세 개, 네 개로 늘어 있으면 그만큼 손댈 곳이 많아진다. 새 제품군(변형)을 더하기는 쉽지만 새 제품 종류를 더하기는 번거로운 구조이니, 제품 종류가 유동적인 도메인이라면 다른 방법을 먼저 저울질하는 편이 안전하다.
표준 라이브러리에서 찾는 실제 사례
이 패턴은 책 속 이론에 그치지 않고 표준 라이브러리에도 들어 있다. 자바의 XML 처리 API가 대표적이다. Oracle 자바 문서 기준으로 javax.xml.parsers.DocumentBuilderFactory의 newInstance() 메서드는 실행 환경에 맞는 구현체를 골라 팩토리를 돌려주고, 그 팩토리가 다시 파서 객체들을 만들어 낸다. 호출하는 쪽은 어떤 구현이 붙는지 모른 채 표준 인터페이스만 쓴다. javax.xml.transform.TransformerFactory, javax.xml.xpath.XPathFactory도 같은 방식으로 설계돼 있다. 자세한 클래스 명세는 Oracle 자바 문서에서 확인할 수 있다.
결국 추상 팩토리는 '무엇을 만들지'와 '그것을 어떻게 쓸지'를 갈라놓아, 제품 계열 전체를 한 번에 교체할 수 있게 하는 도구다. 대신 인터페이스와 클래스가 늘어난다는 비용, 그리고 제품 종류 추가에 약하다는 성질을 알고 쓰면 필요 없는 곳에까지 남발하는 실수를 줄일 수 있다.





