Pv_log

12. 팩토리 메서드 패턴 본문

Develop Study/Design Pattern

12. 팩토리 메서드 패턴

Priv 2022. 7. 3. 23:12


 

 

1. 여러 가지 방식의 엘리베이터 스케줄링 방법 지원하기

엘리베이터가 여러 대 있을 경우, 사용자가 엘리베이터 버튼을 누를 때 여러 대의 엘리베이터 중 1대만 작동하도록 만들어야 한다.

이처럼 주어진 요청(목적지 층과 방향)을 전달받았을 때, 여러 엘리베이터 중 하나를 선택하는 것을 '스케줄링'이라고 부른다.

엘리베이터 스케줄링에는 여러 방법들이 존재한다.

목적지 층과 가깝고 해당 방향으로 움직이는 중인 엘리베이터를 호출하는 것도 하나의 방법일 것이다.

이 방법은 하나의 엘리베이터로 최대한 많은 이용자가 탑승할 수 있도록 만드는 방법이다.

그림 12-1에서 ElevatorManager 클래스는 이동 요청을 처리하는 스케줄링 클래스이다.

스케줄링 작업을 처리하는 ThroughputScheduler 객체를 가지고 있으며, 엘리베이터 이동을 책임지는 ElevatorController 객체를 여러 개 가진다.

ElevatorManager 클래스의 requestElevator 메서드는 목적지 층과 방향을 얻었을 때 ThroughputScheduler 클래스의 selectElevator 메서드를 호출하여 적절한 엘리베이터를 선택해야 한다.

여기서 선택된 엘리베이터에 해당하는 ElevatorController는 gotoFloor 메서드를 통해 움직이게 된다.

package P12;

public class ElevatorController {
    private int id;
    private int curFloor;


    public ElevatorController(int id) {
        this.id = id;
        curFloor = 1;
    }

    public void gotoFloor(int destination) {
        System.out.println("Elevator: [" + id + "] Floor: " + curFloor);
        curFloor = destination;
        System.out.println(" ==> " + curFloor);
    }
}
package P12;

import P11.Direction;

public class ThroughputScheduler {
    public int selectedElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }
}
package P12;

import java.util.ArrayList;
import java.util.List;

import P11.Direction;

public class ElevatorManager {
    private List<ElevatorController> controllers;
    private ThroughputScheduler scheduler;

    public ElevatorManager(int controllerCount) {
        controllers = new ArrayList<ElevatorController>(controllerCount);

        for (int i = 0; i < controllerCount; i++) {
            ElevatorController controller = new ElevatorController(i);
            controllers.add(controller);
        }

        scheduler = new ThroughputScheduler();
    }

    void requestElevator(int destination, Direction direction) {
        int selectedElevator = scheduler.selectedElevator(this, destination, direction);

        controllers.get(selectedElevator).gotoFloor(destination);
    }
}

 


 

2. 문제점

만약 사용자의 대기 시간을 최소화하는 다른 스케줄링 전략을 사용해야 한다면 어떻게 해야 할까?

기존에는 ElevatorManager 클래스에서 ThroughputScheduler 객체를 이용했지만, 이제는 주어진 전략에 따라 실행 중에 전략이 동적으로 변경되어야 한다.

package P12;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import P11.Direction;

public class ElevatorManager {
    private List<ElevatorController> controllers;
    

    public ElevatorManager(int controllerCount) {
        controllers = new ArrayList<ElevatorController>(controllerCount);

        for (int i = 0; i < controllerCount; i++) {
            ElevatorController controller = new ElevatorController(i);
            controllers.add(controller);
        }
    }

    void requestElevator(int destination, Direction direction) {
        ElevatorScheduler scheduler;
        int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);

        if (hour < 12) {
            scheduler = new ResponseTimeScheduler();
        }
        else {
            scheduler = new ThroughputScheduler();
        }

        int selectedElevator = scheduler.selectedElevator(this, destination, direction);
        controllers.get(selectedElevator).gotoFloor(destination);
    }
}
package P12;

import P11.Direction;

public interface ElevatorScheduler {
    public int selectedElevator(ElevatorManager manager, int destination, Direction direction);
}
package P12;

import P11.Direction;

public class ThroughputScheduler implements ElevatorScheduler {
    public int selectedElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0;
    }
}
package P12;

import P11.Direction;

public class ResponseTimeScheduler implements ElevatorScheduler {
    public int selectedElevator(ElevatorManager manager, int destination, Direction direction) {
        return 1;
    }
}

위의 코드는 시간에 따라 대기 시간 최소화 또는 처리량 최대화 전략을 동적으로 사용하는 코드이다.

처리량 최대화, 대기 시간 최소화 모두 ElevatorManager 클래스 입장에서는 엘리베이터 스케줄링 전략의 일종이므로, ElevatorScheduler라는 인터페이스를 사용한다.

이제 필요에 따라 동적으로 클래스를 선택할 수 있게 되었지만, 스케줄링 전략이 변경될 때마다 requestElevator 코드를 수정해야 한다는 문제점이 남아있다.

현재 requestElevator 메서드는 엘리베이터 선택/이동이 근본 책임이다.

