본문 바로가기
IT/Java

[Java] Java Factory Pattern

by kyu-nahc 2024. 9. 11.
반응형

 

Factory Pattern

팩토리 패턴(Factory Pattern)객체 생성을 공장(Factory) 클래스로 캡슐화 처리하여,

객체를 대신 생성하게 하는 생성 디자인 패턴이다.
즉, 클라이언트에서 직접 new 연산자를 통해 제품 객체를 생성하는 것이 아닌, 

제품 객체들을 도맡아 생성하는 공장 추상 클래스를 만들고,

이를 상속하는 서브 공장 클래스의 메서드에서 여러 가지 제품 객체 생성을 각각 책임지는 것이다.
또한 객체 생성에 필요한 과정을 템플릿처럼 미리 구성해 놓고, 객체 생성에 관한 

전처리나 후처리를 통해 생성 과정을 다양하게 처리하여 객체를 유연하게 정할 수 있는 특징도 있다.

 

즉 팩토리 패턴은 생성 패턴(Creational Pattern) 중 하나이다.
생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴이다.
생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현방법을 시스템과 분리해 준다.

생성 패턴은 시스템이 상속보다 복합방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있다.

생성 패턴에는 중요한 이슈가 2가지 있다.

  • 생성 패턴은 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화한다.
  • 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고
    어떻게 결합하는지에 대한 부분을 완전히 가려준다.

이 두 가지를 정리해 보면, 생성 패턴을 이용하여 무엇이 생성되고,

누가, 어떻게, 언제 육하원칙 비슷하게 결정하는 데에 유연성을 확보할 수 있다.

다시 설명하자면 팩토리 패턴은 객체를 생성하는 추상 클래스를 미리 정의하지만,

인스턴스를 만들 클래스의 결정은 서브 클래스 쪽에서 결정하는 패턴이다.

여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때,

들어오는 인자에 따라서 하나의 자식클래스의 인스턴스를 반환해 주는 방식이다.

팩토리 패턴은 클래스의 인스턴스를 만드는 시점 자체를 서브 클래스로 미루는 것이다.

 

팩토리 패턴은 Simple Factory PatternFactory Method Pattern으로 구분된다.

Simple Factory Pattern은 공장 클래스를 한 개 생성한다.

Client 측에서 객체를 생성하지는 않지만,

분기문을 사용하여 매개변수 Type에 따라 객체를 생성하여 반환한다.

비록 객체의 생성을 공장 클래스에게 위임하였지만,

객체의 종류가 늘어날수록 분기문이 늘어나고, OCP원리에도 위배된다.

 

Factory Method Pattern은 추상 공장 클래스를 하나 생성하고,

객체를 생성하는 메서드는 추상 메서드로 정의한다.

이를 상속받는 여러 개의 서브 클래스를 생성하고,

해당 서브 클래스에서 추상 메서드를 구현하여 객체의 생성을 담당한다.

따라서 분기문을 사용하지 않고 객체를 생성할 수 있고, OCP원리에도 부합한다.

 

 

 

Factory Pattern 구조

Simple Factory Pattern은 아래 예시 코드에서 살펴볼 수 있고,

밑의 구조는 Factory Method Pattern의 구조이다.

 

AbstractFactory
최상위 공장 클래스로서, 팩토리 메서드를 추상화하여 서브 클래스로 하여금 구현하도로 한다.

객체 생성 처리 메서드(settingProduct) - 객체 생성에 관한 전처리, 후처리를 템플릿화한 메서드

팩토리 메서드(createProduct) - 서브 공장 클래스에서 재정의할 객체 생성 추상 메서드

 

ConcreteFactory

각 서브 공장 클래스들은 이에 맞는 제품 객체를 반환하도록 생성 추상 메서드를 재정의한다.

즉, 제품 객체 하나당 그에 걸맞은 생산 공장 객체가 위치된다.

 

Product

제품 구현체를 추상화하는 인터페이스

 

ConcreteProduct

Product 인터페이스를 구현하는 제품 구현체 객체

 

정리하자면, 팩토리 메서드 패턴은 객체를 만들어내는 공장을 만드는 패턴이라고 보면 된다.

그리고 어떤 클래스의 인스턴스를 만들지는 미리 정의한 공장 서브 클래스에서 결정한다.

이를 통해 객체 간의 결합도가 낮아지고 유지보수에 용이해진다.

이에 대해 코드로 살펴보면 다음과 같다.

// 제품 객체 추상화
public interface Product {
    void settingProduct();
}
// 제품에 대한 구현체
// 인터페이스 메소드를 구현
public class ProductA implements Product{

    @Override
    public void settingProduct() {
        System.out.println("Product A Settings");
    }
}
// 제품에 대한 구현체
// 인터페이스 메소드를 구현
public class ProductB implements Product{

