728x90

커맨드 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

커맨드 패턴을 이용하면 요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업 취소 기능도 지원 가능하다.

  • 호출 캡슐화
  • 한 차원 높은 단계의 캡슐화인 메소드 호출을 캡슐화하는 것을 배워보자
  • 메소드 호출을 캡슐화 하면 계산 과정의 각 부분들을 결정화시킬 수 있끼 때문에, 계산하는 코드를 호출한 객체에서는 어떤 식으로 일을 처리해야 하는지에 대해 전혀 신경쓰지 않아도 된다.
  • 그 외에도 캡슐화된 메소드 호출을 로그 기록용으로 저장을 한다거나 취소 기능을 구현하기 위해 재사용하는 것과 같은 작업을 할 수 도 있다.
  • 요청하는 객체와 요청을 수행하는 객체를 분리하고 싶다면 커맨드 패턴을 사용하자

개요 - 홈 오토메이션 리모콘

리모컨 API 디자인을 해보자. 해당 리모컨에는 일곱 가지 프로그래밍이 가능한 슬롯과 각 슬롯에 대한 온오프 스위치가 있다. 각 슬롯은 서로 다른 가정용 기기에 연결할 수 있다. 리모컨에는 작업 취소 버튼도 장착되어 있다.

조명, 팬, 욕조, 오디오를 비롯한 각종 홈 오토메이션 장비들을 제어하기 위한 용도로 다양한 업체에서 공급 받은 자바 클래스들을 같이 받았다.

각 슬롯을 한 가지 기기 또는 하나로 엮여 있는 일련의 기기들에 할당할 수 있도록 리모컨을 프로그래밍하기 위한 API를 제작해보자.


제공 받은 클래스

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

제공 받은 클래스들을 살펴보자 리모컨에서 제어해야 하는 객체의 인터페이스에 대한 정보를 얻을 수 있을 것이다.

공통적인 인터페이스가 있는 것 같진 않다. 리모컨에는 on, off 버튼만 있지만, 가전제품 클래스에는 여러 메서드가 존재하고 더 큰 문제는 앞으로 이런 클래스들이 더 추가될 수 있다는 점이다.

따라서 리모컨 버튼을 누르면 자동으로 해야할 일을 처리할 수 있도록 하고 리모컨에서 제품 업체에게 전달받은 클래스에 대해 자세히 알 필요가 없도록 디자인을 진행해야 할 것 같다.

해결책 : 커맨드 패턴

이를 해결하기 위해 어떻게 해야할까??

  • 작업을 요청한 쪽과 작업을 처리하는 쪽을 분리시킨다.
  • 여기서는 리모컨이 작업을 요청하는 쪽, 업체에서 공급한 클래스의 인스턴스는 작업을 처리하는 쪽
  • 이를 위해 커맨드 객체를 추가하여 분리시킬수 있다.
    • 커맨드 객체는 특정 객체에 대한 특정 작업 요청을 캡슐화한다.
    • 따라서 버튼마다 커맨드 객체를 저장해 두면 사용자가 버튼을 눌렀을 때 커맨드 객체를 통해서 작업을 처리하도록 만든다.
    • 리모컨은 작업이 무엇인지 전혀 모르고 작업을 완료하기 위한 객체와 상호작용하는 방법을 알고 있는 커맨드 객체만 존재하므로, 리모컨은 벤더쪽 클래스와 분리된다.
  • 이러한 패턴을 커맨드 패턴 이라고 한다.

커맨드 패턴 다이어그램

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

클라이언트

  • 클라이언트는 커맨드 객체를 생성해야 한다.
  • 커맨드 객체는 리시버에 전달할 일련의 행동으로 구성된다.

리시버

  • 커맨드 객체에는 행동과 리시버에 대한 정보가 같이 들어 있다.

커맨드

  • 커맨드 객체에서 제공하는 메소드는 excute() 하나 뿐이다.
  • 이 메소드에는 행동을 캡슐화하며, 리시버에 있는 특정 행동을 처리하기 위한 메소드를 호출하기 위한 메서드이다.

인보커

  • 클라이언트에서는 인보커 객체의 setCommand() 메소드를 호출하는데, 이 때 커맨드 객체를 넘겨줍니다.
  • 그 커맨드 객체는 나중에 쓰으기 전까지 인보커 객체에 보관됩니다.
  • 인보커에서 커맨드 객체의 excute() 메소드를 호출하면 리시버에 있는 특정 행동을 하는 메소드가 호출된다.

인보커 로딩

  1. 클라이언트에서 커맨드 객체 생성
  2. setCommand()를 호출하여 인보커에 커맨드 객체를 저장
  3. 나중에 클라이언트에서 인보커한테 그 명령을 실행시켜 달라는 요청을 함

커맨드 패턴과 식당 비유해보기

  • 클라이언트 == 손님
  • 주문서 == 커맨드 객체
  • 주문받기(takeOrder()) == setCommand()
  • 웨이트리스 == 인보커 객체
  • 주문(orderUp()) == execute()
  • 주방장 == 리시버 객체

커맨드 객체

이제 첫 커맨드 객체를 만들어 보자.

Command 인터페이스 만들기

  • 커맨드 객체는 모두 같은 인터페이스를 구현해야 한다.
  • 해당 인터페이스에는 메소드가 하나 밖에 없다.
  • 일반적으로 execute() 이름을 많이 사용한다.
public interface Command {

    void execute();

}

이제 전등을 켜기 위한 커맨드 클래스를 구현해보자. 벤더사에서 제공한 클래스를 보니 Light 클래스에는 on(), off() 두 개의 메소드가 있다.

public class LightOnCommand implements Command {

    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }
}
  • 생성자에 이 커맨드 객체로 제어할 특정 전등에 대한 정보가 전달된다.
  • 해당 객체는 light라는 인스턴스 변수에 저장이 되며, execute() 메소드가 호출되면 light 객체가 바로 그 요청에 대한 리시버가 된다.
  • execute() 메소드에서는 리시버 객체에 있는 on() 메서드를 호출한다.

커맨드 객체 사용하기

이제 커맨드 객체를 써서 가정요 기기를 조작하기 위해 버튼이 하나 밖에 없는 리모콘이 있다고 가정하고 코드를 작성해보자.

public class SimpleRemoteControl { // 인보커

    Command slot;

    public SimpleRemoteControl() {
    }

    public void setCommand(Command command) {
        slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}
class SimpleRemoteControlTest { // 1

    public static void main(String[] args) {
        SimpleRemoteControl remoteControl = new SimpleRemoteControl(); // 2
        Light light = new Light(); // 3
        LightOnCommand lightOn = new LightOnCommand(light); // 4

        remoteControl.setCommand(lightOn); // 5
        remoteControl.buttonWasPressed();
    }

}
  • 1 : 클라이언트에 해당하는 부분(SimpleRemoteControlTest)
  • 2 : remoteControl 변수가 인보커 역할을 한다.
    • 필요한 작업을 요청할 때 사용할 커맨드 객체를 인자로 전달받는다.
  • 3 : 요청을 받아서 처리할 리시버인 Light 객체를 생성한다.
  • 4 : 커맨드 객체를 생성한다.
    • 이 때 리시버를 전달해 준다.
  • 5 : 커맨드 객체를 인보커한테 전달해 준다.


커맨드 패턴의 정의

이제 커맨드 패턴의 정의를 알아보고 더 자세히 살펴보자.

  • 커맨드 객체는 일련의 행동을 특정 리시버하고 연결시킴으로써 요구 사항을 캡슐화한 것이다.
    • 이렇게 하기 위해서 행동과 리시버를 한 객체에 집어넣고 execute()라는 메소드 하나만 외부에 공개하는 방법을 쓴다.
    • 이 메소드 호출에 의해서 리시버에서 일련의 작업이 처리된다.
    • 외부에서 볼 때는 어떤 객체가 리시버 역할을 하는지, 그 리시버에서 실제로 어떤 일을 하는지 알 수 없다. 그냥 execute() 메소드를 호출하면 요구 사항이 처리된다는 것만 알 수 있을 뿐이다.
    • 즉, 커맨드는 캡슐화된 요구사항이다.
      • 리시버 : action
      • execute() : recevier.action();
  • 명령을 통해서 객체를 매개변수화하는 예도 몇 가지 볼 수 있었다.
    • 리모컨 입장에서는 특정 인터페이수만 구현이 되어 있다면 그 커맨드 객체에서 실제로 어떤일을 하는지 신경 쓸 필요가 없다.
    • 인보커(리모컨에 있는 슬롯 등)에 매개변수를 써서 여러가지 요구사항을 전달할 수도 있다.
  • 커맨드 객체들을 써서 큐나 로그를 구현하거나 작업 취소를 할 수 있으며, 메타 커맨드 패턴이라는 것을 이용하여 명령들로 이루어진 매크로를 만들어서 여러 개의 명령을 한 번에 실행할 수도 있다.

커맨드 패턴 클래스 다이어그램

https://faun.pub/head-first-design-patterns-using-go-5-encapsulating-invocation-the-command-pattern-2f8c0a79d1c7

클라이언트

  • ConcreteCommand를 생성하고 Receiver를 설정한다.

인보커

  • 인보커에는 명령이 들어 있으며, execute() 메소드를 호출함으로써 커맨드 객체에게 특정 작업을 수행해 달라는 요구를 하게 된다.

리시버

  • 리시버는 요구 사항을 수항해기 위해 어떤 일을 처리해야 하는지 알고 있는 객체이다.

커맨드 인터페이스

  • 모든 커맨드 객체에서 구현해야 하는 인터페이스
  • 모든 명령은 execute() 메소드 호출을 통해 수행되며, 이 메소드에서는 리시버에 특정 작업을 처리하라는 지시를 전달한다.
  • 이 인터페이스를 보면 undo() 메소드도 들어있는데, 잠시 후에 알아보자

구상 커맨드

  • 구상 커맨드는 특정 행동과 리시버 사이를 연결해 준다.
  • 인보커에서 execute() 호출을 통해 요청을 하면 구상 커맨드 객체에서 리시버에 있는 메소드를 호출함으로써 그 작업을 처리한다.
  • execute() → receiver.action();

슬롯에 명령 할당하기

이제 리모컨의 각 슬롯에 명령을 할당해보자. 이제 리모컨이 인보커가 되는 것이다.

사용자가 버튼을 누르면 그 버튼에 상응하는 커맨드 객체의 execute() 메소드가 호출되고, 그러면 리시버(vendor class)에서 특정 행동을 하는 메소드가 실행될 것이다.

  • 각 슬롯마다 커맨드 객체가 할당된다.
  • 사용자가 버튼을 누르면 해당 커맨드 객체의 execute() 메소드가 호출된다.
  • execute() 메소드에서는 리시버로 하여금 특정 작업을 처리하도록 지시한다.
public class RemoteControl {

    private static final int SLOT_SIZE = 7;
    Command[] onCommands;
    Command[] offCommands;

    public RemoteControl() {
        offCommands = new Command[SLOT_SIZE];
        onCommands = new Command[SLOT_SIZE];

        Command noCommand = new NoCommand();
        for (int i = 0; i < SLOT_SIZE; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;

        }
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
    }

    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n------ Remote Control -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuilder
                .append("[slot ")
                .append(i)
                .append("] ")
                .append(onCommands[i].getClass().getName())
                .append("    ")
                .append(offCommands[i].getClass().getName())
                .append("\n");
        }
        return stringBuilder.toString();
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");
        Light kitchenLight = new Light("Kitchen");
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        GarageDoor garageDoor = new GarageDoor("Garage");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
        LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);

        CeilingFanOnCommand ceilingFanOn = new CeilingFanOnCommand(ceilingFan);
        CeilingFanOffCommand ceilingFanOff = new CeilingFanOffCommand(ceilingFan);

        GarageDoorUpCommand garageDoorUp = new GarageDoorUpCommand(garageDoor);
        GarageDoorDownCommand garageDoorDown = new GarageDoorDownCommand(garageDoor);

        StereoOnWithCDCommand stereoOnWithCD = new StereoOnWithCDCommand(stereo);
        StereoOffCommand stereoOff = new StereoOffCommand(stereo);

        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
        remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

        System.out.println(remoteControl);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(1);
        remoteControl.offButtonWasPushed(1);
        remoteControl.onButtonWasPushed(2);
        remoteControl.offButtonWasPushed(2);
        remoteControl.onButtonWasPushed(3);
        remoteControl.offButtonWasPushed(3);
    }
}

NoCommand 객체

NoCommand 객체는 일종의 널 객체이다. 딱히 리턴할 객체는 없지만 클라이언트 쪽에서 null을 처리하지 않아도 되도록 하고 싶을 때 널 객체를 활용하면 좋다. 특정 슬롯을 쓰려고 할 때 마다 거기에 뭔가가 로딩되어 있는지 확인하려면 좀 귀찮기 때문이다.


완성된 리모컨 api 디자인

https://blog.yevgnenll.me/posts/what-is-command-pattern

  • 리모컨 코드를 최대한 단순하게 만들어서 협력 업체가 새로운 벤더 클래스를 공급하더라도 리모컨 코드를 고치지 않도록 하는 것에 중점을 두었다.
  • 커맨트 패턴을 도입해서 RemoteControl 클래스와 협력 업체로부터 제공되는 클래스를 논리적으로 분리했다.
  • 이러한 디자인은 리모컨의 유지보수 비용을 줄이는데 굉장히 도움이 된다.
  • 커맨드 클래스는 람다 표현식을 사용하여 커맨드 객체를 생성하는 단계를 건너뛰고 인스턴스를 생성하는 대신 그 자리에 함수 객체를 사용할 수 있다.
    • 물론 추상 메서드가 하나일 때만 가능
