[GoF의 디자인 패턴] 팩토리 메서드 패턴 (Factory Method) (3/23)

Seyeong·2023년 1월 15일
0

GoF의 디자인패턴

목록 보기
4/5

이번 장에서도 역시 예시 코드를 통해 팩토리 메서드 패턴에 대해 이해해보겠습니다.

우선 팩토리 메서드 패턴이 무엇인지 알아봅시다.

팩토리 메서드 패턴이란?

객체를 생성하는 인터페이스는 미리 정의하되, 인스턴스를 만들 클래스의 결정은 서브 클래스 쪽에서 내리는 패턴입니다. 팩토리 메서드 패턴에서는 클래스의 인스턴스를 만드는 시점을 서브클래스로 미룹니다.

음.. 무슨말인지 하나도 모르겠어요

간단하게 팩토리 메서드 패턴의 구조를 살펴보면 아래와 같습니다.

  • 상품(Product)

    팩토리 메서드가 생성하는 제품을 의미합니다.

  • 생성기(Creator)

    상품을 생성하고 반환합니다. 그리고 그 과정에서 팩토리 메서드를 호출합니다.

이제 예제 코드를 보며 이해해봅시다.

팩토리 메서드 패턴 적용 (1)

데이터 베이스를 연결하기 위한 커넥션을 생성하는 팩토리 메서드를 만들어 보겠습니다.

커넥션 종류로는 MySQL, Oracle로 쉽게 두 가지 DB 종류로만 구성하였습니다.

상품 종류

  • MySQL 커넥션
  • Oracle 커넥션

생성기 종류

  • MySQL 커넥션 생성기
  • Oracle 커넥션 생성기

상품 만들기

상품을 먼저 만들어봅시다.

상품을 정의하기 위해 인터페이스를 설계합니다.

Connection 인터페이스

public interface Connection {
    void connect();
}

위의 connect( ) 메서드는 상품을 만들고 제대로 만들어졌는지 확인하기 위한 연결 작업입니다.

이제 이 Connection의 구현체들을 만들어봅시다.

MySQLConnection 구현체

public class MySQLConnection implements Connection {
    @Override
    public void connect() {
        System.out.println("MySQL에 연결되었습니다.");
    }
}

예제이므로 connect( ) 메서드에서는 연결 확인 메시지만 출력하였습니다.
마찬가지로 Oracle 커넥션 구현체를 만들면.

OracleConnection 구현체

public class OracleConnection implements Connection {
    @Override
    public void connect() {
        System.out.println("Oracle에 연결되었습니다.");
    }
}

상품들을 만들어주었으니, 이 상품들을 책임지고 생성할 생성기들을 만들어봅시다.

생성기 만들기

마찬가지로 MySQL과 Oracle 을 만드는 두 개의 생성기를 만들어줄텐데, 그 전에 커넥션 생성기라는 추상 클래스를 먼저 만들어주겠습니다.

ConnectionCreator 추상 클래스

public abstract class ConnectionCreator {

    public Connection create() { // 팩토리 메서드 (create)
        createConnection(); // 1. 커넥션 만들기
        setConnectionId(); // 2. 커넥션에 ID 세팅하기
        setConnectionPassword(); // 3. 커넥션에 Password 세팅하기
        return getConnection(); // 4. 커넥션 반환하기
    }

    protected abstract void createConnection(); // 나머지는 추상 메서드

    protected abstract void setConnectionId();

    protected abstract void setConnectionPassword();

    protected abstract Connection getConnection();
}

아까 커넥션은 인터페이스인데 커넥션 생성기는 왜 추상 클래스로 선언한 걸까요?

코드를 보면 아시겠지만 커넥션 생성기 추상 클래스를 상속받은 하위 클래스가 자신이 만들고자 하는 종류의 커넥션을 만들 때, 만드는 일련의 과정만을 정한 채로 서브 클래스가 만들게 하고 싶기 때문입니다.

위의 create( ) 메서드에서는

  1. 커넥션을 만든다.
  2. 커넥션에 아이디를 세팅한다.
  3. 커넥션에 비밀번호를 세팅한다.
  4. 커넥션을 반환한다.

