팩토리 패턴
포스트
취소

팩토리 패턴

팩토리 패턴은 객체지향 디자인 패턴 중 하나로, 객체 생성 로직을 직접 사용하는 것보다 인터페이스나 추상 클래스를 통해 감싸서 객체 생성을 위임하는 패턴입니다. 이를 통해 코드는 구체적인 클래스가 아닌 인터페이스에 의존하게 되어, 확장성과 유지 보수성이 향상됩니다.

종류

  1. Simple Factory: 단순히 객체를 생성하고 반환하는 클래스를 포함합니다.
  2. Factory Method: 하위 클래스에서 구체적인 객체 생성 로직을 결정하도록 하는 패턴입니다.
  3. Abstract Factory: 연관된 객체의 그룹을 생성하기 위한 인터페이스를 제공하는 패턴입니다.

장점

  1. 객체 생성과 사용의 분리: 팩토리 패턴은 객체 생성 로직과 객체 사용 로직을 분리하여 코드의 결합도를 낮추고 유연성을 높입니다.
  2. 코드 재사용성 향상: 특정 클래스의 인스턴스 생성 로직이 한 곳에서 관리되므로 동일한 로직을 재사용할 수 있습니다.
  3. 확장성: 새로운 클래스 타입이 추가될 때 팩토리 클래스만 수정하면 되므로, 기존 코드의 변경 없이 확장이 용이합니다.
  4. 유지 보수성: 객체 생성 로직 변경이 필요할 때 팩토리 메서드나 팩토리 클래스만 수정하면 되므로 유지 보수가 간편합니다.

단점

  1. 클래스 수 증가: 각각의 구체적인 제품에 대한 클래스와, 그것들을 생성하는 팩토리 클래스가 필요하므로 클래스의 수가 증가할 수 있습니다.
  2. 복잡성 증가: 상황에 따라 여러 팩토리 클래스와 메서드가 필요하게 되면, 시스템의 복잡성이 증가할 수 있습니다.
  3. 고정된 방식의 객체 생성: 팩토리 패턴은 정해진 규칙에 따라 객체를 생성하므로, 동적인 객체 생성이 필요한 경우에는 제약이 될 수 있습니다.

팩토리 패턴 사용시 고려사항

1. 명확한 의도

팩토리 패턴을 사용하려는 목적과 의도를 명확하게 알고 있어야 합니다. 단순히 패턴을 사용하기 위해서가 아니라, 객체 생성에 관한 책임을 분리하거나, 특정 인터페이스를 따르는 객체를 생성하는 등의 목적이 필요합니다.

2. 유연성 vs 복잡성

팩토리 패턴은 유연성을 제공하지만, 동시에 복잡성을 증가시킬 수 있습니다. 따라서 필요 이상의 복잡한 팩토리 구조를 피하고, 실제로 필요한 수준에서만 팩토리 패턴을 적용해야 합니다.

3. 명명 규칙

팩토리 메서드나 클래스의 이름을 명확하게 지어, 해당 팩토리가 어떤 객체를 생성하는지 명확히 알 수 있도록 해야 합니다.

4. 확장 고려

시스템이 확장될 가능성을 고려하여, 팩토리 클래스나 메서드를 설계합니다. 예를 들어, 새로운 제품 클래스가 추가될 가능성이 있다면, 이를 쉽게 추가할 수 있는 구조를 선택하는 것이 좋습니다.

5. 의존성 주입(Dependency Injection)과의 관계

의존성 주입 프레임워크를 사용하는 경우, 팩토리 패턴과 어떻게 협력할지 고려해야 합니다. 때때로 의존성 주입이 팩토리 패턴의 일부 기능을 대체할 수 있기 때문입니다.

6. 테스트 용이성

팩토리 패턴을 사용하면 테스트하기 쉬운 코드를 작성할 수 있습니다. 팩토리를 통해 mock 객체나 다른 테스트 전용 객체를 주입할 수 있기 때문입니다.

아래는 테스트 용이성을 나타내는 예제 코드입니다.

먼저, 팩토리 패턴의 기본 구조를 잡겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Product 인터페이스
interface Product {
    String use();
}

// Product 인터페이스의 구현체, ConcreteProduct
class ConcreteProduct implements Product {
    @Override
    public String use() {
        return "Using ConcreteProduct";
    }
}

// Product 객체를 생성하는 팩토리
class ProductFactory {
    public Product createProduct() {
        return new ConcreteProduct();
    }
}