    @Override
    public void settingProduct() {
        System.out.println("Product B Settings");
    }
}
// 공장 객체 추상화 (추상 클래스)
public abstract class AbstractFactory {

    // 객체를 생성하기 전처리/후처리 하기 위한 메소드
    // final을 통해 오버라이드를 방지
    final Product settingProduct(){
    	// 서브 클래스에서 구현된 메소드를 실행
        Product product = createProduct();
        product.settingProduct();
        // 생성된 제품을 반환
        return product;
    }

    // 팩토리 메소드 : 구체적인 객체 생성 종류는 각 서브 클래스에 위임
    // protected 이기 때문에 외부에 노출이 안됨
    protected abstract Product createProduct();
}

// 공장 객체 A - ProductA를 반환한다.
public class ConcreteFactoryA extends AbstractFactory{

    @Override
    protected Product createProduct() {
        System.out.println("This is Factory A");
        return  new ProductA();
    }
}
// 공장 객체 B - ProductB를 반환한다.
public class ConcreteFactoryB extends AbstractFactory{

    @Override
    protected Product createProduct() {
        System.out.println("This is Factory B");
        return new ProductB();
    }
}
public class Main {

    public static void main(String[] args) {
    
    	// 공장 리스트
        AbstractFactory[] factories = {
                new ConcreteFactoryA(),
                new ConcreteFactoryB()
        };
        // 공장을 돌면서 settingProduct()를 실행하여
        // 제품을 생성한다.
        for (AbstractFactory factory : factories) {
            factory.settingProduct();
        }
    }
}

 

 

Factory Pattern 특징

패턴 적용 시기

  • 클래스 생성과 사용의 처리 로직을 분리하여 결합도를 낮추고자 할 때
  • 코드가 동작해야 하는 객체의 유형과 종속성을 캡슐화를 통해 정보 은닉 처리 할 경우
  • 기존 객체를 재구성하는 대신 기존 객체를 재사용하여 리소스를 절약하고자 하는 경우

상황에 따라 적절한 객체를 생성하는 코드는 자주 중복될 수 있다.

그리고 객체 생성 방식의 변화는 해당되는 모든 코드 부분을 변경해야 하는 문제가 발생한다. 

따라서 객체의 생성 코드를 별도의 클래스 / 메서드로 분리함으로써 

객체 생성의 변화에 대해 대비를 하기 위해 팩토리 메서드 패턴을 이용한다고 보면 된다. 

특정 기능의 구현은 별개의 클래스로 제공되는 것이 바람직한 설계이기 때문이다.

 

패턴 장점

  • 생성자(Factory)와 구현 객체(concrete product)의 강한 결합을 피할 수 있다.
  • 팩토리 메서드를 통해 객체의 생성 후 공통으로 할 일을 수행하도록 지정해 줄 수 있다.
  • 캡슐화, 추상화를 통해 생성되는 객체의 구체적인 타입을 감출 수 있다.
  • 단일 책임 원칙(SRP) 준수
    객체 생성 코드를 한 곳 (패키지, 클래스 등)으로 이동하여
    코드를 유지보수하기 쉽게 할 수 있으므로 원칙을 만족
  • OCP 원칙 준수
    기존 코드를 수정하지 않고 새로운 유형의 제품 인스턴스를 프로그램에 도입 가능

 

패턴 단점

  • 각 제품 구현체마다 팩토리 객체들을 모두 구현해주어야 하기 때문에,
    구현체가 늘어날 때마다 팩토리 클래스가 증가하여 서브 클래스 수가 너무 많아진다.
  • 코드의 복잡성이 증가한다.

 

Factory Pattern 적용 예시

실무에서 적합한 Factory Method Pattern 예제로,

자주 사용하는 데이터베이스 연결 또는 로깅 시스템에서의 활용할 수 있다.

이 패턴은 다양한 유형의 객체를 생성해야 하는 상황에서 유용하다.

예를 들어, 여러 종류의 데이터베이스(MySQL, PostgreSQL 등)에 대한 연결을 관리하거나,

다양한 방식의 로깅(File, Console, Database 등)을 처리할 때 Factory 패턴을 사용할 수 있다.

해당 예시로는 로깅 시스템을 위한 Factory Method Pattern을 구현할 것이다.

여기서는 콘솔 로그, 파일 로그, 데이터베이스 로그

세 가지 로깅 방식을 생성하는 팩토리를 구현할 것이다.

public interface Logger {
    void log(String message);
}

public class ConsoleLogger implements Logger{
    @Override
    public void log(String message) {
        System.out.println("Console log - "+message);
    }
}

public class FileLogger implements Logger{

