추상 팩토리 패턴은 여러 관련 개체의 그룹, 서로 다르지만 특정 주제나 시나리오에 맞는 개체들을 생성하는 인터페이스를 제공하는 디자인 패턴이다. 이 패턴은 팩토리의 팩토리라고 표현할 수 있을 정도로 구체적인 클래스에 의존하지 않고 제품의 그룹을 생성할 수 있게 해 준다.
추상 팩토리의 구성은 팩토리 메서드 패턴과 비슷하다.
Abstract Factory : 객체의 생성을 위한 인터페이스를 정의한다. 이 인터페이스는 여러 종류의 관련 있는 제품을 생성하는 메서드들을 포함한다.
Concreate Factory : Abstract Factory 인터페이스를 구현하는 클래스이다. 특정 제품 그룹을 생성한다.
Abstract Product : 생성될 제품들의 공통 인터페이스다.
Concreate Product : 실제 제품 개체를 나타내며 Abstract Product 인터페이스를 구현한다
쉽게 이해한다고 하면...
만약 게임을 만든다고 가정해 보자.
다양한 종류의 게임 캐릭터와 무기가 필요한 상황이다. 게임에는 미래와 중세 그리고 현대로 나눠진다면 미래에는 레이저 검과 로봇 등 있을 수 있고, 중세에는 검과 기사가 있을 수 있다. 현대는 군인과 총, 탱크 등이 필요할 것이다.
추상 팩토리 패턴을 사용하면 미래, 중세, 현대 이것을 각각 세트라고 보면 캐릭터와 무기의 구체적인 종류를 몰라도 게임 세트에 맞는 적절한 개체들을 쉽게 설명할 수 있다.
추상 팩토리의 장점으로는 클라이언트 코드는 구체적인 제품에 의존하지 않고 인터페이스를 통해 제품을 사용하기 때문에 다른 팩토리를 사용하여 다른 제품 그룹을 쉽게 교체할 수 있다. 또한 관련 있는 객체들을 함께 일관성 있게 생성할 수 있기에 위의 게임 예시에서 미래, 중세, 현대 팩토리를 선택하면 해당 시대에 맞는 무기와 캐릭터가 자동으로 맞춰 생성되는 것이다. 확장성도 좋은데 새로운 종류의 제품을 추가하려면 기존코드에서 변경하지 않고 새로운 팩토리와 제품 클래스를 추가하기만 하면 된다.
단점은 복잡성이 늘어날 수 있다. 이는 팩토리 메서드와도 비슷한 개념인데 시스템이 커지고 관리포인트가 늘어날수록 많은 클래스와 인터페이스가 필요하기에 복잡할 수 있다는 것이다. 또한 유연함은 팩토리 메서드보다 다소 떨어지는데 추상 팩토리가 생성할 수 있는 제품 종류를 확장하려 한다면 모든 팩토리 클래스에 변경을 가해야하기에 유연성이 다소 제한될 수 있다.
예제 소스코드로 보자
// 캐릭터 인터페이스
public interface Character {
void display();
}
// 무기 인터페이스
public interface Weapon {
void wield();
}
// 게임 세트 팩토리 인터페이스
public interface GameSetFactory {
Character createCharacter();
Weapon createWeapon();
}
각 캐릭터와 무기를 생성하는 추상 팩토리와 각 제품의 추상 인터페이스를 정의했다.
그리고 각 시대별로 무기와 캐릭터를 구현한다.
// 미래 시대의 캐릭터와 무기
public class Robot implements Character {
public void display() {
System.out.println("로봇");
}
}
public class LaserSword implements Weapon {
public void wield() {
System.out.println("레이저 검");
}
}
// 중세 시대의 캐릭터와 무기
public class Knight implements Character {
public void display() {
System.out.println("기사");
}
}
public class Sword implements Weapon {
public void wield() {
System.out.println("검");
}
}
// 현대 시대의 캐릭터와 무기
public class Soldier implements Character {
public void display() {
System.out.println("군인");
}
}
public class Gun implements Weapon {
public void wield() {
System.out.println("총");
}
}
그리고 구체적인 팩토리 클래스를 구현한다.
// 미래 시대 팩토리
public class FutureSetFactory implements GameSetFactory {
public Character createCharacter() {
return new Robot();
}
public Weapon createWeapon() {
return new LaserSword();
}
}
// 중세 시대 팩토리
public class MedievalSetFactory implements GameSetFactory {
public Character createCharacter() {
return new Knight();
}
public Weapon createWeapon() {
return new Sword();
}
}
// 현대 시대 팩토리
public class ModernSetFactory implements GameSetFactory {
public Character createCharacter() {
return new Soldier();
}
public Weapon createWeapon() {
return new Gun();
}
}
클라이언트는 팩토리 인터페이스를 통해 각 시대에 맞는 캐릭터와 무기를 생성하고 사용할 수 있다.
public class Game {
public static void main(String[] args) {
GameSetFactory factory = new FutureSetFactory(); // 미래 세트 선택
Character character = factory.createCharacter();
Weapon weapon = factory.createWeapon();
character.display();
weapon.wield();
// 다른 시대의 팩토리를 선택하여 다른 캐릭터와 무기 생성 가능
factory = new MedievalSetFactory(); // 중세 세트 선택
character = factory.createCharacter();
weapon = factory.createWeapon();
character.display();
weapon.wield();
}
}
미래, 중세, 현대 3가지의 게임의 세트에 따라 캐릭터와 무기를 생성할 때 각 팩토리는 해당 시대의 특징에 맞는 캐릭터와 무기를 생성하는 책임을 가지고, 클라이언트 코드는 생성된 개체의 구체적인 타입을 몰라도 사용할 수 있다.
팩토리 메서드 패턴과 추상적 팩토리 패턴이 헛갈릴 수 있다.
팩토리 메서드 패턴의 목적은 객체를 생성하는 인터페이스를 정의하지만, 인스턴스화할 클래스의 결정을 서브클래스에 위임하는 것이다. 서브 클래스가 개체의 생성을 컨트롤하는 것이다. 구현하는 방식에서는 한 클래스에 하나의 생성 메서드를 가지고 이 메서드는 보통은 추상 메서드로 정의되고 서브클래스에서 구체적으로 개체 유형을 생성하는 식으로 구현한다.
용도로는 팩토리 메서드 패턴은 하나의 개체만을 생성하고, 그 개체의 타입이 서브 클래스에 의해 결정될 때 사용된다.
추상 팩토리 패턴의 목적은 여러 개체의 그룹을 생성하기 위한 인터페이스를 제공하는 것이다. 이 패턴은 하나의 팩토리 메서드가 아닌 여러 생성 메서드를 제공하여 각 메서드가 서로 다른 타입의 객체를 생성할 수 있다. 구현하는 방식으로는 여러 개의 팩토리 메서드로 구성된 인터페이스를 구현하고 관련된 개체의 그룹을 생성하는 각 메서드의 구체적인 구현을 제공한다.
용도로는 일련의 관련된 객체들을 함께 생성해야 할 때 사용한다. 특히 여러 제품군이 있고 각 제품군에 속하는 개체들이 함께 작동해야 하는 경우 유용하다.
주요한 차이점을 다시 요약해 보면
단일개체이냐 개체그룹이냐에 따라 차이가 난다. 또한 구현에 단순함 역시 팩토리 메서드 패턴이 다소 단순하다.
확장성은 팩토리 메서드 패턴은 서브 클래스를 생성해야 하는 반면에 추상 팩토리는 새로운 메서드를 추가하고 모든 구체적인 팩토리 클래스를 수정해야 하기에 복잡할 수 있다.
팩토리 메서드는 좀 더 단순한 상황에서 개체의 생성을 자식 클래스로 위임할 때 유용하고, 추상 팩토리는 여러 관련 개체를 일관되게 생성해야 할 때 더 적합하다.
Node.js로는 어떻게 구현하는지 소스코드를 살펴보자.
게임 세트의 추상 팩토리와 제품을 먼저 정의한다.
// Character와 Weapon을 위한 인터페이스를 정의.
// JavaScript에서는 인터페이스를 지원하지 않으므로, 클래스로 표현.
class Character {
display() {
console.log("캐릭터");
}
}
class Weapon {
wield() {
console.log("무기");
}
}
// GameSetFactory 추상 팩토리의 기본 클래스를 정의.
class GameSetFactory {
createCharacter() {}
createWeapon() {}
}
그리고 각 시대에 맞는 구체적인 제품 클래스를 구현한다.
// 미래 시대 캐릭터와 무기
class Robot extends Character {
display() {
console.log("로봇");
}
}
class LaserSword extends Weapon {
wield() {
console.log("레이저 검");
}
}
// 중세 시대 캐릭터와 무기
class Knight extends Character {
display() {
console.log("기사");
}
}
class Sword extends Weapon {
wield() {
console.log("검");
}
}
// 현대 시대 캐릭터와 무기
class Soldier extends Character {
display() {
console.log("군인");
}
}
class Gun extends Weapon {
wield() {
console.log("총");
}
}
각 시대별 팩토리를 구현한다.
// 미래 시대 팩토리
class FutureSetFactory extends GameSetFactory {
createCharacter() {
return new Robot();
}
createWeapon() {
return new LaserSword();
}
}
// 중세 시대 팩토리
class MedievalSetFactory extends GameSetFactory {
createCharacter() {
return new Knight();
}
createWeapon() {
return new Sword();
}
}
// 현대 시대 팩토리
class ModernSetFactory extends GameSetFactory {
createCharacter() {
return new Soldier();
}
createWeapon() {
return new Gun();
}
}
클라이언트는 다양한 시대의 팩토리를 사용할 수 있다.
function simulateGame(setFactory) {
const character = setFactory.createCharacter();
const weapon = setFactory.createWeapon();
character.display();
weapon.wield();
}
// 게임 시뮬레이션을 위해 다양한 시대의 팩토리를 사용.
const futureFactory = new FutureSetFactory();
simulateGame(futureFactory);
const medievalFactory = new MedievalSetFactory();
simulateGame(medievalFactory);
const modernFactory = new ModernSetFactory();
simulateGame(modernFactory);
여기까지 추상 팩토리에 대해서 알아봤는데, 그럼 추상 팩토리 패턴은 실무에서 어떤부분이 많이 쓰일까?
앞서 팩토리 메서드 패턴에서는 데이터 베이스를 연결할때 사용하는 예제를 넣었었는데 그것을 테스트 환경과 운영 환경으로 나눌때도 사용할 수 있다.
// 데이터베이스 연결 팩토리 인터페이스
interface ConnectionFactory {
connectDatabase();
}
// 실제 데이터베이스 연결 팩토리
class ProductionConnectionFactory implements ConnectionFactory {
connectDatabase() {
return new prodDatabase
prodDatabase 그리고 testDatabase, qaDatabase 등으로 말이다.
그럼 데이터베이스 연결하는 부분의 소스코드를 팩토리 메서드 패턴에서 추상팩토리까지 변경할때 어떻게 변경되는지 예제 소스코드 확인하자.
기존 팩토리 메서드 패턴으로 작성된 소스코드
public interface Database {
Connection getConnection();
}
public class MySQLDatabase implements Database {
public Connection getConnection() {
// MySQL 연결 반환
}
}
public class PostgreSQLDatabase implements Database {
public Connection getConnection() {
// PostgreSQL 연결 반환
}
}
public class DatabaseFactory {
public Database getDatabase(String type) {
if ("MySQL".equals(type)) {
return new MySQLDatabase();
} else if ("PostgreSQL".equals(type)) {
return new PostgreSQLDatabase();
} else {
throw new IllegalArgumentException("No such database type");
}
}
}
추상 팩토리 인터페이스 제품 인터페이스
public interface DatabaseFactory {
Connection getConnection();
Command getCommand();
}
public interface Connection {
void connect();
}
public interface Command {
void execute(String query);
}
각 데이터 베이스 유형에 맞는 팩토리와 제품을 구현
public class MySQLDatabaseFactory implements DatabaseFactory {
@Override
public Connection getConnection() {
return new MySQLConnection();
}
@Override
public Command getCommand() {
return new MySQLCommand();
}
}
public class PostgreSQLDatabaseFactory implements DatabaseFactory {
@Override
public Connection getConnection() {
return new PostgreSQLConnection();
}
@Override
public Command getCommand() {
return new PostgreSQLCommand();
}
}
public class MySQLConnection implements Connection {
@Override
public void connect() {
System.out.println("Connecting to MySQL database...");
}
}
public class MySQLCommand implements Command {
@Override
public void execute(String query) {
System.out.println("Executing MySQL query: " + query);
}
}
public class PostgreSQLConnection implements Connection {
@Override
public void connect() {
System.out.println("Connecting to PostgreSQL database...");
}
}
public class PostgreSQLCommand implements Command {
@Override
public void execute(String query) {
System.out.println("Executing PostgreSQL query: " + query);
}
}
클라이언트에서의 호출
public class Application {
public static void main(String[] args) {
DatabaseFactory factory = getDatabaseFactory("MySQL");
Connection connection = factory.getConnection();
Command command = factory.getCommand();
connection.connect();
command.execute("SELECT * FROM users");
}
public static DatabaseFactory getDatabaseFactory(String type) {
if ("MySQL".equals(type)) {
return new MySQLDatabaseFactory();
} else if ("PostgreSQL".equals(type)) {
return new PostgreSQLDatabaseFactory();
} else {
throw new IllegalArgumentException("No such database type");
}
}
}
이 코드들을 살펴보면 데이터베이스 유형에 따라 서로 다른 팩토리를 제공한다. 각 팩토리는 데이터 베이스 연결과 쿼리 실행에 필요한 개체들을 생성하는데 데이터 베이스 작업과 관련된 여러 개체들이 서로 협력하여 작동해야할때 추상 팩토리 패턴이 얼마나 유용한지 확인할 수 있다. 클라이언트코드는 생성되는 개체의 구체적인 타입을 몰라도 되고 생성 로직에 대한 의존성이 감소하는것을 볼 수 있다.
'Design Pattern > 생성 패턴(Creational Patterns)' 카테고리의 다른 글
프로토타입(Prototype) - 생성 패턴(Creational Patterns) (0) | 2024.05.15 |
---|---|
빌더(Builder) - 생성 패턴(Creational Patterns) (0) | 2024.05.14 |
팩토리 메서드(Factory Method) - 생성 패턴(Creational Patterns) (0) | 2024.05.13 |
싱글톤(Singleton) - 생성 패턴(Creational Patterns) (0) | 2024.05.10 |