이제 팩토리 패턴을 사용하는 클라이언트 코드와 함께 이를 테스트하는 코드를 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 팩토리를 사용하는 Client
class Client {
    private Product product;

    public Client(ProductFactory factory) {
        this.product = factory.createProduct();
    }

    public String operate() {
        return product.use();
    }

    public static void main(String[] args) {
        testClientOperate();
    }

    public static void testClientOperate() {
        // MockProduct 정의
        class MockProduct implements Product {
            @Override
            public String use() {
                return "Using MockProduct";
            }
        }

        // MockProduct를 생성하는 팩토리
        class MockProductFactory extends ProductFactory {
            @Override
            public Product createProduct() {
                return new MockProduct();
            }
        }

        Client client = new Client(new MockProductFactory());
        assert "Using MockProduct".equals(client.operate());
        System.out.println("Test passed!");
    }
}

위의 자바 코드에서 ClientProductFactory를 통해 Product 객체를 생성하고 사용합니다. 테스트 시에는 실제 ConcreteProduct 대신 MockProduct를 사용하여 테스트를 수행하게 됩니다. 이렇게 팩토리 패턴을 사용하면 테스트 시에 mock 객체나 다른 테스트 전용 객체를 쉽게 주입할 수 있게 되어 테스트의 용이성이 높아집니다.

7. 실제 필요한 시점에 적용

디자인 패턴은 “문제를 해결하는 도구”입니다. 문제가 생기기 전이나 문제가 실제로 존재하지 않는 상황에서 패턴을 미리 적용하기 보다는, 실제로 문제가 발생했을 때 적절한 패턴을 선택하여 적용하는 것이 효율적입니다.

자바로 본 팩토리 패턴 예제

Simple Factory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
interface Product {
    void create();
}

class ConcreteProductA implements Product {
    public void create() {
        System.out.println("Product A is created.");
    }
}

class ConcreteProductB implements Product {
    public void create() {
        System.out.println("Product B is created.");
    }
}

class SimpleFactory {
    public Product createProduct(String type) {
        if ("A".equals(type)) {
            return new ConcreteProductA();
        } else if ("B".equals(type)) {
            return new ConcreteProductB();
        }
        return null;
    }
}

public class Client {
    public static void main(String[] args) {
        SimpleFactory factory = new SimpleFactory();
        Product productA = factory.createProduct("A");
        productA.create();
    }
}

가상 시나리오 예제

동물원 관리 시스템: 팩토리 패턴을 이용한 동물 생성

동물원에서 다양한 종류의 동물들을 관리하려고 합니다. 이 때, 동물들은 특정 행동을 갖고 있으며, 새로운 동물이 추가될 때마다 동물을 생성하는 코드를 수정하지 않기 위해 팩토리 패턴을 사용합니다.

1. 동물과 행동 정의

우리가 관리하는 동물은 LionElephant입니다. 각각의 동물은 speak라는 행동을 가지고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Animal {
    void speak();
}

class Lion implements Animal {
    @Override
    public void speak() {
        System.out.println("Roar!");
    }
}

class Elephant implements Animal {
    @Override
    public void speak() {
        System.out.println("Trumpet!");
    }
}

2. 동물 생성을 위한 팩토리 정의

1
2
3
4
5
6
7
8
9
10
class AnimalFactory {
    public Animal createAnimal(String type) {
        if ("Lion".equalsIgnoreCase(type)) {
            return new Lion();
        } else if ("Elephant".equalsIgnoreCase(type)) {
            return new Elephant();
        }
        return null;
    }
}

3. 클라이언트 코드

1
2
3
4
5
6
7
8
9
10
11
public class ZooKeeper {
    public static void main(String[] args) {
        AnimalFactory factory = new AnimalFactory();

        Animal lion = factory.createAnimal("Lion");
        lion.speak();

        Animal elephant = factory.createAnimal("Elephant");
        elephant.speak();
    }
}

2번과 3번 코드에서 보면 문자열 비교 기반으로 로직이 구성됨을 볼 수 있습니다. 이는 Enum 또는 Map을 이용하여 if 문을 쓰지 않고 매핑해서 할 수도 있습니다. 아래에서 Enum을 활용한 예제 코드를 보여드리겠습니다.

2-수정본. Enum과 Supplier를 활용한 동물 팩토리

Java 8에서는 Supplier라는 함수형 인터페이스를 제공합니다. 이를 활용하여 간결하고 유연한 팩토리를 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.function.Supplier;
import java.util.Map;
import java.util.HashMap;