    @Override
    public void log(String message) {
        try (FileWriter fw = new FileWriter("log.txt", true)) {
            System.out.println("File log - "+message);
            fw.write("File log: " + message + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

public class DatabaseLogger implements Logger{

    @Override
    public void log(String message) {
        // 실제로는 DB 연결 및 데이터 삽입 로직 포함
        System.out.println("Database log: " + message);
    }
}

 

모든 로깅 클래스가 공통으로 구현해야 하는 Logger 인터페이스를 정의하고,

인터페이스에 대한 구현체를 생성하고 로킹 클래스에 따라 로직을 작성한다.

public abstract class LoggerFactory {

    final Logger settingLog(){
        System.out.println("Setting Log Factory");
        return createLogger();
    }

    protected abstract Logger createLogger();
}

public class ConsoleLoggerFactory extends LoggerFactory{

    @Override
    protected Logger createLogger() {
        return new ConsoleLogger();
    }
}

public class FileLoggerFactory extends LoggerFactory{

    @Override
    protected Logger createLogger() {
        return new FileLogger();
    }
}

public class DatabaseLoggerFactory extends LoggerFactory{

    @Override
    protected Logger createLogger() {
        return new DatabaseLogger();
    }
}
public class Client {

    public static void main(String[] args) {

        Logger consoleLogger = new ConsoleLoggerFactory().settingLog();
        consoleLogger.log("Create Console Logger");

        Logger fileLogger = new FileLoggerFactory().settingLog();
        fileLogger.log("Create File Logger");

        Logger databaseLogger = new DatabaseLoggerFactory().settingLog();
        databaseLogger.log("Create Database Logger");
    }
}

 

이제 로깅 객체를 생성하는 팩토리 클래스를 구현한다. 

추상 클래스인 팩토리 클래스를 하나 만들고,

객체를 생성하는 메서드는 추상 메서드로 정의한다.

추상 메서드를 통해 객체 생성을 서브 클래스로 위임하는 것이다.

추상 클래스를 상속받는 서브 클래스에서 객체 생성 메서드를 오버라이드하여 구현한다.

해당 서브 클래스에 맞게 적절한 로깅 객체를 생성하여 반환하도록 한다.

따라서 클라이언트 측에서는 어떤 코드 동작을 통해 객체가 반환되는지 알지 못하고,

팩토리를 제공하는 입장에서는 객체 유형 및 의존성을 캡슐화하여, 정보를 은닉할 수 있다.

또한 새로운 객체의 종류를 추가해야 한다면,

팩토리 클래스와 객체 클래스 2개만 생성하고 추가적으로 코드를 수정할 필요도 없다.

 

실행 결과는 다음과 같고,

위의 패턴을 Factory Method Pattern이라고 명칭 한다.

위와 다른 Simple Factory Pattern도 존재한다.

이는 객체 생성 자체는 공장 클래스에 위임하지만,

공장 클래스는 1개로 분기문을 통해 객체 생성을 진행한다.

따라서 OCP 원리에 부합하지 않는다. 이에 대한 예시를 코드를 통해 살펴보자.

public class SimpleLoggerFactory {
    
    public static Logger getLogger(String type) {
        if (type.equalsIgnoreCase("console")) {
            return new ConsoleLogger();
        } else if (type.equalsIgnoreCase("file")) {
            return new FileLogger();
        } else if (type.equalsIgnoreCase("database")) {
            return new DatabaseLogger();
        } else {
            throw new IllegalArgumentException("Unknown logger type: " + type);
        }
    }
}

public class Client {

    public static void main(String[] args) {

        Logger logger = SimpleLoggerFactory.getLogger("console");
    }
}

 

위의 공장클래스를 모두 대신해서 하나의 LoggerFactory 클래스를 만들고,

if 분기문을 통해 객체를 생성한다.

객체 생성 자체는 공장 클래스에서 진행하지만 객체의 종류가 늘어날수록

분기문이 늘어나고, 기존에 존재하는 Factory 클래스 코드 또한 수정해야 한다.

따라서 생성할 제품 구현체의 개수가 몇 개 존재하지 않거나 종류가 추가되지 않는다면,

Simple Factory Pattern으로 구현해도 상관없다.

 

하지만 Factory Method Pattern으로 구성한다면,

존재하던 분기문을 제거하고, 객체 지향적으로 코드를 구현할 수 있다.

또한 코드 동작을 위한 객체의 유형과 종속성을 캡슐화하여 정보 은닉을 할 수 있다.

따라서 제품 구현체의 수와 캡슐화 정도를 고려하여

어떤 종류의 Factory Pattern을 사용할 것인지 결정해야 한다.

 

참고자료

https://velog.io/@lsj8367/%EC%9E%90%EB%B0%94-%ED%8C%A9%ED%86%A0%EB%A6%AC%ED%8C%A8%ED%84%B4

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9CFactory-Method-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Command Pattern  (0) 2024.09.13
[Java] Java Builder Pattern  (2) 2024.09.12
[Java] Java Decorator Pattern  (0) 2024.09.09
[Java] Java Observer Pattern  (7) 2024.09.08
[Java] Java Strategy Pattern  (0) 2024.08.29

loading