라는 일련의 과정을 강제하고 이를 상속받는 여러 종류의 커넥션 생성기들에게 직접 원하는 커넥션을 만드는 책임을 지게 하는 것입니다.

설명이 이해 안돼도 코드를 보면 이해가 갈겁니다.

먼저 MySQL 커넥션을 만드는 생성기를 만들어줍시다.

MySQLConnectionCreator 서브 클래스

public class MySQLConnectionCreator extends ConnectionCreator {
    private Connection connection; // 커넥션 선언

    @Override
    protected void createConnection() { // 원하는 커넥션 생성
        connection = new MySQLConnection();
    }

    @Override
    protected void setConnectionId() { // 아이디 설정
        System.out.println("MySQL 아이디 설정");
    }

    @Override
    protected void setConnectionPassword() { // 비밀번호 설정
        System.out.println("MySQL 비밀번호 설정");
    }

    @Override
    protected Connection getConnection() { // 커넥션 
        return connection;
    }
}

MySQL 커넥션 생성기는 따로 create( ) 라는 메서드를 재정의 할 필요가 없습니다. 이는 부모 클래스에서 구현은 물론이고 원하는 작업 순서까지 명시하였죠. 저희는 단지 부모 클래스가 명시한 작업들을 구현하기만 하면 됩니다. 그럼 부모 클래스에서 알아서 작업 순서에 맞추어서 커넥션을 생성해 줄겁니다. 여기서 부모 클래스의 create( ) 메서드가 팩토리 메서드가 되는 겁니다.

이제 감이 좀 잡히시나요? 그럼 이번엔 Oracle 커넥션 생성기를 만들어봅시다.

OracleConnectionCreator 서브 클래스

public class OracleConnectionCreator extends ConnectionCreator {
    private Connection connection;

    @Override
    protected void createConnection() {
        connection = new OracleConnection();
    }

    @Override
    protected void setConnectionId() {
        System.out.println("Oracle 아이디 설정");
    }

    @Override
    protected void setConnectionPassword() {
        System.out.println("Oracle 비밀번호 설정");
    }

    @Override
    protected Connection getConnection() {
        return connection;
    }
}

여기까지 커넥션 생성기를 만들어보았습니다. 그런데 MySQL 및 Oracle 커넥션 생성기에서 중복이 발생합니다. 바로 커넥션을 선언하는 부분과 커넥션을 반환하는 부분입니다.

public class OracleConnectionCreator extends ConnectionCreator {
    private Connection connection; << 이 부분

	...

    @Override
    protected Connection getConnection() { << 이 부분
        return connection;
    }
}

이 부분은 여러 커넥션 생성기를 만들더라도 코드가 변하지 않고 항상 같기 때문에 코드가 중복됩니다. 따라서 이 부분을 리팩토링 해주겠습니다.

이 중복되는 부분들을 지우고 추상 클래스에서 커넥션을 만들고 반환해주면 됩니다.

public abstract class ConnectionCreator {
    Connection connection; << 추가

    public Connection create() {
        createConnection(); // 1. 커넥션 만들기
        setConnectionId(); // 2. 커넥션에 ID 세팅하기
        setConnectionPassword(); // 3. 커넥션에 Password 세팅하기
        return connection; << 변경 // 4. 커넥션 반환하기
    }

    protected abstract void createConnection();

    protected abstract void setConnectionId();

    protected abstract void setConnectionPassword();
    
    // protected abstract Connection getConnection(); 제거
}

만약 서브 클래스에서 Connection 선언한 부분을 지우지 않으면 정상적으로 동작하지 않습니다. 서브 클래스에서 새로 커넥션을 만들고 하는 등의 작업이 상위 클래스의 커넥션이 아닌, 자기가 만든 커넥션에 적용하기 때문이죠.

여기까지 커넥션과 생성기를 만들어보았습니다.
이제 실제로 동작하는지 확인해봅시다.

Main

public class Main {
    public static void main(String[] args) {
        ConnectionCreator creator = getConnectionCreator();
        Connection connection = creator.create();
        connection.connect();
    }