//        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);
        remoteControl.setCommand(0, () -> livingRoomLight.on(), () -> livingRoomLight.off());
        remoteControl.setCommand(1, kitchenLightOn, kitchenLightOff);
        remoteControl.setCommand(2, ceilingFanOn, ceilingFanOff);
        remoteControl.setCommand(3, stereoOnWithCD, stereoOff);

작업 취소 기능 추가하기

커맨드에서 작업 취소 기능을 지원하려면 execute() 메소드와 비슷한 undo() 메소드가 있어야 한다.

excute() 메소드에서 했던 작업과 정반대의 작업을 처리하면 된다. 커맨드 클래스에 작업 취소 기능을 추가하기 전에 우선 Command 인터페이스에 undo() 메소드를 추가해야 한다.

public interface Command {
        void excete();
    void undo();
}
  • 그리고 RemoteControl 클래스에 사용자가 마지막으로 누른 버튼을 기록하고, UNDO 버튼을 눌렀을 때 필요한 작업을 처리하는 코드를 추가해야 한다.
package command.client;

import command.cmd.Command;
import command.cmd.NoCommand;

public class RemoteControl {

    private static final int SLOT_SIZE = 7;
    Command[] onCommands;
    Command[] offCommands;
    Command undoCommand;

    public RemoteControl() {
        offCommands = new Command[SLOT_SIZE];
        onCommands = new Command[SLOT_SIZE];

        Command noCommand = new NoCommand();
        for (int i = 0; i < SLOT_SIZE; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;

        }
        // 다른 슬롯과 마찬가지로 사용자가 다른 버튼을 한 번도 누르지 않은 상태에서 undo 버튼을 누르더라도 별 문제가 없도록 한다.
        undoCommand = noCommand;
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void onButtonWasPushed(int slot) {
        onCommands[slot].execute();
        // 사용자가 버튼을 누르면 해당 커맨드 객체의 execute() 메서드를 호출한 다음
        // 그 객체의 레퍼런스를 undoCommand 인스턴스 변수에 저장한다.
        // on과 off 버튼을 처리할 때도 같은 방법 사용
        undoCommand = onCommands[slot];
    }

    public void offButtonWasPushed(int slot) {
        offCommands[slot].execute();
        undoCommand = offCommands[slot];
    }

    public void undoButtonWasPushed() {
        undoCommand.undo();
    }

    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("\n------ Remote Control -------\n");
        for (int i = 0; i < onCommands.length; i++) {
            stringBuilder
                .append("[slot ")
                .append(i)
                .append("] ")
                .append(onCommands[i].getClass().getSimpleName())
                .append("    ")
                .append(offCommands[i].getClass().getSimpleName())
                .append("\n");
        }
        return stringBuilder
            .append("[undo]")
            .append("    ")
            .append(undoCommand.getClass().getSimpleName())
            .toString();
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();

        Light livingRoomLight = new Light("Living Room");

        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);

        remoteControl.setCommand(0, livingRoomLightOn, livingRoomLightOff);

        remoteControl.onButtonWasPushed(0);
        remoteControl.offButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();
        remoteControl.offButtonWasPushed(0);
        remoteControl.onButtonWasPushed(0);
        System.out.println(remoteControl);
        remoteControl.undoButtonWasPushed();

    }
}
Living Room light is on
Living Room light is off

------ Remote Control -------
[slot 0] LightOnCommand    LightOffCommand
[slot 1] LightOnCommand    LightOffCommand
[slot 2] CeilingFanOnCommand    CeilingFanOffCommand
[slot 3] StereoOnWithCDCommand    StereoOffCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    LightOffCommand >>>>>> undoCmd에 마지막으로 호출되었던 커맨드 저장
Living Room light is on >>>>>> 사용자가 undo 버튼 클릭
Living Room light is off
Living Room light is on

------ Remote Control -------
[slot 0] LightOnCommand    LightOffCommand
[slot 1] LightOnCommand    LightOffCommand
[slot 2] CeilingFanOnCommand    CeilingFanOffCommand
[slot 3] StereoOnWithCDCommand    StereoOffCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    LightOnCommand >>>>>> undoCmd에 마지막으로 호출되었던 커맨드 저장
Living Room light is off >>>>>> 사용자가 undo 버튼 클릭

Process finished with exit code 0

작업 취소 기능을 구현할 때 상태를 사용하는 방법

작업 취소 기능을 구현하다 보면 간단한 상태를 저장해야 하는 상황도 종종 생긴다.

CeilingFan 클래스로 간단한 속도와 관련된 상태를 저장해보자.

public class CeilingFan {

    String location;
    int speed; // 속도를 나타내는 상태를 저장
    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;

    public CeilingFan(String location) {
        this.location = location;
        speed = OFF;
    }

    public void high() {
        // turns the ceiling fan on to high
        speed = HIGH;
        System.out.println(location + " ceiling fan is on high");

    }

    public void medium() {
        // turns the ceiling fan on to medium
        speed = MEDIUM;
        System.out.println(location + " ceiling fan is on medium");
    }

    public void low() {
        // turns the ceiling fan on to low
        speed = LOW;
        System.out.println(location + " ceiling fan is on low");
    }

    public void off() {
        // turns the ceiling fan off
        speed = OFF;
        System.out.println(location + " ceiling fan is off");
    }

    public int getSpeed() {
        return speed;
    }
}
package command.cmd;

import command.vendor.CeilingFan;

public class CeilingFanHighCommand implements Command {

    CeilingFan ceilingFan;
    int prevSpeed; // 상태 지역 변수로 선풍기의 속도를 저장

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }

    public void execute() {
              // 속도를 변경하기 전에 작업을 취소해야 할 때를 대비해서 이전 속도를 저장
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }

    @Override
    public void undo() {
        if (prevSpeed == CeilingFan.HIGH) {
            ceilingFan.high();
        } else if (prevSpeed == CeilingFan.MEDIUM) {
            ceilingFan.medium();
        } else if (prevSpeed == CeilingFan.LOW) {
            ceilingFan.low();
        } else if (prevSpeed == CeilingFan.OFF) {
            ceilingFan.off();
        }
    }
}

여러 동작을 한 번에 처리하기

package command.cmd;

public class MacroCommand implements Command {

    Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

    @Override
    public void execute() {
        for (int i = 0; i < commands.length; i++) {
            commands[i].execute();
        }
    }

    @Override
    public void undo() { // 역순으로 undo
        for (int i = commands.length - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }
}
public class RemoteLoader {

    public static void main(String[] args) {
        RemoteControl remoteControl = new RemoteControl();
        Light light = new Light("Living Room");
        Stereo stereo = new Stereo("Living Room");

        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        StereoOnCommand stereoOnCommand = new StereoOnCommand(stereo);
        StereoOffCommand stereoOffCommand = new StereoOffCommand(stereo);
        Command[] partyOn = {lightOnCommand, stereoOnCommand};
        Command[] partyOff = {lightOffCommand, stereoOffCommand};
        MacroCommand partyOnMacro = new MacroCommand(partyOn);
        MacroCommand partyOffMacro = new MacroCommand(partyOff);

        remoteControl.setCommand(0, partyOnMacro, partyOffMacro);
        System.out.println(remoteControl);
        System.out.println("---- macro on ------");
        remoteControl.onButtonWasPushed(0);
        System.out.println("---- macro off ------");
        remoteControl.offButtonWasPushed(0);
    }
}
------ Remote Control -------
[slot 0] MacroCommand    MacroCommand
[slot 1] NoCommand    NoCommand
[slot 2] NoCommand    NoCommand
[slot 3] NoCommand    NoCommand
[slot 4] NoCommand    NoCommand
[slot 5] NoCommand    NoCommand
[slot 6] NoCommand    NoCommand
[undo]    NoCommand
---- macro on ------
Living Room light is on
Living Room stereo is on
Living Room stereo is set for CD input
Living Room stereo volume set to 11
---- macro off ------
Living Room light is off
Living Room stereo is off

Process finished with exit code 0

Q) 항상 리시버가 필요할까?? 커맨드 객체에서 execute()를 구현하면 안될까??

A) 일반적으로 리시버에 있는 행동을 호출하는 ‘더미’ 커맨드 객체를 만든다. 하지만 요구 사항의 전부는 아니더라도 대부분을 구현하는 ‘스마트’ 커맨드 객체를 만드는 경우도 자주 볼 수 있다. 물론 커맨드 객체에서 대부분의 행동을 처리해도 됩니다. 하지만 그러면 인보커와 리시버를 분리하기 어렵고, 리시버로 커맨드를 매개변수화할 수 없다는 점을 염두하자.

Q) 작업 취소를 할 때 히스토리 기능은 어떻게 구현할 수 있을까? 즉, undo 버튼을 여러 번 누를 수 있도록 하려면 어떻게 해야 할까??

A) 사실 그리 어려운 일은 아니다. 앞에서는 마지막으로 실행한 커맨드의 레퍼런스만 저장했었는데, 그 대신 전에 실행한 커맨드 자체를 스택에 넣으면 됩니다. 그리고 나서 사용자가 undo 버튼을 누를 때마다 인보커에서 스택 맨 위에 있는 항목을 꺼내서 undo() 메소드를 호출하도록 만들면 된다.

Q) 파티 모드를 구현할 때 PartyCommand의 execute() 메소드에서 다른 커맨드 객체의 excute()를 호출하는 방법을 써도 될까?

A) 그렇게 해도 되지만, 그러면 PartyComman에 파티 모드 코드를 직접 넣어야 하는데, 나중에 문제가 생길 수도 있습니다. MacroCommand를 사용하면 PartyCommand에 넣을 커맨드를 동적으로 결정할 수 있기에 유연성이 훨씬 좋아진다. 일반적으로 MacroCommand 만들어서 쓰는 방법이 더 우아한 방법이며, 추가해야 할 코드를 줄이는데도 도움이 된다.


커맨트 패턴 활용하기

커맨드로 컴퓨테이션의 한 부분(리시버와 일련의 행동)을 패키지로 묶어서 일급 객체 형태로 전달할 수도 있다. 그러면 클라이언트 애플리케이션에서 커맨드 객체를 생성 한 뒤 오랜 시간이 지나도 그 컴퓨테이션을 호출할 수 있다. 심지어 다른 스레드에서 호출할 수도 있다. 이점을 활용해서 커맨드 패턴을 스케줄러나 스레드 풀, 작업 큐와 같은 다양한 작업에 적용할 수 있다.

작업 큐를 떠올려 보자. 큐 한 쪽 끝은 커맨드를 추가할 수 있도록 되어 있고, 다른 쪽 끝에는 커맨드를 처리하는 스레드들이 대기하고 있다. 각 스레드는 우선 execute() 메소드를 호출하고 호출이 완료되면 커맨드 객체를 버리고 새로운 커맨드 객체를 가져옵니다.

작업 큐

  • 커맨드 인터페이스를 구현하는 객체를 큐에 추가한다.

컴퓨테이션(?)

  • 컴퓨테이션을 고정된 개수의 스레드로 제한할 수 있다.

작업 처리 스레드

  • 스레드는 큐에서 커맨드를 하나씩 제겋면서 커맨드의 execute() 메소드를 호출한다.
  • 메소드 실행이 끝나면 다시 큐에서 새로운 커맨드 객체를 가져간다.

작업 큐 클래스는 계산 작업을 하는 객체들과 완전히 분리되어 있다. 한 스레드가 한동안 금융 관련 계산을 하다가 잠시 후에는 네트워크로 뭔가를 내려받을 수도 있다. 작업 큐 객체는 전혀 신경쓸 필요가 없다. 큐에 커맨드 패턴을 구현하는 객체를 넣으면 그 객체를 처리하는 스레드가 생기고 자동으로 execute() 메소드가 호출된다.


커맨드 패턴 더 활용하기

어떤 애플리케이션은 모든 행동을 기록해 두었다가 애플리케이션이 다운되었을 때 그 행동을 다시 호출해서 복구할 수 있어야 한다. 커맨드 패턴을 사용하면 store()load() 메소드를 추가해서 이런 기능을 구현할 수 있다. 자바에서는 이런 메소드를 객체 직렬화로 구현할 수도 있지만, 직렬화와 관련된 제약 조건 때문에 쉽지 않다.

로그 기록은 어떤 명령을 실행하면서 디스크에 실행 히스토리를 기록하고, 애플리케이션이 다운되면 커맨드 객체를 다시 로딩해서 execute() 메소드를 자동으로 순서대로 실행하는 방식으로 작동한다.

지금까지 예로 든 리모컨에는 이런 로그 기록이 무의미하다. 하지만 데이터가 변경될 때마다 매번 저장할 수 없는 방대한 자료구조를 다루는 애플리케이션에 로그를 사용해서 마지막 체크 포인트 이후로 진행한 모든 작업을 저장한 다음 시스템이 다운되었을 때 최근 수행된 작업을 다시 적용하는 방법으로 사용할 수 있다.

스프레드시트 애플리케이션을 예를 들어 볼까요? 매번 데이터가 변경될 때마다 디스크에 저장하지 않고, 특정 체크 포인트 이후의 모든 행동을 로그에 기록하는 방식으로 복구 시스템을 구축할 수 있다. 더 복잡한 애플리케이션에는 이런 테크닉을 확장해서 일련의 작업에 트랜잭션을 활용해서 모든 작업이 완변하게 처리되도록 하거나, 아무것도 처리되지 않게 롤백되도록 할 수 있다.


