본문 바로가기

소프트웨어/디자인패턴

[소프트웨어/디자인패턴] 팩토리 메서드 패턴(Factory Method Pattern)

팩토리 메서드 패턴은 인스턴스 생성 정책을 사용 클래스로부터 분리한다.

Context

엘리베이터 내부에는 어디로 갈 것인지에 대한 컨트롤러가 있고, 빌딩의 각 층에는 엘리베이터를 방향에 따른 엘리베이터를 호출하는 버튼이 있다. 이때, 어느 엘리베이터를 해당 층에 보낼 것 정하는 것을 스케줄링이라한다.

보통의 경우에는 엘리베이터가 사람을 많이 이동시키려고 처리량에 따른 스케줄링 정책을 가지고 있기 마련이다.


ElevatorManager은 많은 엘리베이터를 거느리고 있으며, 스케줄링에 대해 처리량에 따른 정책을 가지고 있는 TroughputSceduler룰 속성으로 가지고 있다. 각 층에서 요청이 오면 ElevatorManager::requestElevator를 통해 ThroughputScheduler에 요청을 보내고 엘리베이터 아이디를 반환받아 해당 엘리베이터를 요청한 층에 이동시키는 형식이다.

enum Direction {UP, DOWN}

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

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

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

        scheduler = new ThroughputScheduler();
    }


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

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

class ElevatorController {
    private int id;
    private int currentFloor;

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

    public void gotoFloor(int destination) {
        System.out.print("Elevator [" + id + "] Floor: " + currentFloor);

        currentFloor = destination;

        System.out.println(" ==> " + currentFloor);
    }
}

class ThroughputScheduler {
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0; // 임의 선택
    }
}

Problem

빌딩에 사람들이 입주된 뒤에 엘리베이터를 기다리는데 너무 시간이 많이 걸려요라는 컴플레인이 자주 들어왔다. 그래서 우리는 처리량보단 응답시간(ResponseTime)을 우선하는 ResopnseTimeScheduler를 새로 만들어 교체하려고 한다.

하지만 기존의 Elevator는 ThroughputScheduler와 직접 의존하고있서 교체가 쉽지 않다. 따라서 우리는 데커레이터 패턴을 이용해서 구현클래스 간의 강결합을 해소해줄 것이다. Throughput, ResponseTime 스케줄러 간의 공통적인 부분인 requestElevator를 일반화 하여 Elevator인터페이스를 생성하고 상속받아 구현하는 방향으로 가면 될 것 같다.

interface Scheduler {
    public int selectElevator(ElevatorManager manager, int destination, Direction direction);
}

class ThroughputScheduler implements Scheduler {
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0; // 임의 선택
    }
}

class RespenseTimeScheduler implements Scheduler {
    @Override
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return 0; /// 임의 선택
    }
}

이후 ElevatorManagersetScheduler 등의 메서드를 통해 외부에서 스케줄링을 교체할 수 있도록 교체하면 된다.

고객들은 까다롭다. 응답시간을 우선하는 스케줄러로 교체했더니 피크타임에 점심시간이나 출퇴근시간에 엘리베이터가 별로 효율적이지 못한 것 같아요 라던가, 한 엘리베이터에서 1층에서 내리는 사람이 너무 적어요 등의 처리량 관련 문의가 들어오기 시작했다. 이는 기존의 처리량 스케줄링 ThroughputScheduler 가 담당하고 있던 내용이다. 즉 동적으로 스케줄링이 되어야한다는 내용이다.

사실은 프록시패턴을 이용하면 비교적 쉽게 해결이 가능하다. DynamicScheduler를 만들고 그 안의 알고리즘으로 적절한 시간에 ThroughputSchedulerResponseTimeScheduler를 의 selectElevator메서드로 전달하여 리턴해주면된다. ElevatorManager의 생성시점이나 런타임 중에 setter을 통해 동적으로 그떄 그때 전략을 변경해주면 된다.

class DynamicProxyScheduler implements Scheduler {
    ...

    @Override
    public int selectElevator(ElevatorManager manager, int destination, Direction direction) {
        return (isNowPeekTime() ?
                    getThroughputScheduler() :
                    getResponseTimeScheduler())
                    .selectElevator(manager, destination, direction);
    }

    ...
}

하지만 이 포스트는 팩토리 패턴을 설명하는 예제이니 해당 내용을 활용하여 해결한 내용은 아래와 같다.

Solution

팩토리 메서드 패턴은 해당 클래스가 어떤 형식의 객체를 사용할 지 모르거나 변할 수 있을 대 사용하는 패턴이다. 위 예제에서는 스케줄링 객체에 해당한다. 또한 팩토리 메서드 패턴은 인스턴스의 생성과정을 캡슐화하거나 상속을 통해 정의한다. 그리고 해당 팩토리 메서드를 사용함으로써 많은 코드에 분산되어 있던 중복코드를 하나의 클래스로 통합시킬 수 있다. 사용할 인스턴스의 타입 결정이나 싱글톤인지 프로토 타입인지, 동적으로 할당할 것인지에 대한 정책에 대해서 팩토리 메서드가 완전히 책임을 가지고 있기 때문에 사용클래스는 많은 변경에 대해서 안전하다.