    private static ConnectionCreator getConnectionCreator() { // 생성기 선택
        return new MySQLConnectionCreator();
//        return new OracleConnectionCreator();
    }
}

원하는 커넥션 생성기를 선택하여 커넥션 생성을 요청합니다.
그리고 생성된 커넥션에게 연결 요청을 보내는 코드입니다.

위의 코드의 실행 결과로는 아래와 같습니다.

실행 결과

지금까지 팩토리 메서드를 만들어보았는데요. 위 처럼 Creator를 추상 클래스로 만드는 첫 번째 방법이 있고 또 다른 방법이 있습니다. 바로 팩토리 메서드가 매개변수를 받아서 어떤 종류의 제품을 생성할지 식별하게 하는 것입니다.

이제부터 이 두 번째 방법을 이용하여 팩토리 메서드를 만들어 보겠습니다.
여기서부터는 효과적으로 코드를 작성하기 위해 Enum과 스트림을 활용하였으니, 혹시 기반 지식이 부족하시다면 이번 기회에 공부하면서 보시는걸 추천드립니다.

팩토리 메서드 패턴 적용 (2)

이번 방법은 매개변수를 이용하여 어떤 종류의 제품을 생성할지 식별하는 방법입니다. 기존에는 new MySQLConnectionCreator( ) 같은 방식으로 생성기를 지정해주고 .create( ) 를 호출하여 커넥션을 얻어왔습니다.

이제부터 저희가 하려는 방식은 처음부터 구현체를 정해서 하는 것이 아니라 .create("MySQL") 또는 .create("Oracle") 로 파라미터로만 제품의 종류를 전달하여 바로 커넥션을 얻어오려는 겁니다.

그래서 이를 위해 커넥션의 종류를 Enum 상수로 지정하겠습니다. 위처럼 그냥 문자열로 보내도 상관없지만 하드코드보다는 상수로 넘겨주는게 안전하고 깔끔합니다.

ConnectionType Enum

public enum ConnectionType {  // 커넥션 종류로 MYSQL과 ORACLE을 상수로 가진다.
    MYSQL, ORACLE
}

별 거 없습니다. 이렇게만 해주면 됩니다. 이렇게 만들어두면 앞으로 .create(ConnectionType.MYSQL) 또는 .create(ConnectionType.ORACLE) 방식으로 커넥션 생성 요청을 보낼 수 있겠죠.

이젠 .create(커넥션 타입) 메시지로 요청을 보내면 이를 받아서 커넥션을 만들어주는 누군가를 만들어야 합니다. 여기서는 파라미터로 넘긴 타입이 무엇인지 식별하는 것은 물론, 그 식별한 타입대로 커넥션을 생성하는 역할까지 한번에 수행할 것입니다. 따라서 기존에 있었던 커넥션 생성기는 그대로 유지해도 괜찮습니다.

잘 이해가 안되셔도 괜찮습니다. 코드를 보면서 이해해봅시다.

SpecificConnectionCreator Enum

public enum SpecificConnectionCreator {
    MYSQL(ConnectionType.MYSQL),
    ORACLE(ConnectionType.ORACLE);

    private ConnectionType type;

    SpecificConnectionCreator(ConnectionType type) {
        this.type = type;
    }
}

처음부터 모든 코드를 나열하면 이해하기 힘들 것 같아 점진적으로 코드를 추가하겠습니다.

특정 커넥션을 생성하는 Enum을 만들었습니다. 만들 수 있는 종류로는 MYSQL, ORACLE 두 가지의 커넥션을 만들 수 있다고 선언한 겁니다. 필드로 커넥션 타입을 가지고 있는데 이 필드로 저희가 원하는 커넥션을 식별할 겁니다. 이 필드는 나중에 if문에서 조건문처럼 어느 커넥션을 원하는지를 식별하는데 쓰입니다.

저희가 원하는건 커넥션을 생성하는 겁니다. 따라서 이제 이 부분을 만들어봅시다.

public enum SpecificConnectionCreator {
	
    ...
    