실전 커맨드 패턴

자바의 스윙 라이브러리에는 사용자 인터페이스 구성 요소에서 발생하는 이벤트에 귀를 기울이는 ActionListener 형태의 옵저버가 어마어마하게 많다는 걸 배웠습니다. 그런데 ActionListener 는 Observer 인터페이스이자 Command 인터페이스이기도 하며, AngelListener와 DevilListenr 클래스는 그냥 Observer가 아니라 구상 Command 클래스이다. 즉, 두 패턴이 한꺼번에 들어가 있는 예제이다.

public class SwingObserverEx { // 클라이언트
        JButton button = new JButton("할까 말까"); // 인보커
    button.addActionListener(new AngelListener());
    button.addActionListener(new DevilListener());
}
    class AngelListener implements ActionListenr { // ActionListenr 커맨드 인터페이스
                public void actionPerformed(ActionEvent event) {
                        System.out.println("하지마!") // System 리시버
                }
    }
    class DevlilListener implements ActionListenr { // Angel, DevlilListener  구상 커맨드
                public void actionPerformed(ActionEvent event) {
                        System.out.println("해!")
                }
    }

핵심 정리

  • 커맨드 패턴을 사용하면 요청하는 객체와 요청을 수행하는 객체를 분리할 수 있다.
  • 이렇게 분리하는 과정의 중심에는 커맨드 객체가 있으며, 이 객체가 행동이 들어있는 리시버를 캡슐화한다.
  • 인보커는 무언가 요청할 때 커맨드 객체의 execute() 메소드를 호출하면 된다.
  • 커맨드는 인보커를 매개변수화할 수 있다. 실행 중에 동적으로 매개변수화를 설정할 수도 있다.
  • execute() 메소드가 마지막으로 호출되기 전의 상태로 되돌리는 작업 취소 메소드를 구현하면 커맨드 패턴으로 작업 취소 기능을 구현할 수도 있다.
  • 매크로 커맨드는 커맨드를 확장해서 여러 개의 커맨드를 한 번에 호출할 수 있게 해주는 가장 간편한 방법이다. 매크로 커맨드로도 어렵지 않게 작업 취소 기능을 구현할 수 있다.
  • 프로그래밍을 하다 보면 요청을 스스로 처리하는 ‘스마트’ 커맨드 객체를 사용하는 경우도 종종 있다.
  • 커맨드 패턴을 활용해서 로그 및 트랜잭션 시스템을 구현할 수 있다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
    • 커맨드 패턴 : 요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화할 수 있다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있다.
728x90
728x90

싱글턴 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.

고전적인 싱글턴 패턴 구현법

package singleton.before;

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }

}

초콜릿 공장

초콜릿 공장에서는 초콜릿을 끓이는 장치인 초콜릿 보일러를 컴퓨터로 제어한다.

이 보일러에서는 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨준다. 여기에 초콜릿 보일러를 제어하기 위한 클래스가 나와 있다.

public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    public ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}
  • 코드를 보면 실수를 하지 않도록 주의를 기울여져 있다.
  • 하지만 두 개의 ChocolateBoiler 인스턴스가 따로 돌아가게 되면 상당히 안 좋은 상황이 일어날 수 있다는 것을 알 수 있다.
  • 만약 애플리케이션에서 ChocolateBoiler 인스턴스가 두 개 이상 만들어지게 되면 어떤 문제가 생길까??
    • 자원을 불필요하게 잡아먹고, 애플리케이션의 동작이 이상하게 돌아가는 결과에 일관성이 없어지는 심각한 문제가 발생할 것이다.
    • ChocolateBoiler 클래스를 싱글턴으로 업그레드해보자
public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;

    private static ChocolateBoiler uniqueInstance;

    private ChocolateBoiler() {
        this.empty = true;
        this.boiled = false;
    }

    public static ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }

    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
        }
    }

    public void drain() {
        if (!isEmpty() && !isBoiled()) {
            empty = true;
        }
    }

    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    public boolean isEmpty() {
        return this.empty;
    }

    public boolean isBoiled() {
        return this.boiled;
    }

}

싱글턴 패턴의 정의

싱글턴의 고전적인 구현법을 배웠다. 그렇다면 싱글턴 패턴의 정의는 무엇이고, 실제로 어떤 식으로 싱글턴 패턴을 적용해야할까??

  • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 된다.
    • 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 한다.
    • 따라서 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야 될 것이다.
  • 어디서든 그 인스턴스를 접근할 수 있도록 만들어야 한다.
    • 다른 객체에서 이 인스턴스가 필요하면 언제든지 클래스한테 요청을 할 수 있게 만들고, 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 한다.
    • 앞에서 보았듯이, 싱글턴이 Lazy하게 생성되도록 구현할 수도 있다. 싱글턴 클래스의 객체가 자원을 많이 잡아먹는 경우에는 유용하다.

클래스 다이어그램

https://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

골칫덩어리 스레드

고전적인 싱글턴을 이용해서 코드를 고쳤음에도 ChocolateBoiler에 있는 fill() 메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣고 말았다. 무슨 일이 일어난 것일까??

  • 조금 전에 다중 스레드를 사용하도록 ChocolateBoiler 컨트롤러를 최적화시킨 것이 문제일까??
  • 스레드가 추가된 것 때문에 이런 문제가 생긴 것일까??

JVM의 입장

두 개의 스레드에서 여기에 있는 코드를 실행시킨다고 가정해보고 두 스레드가 다른 보일러 객체를 사용하게 될 가능성이 있는지 따져보자.

ChocolateBoiler boiler = ChocolateBoiler.getInstacne();
boiler.fill();
boiler.boil();
boiler.drain();
public static ChocolateBoiler getInstance() {
      if (uniqueInstance == null) {
          uniqueInstance = new ChocolateBoiler();
      }
      return uniqueInstance;
}
  • 바로 두 스레드가 동시에 getInstance() 메소드를 수행하게 되면 uniqueInstance null 상태라 각 스레드마다 ChocolateBoiler 인스턴스를 생성하여 리턴하여 결국 서로 다른 두 인스턴스가 만들어진다.

멀티스레딩 문제 해결 방법

문제를 해결하는 방법은 간단한데 바로 getInstance()를 동기화시키기만 하면 된다.

public static synchronized ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
}
  • synchronized 키워드를 추가하면 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 한다.
  • 즉, 두 스레드가 이 메소드를 동시에 실행시키는 일은 일어나지 않는다.
  • 하지만 이렇게 하면 동기화로 인한 속도 문제가 생긴다.
    • 사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐이다.
    • 즉, 일단 uniqueInstance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없다.
    • 불필요한 오버헤드만 증가시킬뿐인 것

더 효율적인 방법은 없을까?

대부분의 자바 애플리케이션에서 싱글턴이 다중 스레드 환경에서 돌아갈 수도 있도록 만들어야 한다. 하지만 getInstance() 메소드를 동기화시키려면 대가를 치뤄야 한다. 다른 방법은 없을까??

1. getInstance()의 속도가 중요하지 않다면 그냥 둔다.

만약 getInstance() 메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔둬도 된다. getInstance()를 동기화시키는게 굉장히 쉽고, 효율 면에서도 나쁘지 않을 수있다.

하지만 메소드를 동기화하면 성능이 100배 정도 저하된다는 것을 기억하자. 만약 getInstance()가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해야 한다.

2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.

애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 식으로 처음부터 Singleton 인스턴스를 만들어버리는 것도 괜찮은 방법이다.

public class Singleton {

    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }

}
  • 이런 접근법을 사용하면 클래스가 로딩될 때, JVM에서 Singleton의 유일한 인스턴스를 생성해 주고, JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

3. DCL(Double Checking Locking)을 써서 getInstance()에서 동기화되는 부분일 줄인다.

DCL을 사용하면 , 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있다. 이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않도록 동작하여, 바로 원하던 동작이 수행된다.

public class Singleton {

        // 자바 5 이전 버전은 동기화 x
    private volatile static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        if(uniqueInstance == null) { // 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으러 진입
            synchronized (Singleton.class) {
                if(uniqueInstance == null) { // 블록으로 들어온 후레도 다시 한번 널체크한 후, 인스턴스를 생성한다.
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }

}

싱글턴 관련 Q&A

Q) 모든 메서드와 변수가 static으로 선언된 클래스를 만들어도 결과적으로는 같지 않을까?

A) 맞다. 하지만 필요한 내용이 클래스에 다 들어있고, 복잡한 초기화가 필요없는 경우에만 해당 방법을 사용할 수 있다. 그리고 자바에서 정적 초기화를 처리하는 방법 때문에 복잡해질 수 있다. 특히 여러 클래스가 얽혀 있는 경우에는 지저분하고, 초기화 순서와 관련된 버그는 찾기 어렵기 때문에 해당 방식으로 싱글턴 비슷한 걸 만들어야 한다면 좋지 않을 수 있다.

Q) 클래스 로더와 관련된 문제는 없을까?

A) 클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문에 클래스 로더가 두 개 이상이라면 같은 클래스를 여러 번 로딩할 수도 있다. 만약 싱글턴을 그런 식으로 로딩하면 인스턴스가 여러 개 만들어지는 문제가 발생할 수 있다. 따라서 클래스 로더를 여러 개 사용하면서 싱글턴을 사용한다면 조심해야 하고, 클래스 로더를 직접 지정해서 문제를 회피할 수도 있다.

Q) 전역 변수가 싱글턴보다 나쁜 이유는 무엇일까??

A) 자바의 전역 변수는 기본적으로 객체에 대한 정적 레퍼런스다. 전역 변수를 이런 식으로 사용한다면 게으른 인스턴스를 사용할 수 없는 단점과 싱글턴 패턴을 쓰는 두 가지 이유 중, 클래스의 인스턴스가 하나만 있을 수 있도록 할 수 없다. 전역 변수를 사용한다면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 도면서 네임스페이스를 지저분한게 만드는 경향이 생긴다.

핵심 정리

  • 어떤 클래스를 싱글턴 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한 개 까지만 있도록 할 수 있다.
  • 싱글턴 패턴을 이용하면 유일한 인스턴스를 어디서든지 접근할 수 있도록 할 수 있다.
  • 자바에서 싱글턴 패턴을 구현할 때는 private 생성자와 정적 메소드, 정적 변수를 사용한다.
  • 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용해야 한다.
    • 사실상 멀티스레딩을 기본으로 가정해야한다.
  • DCL을 사용하는 방법은 자바 2 버전5보다 전에 나온 버전에서는 쓸 수 없다.
  • 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고, 여러 개의 인스턴스가 생길 수 있다.
  • 1.2 버전보다 전에 나온 JVM을 사용하는 경우에는 가바지 컬렉터 관련 버그 때문에 싱글턴 레지스트리를 사용해야 할 수도 있다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
    • 싱글턴 패턴 : 클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
728x90
728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

원재료군

뉴옥과 시카고에서 사용하는 재료 종류는 서로 다르다. PizzaStore 분점이 점점 생기면서 해당 분점들은 또 다른 자신들만의 재료들을 사용해야 될 것이다.

이렇게 서로 다른 종유릐 재료들을 제공하기 위해 원재료군을 처리할 방법을 생각해야한다.

원재료 공장 만들기

따라서 이제 원재료를 생산하기 위한 공장을 만들어보자.

원재료 공장에서는 원재료군에 들어있는 반죽, 소스, 치즈 같은 각각의 원재료를 생산한다.

우선 모든 원재료를 생산할 팩토리를 위한 인터페이스를 정의해보자

public interface PizzaIngredientFactory {

    Dough createDough();

    Sauce createSauce();

    Cheese createCheese();

    Veggies[] createVeggies();

    Pepperoni createPepperoni();

    Clams createClam();

}
public class NYPizzaIngredientFactory implements PizzaIngredientFactory {

    @Override
    public Dough createDough() {
        return new ThinCrustDough();
    }

    @Override
    public Sauce createSauce() {
        return new MarinaraSauce();
    }

    @Override
    public Cheese createCheese() {
        return new ReggianoCheese();
    }

    ...
}

피자 클래스 변경

원재료 팩토리가 준비되고 Pizza 클래스에서 팩토리에서 생산한 원재료만 사용하도록 코드를 수정한다.

public abstract class Pizza {

    String name;
    Dough dough;
    Sauce sauce;
    Cheese cheese;
        ...

    public void box() {
        System.out.println("포장");
    }

    public void cut() {
        System.out.println("커팅");
    }

    public void bake() {
        System.out.println("굽기");
    }

    abstract void prepare();
}
  • prepare() 메서드를 제외한 다른 메서드들은 바뀌지 않는다.
public class CheesePizza extends Pizza {

    private final PizzaIngredientFactory pizzaIngredientFactory;

    public CheesePizza(PizzaIngredientFactory pizzaIngredientFactory) { // 생성자를 통해 원재료를 제공하는 팩토리를 주입받는다.
        this.pizzaIngredientFactory = pizzaIngredientFactory;
    }

    @Override
    void prepare() { // 팩토리가 작동하는 부분
        dough = pizzaIngredientFactory.createDough();
        sauce = pizzaIngredientFactory.createSauce();
        cheese = pizzaIngredientFactory.createCheese();
    }
}