팩토리 메서드 패턴은 아래와 같이 2가지 패턴으로 나뉜다.

  1. 정적 팩토리 메서드 통한 구현
  2. 다형성(상속)을 통한 구현

다만 상속을 통한 구현일 때 반환타입이 인터페이스가 아니면 단순히 템플릿 메서드 패턴의 생성 패턴이지 팩터리 메서드 패턴이 아니다. (비슷하게는 쓸 수 있지만 그런경우 OCP를 위반한다.)

정적 팩토리 메서드

아마 개발자라면 어디서 한번 쯤 생각해놓고 알게모르게 많이 쓴 패턴이다. static키워드를 이용해서 인터페이스의 구현 클래스 타입을 생성하여 반환하는 형식이다.

각 스케줄러는 스케줄링을 하는 것에 2개 이상의 객체를 생성할 필요가 없음으로 인스턴스를 싱글톤 패턴을 적용하여 생성한다.

enum SchedulingStrategyID { RESPONSE_TIME, THROUGHPUT, DYNAMIC }

class SchedulerFactory {
    public static Scheduler getScheduler(SchedulingStrategyID id) {
        switch (id) {
            case THROUGHPUT:
                return ThroughputScheduler.getInstance();
            case RESPONSE_TIME:
                return RespenseTimeScheduler.getInstance();
            case DYNAMIC: default:
                if(Calendar.getInstance().get(Calendar.HOUR_OF_DAY) < 12) {
                    return ThroughputScheduler.getInstance();
                } else {
                    return RespenseTimeScheduler.getInstance();
                }
        }
    }
}

위의 예제에서는 미리 정의된 정책 아이디를 정적 책터리 메서드에 넘겨주면 해당 형태에 맞춰서 적절한 객체를 반환한다. 설령 해당 정책 아이디에 대해서 수정 및 추가가 있다고 해서 사용 클래스에서는 아무런 변화가 없다는 것을 알 수 있다.

class ElevatorManager {
    private List<ElevatorController> controllers;
    private SchedulingStrategyID schedulingStrategyID;
    public ElevatorManager(int controllerCount, SchedulingStrategyID schedulingStrategyID) {
        controllers = new ArrayList<>();

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

        this.schedulingStrategyID = schedulingStrategyID;
    }

    public void setSchedulingStrategyID(SchedulingStrategyID schedulingStrategyID) {
        this.schedulingStrategyID = schedulingStrategyID;
    }

    void requestElevator(int destination, Direction direcion) {
        Scheduler scheduler = SchedulerFactory.getScheduler(schedulingStrategyID);

        int selectedElevator = scheduler.selectElevator(this, destination, direcion);

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

다형성을 통한 구현

슈퍼클래스에서 인스턴스를 만드는 메서드를 추상메서드(hook method)로 설정하고 해당 메서드를 실제 템플릿 메서드에서 사용하는 형식이다, 다만 반환 형식이 만드시 인터페이스나 추상클래스(OCP를 만족해야한다는 의미이다.)일 때 팩토리 메서드 패턴이라고 할 수 있다. 본 예제에서는 Scheduler가 그에 해당한다.

코드는 아래와 같다.

abstract class ElevatorManager {
    private List<ElevatorController> controllers;
    public ElevatorManager(int controllerCount) {
        controllers = new ArrayList<>();

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

    void requestElevator(int destination, Direction direcion) {
        Scheduler scheduler = getScheduler();

        int selectedElevator = scheduler.selectElevator(this, destination, direcion);

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

    protected abstract Scheduler getScheduler();
}

class ElevatorManagerWIthThroughput extends ElevatorManager {
    public ElevatorManagerWIthThroughput(int controllerCount) {
        super(controllerCount);
    }

    @Override
    protected Scheduler getScheduler() {
        return ThroughputScheduler.getInstance();
    }
}

class ElevatorManagerWithResponseTime extends ElevatorManager {
    public ElevatorManagerWithResponseTime(int controllerCount) {
        super(controllerCount);
    }

    @Override
    protected Scheduler getScheduler() {
        return RespenseTimeScheduler.getInstance();
    }
}

class ElevatorManagerWithDinamic extends ElevatorManager {
    public ElevatorManagerWithDinamic(int controllerCount) {
        super(controllerCount);
    }

    @Override
    protected Scheduler getScheduler() {
        if(Calendar.getInstance().get(Calendar.HOUR_OF_DAY) < 12) {
            return ThroughputScheduler.getInstance();
        } else {
            return RespenseTimeScheduler.getInstance();
        }
    }
}

Summary

팩터리 메서드 패턴은 객체의 생성하는 코드를 별도의 클래스/메서드로 분리함으로써 객체의 생성 방식와 변화에 대응하는 데 유용하다.

메서드 타입

상속 타입