객체 생성의 레시피
프로그래밍은 앞서 간략하게 이야기한 객체(Object)를 만들면서 코딩합니다. 아무것도 없는 상태에서 맨땅에 헤딩하는 것이 아니라는 말이죠. 카페를 인테리어 할 때 조명, 의자, 테이블, 컵, 접시, 에스프레소 머신 등등 아무것도 없는 상태에서 하나하나 직접 만들면서 하지는 않죠. (우스게 소리로 커피가 늦게 나올 때 커피콩을 재배해서 콩을 추출하고 추출된 콩을 로스팅해서… 와 같이 주문이 들어간 시점에서 콩부터 재배하느냐는 농담을 하는 것과 같은 느낌입니다.)
1980년대 후반 Kent Beck과 Ward Cunningham이 Smalltalk라는 프로그래밍 언어를 사용하면서 객체 생성의 유연성 문제를 해결하려고 노력하던 중에 팩토리 메서드 패턴의 기초를 다졌습니다.
초기에는 객체를 생성하는 코드가 프로그램 전체에 흩어져 있었습니다. 동일한 객체를 생성하는 곳이 여기저기 중복되어 있고, 객체 생성 방식이 변경될 때마다 흩어져 있는 모든 코드를 수정해야 했습니다. 카페 의자를 만드는 공장이 하나이면 되는데, 같은 의자를 여러 군데에서 만들다 보면 의자를 수정해야 할 때 여러 공장에 다 연락해야 한다는 말이죠.
재미있는 점은 팩토리 메서드 패턴의 이름이 실제 공장(Factory)에서 영감을 받았다는 것입니다. 공장에서 제품을 만드는 것처럼, 객체를 만드는 공정을 표준화하고 체계화하자는 아이디어였죠. 이를 1994년 GoF의 책에서 정립한 것입니다.
시간이 지나면서 팩토리 메서드 패턴은 여러 번 변형이 생깁니다. 1990년대 중반에는 Simple Factory라고 가장 단순한 팩토리 패턴이고, 1994년에 GoF가 연관된 객체들의 집합을 생성하는 데 초점을 맞춘 Abstract Factory를 제안합니다. 2000년대에는 Static Factory Method라고 Joshua Bloch의 “Effective Java”에서 제안합니다.
현대에는 마이크로 서비스 아키텍처(MicroService Architecture: MSA)에서 객체 생성을 추상화하는 중요한 패턴으로 사용되고 있습니다.
프랜차이즈 카페를 가보신 적이 있을 겁니다. 어느 지점을 가더라도 같은 맛, 같은 품질의 음료를 마실 수 있죠. 이것이 가능한 이유는 ‘표준화된 레시피’때문입니다. 팩토리 메서드 패턴은 마치 이런 카페의 레시피처럼, 객체를 만드는 표준화된 방법을 제공하 빈다.
카페에서 배우는 팩토리 메서드 패턴
카페에서는 음료를 주문하면 바리스타가 정해진 레시피대로 음료를 만들어냅니다. 손님은 음료가 어떻게 만들어지는지 자세히 알 필요가 없죠. 그저 ‘아메리카노 한 잔이요’라고 주문하면 됩니다.
프로그래밍에서의 팩토리 메서드도 같습니다. 객체가 필요한 곳에서는 그저 ‘이런 객체가 필요해요’라고 요청만 하면 되고, 실제로 그 객체를 어떻게 만들지는 팩토리 메서드가 담당합니다.
// 음료를 나타내는 인터페이스 (방법)
public interface Coffee {
void prepare(); // 사전작업
void brew(); // 제작
void serve(); // 완료
}
// 구체적인 커피 클래스들 (레시피)
public class Americano implements Coffee {
public void prepare() {
System.out.println("에스프레소 샷을 추출합니다.");
}
public void brew() {
System.out.println("뜨거운 물을 부어 아메리카노를 만듭니다.");
}
public void serve() {
System.out.println("아메리카노가 준비되었습니다.");
}
}
public class Latte implements Coffee {
public void prepare() {
System.out.println("에스프레소 샷을 추출합니다.");
}
public void brew() {
System.out.println("스팀밀크를 추가하여 라테를 만듭니다.");
}
public void serve() {
System.out.println("라떼가 준비되었습니다.");
}
}
// 실제 커피를 만드는 구체적인 팩토리 (주문에 따라 레시피 실행 - 객체 생성 코드)
public class CoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if(type.equals("americano")) {
coffee = new Americano();
}
else if(type.equals("latte")) {
coffee = new Latte();
}
return coffee;
}
}
// 객체 사용 코드
public class Cafe {
private CoffeeFactory factory = new CoffeeFactory();
public void serveCustomer(String ordertype) {
// 객체 생성은 팩토리에 위임
Coffee coffee = factory.createCoffee(orderType);
// 생성된 객체 사용에만 집중
coffee.prepare();
coffee.brew();
coffee.serve();
}
}
이처럼 새로운 종류의 객체를 추가할 때 기존 코드를 수정하지 않아도 됩니다. 마치 카페에서 새로운 메뉴를 추가하는 것처럼 쉽게 확장이 가능하죠. (brew만 정의하면 됩니다.)
객체 생성 코드와 사용 코드가 분리되어 있어 유지보수가 쉬워집니다. 이 말은 주방에서 음식을 만드는 과정(객체 생성)과 홀에서 손님에게 서빙하는 과정(객체 사용)이 분리되어 있는 것과 같습니다.
객체 생성 코드가 한 곳에서 관리할 수 있어서 코드 중복을 감소시킬 수 있습니다. 홀이나 주방이나 음식을 만들지 않고 주방에서만 음식을 만들 수 있는 것이죠. 홀에서는 주방에 음식을 요청하고 서빙만 하면 됩니다.
실제적으로 UI를 만들 때 같은 형태의 버튼이면 하나를 만들고 복사해서 사용하는 것입니다. 일일이 버튼을 다 만들 필요는 없죠. UI에서는 이를 컴포넌트(Component)라고 합니다. 객체와 같은 거죠. 또한, 문서를 작성한 후에 다양한 형식(PDF, docx, html 등)으로 생성할 때도 해당 패턴을 사용합니다.
다만, 팩토리를 하나 만들었다고 만능은 아닙니다. 전혀 새로운 제품이나 객체를 만들 때는 다른 팩토리를 만들어야 하죠. 그리고 간단하고, 많이 사용하지 않는 객체는 오히려 복잡한 코드를 만들 수 있어, 이런 경우에는 팩토리 패턴보다는 그냥 만들어서 사용하는 게 효율적일 수 있습니다.
팩토리 메서드 패턴은 객체 지향 프로그래밍(OOP)의 핵심 원칙 중 하나인 “개방-폐쇄 원칙’을 잘 따르는 패턴입니다. 기존 코드를 변경하지 않으면서도 새로운 기능을 추가할 수 있게 하죠. 카페에서 기존 메뉴는 그대로 두면서 새로운 메뉴를 추가하는 것처럼 말입니다.
생성과 사용을 분리, 홀과 주방의 분리, 이는 책임을 적절히 분배하고, 자유를 보장함으로써 복잡성을 관리합니다. 주방이 홀을 간섭하거나, 홀이 주방을 간섭하게 되면 자유가 없어지고, 책임도 불분명해집니다. 서로 얽혀있게 되면 실타래처럼 복잡해집니다. 역할을 명확히 해야 단순하면서 견고한 코드가 만들어집니다.