앞선 예제에서 NYStyleCheesePizza , ChicagoStyleCheesePizza 클래스를 기억해보자. 그 두 클래스를 살펴보면 지역별로 다른 재료를 사용한다는 것만 빼면 다른 점이 없다.

  • 따라서 피자마다 클래스를 지역별로 따로 만들 필요가 없다. 지역별로 다른 점은 원재료 공장에서 처리하기 때문

이제 피자 코드에서는 팩토리를 이용하여 피자에서 쓰이는 재료를 만든다.

  • 만들어지는 재료는 어떤 팩토리를 쓰는지에 따라 달라지며 피자 클래스에서는 전혀 신경을 쓰지 않는다.
  • 이제 피자 클래스와 지역별 재료가 분리되어 있기 때문에 어떤 지역의 재료 팩토리를 사용하든 피자 클래스는 그대로 재사용할 수 있다.

마찬가지로 피자 가게를 수정해보자

public class NYPizzaStore extends PizzaStore {

    @Override
    protected Pizza createPizza(String type) {
        Pizza pizza = null;
        PizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

        if (type.equals("cheese")) {
            pizza = new CheesePizza(ingredientFactory);
        } else if (type.equals("greek")) {
            pizza = new GreekPizza(ingredientFactory);
        }
        return pizza;
    }
}
  • 뉴욕 피자 가게에서는 뉴욕 피자 원재료 공장을 주입시켜 준다.

정리

기존 팩토리 패턴에서 추상 팩토리라고 부르는 새로운 형식의 팩토리를 도입해서 서로 다른 피자에서 필요로 하는 원재료군을 생산하기 위한 방법을 구축했다.

  • 추상 팩토리를 통해서 제품군을 생성하기 위한 인터페이스를 제공할 수 있다.
  • 이 인터페이스를 이용하는 코드를 만들면 코드를 제품을 생산하는 실제 팩토리와 분리시킬 수 있다.
  • 이렇게 함으로써 서로 다른 상황별로 적당한 제품을 생산할 수 있는 다양한 팩토리를 구현할 수 있게 된다.

추상 팩토리 패턴 정의

제품군을 만들 때 쓸 수 있는 추상 팩터리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

  • 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스를 통해서 일련의 제품들을 공급받을 수 있다.
  • 이 때, 실제로 어떤 제이품이 생산되는지 전혀 알 필요가 없다.
  • 따라서 클라이언트와 팩토리에서 생산되는 제품을 분리시킬 수 있다.

클래스 다이어그램

 

추상 팩토리 패턴과 팩토리 메서드 패턴의 차이

추상 팩토리 패턴에 있는 createDough(), createSauce() 같은 메서드는 전부 팩토리 메서드 같이 보인다.

그렇다면 추상 팩토리 패턴 뒤에는 팩토리 메서드 패턴이 숨어져 있는 것일까?

  • 각 메서드는 추상 메서드로 선언되어 있고, 서브 클래스에서 메소드를 오버라이드해서 객체를 만드는 방식이기 때문
  • 추상 팩터리가 일련의 제품들을 생성하는데 쓰일 인터페이스를 정의하기 위해 만들어진 것이므로, 해당 인터페이스에 있는 메서드는 구상 제품을 만드는 일을 맡고 있고, 추상 팩토리의 서브클래스를 만들어서 각 메서드의 구현을 제공한다.
  • 따라서 추상 팩토리 패턴에서 제품을 생성하기 위한 메서드를 구현하는데 있어서 팩토리 메서드를 사용하는것은 자연스러운 일이다.

하지만 팩토리 메서드 패턴은 상속을 통해 객체를 생성하고 추상 팩토리 패턴은 객체 구성을 통해 객체를 생성한다.

또한 추상 팩토리 패턴에서는 제품군에 제품을 추가하는 식으로 관련 제품들을 확대해야 하는 경우에 인터페이스를 수정해야 하지만 팩토리 메서드 패턴에서는 한 가지 제품만 생산하므로 복잡한 인터페이스도 필요하지 않고, 메서드도 하나만 있으면 된다.

  • 추상 팩토리 패턴은 클라이언트에서 서로 연관된 제품군을 만들어야 할 때
  • 팩토레 메소드 패턴은 클라이언트 코드와 인스턴스를 만들어야 할 구상 클래스를 분리시켜야하거나, 어떤 구상 클래스를 필요로 하게 될지 미리 알 수 없는 경우에 매우 유용하다.

핵심 정리

  • 팩토리를 쓰면 객체 생성을 캡슐화할 수 있다.
  • 간단한 팩토리는 엄밀히 디자인 패턴은 아니지만, 클라이언트와 구상 클래스를 분리시키기 위한 간단한 기법으로 활용 가능하다.
  • 팩토리 메소드 패턴에서는 상속을 활용한다. 객체 생성이 서브클래스에게 위임된다. 서브 클래스에서는 팩토리 메소드를 구현하여 객체를 생산한다.
  • 추상 팩토리 패턴에서는 객체 구성을 활용한다. 객체 생성이 팩토리 인터페이스에서 선언한 메소스들에서 구현된다.
  • 모든 팩토리 패턴에서는 애플리케이션의 구상 클래스에 대한 의존성을 줄여줌으로써 느슨한 결합을 도와준다.
  • 추상 팩토리 패턴은 구상 클래스에 직접 의존하지 않고도 서로 관련된 객체들로 이루어진 제품군을 만들기 위한 용도로 쓰인다.
  • DIP에 따르면 구상 형식에 대한 의존을 피하고 추상화를 지향할 수 있다.
  • 팩토리는 구상 클래스가 아닌 추상 클래스, 인터페이스에 맞춰 코딩할 수 있게 해주는 강력한 기법이다.

객체지향 도구 상자

  • 객체지향의 기초(4요소)
    • 캡슐화
    • 상속
    • 추상화
    • 다형성
  • 객체지향 원칙
    • 바뀌는 부분을 캡슐화한다.
    • 상속보다는 구성을 활용한다.
    • 구현이 아닌 인터페이스(super type)에 맞춰서 프로그래밍한다.
    • 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
    • 클래스는 확장에 대해서는 열려 있지만 변경에 대해서는 닫혀 있어야 한다. (OCP)
    • 추상화된 것에 의존하라. 구상 클래스에 의존하지 않도록 한다.
  • 객체지향 패턴
    • 스트레지티 패턴 : 알고리즘군을 정의하고 각각의 알고리즘을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
    • 옵저버 패턴 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.
    • 데코레이터 패턴: 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
    • 추상 팩토리 패턴 : 서로 연관된, 또는 의존적인 객체들로 이루어진 제품군을 생성하기 위한 인터페이스를 제공한다. 구상 클래스는 서브 클래스에 의해 만들어진다.
    • 팩토리 메소드 패턴 : 객체를 생성하기 위한 인터페이스를 만든다. 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하도록 한다. 팩토리 메소드를 이용하면 인스턴스를 만드는 일을 서브클래스로 미룰 수 있다.
728x90
728x90

팩토리 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

  • 느슨한 결합을 이용하는 객체지향 디자인, 객체의 인스턴스를 만드는 작업이 항상 공개되어 있어야 하는 것은 아니며, 오히려 결합과 관련된 문제가 생길 수 있다. 팩토리 패턴을 이용하여 불필요한 의존성을 없애보자

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

“new”는 “구상 객체”를 뜻한다.

new를 사용하는 것은 구상 클래스의 인스턴스를 만드는 것이다. 당연히 인터페이스가 아닌 특정 구현을 사용하는 것, 앞에서 디자인 패턴을 공부하면서 구상 클래스를 바탕으로 코딩을 하게 되면 코드를 수정해야 할 가능성이 높아지고, 유연성이 떨어지는 것을 볼 수 있었다.

Duck duck = new MallardDuck();

일련의 구상 클래스들이 존재하는 경우, 다음과 같은 코드를 만들어야 하는 경우가 있다.

Duck duck;

// 컴파일시에는 어떤 것의 인스턴스를 만들어야 할지 알 수 없다.
if(picnic) {
        duck = new MallardDuck();
} else if() {
        duck = new DecoyDuck();
} else if() {
        duck = new RubberDuck();
}

이런 코드가 있다는 것은, 변경하거나 확장해야 할 때 코드를 다시 확인하고 추가 또는 제거해야 한다는 것을 뜻한다. 따라서 코드를 이런식으로 만들면 관리 및 갱신하기가 어려워지고, 오류가 생길 가능성이 높아지게 된다.

사실 “new” 자체에 문제가 있는 것은 아니다. 가장 문제가 되는 점은 “변경”이다. 뭔가 변경되는 것 때문에 new를

사용하는데 있어서 조심해야 하는 것이다.

그렇기 때문에 인터페이스에 맞춰서 코딩을 하면 다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 사용할 수 있기 때문에 여러 변경에 대해 유연함을 가질 수 있는 것이다. 반대로 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하기 때문에 많은 문제가 생길 수 있다. 즉 OCP 원칙

바뀌는 부분을 찾아보자

피자 가게를 운영하고 있다고 생각해보자.

피자의 종류가 다양할 것이나, 새로운 피자 신메뉴를 출시하거나 메뉴가 사라질 수 있을 것이다.

따라서 oderPizza() 메소드에서 가장 문제가 되는 부분은 바로 인스턴스를 만들 구상 클래스를 선택하는 부분이다. 해당 부분 때문에 변화에 따라 코드를 변경할 수 밖에 없다. 이제 바뀌는 부분과 바뀌지 않는 부분을 파악했으니 캡슐화할 차례이다.

Pizza orderPizza(String type) {
        Pizza pizza = null; // 인터페이스

                // 바뀌는 부분
        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }

                // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

객체 생성 부분을 캡슐화해보자

이제 객체를 생성하는 부분을 메소드에서 뽑아내어 피자 객체를 만드는 일만 전담하는 다른 객체에 집어넣어 보자.

피자를 만드는 일만 처리하는 객체를 집어 넣어 다른 객체에서 피자를 만들어야 하는 일이 있으면 해당 객체에게 부탁하는 객체 추가해보자. 새로 만들 객체에는 팩토리라는 이름을 붙이기로 하자. SimplePizzaFactory를 만들고 나면 orderPizza() 메소드는 새로 만든 객체의 클라이언트가 된다. 즉 새로 만든 객체를 호출하는 것. 피자 공장에 피자 하나 만들어 달라고 부탁한다고 생각하면 쉽다.

SimplePizzaFactory를 추가하자

피자 객체 생성을 전달한 클래스를 정의한다.

// 해당 클래스에서 하는 일은 클라이언트를 위해 피자를 만들어 주는 일 뿐이다.
public class SimplePizzaFactory {

    public Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new CheesePizza();
        } else if (type.equals("greek")) {
            pizza = new GreekPizza();
        } else if (type.equals("pepperoni")) {
            pizza = new PepperoniPizza();
        }
        return pizza;
    }

}

Q) 이렇게 하면 어떤 장점이 있는 것일까?? 얼핏 보면 아까 문제를 다른 객체로 넘겨 버린 것 처럼 보일 수 있어 보인다.

  • SimplePizzaFactory를 사용하는 클라이언트가 매우 많을 수 있는 점을 생각 하자
  • 피자 객체를 받아서 가격, 설명 등을 찾아서 활용하는 클래스 또는 피자 주문을 처리하는 클래스에서도 이 팩토리를 사용할 수 있을 것이다.
  • 따라서 피자리를 생성하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우에 여기저기 다 들어가서 고칠 필요 없이 팩토리 클래스 하나만 고치면 된다.
  • 추후 클라이언트 코드에서 구상 클래스의 인스턴스를 만드는 코드를 없애는 작업 진행

Q) 비슷한 식으로 메소드를 정적 메소드를 선언한 디자인(정적 팩터리 메소드)과 차이점은 무엇일까?

  • 정적 팩토리 메서드를 사용하면 객체를 생성하기 위한 메소드를 실행시키기 위해서 객체의 인스턴스를 만들지 않아도 되기 때문에 간단한 팩토리를 정적 메소드를 정의하는 기법도 일반적으로 많이 쓰인다.
  • 하지만 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경시킬 수 없다는 단점이 존재한다.

간단한 팩토리를 이용한 PizzaStore 수정

public class PizzaStore {

    SimplePizzaFactory simplePizzaFactory;

    public PizzaStore(SimplePizzaFactory simplePizzaFactory) {
        this.simplePizzaFactory = simplePizzaFactory;
    }

    Pizza orderPizza(String type) {
        Pizza pizza = simplePizzaFactory.createPizza(type);

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

}

PizzaStore

  • 팩토리를 사용하는 클라이언트
  • 팩토리를 통해 피자 인스턴스를 받게 된다.

SimplePizzaFactory