    public static Connection create(ConnectionType type) {
        if (type == MYSQL.type) {
        	return MySQL 커넥션 반환;
        }
        return Oracle 커넥션 반환;
    }
}

이런식으로 create( ) 을 만들어주면 원하는 커넥션 타입에 맞는 커넥션을 반환할 것입니다. 이제 이 부분을 구현하기 위해선 커넥션 타입에 맞는 커넥션 생성기가 필요합니다. 그 생성기를 이용하여 커넥션을 생성할 것이기 때문이죠.

따라서 ConnectionCreator 인터페이스를 필드에 추가해야 합니다.

public enum SpecificConnectionCreator {
    MYSQL(ConnectionType.MYSQL, new MySQLConnectionCreator()), // 구현체 추가
    ORACLE(ConnectionType.ORACLE, new OracleConnectionCreator());

    private ConnectionType type;
    private ConnectionCreator creator; // 커넥션 생성기 추가

    SpecificConnectionCreator(ConnectionType type,
                              ConnectionCreator creator) {
        this.type = type;
        this.creator = creator;
    }

    public static Connection create(ConnectionType type) {
        if (type == MYSQL.type) {
        	return MySQL 커넥션 반환;
        }
        return Oracle 커넥션 반환;
    }
}

이제 점점 모양이 잡히고 있습니다. MYSQL 필드는 원하는 커넥션 타입이 MySQL이라면 MySQL 커넥션 생성기를 가지고 있다가 이를 이용하여 커넥션을 생성해주면 되겠습니다. 이제 마저 create( ) 메서드를 구현해보겠습니다.

public enum SpecificConnectionCreator {
    MYSQL(ConnectionType.MYSQL, new MySQLConnectionCreator()),
    ORACLE(ConnectionType.ORACLE, new OracleConnectionCreator());

    private ConnectionType type;
    private ConnectionCreator creator;
    
    SpecificConnectionCreator(ConnectionType type,
                              ConnectionCreator creator) {
        this.type = type;
        this.creator = creator;
    }

    public static Connection create(ConnectionType type) {
        return Arrays.stream(values())
                .filter(selector -> selector.type == type)
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("일치하는 커넥션 타입이 존재하지 않습니다."))
                .creator
                .create(); // 해당 커넥션 생성기의 create() 호출
    }
}

스트림을 사용하여 조건문을 포함한 여러 로직을 가독성 좋게 만들었습니다.
스트림이 익숙하지 않은 분들을 위해서 간단히 설명을 하면

요청한 타입에 일치하는 커넥션 생성기를 가져와서 커넥션을 생성하도록 지시한 것입니다. 또한 여기에 매치가 되지 않는, 예를들어 Mongo 커넥션 타입이 요청으로 들어온다면 예외를 던지게 해주었습니다.

모든 구현이 끝났습니다.
이제 메인 함수에서 아주 간단하고 쉽게 원하는 커넥션을 만들 수 있습니다.

Main

public class Main {
    public static void main(String[] args) {
		
        --- 팩토리 메서드 패턴 적용 1 (추상 클래스) ---
        /*ConnectionCreator creator = getConnectionCreator();
        Connection connection = creator.create();
        connection.connect();*/
        
        --- 팩토리 메서드 패턴 적용 2 (파라미터) ---
        Connection connection = SpecificConnectionCreator.create(ConnectionType.MYSQL);
        connection.connect();
    }

코드가 매우 직관적으로 변했습니다. 아래와 같이 원하는 커넥션을 선택하는 부분에서도 변경에 유연하게 대처할 수 있게 되었습니다.

.create(ConnectionType.MYSQL); // MySQL 커넥션 생성 요청

.create(ConnectionType.ORACLE); // Oracle 커넥션 생성 요청

실행해보면 결과도 위와 똑같이 나오는걸 볼 수 있습니다.

결과

마무리

매번 디자인 패턴 포스팅이 그렇지만 스스로 공부해보며 직접 예제를 만들고 정리하고 있습니다. 그래서 혹시나 틀린 부분이 있을 수 있으며, 이 부분에 대해서 언제든지 알려주시면 감사하겠습니다.

0개의 댓글