public enum AnimalType {
    LION, ELEPHANT
}

class AnimalFactory {
    private static final Map<AnimalType, Supplier<Animal>> animalMap = new HashMap<>();

    static {
        animalMap.put(AnimalType.LION, Lion::new);
        animalMap.put(AnimalType.ELEPHANT, Elephant::new);
    }

    public Animal createAnimal(AnimalType type) {
        Supplier<Animal> supplier = animalMap.get(type);
        if (supplier != null) {
            return supplier.get();
        }
        throw new IllegalArgumentException("Invalid animal type.");
    }
}

이렇게 하면 AnimalFactory 클래스는 새로운 동물을 추가할 때마다 Map에 항목을 추가하기만 하면 됩니다. 또한, 객체 생성 로직도 Map에서 관리되기 때문에 if-else 문이나 switch 문을 사용하지 않아도 됩니다.

3-수정본. 클라이언트 코드

1
2
3
4
5
6
7
8
9
10
11
public class ZooKeeper {
    public static void main(String[] args) {
        AnimalFactory factory = new AnimalFactory();

        Animal lion = factory.createAnimal(AnimalType.LION);
        lion.speak();

        Animal elephant = factory.createAnimal(AnimalType.ELEPHANT);
        elephant.speak();
    }
}

시나리오 결론

팩토리 패턴을 사용하면 객체 생성 로직을 한곳에서 관리할 수 있어 유지 보수가 용이하고 확장성이 좋아집니다. 특히 Java 8의 SupplierMap을 활용하면 코드의 간결성과 유연성을 크게 향상시킬 수 있습니다.

스프링 프레임워크에서의 팩토리 패턴 사용사례

스프링 프레임워크는 IoC(Inversion of Control)를 기반으로 하며, Bean Factory와 Application Context는 팩토리 패턴을 활용하는 대표적인 예시입니다.

1. XML 기반 설정

초기의 스프링 프레임워크는 XML을 통해 이러한 빈 설정을 제공하였습니다.

1
2
3
4
5
<bean id="messageService" class="com.example.MessageServiceImpl">
    <property name="messageRepository" ref="messageRepository"/>
</bean>

<bean id="messageRepository" class="com.example.MessageRepositoryImpl"/>

위의 XML 설정에서 MessageServiceImpl 클래스의 인스턴스를 생성하고, messageRepository 속성에 MessageRepositoryImpl의 인스턴스를 주입합니다.

2. 자바 기반 설정

스프링이 발전하면서, XML 대신 자바 코드를 사용한 설정 방식이 인기를 얻기 시작하였습니다. 이 방식은 타입 세이프하며, IDE의 지원을 더욱 잘 받을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MessageRepository messageRepository() {
        return new MessageRepositoryImpl();
    }

    @Bean
    public MessageService messageService() {
        return new MessageServiceImpl(messageRepository());
    }
}

이 설정에서, @Configuration 어노테이션이 붙은 AppConfig 클래스는 스프링 설정 클래스로 동작합니다. @Bean 어노테이션이 붙은 메서드는 해당 메서드가 반환하는 객체를 스프링 컨테이너에 빈으로 등록합니다.

원리:

  1. @Component 및 그것의 파생된 어노테이션(@Service, @Repository 등)은 클래스가 스프링 빈으로 관리되어야 함을 나타냅니다. 이러한 빈들은 기본적으로 싱글턴 패턴으로 관리됩니다.
  2. @Configuration은 특별한 종류의 @Component이며, 빈의 생성 및 구성 로직을 포함하는 스프링 설정 클래스를 나타냅니다.
  3. @Bean 메서드는 @Configuration 클래스 내에서 정의되며, 스프링 컨테이너(ApplicationContext)에 의해 호출되어 빈의 인스턴스를 생성하고 반환합니다. 이러한 과정은 팩토리 패턴의 원리와 일치합니다.

스프링 프레임워크는 이러한 방식으로 객체의 생성, 구성, 관리 등의 역할을 수행하며, 팩토리 패턴 및 싱글턴 패턴의 개념을 극대화하여 활용합니다.

결론

팩토리 패턴은 객체 생성에 관한 책임을 감추고, 구체적인 클래스 생성 로직을 간접화하여 유지 보수성과 확장성을 개선합니다. 이러한 패턴은 대표적으로 스프링과 같은 프레임워크에서도 활용되며, 객체 지향 프로그래밍의 핵심 원칙인 “객체 생성과 사용의 분리”를 잘 보여줍니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.