  • 피자 객체를 만드는 팩토리
  • 애플리케이션에서 유일하게 구상 Pizza 클래스를 직접 참조한다.

팩토리 메서드

피자 가게가 큰 성공을 거두어 여러 지점을 가지게 되었고, 지역별로 조금씩 다른 차이점이 존재했고, 각각 지역의 특성을 반영하여 피자를 만들어야 했다. 이런 차이점을 어떤 식으로 적용해야 할까??

간단하게 생각하면 SimplePizzaFactory를 빼고 지역별 피자 팩토리(NYPizzaFactory, ChicagoPizzaFactory)를 만든 다음, PizzaStore에서 해당하는 팩토리를 사용하도록 하면 될 것이다.

NYPizzaFactory nyFactory = new NYPizzaFactory();
PizzaStore nyStore = new PizzaStore(nyFactory);
nyStore.orderPizza("Veggie");

ChicagoPizzaFactory chicagoFactory = new ChicagoPizzaFactory();
PizzaStore chicagoStore = new PizzaStore(chicagoFactory);
chicagoStore.orderPizza("Veggie");

피자 가게 프레임워크 - 팩토리 메소드 선언

피자를 만드는 활동 자체를 전부 PizzaStore 클래스에 국한시키면서도 분점마다 고유의 스타일을 살릴 수 있도록 하기 위해서 createPizza() 메소드를 다시 PizzaStore에 집어 넣고 추상 메서드로 선언하고, 각 지역마다 고유의 스타일에 맞게 PizzaStore의 분점을 나타내는 서브클래스를 만들도록 할 것이다. 즉 피자의 스타일은 각 서브클래스에서 결정하는 것

public abstract class PizzaStore {

    Pizza orderPizza(String type) {

        Pizza pizza = createPizza(type); // 팩토리 객체가 아닌 메소드 호출

        // 바뀌지 않는 부분
        pizza.prepare();
        pizza.bake();
        pizza.cut();
        pizza.box();

        return pizza;
    }

    // 팩토리 객체 대신 해당 "메소드" 사용
    // "팩토리 메소드"가 추상 메소드로 바뀌었다.
    abstract Pizza createPizza(String type);

}

서브클래스에서 결정되는 것

각 서브클래스에서 달라질 수 있는 것은 피자의 만들 때의 스타일 뿐이다. 이렇게 달라지는 점들을 createPizza() 메소드에 집어넣고 그 메소드에서 해당 스타일의 피자를 만드는 것을 모두 책임진다. PizzaStore의 서브클래스에서 createPizza() 메소드를 구현하도록 하면 PizzaStore 프레임워크에 충실하면서도 각각의 스타일을 제대로 구현할 수 있는 PizzaStore 서브클래스들을 구비할 수 있다.

  • PizzaStore에서 정의한 메서드를 서브클래스에서 고쳐서 쓸 수 없게 하고 싶다면 메서드를 final로 선언할 수도 있다.
  • ex) final Pizza orderPizza(String type)
public class NYPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new NYStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new NYStyleGreekPizza();
        }
        return pizza;
    }
}

public class ChicagoPizzaStore extends PizzaStore {

    @Override
    Pizza createPizza(String type) {
        Pizza pizza = null;

        if (type.equals("cheese")) {
            pizza = new ChicagoStyleCheesePizza();
        } else if (type.equals("greek")) {
            pizza = new ChicagoStyleGreekPizza();
        }
        return pizza;
    }
}
  • 슈퍼클래스에 있는 orderPizza() 메소드에서는 Pizza 객체를 가지고 여러 작업을 하긴 하지만, Pizza는 추상 클래스기 때문에 어떤 구상 클래스에서 작업이 처리되고 있는지 전혀 알 수없다.
    • 즉 PizzaStore와 Pizza는 서로 완전히 분리되어 있다.
    • 그렇다면 피자 종류를 결정하는 것은 누구일까??
  • orderPizza() 입장에서 볼 때는 PizzaStore 서브클래스에서 피자 종류를 결정한다고 할 수 있을 것이다.
    • 따라서 서브 클래스에서 실제로 뭔가를 “결정”하는 것이 아니라, 우리가 선택하는 PizzaStore의 서브클래스 종류에 따라 결정되는 것이지만, 만들어지는 피자의 종류를 해당 PizzaStore 서브클래스에서 결정한다고 할 수 있다.

팩토리 메소드

abstract Product factoryMethod(String type);

팩토리 메소드는 객체 생성을 처리하며, 팩토리 메소드를 이용하면 객체를 생성하는 작업을 서브클래스에 캡슐화시킬 수 있다. 이렇게 하면 슈퍼 클래스에 있는 클라이언트 코드와 서브클래스에 있는 객체 생성 코드를 분리시킬 수 있다.

  • abstract
    • 팩토리 메소드는 추상 메소드로 선언하여 서브클래스에서 객체 생성을 책임지도록 한다.
  • Product
    • 팩토리 메소드에서는 특정 객체를 리턴하며, 그 객체는 보통 수퍼 클래스에서 정의한 메소드 내에서 쓰이게 된다.
  • factoryMethod
    • 팩토리 메소드는 클라이언트 (ex : orderPizza method)에서 실제로 생성되는 구상 객체가 무엇인지 알 수 없게 만드는 역할도 한다.
  • type
    • 팩토리 메소드를 만들 때 매개변수를 써서 만들어낼 객체 종류를 선택할 수도 있다.

사용 code

public class Main {

    public static void main(String[] args) {
        PizzaStore nyStore = new NYPizzaStore();
        PizzaStore chicagoStore = new ChicagoPizzaStore();

        Pizza pizza = nyStore.orderPizza("cheese");
        pizza = chicagoStore.orderPizza("cheese");

    }

}

팩토리 메소드 패턴

팩토리 메소드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다. 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

  • 모든 팩토리 패턴에서는 객체 생성을 캡슐화한다.
  • 팩토리 메소드 패턴에서는 서브 클래스에서 어떤 클래스를 만들지를 결정하게 함으로써 객체 생성을 캡슐화한다.
  • 즉 팩토리 메서드 패턴에서는 객체를 생성하기 위한 인터페이스를 정의하는데, 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
  • 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것

클래스 다이어그램을 살펴보자

Creator : 생산자

  • ex) PizzaStore Class
  • 제품을 가지고 원하는 일을 하기 위한 모든 메소드들이 구현되어 있다.
  • 하지만 제품을 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있을 뿐, 구현되어 있진 않다.
  • Creator의 모든 서브 클래스에서 factoryMethod() 추상 메소드를 구현해야 한다.

ConreteCreator : 구상 생산자

  • 실제로 제품을 생산하는 factoryMethod()를 구현한다.

Product / ConreteProduct

  • 제품 클래스에서는 모두 똑같은 인터페이스 구현
  • 그래야 제품을 사용할 클래스(클라이언트)에서 구상 클래스가 아닌 인터페이스에 대한 레퍼런스를 써서 객체를 참조할 수 있기 때문

ConreteCreator → ConreteProduct

  • 구상 클래스 인스턴스를 만들어내는 일은 ConreteCreator가 책임진다.
  • 실제 제품인 ConreteProduct 객체를 만들어내는 방법을 알고 있는 클래스는 ConreteCreator 클래스 뿐

병렬 클래스 계층 구조

  • Product, Creator 클래스 둘 다 추상 클래스로 시작하고, 그 클래스를 확장하는 구상 클래스들을 가지고 있다.
  • 구체적인 구현은 구상 클래스들이 책임지고 있다.
  • 구상 생산자 클래스에는 특정 구상 제품군에 대한 모든 지식이 캡슐화 되어 있으며 팩토리 메소는 이러한 지식을 캡슐화 시키는데 있어서 가장 핵심적인 역할을 맡고 있다.

Simple Factory vs Factory Method Pattern

  • 뉴욕과 시카고 분점을 만들 때 간단한 팩토리를 사용했다고 할 수 있지 않을까??
    • 비슷하긴 하지만 방법이 조금 다르다.
    • 구상 클래스를 만들 때 createPizza() 추상 메소드가 정의되어 있는 추상 클래스를 확장해서 만들었다는 점이 중요한 차이
      • createPizza() 메소드에서 어떤 일을 할지는 각 분점에서 결정한다.
    • 간단한 팩토리를 사용할 때는 팩토리가 PizzaStore 안에 포함되는 별개의 객체였다는 큰 차이점이 존재한다.
  • simple factory
    • 일회용 처방에 불과하다.
    • 객체 생성을 캡슐화하는 방법을 사용하긴 하지만 생성하는 제품을 마음대로 변경할 수 없기 때문에 강력한 유연성을 제공하진 못한다.
  • factory method pattern
    • 어떤 구현을 사용할지를 서브클래스에서 결정하는 프레임워크를 만들 수 있다.
    • 강력한 유연성을 제공한다.

의존적인 PizzaStore

  • 팩토리를 사용하지 않는 PizzaStore 클래스는 피자 객체가 의존하고 있는 구상 피자 객체의 개수만큼 의존하게 된다.
  • 왜냐하면 객체 인스턴스를 직접 만들어 구상 클래스에 의존하기 때문이다.

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 적용 전

https://msyu1207.tistory.com/entry/4장-헤드퍼스트-디자인-패턴-팩토리-패턴

  • 팩토리 메소드 패턴을 적용한 다이어 그램

DIP : 의존성 역전 원칙

  • 구상 클래스에 대한 의존성을 줄이는 것이 좋다는 내용을 정리해 놓은 객체지향 디자인 원칙
  • 이 원칙은 다음과 같이 일반화 시킬 수 있다.
    • 추상화된 것에 의존하도록 만들어라
    • 구상 클래스에 의존하도록 만들지 않아야 한다.
  • 이 원칙하고 “특정 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다”는 원칙이 똑같다고 생각할 수 있다.
    • 비슷하긴 하지만 dip에서는 추상화를 더 많이 강조한다.
    • 해당 원칙에는 고수준 구성요소가 저수준 구성요소에 의존하면 안된다는 것이 내포되어 있다.
    • 항상 추상화에 의존하도록 만들어야 한다.
  • 그럼 고수준, 저수준은 어떤 의미일까??
    • 고수준 구성요소
      • 고수준 구성요소는 다른 저수준 구성요소에 의해 정의되는 행동이 들어있는 구성요소를 뜻한다.
      • ex) PizzaStore
      • PizzaStore의 행동은 피자에 의해 정의되기 때문에 고수준 구성요소라고 할 수 있다.
    • 저수준 구성요소
      • 이 때 PizzaStore에서 사용하는 피자 객체들은 저수준 구성요소라고 할 수 있다.
  • 팩토리 패턴을 사용하지 않은 기존의 PizzaStore 클래스는 구상 피자 클래스들에 의존하고 있다.
  • dip 원칙에 의하면, 구상 클래스처럼 구체적인 것이 아닌 추상 클래스나 인터페이스와 같이 추상적인 것에 의존하는 코드를 만들어야 한다.
    • 이 원칙은 고수준 모듈과 저수준 모듈에 모두 적용될 수 있다.
  • dip 원칙을 지키는데 도움이 될만한 가이드라인
    1. 어떤 변수에도 구상 클래스에 대한 레퍼런스를 저장하지 않는다.
    2. 구상 클래스에서 유도된 클래스를 만들지 않는다.
    3. 베이스 클래스에 이미 구현되어 있던 메소드를 오버라이드 하지 않는다.
    • 해당 가이드들은 항상 지켜야 하는 규칙이 아니라 지향하는 바를 나타낸 것
    • 이 가이드라인을 완벽하게 따를 순 없다.
728x90
728x90

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

데코레이터 패턴을 이용하면 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.

개요

OO커피는 단기간에 급속도로 성장한 대형 커피 전문점이다. 빠르게 성장한 만큼, 음료들을 모두 포괄하는 주문 시스템이 이제서야 개발되려고 하는 상황이다. 처음 시스템 시작할 무렵에 만들어진 클래스는 위의 사진과 같다.

Beverage : 음료를 나타내는 추상 클래스, 모든 음료는 해당 클래스의 서브클래스

  • description : 해당 인스턴스 변수는 서브클래스에서 설정되며, 음료의 설명이 저장된다.
  • cost() : 추상 메서드이며, 서브 클래스에서 해당 메서드를 구현해야 한다.

하지만 음료에는 스팀 우유, 두유, 모카, 휘핑 크림 등 옵션을 추가할 때마다 가격이 달라진다. 이러한 경우를 모두 고려한다면??

이처럼 클래스 개수가 폭발적으로 증가하게 된다. 만약 우유나 크림 가격이 인상된다면?? 한눈에 보기에도 이렇게 수많은 클래스를 관리하기는 힘들 것이다.

그러면 슈퍼 타입인 Beverage 클래스에 인스턴스 변수로 관리하면 안될까??

  • milk, soy, mocha, ... ,whip : 각 추가 요소에 해당하는 인스턴스 변수 추가
  • cost() : 각 음료 서브 클래스의 인스턴스마다 추가 사항에 해당하는 추가 가격까지 포함할 수 있도록 기본 음료 값을 가져와서 오버라이드 하기위해 추상 메소드가 아닌 구현 메소드로 수정
  • 부울 인스턴스 변수를 위한 게터, 세터
public class Beverage {

    String description;
    boolean hasMilk, hasSoy, hasMocha;
    double milkCost, soyCost, mochaCost;

    public double cost() {
        double condimentCost = 0;
        if (getHasMilk()) {
            condimentCost += milkCost;
        }
        if (getHasSoy()) {
            condimentCost += soyCost;
        }
        if (getHasMocha()) {
            condimentCost += mochaCost;
        }
        return condimentCost;
    }

    // get, set..
    public boolean getHasMilk() {
        return hasMilk;
    }

    public boolean getHasSoy() {
        return hasSoy;
    }

    public boolean getHasMocha() {
        return hasMocha;
    }

}
public class DarkRoast extends Beverage{

    public DarkRoast() {
        description = "다크 로스트";
    }

    @Override
    public double cost() {
        return super.cost() + 3500;
    }
}

