데스크톱 애플리케이션에 화면을 그리는 코드를 짠다고 하자. 버튼과 체크박스와 스크롤바가 필요하다. 처음엔 아무 생각 없이 new WindowsButton(), new WindowsCheckBox()를 여기저기 박아 넣는다. 잘 돈다. 그런데 요구가 붙는다 — 이 앱을 맥에서도 돌려야 하고, 맥에서는 맥 스타일의 버튼과 체크박스가 나와야 한다. 이제 코드를 뒤지며 new WindowsButton()을 찾아 if (mac) new MacButton() else new WindowsButton()으로 하나하나 바꾸기 시작한다. 위젯을 생성하는 자리가 수십 군데면 그 분기를 수십 번 심어야 한다.
더 고약한 것은 섞이면 안 된다는 제약이다. 윈도우 버튼 옆에 맥 체크박스가 붙으면 룩앤필이 깨진다. 한 화면의 위젯은 전부 같은 계열이어야 한다. 그런데 생성 코드가 곳곳에 흩어져 있으면, 어느 한 군데서 실수로 계열을 잘못 고르는 순간 이 “한 벌”이라는 규칙이 소리 없이 무너진다. 추상 팩토리(Abstract Factory)는 이 문제를 정면으로 다룬다 — 서로 어울려야 하는 제품들을 한 공장에서만 만들게 하고, 그 공장을 통째로 갈아 끼운다.
무엇이 문제인가 — 동기
같은 증상을 데이터베이스 드라이버로 옮기면 더 또렷하다. 애플리케이션이 커넥션·명령·트랜잭션 세 종류의 객체를 쓰는데, MySQL이면 MySqlConnection·MySqlCommand·MySqlTransaction을, PostgreSQL이면 PgConnection·PgCommand·PgTransaction을 써야 한다. 이 셋은 한 벌이다 — MySQL 커넥션에서 얻은 트랜잭션을 Postgres 명령에 물릴 수 없다. 구체 클래스 이름을 코드에 직접 박으면 다음이 동시에 아프다.
첫째, 생성 지점이 클라이언트에 흩어져 결합이 박힌다. new MySqlConnection()이라 쓰는 순간 그 코드는 MySQL이라는 구체 클래스에 묶인다. DB를 바꾸려면 이 이름이 나오는 모든 자리를 찾아 고쳐야 한다. 생성이 한 군데도 아니고 커넥션·명령·트랜잭션마다, 그것도 여러 모듈에 퍼져 있으니 변경이 온 코드베이스로 번진다.
둘째, 한 벌이라는 제약을 강제할 방법이 없다. 구체 클래스를 직접 부르는 코드는 계열을 섞을 자유가 있다. MySqlConnection을 만들어 놓고 옆에서 PgCommand를 만드는 실수를 컴파일러가 막아 주지 못한다. “같은 계열끼리만”이라는 규칙이 개발자의 주의력에만 의존한다.
셋째, 제품군 전환이 원자적이지 않다. DB를 MySQL에서 Postgres로 바꾸는 일은 개념적으로는 스위치 하나를 젖히는 일이어야 하는데, 구체 클래스가 박혀 있으면 수십 곳을 동시에, 빠짐없이 바꿔야 완결된다. 하나라도 놓치면 두 계열이 섞인 채로 돌아간다.
판단 기준: “서로 어울려야만 하는 객체 여럿을 한 벌로 만들고, 그 벌 전체를 통째로 교체할 일이 있다”가 보이면 추상 팩토리를 의심한다. 함정: 만들 제품이 한 종류뿐이거나 계열이라는 개념이 없다면 이건 과한 도구다 — 제품 하나를 서브클래스가 고르는 문제라면 팩토리메서드로 족하다.
패턴의 구조 — 누가 무엇을 하나
추상 팩토리의 등장인물은 네 종류다. 핵심은 “제품”과 “제품을 만드는 공장”을 각각 인터페이스로 이중으로 추상화한다는 데 있다.
AbstractProduct(제품 인터페이스들). 만들어질 제품의 계약이다. Button, CheckBox, ScrollBar — 계열이 무엇이든 클라이언트가 상대하는 얼굴이다. 클라이언트는 Button만 알지 그게 윈도우 버튼인지 맥 버튼인지 모른다.
ConcreteProduct(구체 제품들). 계열별 실제 구현. WindowsButton·MacButton이 Button을 각자 구현한다. 같은 계열의 제품들끼리 룩앤필이 맞다.
AbstractFactory(공장 인터페이스). “이 계열의 제품들을 만든다”는 계약이다. createButton(), createCheckBox(), createScrollBar() — 제품 종류마다 만드는 메서드 하나씩을 선언한다. 이 인터페이스가 추상 팩토리의 심장이다. 클라이언트는 이 얼굴 하나만 쥐고, 자기가 어떤 계열을 쓰는지 모른 채 위젯을 주문한다.
ConcreteFactory(구체 공장들). 계열 하나당 공장 하나. WindowsFactory는 createButton()에서 WindowsButton만, createCheckBox()에서 WindowsCheckBox만 돌려준다. 한 공장이 만드는 제품은 전부 같은 계열이라는 것이 여기서 보장된다 — 계열을 섞을 방법이 애초에 없다. 이 공장 안에서만 new가 일어난다.
협력의 핵심은 간접의 방향이다. 클라이언트는 AbstractFactory 하나를 주입받아 들고 있다가, 위젯이 필요할 때마다 factory.createButton()을 부른다. 어떤 구체 공장이 주입됐느냐가 어떤 계열이 나올지를 결정하지만, 클라이언트 코드에는 계열 이름이 한 번도 등장하지 않는다. 계열 선택은 딱 한 군데 — 어떤 구체 공장을 만들어 주입하느냐 — 로 모인다. 이 단일 지점이 “흩어진 new”를 “한 곳의 스위치”로 바꾸는 장치다.
판단 기준: 제품 인터페이스와 공장 인터페이스가 둘 다 필요하면 추상 팩토리, 공장 하나로 제품 하나만 만들면 팩토리 메서드다 — “만드는 것이 한 벌(여러 종류)이냐 하나냐”가 갈림길이다. 함정: 제품 종류가 앞으로도 하나뿐인데 공장 인터페이스까지 파면, 계약을 두 겹 쌓아 놓고 정작 계열 개념은 없는 헛간접이 된다.
코드로 — 패턴이 자라나는 과정
흩어진 new에서 출발해 공장 인터페이스를 뽑고, 제품군을 통째로 갈아 끼우는 과정을 단계로 따라간다.
public class LoginScreen { public void render() { Button ok = new WindowsButton(); // 계열이 코드에 박혔다 CheckBox remember = new WindowsCheckBox(); ScrollBar scroll = new WindowsScrollBar(); ok.paint(); remember.paint(); scroll.paint(); }}// 맥을 지원하려면? 이 화면뿐 아니라 위젯을 만드는 모든 화면에서// new WindowsXxx() 를 찾아 분기를 심어야 한다. 게다가 실수로// WindowsButton 옆에 MacCheckBox 를 두어도 컴파일러가 막지 못한다.
첫 스텝의 흩어진 new가 사라진 것이 아니라 구체 공장 안으로 모였다는 점이 중요하다. 추상 팩토리는 객체 생성을 없애 주지 않는다. 어딘가에서는 반드시 new WindowsButton()이 불린다. 다만 그 new들을 클라이언트 곳곳에서 걷어다 계열별 공장 한 곳에 몰아넣어, 계열 선택을 프로그램 전체에서 단 한 지점으로 줄였을 뿐이다. 그 대가로 “한 벌”이라는 제약이 개발자의 주의가 아니라 타입 시스템으로 지켜진다.
판단 기준: 리팩터링 후 “새 계열 추가 = 새 공장 하나 + 새 제품 세트, 기존 클라이언트 0줄 수정”이 성립하면 추상 팩토리가 제대로 앉은 것이다. 함정: 클라이언트가 여전히 if (mac) factory.createButton()처럼 계열을 되묻고 있다면, new만 옮겼을 뿐 선택 분기는 그대로 남은 것이다 — 계열 판단은 공장을 주입하는 그 한 줄에만 있어야 한다.
결과 — 무엇을 얻고 무엇을 내주나
추상 팩토리가 사는 값은 제품군의 원자적 교체와 일관성 보장이다. 구체 공장을 갈아 끼우는 한 줄로 앱 전체의 계열이 바뀐다 — 룩앤필이든 DB 드라이버든, 스위치 하나를 젖히는 일이 된다. 그리고 한 공장이 한 계열만 만드니 “윈도우 버튼 옆 맥 체크박스” 같은 섞임이 타입 수준에서 불가능해진다. 클라이언트는 구체 클래스 이름을 한 번도 부르지 않으므로 계열이라는 세부에 결합되지 않고, 생성 지점이 공장으로 격리돼 “인터페이스에 맞춰 프로그래밍한다”는 원칙이 자연히 지켜진다.
대가는 세 가지다. 첫째, 새 제품 종류를 추가하기가 어렵다. 이것이 추상 팩토리의 가장 아픈 지점이다. 위젯에 Menu를 새로 넣고 싶으면 WidgetFactory 인터페이스에 createMenu()를 추가해야 하고, 그 순간 모든 구체 공장(WindowsFactory·MacFactory·LinuxFactory…)이 이 메서드를 구현하도록 다 같이 열어 고쳐야 한다. 계열(공장) 축으로는 확장에 열려 있지만, 제품 종류 축으로는 닫혀 있다 — 인터페이스가 못 박는 것이 정확히 “제품 종류의 목록”이기 때문이다. 둘째, 클래스와 간접이 우수수 는다. 제품 종류 M개 × 계열 N개면 구체 제품이 M×N개, 거기에 공장 인터페이스와 구체 공장 N개가 더해진다. 위젯 하나 그리는 흐름을 좇으려면 인터페이스와 공장 사이를 여러 번 오가야 한다. 셋째, 계열 선택 로직은 사라지지 않고 옮겨 갈 뿐이다. 어떤 공장을 만들지 결정하는 코드가 어딘가엔 있어야 한다 — 조립 지점으로 밀려났을 뿐, 그 판단 자체는 여전히 누군가의 책임이다.
운영비용 렌즈로 보면 순이득의 조건이 분명하다. 제품 종류는 안정적인데 계열이 자주 바뀌거나 늘어나는 축이라면(룩앤필 테마 추가, DB 벤더 교체), 늘어난 클래스의 대가보다 “계열을 한 줄로 갈아 끼우고 섞임을 원천 봉쇄하는” 이득이 크다. 반대로 제품 종류가 자꾸 늘어나는 축이라면, 매번 모든 공장을 여는 비용이 이득을 잡아먹는다 — 이 경우엔 애초에 축을 잘못 고른 것이다.
판단 기준: 변경의 축이 “계열(제품군 전체)“이면 추상 팩토리가 값을 하고, “제품 종류 하나 추가”면 이 패턴이 가장 취약해진다 — 어느 축이 자주 흔들릴지를 먼저 그려야 한다. 함정: “확장성”이라는 말에 홀려 제품 종류가 늘 것까지 이 패턴으로 대비하면, 정작 종류가 하나 늘 때마다 공장을 전부 뜯어고치는 최악의 자리에 스스로 걸어 들어간 것이다.
언제 쓰고, 언제 과용인가
추상 팩토리가 정확히 맞는 자리는 이렇다. 시스템이 서로 어울려야 하는 객체들의 한 벌을 쓰고, 그 벌을 계열째 갈아 끼워야 할 때(운영체제별 룩앤필, DB 벤더별 드라이버, 테스트용/운영용 리소스 세트). 계열끼리 섞이면 안 된다는 제약을 코드가 아니라 타입으로 지키고 싶을 때. 구체 클래스 이름을 클라이언트에서 걷어 내 생성을 한곳에 격리하고 싶을 때. 자바 표준 라이브러리의 DocumentBuilderFactory나 GUI 툴킷의 테마 팩토리가 이 패턴의 실물이다.
과용의 냄새는 반대편에서 난다. 만들 제품이 한 종류뿐이라면 공장 인터페이스는 허울이다 — 그건 팩토리메서드의 자리다. 계열이 지금 하나고 앞으로 늘 일이 없다면, 두 겹 추상화는 오지 않을 변경에 거는 보험일 뿐 흩어진 new 몇 개가 오히려 읽기 쉽다. 제품 종류가 자주 늘어나는 도메인이라면 이 패턴은 독이다 — 종류가 늘 때마다 모든 공장을 여는 대가를 치른다.
이웃 패턴과의 경계도 그어 둘 만하다. 추상 팩토리의 각 createXxx() 메서드는 그 자체로 팩토리메서드인 경우가 많다 — 추상 팩토리는 “여러 팩토리 메서드를 한 계열로 묶은 공장”이라고 봐도 된다. 팩토리 메서드가 제품 하나의 생성을 서브클래스에 위임한다면, 추상 팩토리는 제품 한 벌의 생성을 공장 객체에 위임한다. 축이 하나면 팩토리 메서드, 계열이라는 두 번째 축이 생기면 추상 팩토리다. 그리고 구체 공장은 대개 애플리케이션당 하나만 있으면 되므로 싱글턴으로 두는 경우가 많다 — 룩앤필 공장이 여러 개일 이유가 없으니, 그 인스턴스를 하나로 보장하는 것이다.
판단 기준: 만드는 것이 “한 벌(여러 종류가 어울려야 함)“이고 그 벌을 통째로 바꾸는 축이 실제로 흔들리면 추상 팩토리, 만드는 것이 “하나”면 팩토리 메서드를 고른다. 함정: 계열이 하나뿐인데 미래를 위해 공장 인터페이스부터 파면, 유연성은 놀고 M×N 클래스의 관리 비용만 먼저 청구된다 — 두 번째 계열이 실제로 보일 때 뽑아도 늦지 않다.
다음으로 빌더