그러므로 엘리베이터 선택에 대한 전략이 변경되더라도 requestElevator는 변경되지 않는 것이 바람직하다.

 


 

3. 해결책

문제를 해결하려면 주어진 기능을 실제로 제공하는 적절한 클래스 생성 작업을 별도의 클래스/메서드로 분리해야 한다.

즉, 엘리베이터 스케줄링 전략에 일치하는 클래스를 생성하는 코드를 requestElevator 메서드에서 분리하여 별도의 클래스/메서드를 정의하는 것이다.

위의 그림 12-3은 ElevatorManager 클래스가 ThroughputScheduler, ResponseTimeScheduler 객체를 생성하는 대신, SchedulerFactory 클래스가 해당 객체를 생성하도록 변경한 것이다.

package P12;

import java.util.Calendar;

enum SchedulingStrategyID { RESPONSE_TIME, THROUGHPUT, DYNAMIC }

public class ShedulerFactory {
    public static ElevatorScheduler getSheduler(SchedulingStrategyID strategyID) {
        ElevatorScheduler scheduler = null;

        switch(strategyID) {
            case RESPONSE_TIME :
                scheduler = new ResponseTimeScheduler();
                break;
            case THROUGHPUT :
                scheduler = new ThroughputScheduler();
                break;
            case DYNAMIC :
                int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
                
                if (hour < 12) {
                    scheduler = new ResponseTimeScheduler();
                }
                else {
                    scheduler = new ThroughputScheduler();
                }
                break;
        }
        return scheduler;
    }
}

SchedulerFactory 클래스의 getScheduler 메서드는 매개변수의 값에 따라 적절한 스케줄링을 선택해 객체를 생성한다.

이제 ElevatorManager 클래스의 gequestElevator 메서드에서 직접 스케줄링 클래스를 생성하는 것 대신, SchedulerFactory 클래스의 getScheduler 메서드를 호출하면 된다.

여기서 만약 동일 스케줄링 방식이라고 한다면, 스케줄링 객체를 여러 번 생성하지 않고 1번 생성한 것을 재활용하는 것이 더 효율적이다.

즉, 싱글턴 패턴을 적용하여 설계하는 것이 더 바람직하다는 것이다.

싱글턴 패턴을 적용한다면 2개의 스케줄링 전략 클래스(ThroughputScheduler, ResponseTimeScheduler)는 생성자로 직접 객체를 생성하지 못하게 막아야 한다.

이를 위해서는 각 생성자들은 private로 설정하고, getInstance라는 정적 메서드로 객체 생성을 구현하면 된다.

 


 

4. 팩토리 메서드 패턴

팩토리 메서드 패턴은 객체의 생성 코드를 별도의 클래스/메서드로 분리하여 객체 생성의 변화에 대비하는 데 유용한 패턴이다.

엘리베이터 스케줄링 예시처럼 프로그램이 제공하는 기능은 상황에 따라 항상 바뀔 수 있다.

또한 특정 기능 구현은 개별 클래스를 통해 제공되는 것이 좋다.

기능의 변경이나 상황에 따른 기능 선택에 대한 문제는 객체 생성 코드 부분의 변경을 초래한다.

상황에 따라 적절한 코드를 작성하는 방법은 코드 중복 문제를 야기하기 쉬우며, 코드의 유지보수에도 치명적이다.

이러한 문제점을 해결할 수 있는 팩토리 메서드 패턴은 객체 생성 코드를 별도의 클래스/메서드로 분리하며, 해당 클래스/메서드만 변경함으로써 객체 생성 방식의 변화에 효과적으로 대응할 수 있도록 만들어준다.

팩토리 메서드 패턴은 객체 생성을 전담하는 별도의 클래스를 두는 대신, 하위 클래스에서 적합한 클래스의 객체를 생성하는 방식으로도 적용이 가능하다.

SchedulerFactory 클래스에서는 3가지 방식(최대 처리량, 최소 대기 시간, 동적 선택)에 맞춰서 ThroughputScheduler, ResponseTimeScheduler 객체를 생성하지 않고, 해당 스케줄링 전략에 따라 엘리베이터를 선택하는 클래스를 ElevatorManager 클래스의 하위 클래스로 정의할 수 있다.

이와 같이 상속 관계를 사용해 팩토리 메서드 패턴을 설계하면, 팩토리 메서드를 사용해 구체적인 클래스 객체를 생성하는 기능은 일반적으로 하위 클래스에서 오버라이드 하게 된다.

그러므로 팩토리 메서드를 호출하는 상위 클래스의 메서드는 템플릿 메서드가 된다.

팩토리 메서드 패턴을 엘리베이터 예제에 적용하면 다음과 같다.

 


 


수고하셨습니다!


'Develop Study > Design Pattern' 카테고리의 다른 글

14. 컴퍼지트 패턴  (0) 2022.08.12
13. 추상 팩토리 패턴  (0) 2022.07.03
11. 템플릿 메서드 패턴  (0) 2022.07.03
10. 데커레이터 패턴  (0) 2022.07.03
9. 옵서버 패턴  (0) 2022.07.03
0 Comments