아까와 같은 클래스 폭발을 막게되었다. 하지만 아직 확신이 서지 않는다. 어떤 문제점이 있을 수 있을까??

  • 음료에 추가되는 옵션(우유, 휘핑, 모카 등)의 가격이 바뀔때마다 코드를 수정해야 한다.
  • 음료에 추가되는 옵션의 종류가 많아지면 그 때마다 메소드를 추가하고, 슈퍼 클래스의 cost() 메소드를 수정해야 한다.
  • 새로운 음료가 추가되는 경우, 옵션이 없는 경우와 우유가 들어가지 않는 음료임에도 관련된 멤버들을 상속받게 된다.
  • 만약 샷이라는 옵션이 추가되고 샷을 두번 추가한 음료는 어떻게 해야할까??

상속은 객체지향 디자인의 강력한 요소 중 하나지만, 이처럼 상속을 사용한다고 해서 무조건 유연하고 관리하기 쉬운 디자인이 만들어지지 않는다. 그 이유는 서브 클래스를 만드는 방식으로 행동을 상속 받으면 해당 행동은 컴파일시에 완전히 결정되고 모든 서브클래스에서 슈퍼 클래스의 멤버들을 상속 받아야 하기 때문이다. 하지만 composite를 통해서 객체의 행동을 실행 중에 동적으로 설정하는 방법을 사용한다면, 즉 객체를 동적으로 구성하면, 기존 코드를 수정하는 대신 새로운 코드를 추가하는 방식으로 새로운 기능을 추가할 수 있다. 기존 코드는 수정되지 않으므로(변경에 대해서는 닫혀있으므로) 버그가 생기거나 사이드 이펙트를 방지하면서 새로운 기능을 추가(확장에 대해서는 열려있는)할 수 있는 것이다.

데코레이터 해보자!

이제 음료에서 추가되는 옵션이 있는 경우 해당 음료를 데코레이터 하는 방식으로 수정해보자. 만약 모카와 휘핑 크림을 추가한 다크 로스트 커피는 다음처럼 할 수 있을 것이다.

  1. DarkRoast 객체를 가져온다
  2. Mocha 객체로 장식한다.
  3. Whip 객체로 장식한다.
  4. cost() 메소드를 호출한다. 이때 추가 옵션의 가격을 계산하는 일은 해당 객체들에게 위임한다.

이 때 장식하고 위임하는 방법은 해당 객체를 래퍼 객체라고 생각하면 쉽다.

이렇게 가장 바깥쪽에 있는 데코레이터 객체에서 cost()를 호출하고, 해당 객체가 장식하고 있는 객체에게 가격을 위임한다. 위임한 객체에게 가격의 값을 얻으면, 자신의 가격을 더한 다음 리턴하는 것이다.

여기서 중요한 점은 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수도 있다는 저이다.

데코레이터 패턴

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기느을 유연하게 확장할 수 있는 방법을 제공한다.

Compent

  • 각 구성요소는 직접 사용할 수도 있고 데코레이터로 감싸져서 쓰일 수도 있다.
  • ex) Beverage 클래스

ConcreteComponent

  • 해당 클래스에 새로운 행동을 동적으로 추가하게 된다.

Decorator

  • 데코레이터는 자신이 장식할 Component와 같은 인터페이스 또는 추상 클래스를 구현한다.
  • 각 데코레이터 안에는 Component 객체가 들어있다. 즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수를 가진다.

ConcreteDecorator

  • ConcreteDecorator 에는 데코레이터가 감싸고 있는 Component 객체를 위한 인스턴스 변수가 있다.
  • Decorator는 Component의 상태를 확장할 수 있다.
  • Decorator에서 새로운 메소드를 추가할 수도 있다. 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component의 메소드를 호출하기 전, 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.

Beverage 클래스 다이어그램

public abstract class Beverage {

    String description = "";

    public abstract double cost();

    public String getDescription() {
        return description;
    }
}

public class Espresso extends Beverage {

    public Espresso() {
        description = "에스프레소";
    }

    @Override
    public double cost() {
        return 3500;
    }
}

public abstract class CondimentDecorator extends Beverage {
    public abstract String getDescription();
}

public class Mocha extends CondimentDecorator {

    Beverage beverage;

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public double cost() {
        return 500 + beverage.cost();
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", 모카";
    }
}
public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + ": " + beverage.cost() + " won");

        Beverage beverage2 = new Espresso();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        System.out.println(beverage2.getDescription() + ": " + beverage2.cost() + " won");
    }

}

데코레이터 패턴을 적용한 코드는 아까의 문제점이 사라진 오류의 코드지만 저런형태로 관리하게 될경우 마지막 Soy를 빼먹는다던가 실수로 두번넣는 경우가 생기게됩니다. 팩토리 패턴과 빌더 패턴을 이용해서 더 쉽게 객체를 만드는 방법이 존재한다.

주의해야 할 점

  • 특정 ConcreteComponent 타입을 바탕으로 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 제대로 작동하지 않는다. (ConcreteDecorator로 감싸져 있기 때문)
  • 만약 여러 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 한다면, 데코레이터 패턴의 의의와 어긋나는 것이다.
  • 데코레이터 패턴에서는 특정한 추상 구성요소를 지정할 필요가 없다. 인터페이스를 사용해도 무방하다.

마무리

  • 상속을 통한 확장은 디자인의 유연성 면에서 좋지 않을 수 있다.
  • 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다 (OCP)
  • 합성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
  • 상속 대신 데코레이터 패턴을 통해 행동을 확장할 수 있다.
  • 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터를 사용한다.
  • 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.
  • 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 확장한다.
  • 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.
  • 구성요소의 클라이언트 입장에서는 데코레이터의 존재를 알 수 없다.
    • 따라서 클라이언트에서 구성 요소의 구체적인 타입에 의존하게 되는 경우는 다시 생각해봐야한다.
  • 데코레이터 패턴을 사용하면 객체들이 많이 추가될 수 있고, 코드가 복잡해질 수 있다.
728x90
728x90

옵저버 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

개요

실제 기상 정보를 수집하는 장비인 기상 스테이션과 기상 스테이션으로부터 오는 데이터를 추적하는 객체인 WeatherData, 그리고 사용자에게 현재 기상 조건을 보여주는 디스플레이, 세 요소로 이루어진다.

WeatherData 객체에서는 기상 스테이션으로부터 데이터를 가져올 수 있다. 데이터를 가져온 후에는 디스플레이 장비에 세 가지 항목을 표시할 수 있다.

  • 현조 조건(온도, 습도, 압력)
  • 기상 통계
  • 기상 예보

WeatherData 객체를 사용하여 디스플레이 장비에서 위의 3가지 요소를 갱신해 가면서 보여주는 애플리케이션을 만들어보자

주어진 WeatherData 클래스와 상황

  • 세가지의 게터 메소드는 각각 가장 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드
  • measurementsChanged 메소드를 현재 조건, 기상 통꼐, 기상 예측 3가지 디스플레이를 갱신할 수 있도록 구현해야 한다.
  • 시스템은 확장 가능해야 한다. 추후 디스플레이 항목들은 추가/제거될 수 있다.

초기 구현

public class WeatherData {

    // 인스턴스 변수 선언

    public void measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

    }

}

위의 코드의 문제는 무엇일까??

  • 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있다.
  • 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
  • 실행중에 디스플레이 항목을 추가/제거할 수 없다.
  • 바뀌는 부분을 캡슐화하지 않았다.

구체적인 구현에 맞춰서 코딩되어 있기 때문에 코드를 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없고 디스플레이에 항목들을 갱신하여 업데이트하는 부분들은 바뀔 수 있는 부분이므로 캡슐화해야 한다.

이제 옵저버 패턴에 대해서 알아보자

옵저버 패턴

쉽게 생각해서 신문 구독 메커니즘과 비슷한다. 즉 출판사 + 구독자 = 옵저버 패턴 인 것

출판사를 주제 or 주체(subject), 구독자를 옵저버(observer)라고 부른다.

  • subject 객체에서 일부 데이터를 관리
  • subejct의 데이터가 달라지면 옵저버한테 소식과 데이터가 전달된다.
  • observer 객체들은 subject 객체를 구독(등록)하고 있으며 subject의 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

  • 일대다 관계는 subject와 observer에 의해 정의되고 observer는 subject에 의존한다.
  • 옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 subject 인터페이스와 observer 인터페이스를 사용한 클래스 디자인을 바탕으로 한다.

Subject 인터페이스

  • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 이 인터페이스에 있는 메소드를 사용한다.

ConcreteSubject

  • Subject 역할을 하는 Concrete 클래스에서는 항상 subject 인터페이스를 구현해야 한다.
  • subject 클래스에서는 등록 및 해지를 위한 메소드 외에 상태가 바뀔때마다 모든 옵저버들에게 연락을 하기 위한 notifyObservers() 메소드도 구현해야 한다.
  • subject 클래스에는 상태를 설정하고 알아내기 위한 세터/게터 메소드가 있을 수도 있다.

Observer 인터페이스

  • observer가 될 가능성이 있는 객체에서는 반드시 observer 인터페이스를 구현해야 한다.
  • observer 인터페이스에는 subject의 상태가 바뀌었을 때 호출되는 update() 메소드 밖에 없다.

ConcreteObserver

  • Observer 인터페이스만 구현한다면 어떤 클래스든 옵저버 클래스가 될 수 있다.
  • 각 옵저버는 특정 주제 객체에 등록을 해서 연락을 받을 수 있다.

옵저버 패턴에서 상태를 저장하고 지배하는 것은 subject 객체이다. 따라서 상태가 들어있는 객체는 하나만 존재하고, 옵저버는 반드시 상태를 가지고 있어야 하는 것은 아니기 때문에 옵저버는 여러 개가 있을 수 있으며 subject 객체에서 상태가 바뀌었다는 알려주기를 기다리는, subject에 의존적인 성질을 가진다.

따라서 하나의 subject와 여러개의 observer가 연관된 일대다 관계가 성립하고 해당 의존을 통해 여러 객체에서 동일한 데이터를 제어하도록 할 수 있다.

느슨한 결합

추가로 옵저버 패턴의 장점은 subject와 observer가 느슨하게 결합되어 있는 디자인을 제공하는 것이다. 두 객체가 느슨하게 결합되어 있다는 것은, 해당 객체들이 상호작용을 하지만 서로에 대해 잘 모른다는 것을 의미한다.

subject가 observer에 대해 아는 것은 해당 옵저버가 observer 인터페이스를 구현한다는 것 뿐이다

  • 옵저버의 구상 클래스, 어떤 행동을 하는지 등, 나머지는 알 필요가 없다.

observer는 언제든지 추가 가능하다

  • subject는 observer 인터페이스를 구현하는 객체 목록에먼 의존하기 때문에 실행중에 한 옵저버를 다른 옵저버로 바꿔도 되고, 언제든지 새로운 옵저버를 추가하거나 삭제할 수 있다.

새로운 형식의 observer를 추가하려고 할 때도 subject를 변경할 필요가 없다.

  • observer 인터페이스에 의조하기 때문에 새로운 옵저버 구상 클래스가 생겨도 문제 없다.

subject와 observer는 서로 독립적으로 재사용할 수 있다.

  • 다른 용도로 활용할 일이 있어도 문제 없다. 느슨하게 결합되어 있기 때문

subject와 observer에 변경이 생겨도 서로에게 영향이 미치지 않는다.

  • 마찬가지로 느슨한 결합 덕분

옵저버 패턴 적용

  • Subject, Observer 인터페이스 생성
  • WeatherData는 Subject 인터페이스를 구현하도록 수정
  • 모든 기상 구성요소에서 Observer 인터페이스를 구현하도록 수정
    • subject 객체에서 갱신된 데이터를 전달할 수 있도록 메소드 제공
  • 모든 디스플레이 항목에서 구현하는 DisplayElement 인터페이스를 하나 더 추가
    • 측정값을 바탕으로 각각 다른 내용을 표시하는 메소드인 display() 메소드 추가
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver();
}

public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

public interface DisplayElement {
    void display();
}
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {

    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if(i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObserver() {
        observers.forEach(observer -> observer.update(temperature, humidity, pressure));
    }

    public void measurementsChanged() {
        notifyObserver();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}
public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(70, 60, 30.4f);
        weatherData.setMeasurements(55, 50, 15.4f);
        weatherData.setMeasurements(90, 70, 40.4f);
    }

}

데이터 전달 시 두가지 방식

현재는 subject의 상태가 변경될 때마다 observer에게 알려주고(push) 있다. 옵저버 입장에서는 필요한 상황에서만 주체의 상태를 가져오는 방식(pull)이 더 편할 수도 있지 않을까??

이처럼 옵저버 패턴은 PUSH 방식과 PULL 방식으로 구분할 수 있다.

PUSH 방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식

PULL 방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식

또한 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원한다. 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스이다. 해당 내장 클래스들은 푸시 방식과 풀 방식 모두 가능하다.

그렇다면 자바 내장 클래스를 이용하여 풀 방식으로 수정해보자

Pull 방식으로 수정(with 자바 내장 옵저버 패턴)

  • Observable은 인터페이가 아니라 클래스이므로 WeatherData 클래스에서 해당 클래스를 상속하면서 메소드들을 상속받는다.
  • setChange() 제공
    • 해당 메소드는 상태가 바뀌었다는 것을 밝히기 위한 용도로 사용된다. setChange() 메소드가 호출되지 않은 상태에서 notifiyObservers()가 호출되면 옵저버들에게 연락이 가지 않는다. 해당 메소드를 조건에 따라서 적절히 호출하여 옵저버들에게 연락이 가는 것을 제어할 수 있다.
  • Observer 인터페이스는 앞에서 만든 클래스와 거의 똑같다.

바뀐 WeatherData

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import observer.after.Observer;
import observer.after.Subject;

public class WeatherDataObservable extends Observable {

    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherDataObservable() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers(); // pull 방식, push 방식인 경우 notifyObservers(Object arg);
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // pull 방식이므로 옵저버가 주체 객체의 상태를 알아야하므로 필요하다.
    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
import java.util.Observable;
import java.util.Observer;
import observer.after.DisplayElement;
import observer.after2.WeatherDataObservable;

public class CurrentConditionsDisplayObserver implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    Observable observable;

    public CurrentConditionsDisplayObserver(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof WeatherDataObservable) {
            WeatherDataObservable weatherData = (WeatherDataObservable) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

java.util.Observable의 단점

Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다. 그 이유는 무엇일까?

  • Observer와 Observable이 제공하는 이벤트 모델이 제한적이다.
  • Observable의 notify는 순서를 보장할 수 없으며, 상태 변경은 1:1로 일치하지 않는다.
  • 더 풍부한 이벤트 모델은 java.beans 패키지가 제공하고 있다.
  • 멀티 스레드에서의 신뢰할 수 있고 순서가 보장된 메시징은 java.util.concurrent 패키지의 concurrent 자료 구조들 중 하나를 골라 쓰는 편이 낫다.
  • reactive stream 스타일 프로그래밍은 Flow API를 쓰기를 권한다.

Observable의 문제는 헤드 퍼스트 디자인 패턴에서도 지적하고 있다

  • Observable이 interface가 아니라 class이다.
    • 다른 클래스를 상속하는 클래스가 Observable을 상속할 수 없다. 따라서 재사용성에 제약이 생긴다.
    • 내장된 Observer API하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다.
  • Obserable 클래스의 핵심 메소드를 외부에서 호출할 수 없다.
    • setChanged() 메소드가 protected으로 선언되어 있기 때문이다.
    • 상속보다 구성을 사용한다는 디자인 원칙을 위배한다.

마무리 핵심 정리

  • 옵저버 패턴에서는 객체들 사이에 일대다 관계를 정의한다.
  • 주체 객체는 동일한 인터페이스를 써서 옵저버에 연락한다.
  • 주체와 옵저버는 서로 느슨한 결합
  • 주체에서 데이터를 보내는 푸시방식과 옵저버가 데이터를 가져오는 풀방식이 있다.
    • 풀 방식 추천
  • 옵저버들한테 연락을 돌리는 순서에 절대로 의존하면 안 된다.
    • 만약 의존하도록 했다면 잘못된 것, 느슨한 결합이라고 볼 수 없다.
728x90
728x90

전략 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

전략 패턴 은 알고리즘군을 정의하고 각각의 알고리즘을 캡슐화하며 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

개요

오리 연못 시뮬레이션 게임에서 오리는 헤엄도 치고 꽥꽥거리는 소리도 내는 매우 다양한 오리 종류를 보여줄 수 있다. 가장 처음에는 표준적인 객체지향 기법을 사용하여 Duck이라는 수퍼클래스를 만든 다음, 그 클래스를 확장하여 다른 모든 종류의 오리를 생성

예제 코드

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

}

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("MallardD Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

이제 오리들이 날아다닐 수 있도록 해야한다고 가정했을 경우 어떻게 해야할까??

public abstract class Duck {

    void quack() {
        System.out.println("duck quack");
    }

    void swim() {
        System.out.println("duck swim");
    }

    abstract void display();

    void fly() {
        System.out.println("duck fly");
    }

}

슈퍼클래스에 fly()메소드를 추가함으로써 모든 서브 클래스에서 fly()를 상속받도록 수정

class MallardDuck extends Duck {

    @Override
    void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    @Override
    void display() {
        System.out.println("Redhead Duck");
    }
}

class RubberDuck extends Duck {

    @Override
    void quack() {
        System.out.println("rubber duck quack");
    }

    @Override
    void display() {
        System.out.println("Rubber Duck");
    }
}

하지만 해당 변경사항은 모든 오리가 날 수 있지 않다는 것을 간과했다. 고무로 된 오리 인형 클래스에도 비행 기능하게 된 것이다. 결론적으로 Duck 슈퍼클래스에 fly() 메소드가 추가되면서 일부 서브클래스에는 적합하지 않은 행동이 추가될 수 있는 사이드 이펙트가 발생하게 된 것이다.

RubberDuck의 quack() 메소드처럼 fly() 오버라이드를 해서 아무것도 하지 않게 한다면 가능은 하지만 어떤 부작용이 있을까??

상속의 문제점

이처럼 Duck 행동을 제공하는데 있어서 상속을 사용할 때의 단점은 많다.

  • 서브 클래스에서 코드가 중복
  • 모든 서브클래스의 행동을 알기가 어려움
  • 코드를 변경했을 때 원치 않는 서브클래스들에게 영향을 끼칠 수 있음
  • 실행시에 특징 변경 어려움
  • 모든 오리의 행동을 알기 힘듬

인터페이스?

그렇다면 상속 대신 Flyable, Quackable 인터페이스를 사용하는 방법은 어떨까??

서브클래스에서 Flyable, Quackable 인터페이스를 구현함으로써 고무오리의 비행기능 같은 문제점을 해결할 수 있지만, 코드 재사용성과 관리측면에서 더욱 큰 문제점이 발생하게 된다. (Java 8이하라고 가정)

문제를 명확하게 파악하기

디자인원칙1 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리킨다.

결국 가장 중요한 것은 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화함으로써 나머지 부분에 영향을 미치지 않도록 한다면 시스템의 유연성을 향상시키는 것이다.

즉 확장에는 열려있고 수정에 대해서는 닫혀있도록 하는 것!!

모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공하기 위한 것이다.

*스트레티지 패턴 (Straetegy 패턴) *

Duck 예제에서 달라지는 부분 뽑아보기

Duck 클래스에서 나는 행동과 꽥꽥거리는 행동을 추출하여 행동을 나타낼 클래스 집합을 새로 추가

  • 최대한 유연하게 만들어야 한다.
  • 오리의 행동을 동적으로 바꾸고싶다
    • 세터 메소드를 포함시켜서 동적으로 바꿀 수 있도록 한다면 좋을 것이다.

디자인원칙2 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다

각 행동들을 인터페이스 (FlyBehavior, QuackBehavior)로 표현하고 행동을 구현할 때 Duck 클래스에서 구현하느 것이 아닌 구체적인 행동 클래스에서 구현함으로써 특정 행동은 Duck 국한되지 않는다.

public interface FlyBehavior {
    void fly();
}

public class FlyNoWay implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("날 수 없다");
    }
}

public class FlyWithWings implements FlyBehavior{

    @Override
    public void fly() {
        System.out.println("나는 방법을 구현");
    }
}
public interface QuackBehavior {

    void quack();
}

public class Quack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("꽥");
    }
}

public class MuteQuack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("소리 낼 수 없어요");
    }
}

public class Squeak implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("삑삑");
    }
}

이로써 비행과 꽥꽥거리는 행동들은 Duck 클래스 안에 숨겨져 있지 않으므로 다른 객체에서도 이러한 행동들을 재사용할 수 있다. 그리고 기존의 Duck 클래스들을 전혀 수정하지 않고도 새로운 행동을 추가할 수 있다.

상속의 단점을 해결하는 것 뿐만 아니라 재사용성을 가질 수 있다.

결과

이제 Duck 클래스는 행동을 다른 클래스에 위임함으로써 특정 행동을 할 수 있게 되었다.

public abstract class Duck {

    private FlyBehavior flyBehavior;
    private QuackBehavior quackBehavior;

    public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        this.flyBehavior = flyBehavior;
        this.quackBehavior = quackBehavior;
    }

    public void performFly() {
        flyBehavior.fly();
    }

    public void performQuack() {
        quackBehavior.quack();
    }

    public abstract void display();

    public void swim() {
        System.out.println("모든 오리는 수영한다.");
    }
}

class RubberDuck extends Duck {

    public RubberDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Rubber Duck");
    }
}

class MallardDuck extends Duck {

    public MallardDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Mallard Duck");
    }
}

class RedheadDuck extends Duck {

    public RedheadDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        super(flyBehavior, quackBehavior);
    }

    @Override
    public void display() {
        System.out.println("Redhead Duck");
    }
}

결론

여태까지 사용했던 행동들을 일련의 행동이 아닌 알고리즘군으로 생각하고 똑같은 테크닉을 적용가능 하다.

ex) 지역별 세금 계산 방식 등등

각 오리에는 나는 행동과 꽥꽥하는 행동을 위임하기 위한 클래스들이 있다. 이처럼 두 클래스를 합치는 것을 구성(Composition)을 이용하는 것이라 한다. 오리 클래스에서는 행동을 상속받는 대신, 행동 객체로 구성됨으로써 행동을 부여받게 된다. 구성을 이용하면 유연성을 크게 향상시킬 수 있다. 캡슐화할 수 있도록 만들어주는 것 뿐만 아니라 실행시에 행동을 바꿀 수도 있게 해주기 때문이다.

디자인 원칙3: 상속보다는 구성을 활용한다.

참고 출처

728x90
728x90

디자인 패턴이란


  • 디자인 패턴 은 소프트웨어 디자인에서 일반적으로 발생하는 문제에 대한 일반적인 솔루션이다.
  • 이는 코드에서 반복되는 디자인 문제를 해결하기 위해 사용자 지정할 수 있는 미리 만들어진 청사진과 같다고 생각하면 좋다고 한다.
  • 패턴은 특정 문제를 해결하기 위한 일반적인 개념이다.
  • 패턴과 알고리즘 두 개념 모두 알려진 문제에 대한 일반적인 솔루션을 설명하기 때문에 비슷해 보이지만 다르다.
    • 알고리즘은 항상 어떤 목표를 달성할 수 있는 명확한 일련의 작업을 정의하고 요리법에 비유될 수 있다.
    • 패턴은 솔루션에 대한 보다 높은 수준의 설명이다. 프로그램 개발 시에 자주 부닥치는 애로 상황에 대한 일반적이고 재사용 가능한 추상화된 해결책이다.
      • 결과와 그 기능이 무엇인지 볼 수 있지만 정확한 구현 순서는 사용자에게 달려 있다.

디자인 패턴 장점


  1. 개발자(설계자) 간의 원활한 의사소통
    • 여러 디자인 패턴의 특성을 잘 알고 있어 문제해결 시 어떤 디자인 패턴을 사용하면 좋을지 해결책을 논의할 수 있다.
  2. 소프트웨어 구조 파악 용이
    • 디자인 패턴의 특성을 잘 알고 있기에 어떤 디자인 패턴이 설계할 때 사용되었는지 알면 소프트웨어 전체구조를 쉽게 파악 가능하다.
  3. 재사용을 통한 개발 시간 단축
    • 이미 만들어 놓은 디자인 패턴을 사용하므로 개발시간을 단축시킬 수 있다.
  4. 설계 변경 요청에 대한 유연한 대처
    • 사용자의 지속적인 추가 요청, 환경 변화 등의 설계 변경 요청에 쉽고 빠르게 대처 가능하다

디자인 패턴 단점


잘못된 대상을 문제로 할 수 있다.

  • 보통 패턴의 필요성은 추상화 능력이 부족한 컴퓨터 언어나 기술을 사용할 때 일어난다.
  • 이럴 때 패턴은 언어에 반드시 필요한 조잡한 인터페이스가 된다.

비효율적인 솔루션

  • 디자인 패턴의 아이디어는 이미 승인된 모범 사례를 표준화하려는 시도이다.
  • 원칙적으로 이것은 유익하게 보일 수 있지만, 실제로는 종종 불필요한 코드 중복을 초래한다.
  • "거의 만족스럽지 못한" 설계 패턴보다는 잘 구성된 구현을 사용하는 것이 더 효율적인 솔루션일 수 있다.

잘못되고 무분별한 사용

  • 패턴에 대해 배운 후 더 간단한 코드가 잘 작동하는 상황에서도 패턴을 어디에나 적용하려고 하면 비효율적일 수 있다.

패턴의 종류


패턴의 분류 기준

  • 목적(purpose)
    • 패턴이 무엇을 하는지에 따라 분류
    • 생성 패턴 : 객체의 생성 과정에 관여하는 패턴
    • 구조 패턴 : 클래스나 객체의 합성에 관한 패턴
    • 행동 패턴 : 클래스나 객체들이 상호작용하는 방법과 책임을 분산하는 방법을 정의하는 패턴
  • 범위(scope)
    • 패턴을 적용하는 범위에 따라 분류
    • 클래스 패턴 : 클래스와 서브클래스 간의 관련성을 다루는 패턴
      • 관련성은 주로 상속이며, 컴파일 타임에 정적으로 결정된다.
    • 객체 패턴 : 객체 관려성을 다루는 패턴으로서, 런타임에 변경할 수 있으며 더 동적인 성격을 가진다.
      • 대부분의 패턴들은 어느정도 상속을 이용한다.
      • 23개의 패턴중 대부분 객체 패턴이다.

)

생성 패턴(Creational Patterns)

인스턴스를 만드는 절차를 추상화하는 패턴, 다양한 객체 생성 메커니즘을 제공하여 기존 코드의 유연성과 재사용성을 향상시킨다.

  • 싱글톤 패턴(Singleton)
  • 추상팩토리 패턴(Abstract Factory)
  • 빌더 패턴(Builder)
  • 팩토리 메서드 패턴(Factory Method)
  • 원형 패턴(Prototype)

구조 패턴(Structural Patterns)

객체 및 클래스를 보다 큰 구조로 조립하는 동시에 이러한 구조를 유연하고 효율적으로 유지하는 패턴

  • 적응자 패턴(Adapter or Wrapper)
  • 브리지 패턴(Bridge)
  • 데코레이터 패턴(Decorator)
  • 퍼사드 패턴(Facade)
  • 프록시 패턴(Proxy)

행위 패턴(Behavioral Patterns)

어떤 처리의 책임을 어느 객체에 할당하는 것이 좋은지, 알고리즘을 어느 객체에 정의하는 것이 좋은지 다루는 패턴

  • 옵저버 패턴(Observer)
  • 상태 패턴(State)
  • 전략 패턴(Strategy)
  • 템플릿 메서드 패턴(Template method)
  • 비지터 패턴(Visitor)
  • 역할 사슬 패턴(Chain of Responsibility)
  • 커맨드 패턴(Command)
  • 인터프리터 패턴(Interpreter)
  • 이터레이터 패턴(Iterator)
  • 미디에이터 패턴(Mediator)

참고 문헌 및 출처


  • GoF의 디자인 패턴(개정판) / 에릭 감마, 리처드 헬름, 랄프 존슨, 존 블라시디스 공저 / 김정아 역
  • https://refactoring.guru/
728x90
728x90

싱글턴

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
    • ex) 함수와 같은 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
    • 타입을 인터페이스로 정의한 다음 해당 인터페이스를 구현해서 만든 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.
  • 싱글턴 만드는 방식
    • 두 방식 모두 생성자는 private로 감춰두고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다.

1. public static 멤버가 final 필드인 방식

  • private 생성자는 public static final 필드인 Elvis.INSTANCE를 초기화할 때 한번만 호출된다.
  • public, protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나뿐임이 보장된다.
    • 예외: 권한이 있는 클라이언트는 리플렉션 api인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다.
    • 이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던지도록 한다.
  • 장점
    • 해당 클래스가 싱글턴임이 API에 명백히 드러난다
    • public static 필드가 final 이니 절대로 다른 객체를 참조할 수 없다
    • 간결하다
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }

    public void leaveTheBuilding() { ... }
}

2. 정적 팩터리 메서드를 public static 멤버로 제공

  • Elvis.getInstance는 항상 같은 객체의 참조를 반환하므로 제2의 Elvis 인스턴스는 만들어지지 않다
    • 리플렉션을 통한 예외는 똑같이 적용
  • 장점
    • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
    • 정적 팩터리를 제네릭 싱글턴 팩터리(아이템 30)를 만들 수 있다.
    • 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
      • Elvis::get Instance → Supplier
    • 이런한 장점들이 굳이 필요하지 않는다면 public 필드 방식이 좋다.
public class Elvis {
         private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    public void leaveTheBuilding() {...}
}
  • 두 가지 방식으로 만든 싱글턴 클래스를 직렬화하는 경우 Serializable을 구현하는 것만으로 부족
    • 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다.
    • 그렇지 않으면 직렬화된 인스턴스를 역직렬화할 때 마다 새로운 인스턴스가 생성된다
public Object readResolve() {
    // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTANCE;
  }
}

Singleton은 왜 안티패턴이라 불리는가?

  • SOLID 원칙의 대부분은 인터페이스 설계와 관련이 되어있다. 의존성을 concrete class(구현 클래스)가 아닌 Interface에 두면, 실제 concrete class의 구현이 변경되어도 이를 사용한 코드는 큰 영향을 받지 않는다. 그렇기 때문에 SOLID원칙(OCP, LSP, ISP, DIP등)을 지키기 위해서는 인터페이스로 설계를 해야한다.
  • 하지만 싱글톤을 이용하는 경우 대부분 인터페이스가 아닌 콘크리트 클래스의 객체를 미리 생성해놓고 정적 메소드를 이용하여 사용하게 된다. 이는 여러 SOLID원칙을 위반할 수 있는 가능성을 열어둠과 동시에, 싱글톤을 사용하는 곳과 싱글톤 클래스 사이에 의존성이 생기게 된다. 클래스 사이에 강한 의존성, 즉 높은 결합이 생기게 되면 수정, 단위테스트의 어려움 등 다양한 문제가 발생한다.

1. private 생성자를 갖고 있어 상속이 불가능하다.

  • 싱글톤은 자신만이 객체를 생성할 수 있도록 생성자를 private으로 제한한다. 하지만 상속을 통해 다형성을 적용하기 위해서는 다른 기본생성자가 필요하므로 객체지향의 장점을 적용할 수 없다. 또한 싱글톤을 구현하기 위해서는 객체지향적이지 못한 static 필드와 static 메소드를 사용해야 한다.

2. 테스트하기 힘들다.

  • 싱글톤은 테스트하기가 힘드며 테스트 방법에 따라 불가능할 수 있다. 싱글톤은 생성 방식이 제한적이기 때문에 Mock 객체로 대체하기가 어려우며, 동적으로 객체를 주입하기도 힘들다.
  • 테스트는 개발의 핵심인데, 테스트 코드를 작성할 수 없다는 것은 큰 단점이 된다.

3. 서버 환경에서는 싱글톤이 1개만 생성됨을 보장하지 못한다.

  • 서버에서 클래스 로더를 어떻게 구성하느냐에 따라 싱글톤 클래스임에도 불구하고 1개 이상의 객체가 만들어질 수 있다. 따라서 Java 언어를 이용한 싱글톤 기법은 서버 환경에서 싱글톤이 꼭 보장된다고 볼 수 없다. 또한 여러 개의 JVM에 분산돼서 설치되는 경우에도 독립적으로 객체가 생성된다.
  • 생성자를 private하게 두었어도 reflection을 통해 하나 이상의 오브젝트가 만들어질 수 있다. 또한 여러개의 JVM에 분산돼서 설치가 되는 경우에도 각각 독립적으로 오브젝트가 생기기 때문에 싱글톤으로서의 가치가 떨어진다.

4. 전역 상태를 만들 수 있기 때문에 바람직하지 못하다.

  • 싱글톤의 스태틱 메소드를 이용하면 언제든지 해당 객체를 사용할 수 있고, 전역 상태(Global State)로 사용되기 쉽다. 아무 객체나 자유롭게 접근하고 수정하며 공유되는 전역 상태는 객체지향 프로그래밍에서 권장되지 않는다.
  • 싱글톤 패턴은 객체를 1번 생성하고 재사용할 수 있다는 장점이 있다. 하지만 다른 단점들이 너무 크기 때문에 활용이 쉽지 않았는데, Spring에서는 컨테이너를 통해 직접 객체(빈)들을 싱글톤으로 관리함으로써 객체를 재사용함과 동시에 객체지향스로운 개발을 할 수 있도록 해주었다.

3. 원소가 하나인 열거 타입을 선언

  • public 필드 방식과 비슷하지만 더 간결하고, 직렬화 가능하고 좋다
  • 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 사용 불가
public enum Elvis {
  INSTANCE;

  public void leaveTheBuilding() {...}
}

참고 출처

728x90
728x90

정적 팩터리와 생성자의 제약

  • 선택적 매개변수가 많을 때 적절히 대응하기 어려움
  • ex) 영양정보를 표현하는 클래스
    • 필수 항목: 1회 내용량, n회 제공량, 1회 제공량당 칼로리
    • 선택 항목: 총 지방, 트랜스지방, 콜레스테롤, 나트륨 등 20가지 이상
    • 대다수 제품은 선택 항목 중 대다수의 값이 0
  • 이런 클래스용 생성자 혹은 정적 팩터리는 주로 점층적 생성자 패턴을 사용해왔음

점층적 생성자 패턴

  • telescoping constructor pattern
  • 필수매개변수 생성자, 필수 + 선택 1 생성자... 형태로 선택 매개변수를 전부 다 받는 생성자까지 늘려가는 방식
  • 해당 클래스의 인스턴스를 만들려면 원하는 매개변수를 모두 포함한 생성자 중 가장 짧은 것을 골라 호출
  • 단점
    • 매개변수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다
public class NutritionFacts{
    private final int servingSize;  // 필수
    private final int servings;     // 필수
    private final int calories;     // 선택
    private final int fat;          // 선택
    private final int sodium;       // 선택
    private final int carbohydrate; // 선택

    public NutritionFacts(int servingSize, int servings){
        this(servingSize, servings, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories){
        this(servingSize, servings, calories, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories,
                          int fat){
        this(servingSize, servings, calories, fat, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories,
                          int fat, int sodium){
        this(servingSize, servings, calories, fat, sodium, 0);
    }

    public NutritionFacts(int servingSize, int servings, int calories,
                          int fat, int sodium, int carbohydrate){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
}

자바 빈즈 패턴(javaBeans pattern)

  • 두번째 대안인 자바빈즈 패턴, 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식
  • 심각한 단점
    • 객체 하나를 만들려면 메서드를 여러 개 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이게 된다
    • 이런 일관성이 무너지는 문제 때문에 클래스를 불변으로 만들 수 없으며 스레드 안정성을 얻으려면 추가 작업이 필요하다.
    • 생성이 끝난 객체를 수동으로 freezing하고 얼리기 전에는 사용할 수 없도록 하기도 한다.
      • 하지만 freeze 메서드를 확실히 호출해줬는지를 컴파일러가 보증 x, 런타임 오류 취약
class NutritionFacts{

    private int servingSize  = -1;  // 필수
    private int servings     = -1;  // 필수
    private int calories     = 0;   // 선택
    private int fat          = 0;   // 선택
    private int sodium       = 0;   // 선택
    private int carbohydrate = 0;   // 선택

    public NutritionFacts() { }

    public void setServingSize(int val) { servingSize = val; }

    public void setServings(int servings) { servings = val; }

    public void setCalories(int calories) { calories = val; }

    public void setFat(int fat) { fat = val; }

    public void setSodium(int sodium) { sodium = val; }

    public void setCarbohydrate(int carbohydrate) { carbohydrate = val; }
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);

빌더 패턴(Builder pattern)

  • 앞의 두가지의 장점을 섞은 세번째 대안

    • 점층적 생성자 패턴의 안정성과 자바 빈즈 패턴의 가독성을 겸비한 패턴
  • 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자(정적 팩토리)를 호출해 빌더 객체를 얻는다

    • 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다
    • 매개변수가 없는 build 메서드를 호출해 필요한(보통은 불변인) 객체를 얻는다.

    cf) 빌더는 생성할 클래스안에 정적 멤버 클래스로 만들어두는게 보통

  • https://devlog-wjdrbs96.tistory.com/206

class NutritionFacts{
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder{
        // 필수 매개변수
        private final int servingSize;
        private final int servings;

        // 선택 매개변수 - 기본값으로 초기화한다.
        private int calories     = 0;
        private int fat          = 0;
        private int sodium       = 0;
        private int carbohydrate = 0;

        public Builder(int servingSize, int servings) {
            this.servingSize = servingSize;
            this.servings = servings;
        }

        public Builder calories(int val){
            this.calories = val;
            return this;
        }

        public Builder fat(int val){
            this.fat = val;
            return this;
        }

        public Builder sodium(int val){
            this.sodium = val;
            return this;
        }

        public Builder carbohydrate(int val){
            this.carbohydrate = val;
            return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder){
        servingSize  = builder.servingSize;
        servings     = builder.servings;
        calories     = builder.calories;
        fat          = builder.fat;
        sodium       = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}
  • 계층적으로 설계된 클래스와 함게 쓰기에 좋다
    • 각 계층의 클래스에 관련 빌더를 멤버로 정의, 추상 클래스는 추상 빌더를, 구체 클래스는 구체 빌더
    • Pizzan.Builder 클래스는 재귀적 타입 한정을 이용하는 제네릭 타입
    • 추상 메서드인 self를 더해 하위 클래스에서는 형변환 하지 않고도 메서드 체이닝 지원
      • self 타입이 없는 자바를 위한 우회 방법을 시뮬레이트한 셀프 타입 관용구라 한다.
public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;

    abstract static class Builder<T extends Builder<T>>{
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping){
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        abstract Pizza build();

                // 하위 클래스는 이 메서드를 오버라이딩하여 this를 반환하도록 해야 한다
        protected abstract T self();
    }

    Pizza(Builder<?> builder){
        toppings = builder.toppings.clone();    // 아이템 50 참조
    }
}
// 뉴욕 피자
public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;

    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;

        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }

        @Override
        public NyPizza build() {
            return new NyPizza(this);
        }

        @Override
        protected Builder self() {
            return this;        
        }

        public NyPizza(Builder builder) {
            super(builder);
            size = builder.size;
        }
    }
}

단점

  • 빌더 생성 비용이 문제 될 수도 있다
  • 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다

결론

  • 생성자나 정적 팩터리가 처리해야할 매개변수가 많다면 빌더 패턴을 선택하는게 더 낫다.
  • 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다.
  • 빌더는 클라이언트 코드의 가독성이 좋고, 자바빈즈보다 훨씬 안전하다.
  • 이런 스타일의 빌더 패턴은 Lombok @Builder로 가능
  • https://johngrib.github.io/wiki/design-pattern/builder-pattern/
728x90

+ Recent posts