adapter의 의미는 '접속 소켓', '확장 카드', '(물건을 다른 것에) 맞추어 붙이다', '맞추다'이다. 그림처럼 전원에서 나오는 전기는 대개 교류 200V이지만 노트북은 직류 120V이다. 그러나 우리는 노트북을 사용할 때 아무런 불편 없이 노트북 선을 전원에 그대로 꽃아 사용한다. 이는 중간에 교류 200V를 직류 120V로 바꾸어 주는 무엇인가가 존재하기 때문이다. 이것이 노트북 선 중간에 붙어 있는 어댑터이다.

 

 

  1. 클래스 adapter 패턴 : 상속을 이용한 어댑터 패턴
  2. 인스턴스 adapter 패턴 : 구성(위임)을 이용한 어댑터 패턴

그림에서 client target 인터페이스를 사용하여 메서드를 호출한다. adapter에서는 adaptee 인터페이스를 사용하여 concreteMethod 호출로 변경한다. 이때 client는 중간에 adapter가 존재한다는 것을 인식하지 못한다.

 

<클래스 Adapter 패턴>

<인스턴스(객체) Adapter 패턴>

 

클래스 어댑터 패턴은 다중 상속을 허용하는 프로그래밍 언어에서만 가능한 패턴이다.

 

콘센트를 예를 들어 사용해보자. 우리나라는 기본적으로 220V 플러그인을 사용하는 나라이다. 하지만 외국은 대부분 110V를 사용하는 나라가 많기 때문에 중간에 어댑터를 이용하여 사용한다. 이것을 객체로 디자인하여 사용해보자.

 

인스턴스(객체) 어댑터 패턴

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface Plugin {
    
    public void connect();
    public void disconnect();
    
}
 
public class Plugin_220V implements Plugin {
    @Override
    public void connect() {
        System.out.println("220V connect");
    }
    @Override
    public void disconnect() {
        System.out.println("220V disconnect");
    }
}
 
public interface PluginAdapter {
    public void connect();
    public void disconnect();
}
 
public class Adapter_110V implements PluginAdapter {
    
    private Plugin plugin;
    
    public Adapter_110V(Plugin plugin) {
        this.plugin=plugin;
    }
    @Override
    public void connect() {
        System.out.println("110V->220V convert");
        this.plugin.connect();
    }
    @Override
    public void disconnect() {
        System.out.println("110V->220V convert");
        this.plugin.disconnect();
    }
}
 
public class AdapterMain {
    public static void main(String[] args) {
        PluginAdapter plugin = new Adapter_110V(new Plugin_220V());
        plugin.connect();
        plugin.disconnect();
    }
}
=>result
110V->220V convert
220V connect
110V->220V convert
220V disconnect
 
cs

 

객체 어댑터 패턴은 위와 같이 구성을 이용하여 어댑터 클래스를 구성한다. 클라이언트는 어댑터 인터페이스로만 의존하고 실제적인 어댑티는 알지 못한다.

posted by 여성게
:

composite의 의미는 '합성의', '합성물', '혼합 양식'이다. 이를 통해 composite 패턴이 뭔가 합쳐진 형태임을 짐작할 수 있다. 또 composite 패턴의 구성을 보면 일반적인 트리 구조를 하고 있는데, [그림 5-34]처럼 부분-전체의 상속 구조이다. 이와 같이 표현되는 조립 객체를 컴포지트 객체(composite object)라고 한다.

composite 패턴은 사용자가 단일 객체와 복합 객체 모두 동일하게 다루도록 한 것이다. 이런 형태는 재귀적인 구조로서, 마치 파일 구조에서 디렉토리 안에 파일이 존재할 수도 있고, 또 다른 디렉토리(서브 디렉토리)가 존재할 수 있는 것과 같다. 즉 composite 패턴은 그릇(디렉토리)과 내용물(파일)을 동일시해서 재귀적인 구조를 만들기 위한 설계 패턴이다.

 

 

컴포지트 패턴을 가장 잘 설명할 수 있는 예제는 파일과 디렉토리 관계이다. 가장 간단하게 파일과 디렉토리를 구현해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class File {
    
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
    
}
 
public class Directory {
    
    private String name;
    private List<File> files;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void addFile(File file) {
        if(!files.contains(file)) files.add(file);
        else System.out.println("동일한 파일이 존재합니다.");
    }
    
}
cs

 

위와 같이 구현했다고 생각해보자. 얼핏보면 괜찮은 것 같다. 하지만 만약 디렉토리 밑에 디렉토리를 넣고 싶다면? 어떻게 해야할까? 여기서 생각해보자. 디렉토리 안에는 파일도 들어갈 수 있고 또 다른 디렉토리가 들어갈 수도 있다. 여기서 파일하나가 단수 객체라면 디렉토리는 복수 객체가 될 수 있다. 또한 최상위는 하나의 디렉토리로 이루어지고 그 밑으로 디렉토리 및 파일이 들어간다. 즉, 전체 관계가 하나의 디렉토리이고 그 밑에 부분이 디렉토리 또는 파일이 될 수 있다. 이렇게 단수 혹은 복수를 동일한 인터페이스로 다룰 수 있게 하는 전체-부분관계를 구현할때 가장 유용한 것이 컴포지트 패턴이다. 

 

컴포지트 패턴을 다시 클래스다이어그램으로 나타내면 아래와 같다.

 

 

Component - 구체적인 부분, 즉 Leaf 클래스와 전체에 해당하는 Composite 클래스에 공통 인터페이스를 제공한다.

Leaf - 구체적인 부분 클래스로 Composite 객체의 부품으로 설정한다.

Composite - 전체 클래스로 복수 개의 Component를 갖도록 정의한다. 그러므로 복수 개의 Leaf, 심지어 복수 개의 Composite 객체를 부분으로 가질 수 있다.

 

우리는 맨 처음의 그림과는 다르게 파일 시스템을 설계할 것이다. 파일 시스템은 추상적으로 보면 트리와 같은 구조를 가지고 있다. 즉, Component를 Node로 표현하고 Leaf로 File Composite로 Directory를 표현할 것이다. 바로 구현해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public abstract class Node {
    
    private String name;
    private int depth=0;
    
    public Node(String name) {
        this.name=name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getDepth() {
        return depth;
    }
 
    public void setDepth(int depth) {
        this.depth = depth;
    }
    
    public abstract int getSize();
    public abstract void print();
}
 
public class File extends Node{
    
    private int size;
    
    public File(String name, int size) {
        super(name);
        this.size=size;
    }
 
    @Override
    public int getSize() {
        return this.size;
    }
 
    @Override
    public void print() {
        System.out.println("File - "+getName());
    }
    
}
 
public class Directory extends Node{
    
    private List<Node> nodes = new ArrayList<>();
    
    public Directory(String name) {
        super(name);
    }
    
    public void addNode(Node node) {
        nodes.add(node);
    }
    
    public void removeNode(Node node) {
        nodes.remove(node);
    }
    
    public int getSize() {
        int size = nodes.size();
        System.out.println("Directory size - "+size);
        return size;
    }
    
    public void print() {
        
        for(Node node : nodes) {
            node.print();
        }
        
    }
    
}
cs

 

완벽한 구현은 아니지만 파일 시스템을 컴포지트 패턴으로 구현 가능하다라는 정도만 알자!

posted by 여성게
:

abstract factory는 '추상적인 공장'이란 뜻으로, 밑의 그림과 같이 여러 개의 concrete Product 추상화시킨 것이다. 따라서 구체적인 구현은 concrete Product 클래스에서 이루어진다. abstract factory에서는 사용자에게 인터페이스(API)를 제공하고, 인터페이스만 사용해서 부품을 조립하여 만든다. 즉 추상적인 부품을 조합해서 추상적인 제품을 만든다.

 

 

2019/08/19 - [디자인패턴] - 디자인패턴 - 팩토리 메소드 패턴(Factory Method pattern)

 

디자인패턴 - 팩토리 메소드 패턴(Factory Method pattern)

factory는 '공장'이란 뜻이고, 공장은 물건을 만드는 곳이다. 여기서 물건에 해당되는 것이 바로 인스턴스이다. factory method 패턴은 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고, 하위 클래스에서..

coding-start.tistory.com

 

이전 포스팅에서 다루어봤던 팩토리 메소드 패턴의 확장판이라고 볼수 있는 것이 추상 팩토리 패턴이다. 팩토리 메소드 패턴을 다시 떠올려보자. 엘리베이터는 특정 스케쥴링 방식에 따라 다른 방식으로 동작한다. 즉, 스케쥴러 클래스의 선택에 따라 엘리베이터의 동작이 달라지는 것이다. 하지만 이런 경우를 생각해보자 !

 

엘리베이터 제조업체가 여러개가 있다. LG,Samsung,Hundai 등의 많은 제조업체가 있고 해당 업체의 엘리베이터를 사용하려면 각각 제조업체의 모터,문,렘프 등을 사용해야한다.(이번 예제에서는 각 업체마다 모터, 문만 다룬다.) 경우에 따라 부품의 수가 더 많아 질 수도 있고 제조업체도 더욱 많아 질 수도 있다. 

 

만약 기존에 사용했던 팩토리 메소드 패턴을 사용한다면 어떻게 될까? Motor,Door라는 인터페이스가 있고 각각을 구현한 LGMotor,LGDoor,SamsungMotor,SamsungDoor 등 업체마다 다른 부품에 해당하는 클래스를 생성해주어야 하고 각각을 생성해주는 팩토리 클래스를 만들어주어야 한다.(LGMotorFactor,SamsungMotorFactory,HundaiFactory...) 3개의 업체라면 객체 생성을 담당하는 팩토리 클래스가 6개가 생성된다.(MotorFactory - LG,Samsung,Hundai / DoorFactory - LG,Samsung,Hundai) 딱 봐도 비효율적일 것같다라는 느낌이 든다.

 

여기서 핵심은 특정 제조업체의 엘리베이터를 이용하면 그들의 부품만 사용한다는 뜻이다. 즉, 관련있는 객체(모터,문)들이 제조업에 따라 같이 생성이 된다는 것이다. 팩토리 메소드 패턴은 한종류의 객체를 생성하는 롤을 갖고 있다면 추상 팩토리 패턴은 다수의 연관있는 객체들의 일련의 생성을 담당하고 있다고 보면 된다. 바로 구현해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class AbstractFactory {
    
    public abstract Motor createMotor();
    public abstract Door createDoor();
    
    enum Vendor{
        LG,SAMSUNG,HUNDAI;
    }
    
    public static AbstractFactory getFactory(Vendor vendor) {
        
        AbstractFactory factory = null ;
        
        switch (vendor) {
        case LG:
            factory = LGElevatorFactory.getInstance();
            break;
        case SAMSUNG:
            factory = SamsungElevatorFactory.getInstance();
            break;
        case HUNDAI:
            factory = HundaiElevatorFactory.getInstance();
            break;
        default:
            break;
        }
        
        return factory;
        
    }
    
}
 
public class LGElevatorFactory extends AbstractFactory{
    
    private static final LGElevatorFactory FACTORY = new LGElevatorFactory();
    
    public static AbstractFactory getInstance() {
        return FACTORY;
    }
 
    @Override
    public Motor createMotor() {
        return new LGMotor();
    }
 
    @Override
    public Door createDoor() {
        return new LGDoor();
    }
    
}
cs

 

추상 팩토리 클래스를 하나 만들어주고 각 제조사에 해당하는 팩토리클래스를 생성해준다. 이전 팩토리메소드 패턴과 같이 부품마다 별도의 팩토리 클래스를 두는 것이 아니라 각 제조사마다 팩토리 메소드를 만들어서 관련있는 모든 부품에 대한 생성을 제조사 팩토리 클래스에 위임한다. 또한 모든 제조사의 팩토리 메소드는 동일한 타입의 부품을 생성하는 팩토리 메소드를 가지니, 추상 클래스도 추상 팩토리 클래스를 만들어 상속받게 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public abstract class Door {
    
    enum DoorStatus{
        OPEN,CLOSE;
    }
    
    public Door() {
        this.status=DoorStatus.CLOSE;
    }
    
    private DoorStatus status;
 
    public DoorStatus getStatus() {
        return status;
    }
 
    public void setStatus(DoorStatus status) {
        this.status = status;
    }
    
    public void open() {
        if(this.status.equals(DoorStatus.OPEN)) return;
        this.status=DoorStatus.OPEN;
        System.out.println("문을 엽니다.");
    }
    
    public void close() {
        if(this.status.equals(DoorStatus.CLOSE)) return;
        this.status=DoorStatus.CLOSE;
        System.out.println("문을 닫습니다.");
    }
    
}
 
public abstract class Motor {
    
    private Door door;
    private MotorStatus status;
    
    public Motor() {
        this.status=MotorStatus.STOP;
    }
    
    enum MotorStatus{
        MOVING,STOP;
    }
    
//템플릿 메소드 패턴 적용.
    public void move() {
        //모터가 이미 움직이고 있다면 아무런 행동을 하지 않는다.
        if(getStatus().equals(MotorStatus.MOVING)) return;
        //문이 열려있다면 닫는다.
        if(door.getStatus().equals(DoorStatus.OPEN)) door.close();
        doMove();
        //모터를 이동중으로 설정한다.
        setStatus(MotorStatus.MOVING);
    }
    
    /*
     * 업체마다 doMove의 행동은 다르다.
     */
    public abstract void doMove();
 
    public MotorStatus getStatus() {
        return status;
    }
 
    public void setStatus(MotorStatus status) {
        this.status = status;
    }
 
    public Door getDoor() {
        return door;
    }
 
    public void setDoor(Door door) {
        this.door = door;
    }
    
    
    
}
cs

 

각 부품에 대한 추상클래스이다. 각 제조사 부품마다 동일한 행동을 하지만 자세한 구동 프로세스가 다를 수 있으므로 모터 추상클래스는 템플릿 메소드 패턴을 이용하여 상속받게 하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LGMotor extends Motor{
 
    @Override
    public void doMove() {
        System.out.println("LG motor 가동");
    }
    
    
}
 
public class LGDoor extends Door{
 
}
cs

 

LG모터와 문 코드이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AbstractFactoryMain {
 
    public static void main(String[] args) {
        
        AbstractFactory factory = AbstractFactory.getFactory(Vendor.LG);
        
        Motor motor = factory.createMotor();
        Door door = factory.createDoor();
        motor.setDoor(door);
        
        door.open();
        motor.move();
        door.open();
    }
 
}
cs

 

모터와 문에 대한 클래스의 구현체를 생각할 필요가 없어졌다. 단순히 추상 팩토리 클래스에 벤더값만 전달하면 모든 연관있는 부품에 대한 생성은 각 업체마다의 팩토리 클래스가 담당하게 된다.

 

어떻게 보면 팩토리 메소드 패턴의 확장판이라고 볼수 있다. 추상 팩토리 패턴은 일련의 관련있는 객체의 생성을 캡슐화 혹은 추상화 하기 위해 사용하는 패턴이다. 이제는 클라이언트 코드에서 업체가 바뀌어도 코드의 변경을 필요 없게 되었다. 단순히 어떠한 업체인지 벤더 값만 전달해주고 팩토리 클래스가 생성해주는 모터와 문의 구현체를 몰라도되고 단순히 사용만 하면 된다.

posted by 여성게
:

factory는 '공장'이란 뜻이고, 공장은 물건을 만드는 곳이다. 여기서 물건에 해당되는 것이 바로 인스턴스이다. factory method 패턴은 상위 클래스에서 객체를 생성하는 인터페이스를 정의하고, 하위 클래스에서 인스턴스를 생성하도록 하는 방식이다. 즉 상위 클래스에서는 인스턴스를 만드는 방법만 결정하고, 구체적인 클래스 이름은 뒤로 미룬다. 따라서 객체를 생성하는 인터페이스와 실제 객체를 생성하는 클래스를 분리할 수 있다. 예를 들어 그림처럼 추상 단계에서는 생성하려는 객체의 클래스를 정확히 지정하지 않고, concreteCreator 클래스에서 인스턴스를 생성한다.

 

 

다시 쉽게 정리하면 특정 클래스에서 사용할 클래스를 직접 new로 생성하면 강한 결합이 생기게 된다. 즉, 무엇인가 전략이 바뀔때마다 코드의 변경이 생기게 된다. 여기서 핵심은 사용할 인스턴스를 생성하는 코드를 특정 클래스가 생성하도록 함으로써 클래스 간의 결합도를 낮추게 된다는 것이다. 위의 그림에서 전략은 Product로 볼 수 있고, concreteCreator가 적절한 Product를 생성하는 역할을 담당하게 된다.

 

하나 예를 들어보자. 건물 내의 엘리베이터는 시간대에 따라 작동 방식이 달라져야한다. 오전 출근시간과 점심시간에는 많은 사람들이 엘리베이터를 사용하게 된다. 그렇다면 이 시간대에는 처리량이 극대화될 수 있는 엘리베이터 스케쥴링방식을 사용해야한다. 그 이후 시간대에는 단순히 기다리는 시간이 최소화되도록 하는 스케쥴링방식을 사용한다. 여기서 또 하나의 조건은 특정 스케쥴링 방식을 직접 선택할 수도 있어야한다.

 

아래와 같이 단순한 구현이 가능하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class ElevatorManager {
    
    List<Elevator> elevators;
    
    public ElevatorManager(int count) {
        elevators = new ArrayList<>(count);
        
        IntStream.range(0, count).forEach(i->elevators.add(new Elevator()));
        
    }
    
    public void requestElevator(int destination, Scheduler schedule) {
        
        ElevatorScheduler scheduler = null ;
        
        int hour = LocalDateTime.now().getHour();
        
        if(schedule.equals(Scheduler.DYNAMIC)) {
            System.out.println("스케줄링방식 - "+schedule);
            if( (9<=hour && hour<=10|| (12<=hour && hour<=1)) {
                scheduler = new ThroughputScheduler();
            }else {
                scheduler = new ResponseTimeScheduler();
            }
        }else if(schedule.equals(Scheduler.THROUGHPUT)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ThroughputScheduler();
        }else if(schedule.equals(Scheduler.RESPONSETIME)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ResponseTimeScheduler();
        }
        
        int elevatorIndex = scheduler.selectElevator(elevatorCount());
        
        elevators.get(elevatorIndex).gotoElevator(destination, elevatorIndex);
    }
    
    private int elevatorCount() {
        return this.elevators.size();
    }
    
    enum Scheduler{
        THROUGHPUT,RESPONSETIME,DYNAMIC;
    }
}
 
public class Elevator {
    
    public void gotoElevator(int destination, int eleNum) {
        System.out.println(eleNum+"번 엘리베이터가 "+destination+"층으로 가는 중입니다.");
    }
}
 
public interface ElevatorScheduler {
    
    public int selectElevator(int count);
    
}
 
public class ThroughputScheduler implements ElevatorScheduler {
 
    @Override
    public int selectElevator(int count) {
        return (int)(Math.random()*count)+1;
    }
 
}
 
public class ResponseTimeScheduler implements ElevatorScheduler {
 
    @Override
    public int selectElevator(int count) {
        return (int)(Math.random()*count)+1;
    }
 
}
cs

 

문제가 무엇일까? 만약에 스케쥴링방식이 추가되거나 혹은 스케쥴링 선택 정책이 변경되었다면 ElevatorManager의 requestEleveator 메소드가 변경되어야 할 것이다. 즉, 특정 상황에 따라 알고리즘을 수행하게 되는 객체(스케쥴러)가 달라지기 때문에 혹은 정책이 바뀌면서 알고리즘을 수행하는 객체가 바뀌게 된다. 그렇다면 이러한 상황에서는 어떻게 하면 좋을까? 바로 팩토리 메소드 패턴이다. 팩토리 메소드 패턴으로 객체 생성을 담당하도록 별도 클래스 및 메소드로 분리하는 것이다. 그렇다면 ElevatorManager는 객체 생성을 직접적으로 신경쓸 필요가 없고 책임을 팩토리 클래스로 위임해버리는 것이다. 

 

팩토리 메소드 패턴으로 변경해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class ElevatorManager {
    
    List<Elevator> elevators;
    
    public ElevatorManager(int count) {
        elevators = new ArrayList<>(count);
        
        IntStream.range(0, count).forEach(i->elevators.add(new Elevator()));
        
    }
    
    public void requestElevator(int destination, Scheduler schedule) {
        
        Factory factory = new SchedulerFactory();
        
        ElevatorScheduler scheduler = factory.createScheduler(schedule);
        
        int elevatorIndex = scheduler.selectElevator(elevatorCount());
        
        elevators.get(elevatorIndex).gotoElevator(destination, elevatorIndex);
    }
    
    private int elevatorCount() {
        return this.elevators.size();
    }
    
    enum Scheduler{
        THROUGHPUT,RESPONSETIME,DYNAMIC;
    }
}
 
public abstract class Factory {
    
    public abstract ElevatorScheduler createScheduler(Scheduler scheduler);
    
}
 
public class SchedulerFactory extends Factory{
 
    @Override
    public ElevatorScheduler createScheduler(Scheduler schedule) {
        
        ElevatorScheduler scheduler = null ;
        
        int hour = LocalDateTime.now().getHour();
        
        if(schedule.equals(Scheduler.DYNAMIC)) {
            System.out.println("스케줄링방식 - "+schedule);
            if( (9<=hour && hour<=10|| (12<=hour && hour<=1)) {
                scheduler = new ThroughputScheduler();
            }else {
                scheduler = new ResponseTimeScheduler();
            }
        }else if(schedule.equals(Scheduler.THROUGHPUT)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ThroughputScheduler();
        }else if(schedule.equals(Scheduler.RESPONSETIME)) {
            System.out.println("스케줄링방식 - "+schedule);
            scheduler = new ResponseTimeScheduler();
        }
        
        return scheduler;
        
    }
    
    
}
cs

 

위와 같이 코드를 변경하였다. 이제 ElevatorManager는 자신의 일만 신경쓰면 된다. 스케쥴러 선택은 팩토리 클래스에게 맡겨버리면 되기 때문이다. 혹시나 스케쥴링 정책 혹은 새로운 스케쥴링 방식이 생겨나도 다른 곳은 아무곳도 신경쓸 필요가 없다. 단순히 팩토리 클래스에만 변경해주면 되기 때문이다. 

 

다시 한번 팩토리 메서드 패턴의 정의를 보자. 

 

객체를 생성하는 코드를 별도의 클래스/메서드로 분리함으로써 객체 생성 방식의 변화에 대비하는데 유용한 패턴이다.
posted by 여성게
:

decoration은 '장식(포장)'이란 뜻이다. 빵집에서 케이크를 만들 때 먼저 둥근 모양의 빵을 만든다. 이 위에 초콜릿을 바르면 초콜릿 케이크가 되고, 치즈를 바르면 치즈 케이크가 된다. 또 생크림을 바르고 과일을 많이 올려놓으면 과일 생크림 케이크가 된다.

 

이처럼 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 설계 패턴을 decorator 패턴이라고 한다. 이것은 기능 확장이 필요할 때 상속의 대안으로 사용한다.

그림에서 decorator 클래스가 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 해당되고, concreteDecorator클래스는 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 것에 해당된다.

 

카페 음료 가격을 계산하기 위해 우리는 클래스를 설계해야 한다고 하자. 보통 가장 많이 생각할 수 있는 방안이 상속을 이용한 구현이다. 아래와 같이 쉽게 구현할 수 있다.

 

기본적으로 커피를 제조할때 에스프레소를 내리고 해당 에스프레소에 다른 첨가물을 첨가하여 커피를 제조한다. 예를 들어 카페모카는 에스프레소에 우유,초코가 들어가므로 에스프레소 가격(2)+우유,초코(2)로 총 4라는 가격이 책정된다고 생각해보자. 그렇다면 상속을 이용하여 아래와 같이 구현할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
 
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
 
public class CafeMocha extends Beverage{
    
    public CafeMocha() {
        super.setDescription("카페모카");
    }
    
    @Override
    public int cost() {
        return 2+2;
    }
    
    
}
cs

 

위와 같이 새로운 음료가 추가되었으므로, 새로운 클래스가 추가된다. 만약 휘핑크림이 추가된 카페모카라면? 아래와 같이 추가적인 클래스가 생성되어야 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CafeMochaWithWhip extends Beverage{
 
    public CafeMochaWithWhip() {
        super.setDescription("카페모카 휘핑크림 추가");
    }
    
    @Override
    public int cost() {
        return 2+2+1;
    }
    
    
}
cs

 

얼핏보면 괜찮은 것 같지만, 만약 음료의 종류가 엄청나게 많고 선택옵션이 엄청나게 많다면? 클래스수는 기하급수적으로 늘어날 것이다. 이것은 역시 디자인 원칙중 OCP를 위반하고 있는 것이다.(개방-폐쇄 원칙)

 

이러한 상황에서 사용하는 디자인 패턴이 데코레이터 패턴이다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
cs

 

데코레이터 패턴은 구성이라는 방법을 사용한다. 즉, 추상 타입을 가지는 데코레이터들 혹은 기본구성 클래스들이어야만 데코레이터를 이용할 수 있다. 왜냐하면 뒤에서 설명하겠지만 데코레이터 역할을 클래스들이 모두 추상타입의 구현체들을 감싸게 되는 구조이기 때문이다. 즉, 위 추상클래스는 기본구성(에스프레쏘) 그리고 데코레이터(선택옵션,모카,휘핑이 들어간 모카 등)들의 타입을 마춰주기 위한 추상타입의 역할을 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
cs

 

추상타입의 Beverage를 상속하는 기본구성을 구현한 예제이다. 데코레이터 패턴의 핵심은 기본 구성으로부터 출발하여 이것저것 데코레이터 옵션을 덧붙여나가는 것이다. 즉, 데코레이터 패턴의 시작이 되는 기본구성은 바로 추상타입의 Beverage를 상속받아 구현한다.

 

1
2
3
public abstract class BeverageDecorator extends Beverage {
    
}
cs

 

데코레이터(추가옵션)들이 상속하게 될 추상클래스이다. 데코레이터들의 모든 타입 그리고 기본구성과 데코레이터들의 타입이 동일해야하므로 Beverage를 상속한다. 만약 데코레이터들의 추가적인 행위를 구현하려면 이쪽에 추상메소드로 선언해도 무방할듯하다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Mocha extends BeverageDecorator{
 
    private Beverage beverage;
    
    public Mocha(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 2+this.beverage.cost();
    }
    
}
 
public class Whipping extends BeverageDecorator{
    
    private Beverage beverage;
    
    public Whipping(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 1+this.beverage.cost();
    }
    
}
cs

 

데코레이터 패턴의 핵심인듯하다. 데코레이터 객체들의 구현체이다. 잘보면 기본구성 혹은 데코레이터 객체들을 래핑하기 위해 하나의 인스턴스 변수를 가지고 있다는 것이 핵심이며 기본구성과 동일한 행위(가격계산,cost())를 오버라이드하여 구현하였으며 래핑한 객체의 동일한 행위 메소드를 호출하고 있는 것을 볼 수 있다. 이렇게 기본구성과 데코레이터 객체들의 상위타입을 마춰줌으로써 계속해서 생성자로 객체를 래핑하여 옵션을 추가 할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DecoratorMain {
 
    public static void main(String[] args) {
        
        Beverage espresso = new Espresso();
        System.out.println("description : "+espresso.getDescription() +" cost : "+espresso.cost());
        
        Beverage mocha = new Mocha(new Espresso());
        mocha.setDescription("카페모카");
        System.out.println("description : "+mocha.getDescription() +" cost : "+mocha.cost());
        
        Beverage mochaWithWhip = new Whipping(new Mocha(new Espresso()));
        mochaWithWhip.setDescription("카페모카 휘핑추가");
        System.out.println("description : "+mochaWithWhip.getDescription() +" cost : "+mochaWithWhip.cost());
        
    }
 
}
 
=>result
description : 에스프레쏘 cost : 2
description : 카페모카 cost : 4
description : 카페모카 휘핑추가 cost : 5
 
cs

 

메인클래스에서 데코레이터 패턴이 적용된 클래스를 사용하고 있는 것을 보니 많이 낯이 익다. 무엇일까? 바로 Java I/O들을 구현할 때, 데코레이터 패턴이 적용되었다! 

 

InputStream is = new BufferedInputStream(new FileInputStream("filepath"));

 

지금까지 간단히 데코레이터 패턴을 다루어보았다.

posted by 여성게
:

command의 의미는 '명령어'이다. 문서편집기의 복사(copy), 붙여넣기(paste), 잘라내기(cut) 등도 모두 명령어이다. 그런데 이런 명령어를 각각 구현하는 것보다는 [그림 5-52]처럼 하나의 추상 클래스 execute() 메서드를 하나 만들고 각 명령이 들어오면 그에 맞는 서브 클래스(copy_command)가 선택되어 실행하는 것이 효율적이다. 이는 함수 오버로딩(overloading)과 같은 추상화 개념을 사용한 것이다.

그러나 command 패턴은 단순히 명령어를 추상 클래스(abstract class)와 구체 클래스(copy_command,cut_command, paste_command)로 분리하여 단순화한 것으로 끝나지 않고, 명령어에 따른 취소(undo) 기능까지도 포함한다(사용자 입장에서는 해당 명령어를 실행했다가 취소(undo)하기도 하기 때문이다). 이렇게 프로그램의 명령어를 구현할 때는 command 패턴을 활용할 수 있다.

 

이러한 커맨드 패턴은 아주 예전에 사용하던 servlet에서도 사용하던 패턴입니다. FrontController를 최앞단에 두고 FrontController에서 모든 요청을 다 받은 후 커맨드 패턴을 이용하여 분기처리했습니다. 즉, 이렇게 어떠한 이벤트에 대해 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할때 유용한 패턴입니다.

 

만능 리모콘을 예제로 살펴보겠습니다. 리모콘은 실내의 전등을 키고 끄는 버튼이 있고 TV를 켜고 끄는 버튼도 있습니다. 추후에는 다양한 전자기기를 다룰 수 있는 버튼이 추가될 가능성이 있습니다. 이럴 경우에는 어떻게 커맨드 패턴을 적용해 볼 수 있을까요? 아래와 같이 쉽게 구현은 가능합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class SimpleRemoteController {
    
    private TV tv;
    private Light light;
    private Mode mode;
    
    enum Mode{
        TV,LIGHT;
    }
    
    public SimpleRemoteController(TV tv, Light light) {
        this.tv=tv;
        this.light=light;
    }
    
    public void setMode(Mode mode) {
        this.mode=mode;
    }
    
    public void pressOnButton() {
        if(this.mode.equals(Mode.TV)) {
            tv.turnOn();
        }else if(this.mode.equals(Mode.LIGHT)) {
            light.turnOff();
        }
    }
    
    ...
    
}
cs

이 코드에는 분명 문제가 있습니다. SOLID 원칙 중 분명 OCP에 위반되기 때문이죠. 확장에는 열려있고 변경에는 닫혀있어야 하는 원칙을 누가 봐도 어기고 있습니다. 기능이 추가될때마다 코드 변경이 필요하기 때문이죠. 하지만 이것을 커맨드 패턴을 이용해 구현한다면 리모콘을 컨트롤하는 클래스는 어떠한 행위에 대한 구체적인 구현을 몰라도 되며 구체적인 행동을 캡슐화하고 있는 커맨드 객체만 받으면 되기에 훨씬 확장이 수월한 코드가 나오게 됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RemoteControlInvoker {
    
    private Command command;
    
    public RemoteControlInvoker(Command command) {
        this.command=command;
    }
 
    public void setCommand(Command command) {
        this.command = command;
    }
    
    public void pressedButton() {
        command.execute();
    }
    
}
cs

 

이제는 리모콘 역할을 하는 클래스에는 구체적인 행위가 정의되어 있지 않습니다. 생성자의 매개변수 혹은 setter를 이용하여 주입받은 커맨드 객체로 행위를 호출하는 호출자(Invoker) 역할을 하게됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public interface Command {
    public void execute();
}
 
public class TvOnCommand implements Command {
    
    private TV tv;
    
    public TvOnCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOn();
    }
 
}
 
public class TvOffCommand implements Command {
    
    private TV tv;
    
    public TvOffCommand(TV tv) {
        this.tv=tv;
    }
    
    @Override
    public void execute() {
        tv.turnOff();
    }
 
}
 
public class LightOnCommand implements Command {
    
    private Light light;
    
    public LightOnCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOn();
    }
 
}
 
public class LightOffCommand implements Command {
    
    private Light light;
    
    public LightOffCommand(Light light) {
        this.light=light;
    }
    
    @Override
    public void execute() {
        light.turnOff();
    }
 
}
 
cs

 

Command라는 인터페이스를 만들고 execute()라는 메소드를 선언합니다. 그리고 구체적인 요청(행위)을 하나의 커맨드 객체로 캡슐화 합니다. 해당 커맨드 객체는 요청을 커맨드 객체에게 간접적으로 받아서 처리하는(Receiver)를 한번 감싸는 행위 캡슐화 역할을 하게 됩니다.

특이하게 이제는 클라이언트 입장에서 어떠한 행위든지 상관없이 execute()만 호출하면 되는 것입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TV {
    
    public void turnOn() {
        System.out.println("TV 켰음");
    }
    public void turnOff() {
        System.out.println("TV 껐음");
    }
}
 
public class Light {
    
    public void turnOn() {
        System.out.println("불 켰음");
    }
    public void turnOff() {
        System.out.println("불 껐음");
    }
}
 
 
cs

 

리시버 객체의 구현입니다.

 

이제 리모콘 역할을 하는 Invoker는 어떠한 기능이 추가되더라도 코드 변경없이 기능을 얼마던지 추가 할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        RemoteControlInvoker invoker = new RemoteControlInvoker(tvOnCommand);
        invoker.pressedButton();
        invoker.setCommand(tvOffCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOnCommand);
        invoker.pressedButton();
        invoker.setCommand(lightOffCommand);
        invoker.pressedButton();
        
    }
 
}
cs

 

마지막으로 일련의 행동을 연속적으로 처리할 수 있는 커맨드 패턴은 어떻게 구현할까 생각해볼 수 있습니다. 이런 패턴을 매크로 커맨드 패턴이라고 합니다. 구현은 간단히 아래와 같이 진행할 수 있습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MacroCommand implements Command {
    
    private List<Command> commands;
    
    public MacroCommand() {
        commands= new ArrayList<>();
    }
    
    public void addCommand(Command...commands) {
        if(commands.length < 1throw new NullPointerException();
        
        for(Command c : commands) {
            this.commands.add(c);
        }
    }
    
    @Override
    public void execute() {
        if(commands.size()>0) {
            commands.stream().forEach(c->c.execute());
        }
    }
 
}
 
public class CommandMain {
 
    public static void main(String[] args) {
        
        TV tv = new TV();
        TvOnCommand tvOnCommand = new TvOnCommand(tv);
        TvOffCommand tvOffCommand = new TvOffCommand(tv);
        
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        LightOffCommand lightOffCommand = new LightOffCommand(light);
        
        MacroCommand macroCommand = new MacroCommand();
        macroCommand.addCommand(tvOnCommand,
                                tvOffCommand,
                                lightOnCommand,
                                lightOffCommand);
        RemoteControlInvoker invoker = new RemoteControlInvoker(macroCommand);
        invoker.pressedButton();
        
    }
 
}
cs

 

여기까지 간단한 커맨드 패턴 예제를 다루어봤습니다. 사실 커맨드 패턴은 위보다도 더욱 복잡하게 사용가능한 패턴입니다. 예를 들어 상태값을 가져 상태에 따른 행위를 할 수 있고, 또한 확장하여 일련의 작업들을 작업 큐에 넣어 쓰레드들이 각각 하나의 Command를 맡아서 작업을 할 수도 있는 등 구현의 범위는 넓습니다.

posted by 여성게
:

state의 의미는 '상태'이다. 엘리베이터의 정지, 하강, 상승 상태처럼 객체 상태도 상황에 따라 달라진다. state 패턴은 동일한 동작을 객체 상태에 따라 다르게 처리해야 할 때 사용한다.

이렇게 하나의 객체에 여러 가지 상태(예, 정지, 상승, 하강)가 존재할 때 패턴을 사용하지 않고 프로그래밍을 하면 if 문 또는 switch 문을 사용하여 처리한다. 그런데 신규 상태(예, 문 열림, 문 닫힘)가 발생하면 프로그램을 다시 수정해야 한다.

이런 경우에 state 패턴은 그림처럼 객체 상태를 캡슐화하여 클래스화(state 인터페이스)함으로써 그것을 참조하게 하는 방식으로 상태에 따라 다르게 처리(upState, stopState, downState)할 수 있도록 한 것이다. 따라서 변경 시(신규 상태 추가) 원시 코드의 수정을 최소화할 수 있고, 유지보수를 쉽게 할 수 있다.

 

형광등을 예로 들어보자. 형광등의 첫 상태는 스위치가 꺼진 OFF 상태라고 생각하자. 상태가 OFF인 상태에서 스위치를 ON하면 불이 켜질 것이다. 그렇지만 상태가 ON인 상태에서 스위치를 ON하면 이미 불이 켜진 상태라 아무런 동작을 하지 않을 것이다. 그 반대도 역시 동일하다. 그렇다면 이렇게 동작하는 형광등 클래스는 어떻게 설계하면 좋을까? 가장 간단한 구현은 아래와 같을 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class NotStatePattern {
    
    public static final int ON=1;
    public static final int OFF=0;
    
    //현 상태
    private int currentState;
    
    public NotStatePattern() {
        currentState=OFF;
    }
    
    //ON 스위치 push
    public void on_button() {
        if(currentState==ON) System.out.println("동작 없음.");
        else {
            System.out.println("불 켜짐!");
            currentState=ON;
        }
    }
    //OFF 스위치 push    
    public void off_button() {
        if(currentState==OFF) System.out.println("동작 없음.");
        else {
            System.out.println("불 꺼짐!");
            currentState=OFF;
        }
    }
    
}
cs

 

ON,OFF 상태값을 가지는 상수와 현재 형광등의 상태를 가지고 있는 변수. 그리고 스위치를 ON,OFF할때 발생되는 행위를 메소드로 구현해놓았다. 얼핏 봤을 때, 나쁘지 않은 구현일 수 있다. 하지만 예를 들어 수면모드가 들어간 형광등이라면? 스위치가 ON인 상태에서 다시 한번 ON 스위치를 누를 경우 수면 모드로 들어간다. 그리고 수면 모드에서 OFF 스위치를 누르면 형광등이 꺼진다. 만약 복잡한 시스템이라면 이러한 상태값은 더 많이 갖게 될 것이고, 그럼 많은 상태에 따른 행위를 위와 같이 if문 혹은 switch 문으로 다뤄야할 것이며 이는 상태가 추가될때 마다 코드가 수정되야하고 가독성도 떨어지며 최종적으로는 유지보수가 힘든 코드가 될 것이다.

 

이러한 상황에서 사용할 수 있는 디자인 패턴이 스테이트 패턴(State Pattern)이다. 아마 예제 코드를 본다면 우리 이미 다루어본 스트레티지 패턴(전략패턴)과 비슷하다 볼 수 있을 것이다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Light {
    
    private State state;
    
    public Light() {
        state=OffState.getInstance();
    }
    
    public void setState(State state) {
        this.state=state;
    }
    
    public void onButton() {
        this.state.onButton(this);
    }
    
    public void offButton() {
        this.state.offButton(this);
    }
    
}
cs

 

형광등 클래스이다. 마치 스트레티지 패턴의 Context 객체와 비슷하다. 즉, 해당 객체는 실제 알고리즘을 수행하는 실체 객체를 알지못한다. 단순히 남에게 행위를 위임해주는 역할이다.

 

1
2
3
4
5
6
public interface State {
    
    public void onButton(Light light);
    public void offButton(Light light);
    
}
cs

 

디자인 원칙을 떠올려보자. 변하는 것은 잘 변하지 않는 것과 분리해라. 즉, 변하는 녀석들을 캡슐화해라! 

 

우리는 변하는 것은 상태(State)라는 것을 알고있다. 이러한 상태를 인터페이스로 분리시켰고 각 상태에 따른 구현 클래스를 만들어 줄 것이다. 여기서 스트래티지 패턴과 조금 다른 점은 Light(Context)객체가 행위를 위임할때 자신의 상태를 변경하기 위하여 자기자신을 메소드 매개변수로 넘기는 것을 주목하자! 차이점은 마지막에 다룰 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class OnState implements State {
    
    private OnState() {}
    
    private static class LazyHolder{
        public static final OnState ON=new OnState();
    }
    
    public static OnState getInstance() {
        return LazyHolder.ON;
    }
 
    @Override
    public void onButton(Light light) {
        light.setState(SleepState.getInstance());
        System.out.println("잠자기 모드!");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
 
public class OffState implements State {
    
    private OffState() {}
    
    private static class LazyHolder{
        public static final OffState OFF=new OffState();
    }
    
    public static OffState getInstance() {
        return LazyHolder.OFF;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("불 켜짐!");
    }
 
    @Override
    public void offButton(Light light) {
        System.out.println("동작 없음!");
    }
 
}
 
public class SleepState implements State {
    
    private SleepState() {}
    
    private static class LazyHolder{
        public static final SleepState SLEEP=new SleepState();
    }
    
    public static SleepState getInstance() {
        return LazyHolder.SLEEP;
    }
    
    @Override
    public void onButton(Light light) {
        light.setState(OnState.getInstance());
        System.out.println("잠자기 모드 해제!(ON 상태)");
    }
 
    @Override
    public void offButton(Light light) {
        light.setState(OffState.getInstance());
        System.out.println("불 꺼짐!");
    }
 
}
cs

 

각 상태들의 구현체이다. 여기서 조금 특이한 것은 각 상태의 구현체들이 싱글톤으로 구현되어 있다는 것이다. 이것은 상태들이 행위를 수행하면서 Light 객체의 상태를 수시로 바꾸어주기 때문에 싱글톤으로 작성하지 않으면 매번 새로운 인스턴스가 생겨 불필요한 메모리를 잡아 먹을 것이고 전체적으로 성능 저하의 원인이 될 것이기 때문에 싱글톤으로 작성하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StatePatterMain {
 
    public static void main(String[] args) {
        Light l = new Light();
        
        l.offButton();
        l.onButton();
        l.onButton();
        l.onButton();
        l.offButton();
        l.onButton();
        l.offButton();
    }
 
}
 
=>result
동작 없음!
불 켜짐!
잠자기 모드!
잠자기 모드 해제!(ON 상태)
불 꺼짐!
불 켜짐!
불 꺼짐!
cs

 

마지막 결과이다. 잘 보면 스트래티지 패턴과 차이점이 보일 것이다. 스트래티지 패턴을 떠올려보면 클라이언트 쪽에서 알고리즘을 변경하기 위하여 setter를 호출해 직접 수행할 알고리즘을 주입해주었던 것을 기억할 것이다. 즉, 클라이언트가 구체적인 알고리즘의 수행까지는 몰라도 어느정도 무엇무엇이 있는지 정도는 알고 있어야 한다는 것이다. 하지만 스테이트 패턴은 각 상태 구현 클래스들이 자신들의 행위를 수행하면서 직접 Context(Light)객체의 상태를 변경해주기 때문에 클라이언트 입장에서는 직접 상태를 조작하거나 하지 않아도 된다는 점이다. 즉, 클라이언트는 상태를 몰라도 된다라는 뜻이다.(전략을 직접 클라이언트가 바꿔서 사용해야하는 스트래티지 패턴과는 조금은 상반된다.)

 

즉, 용도가 조금 다른 패턴이라고 볼 수 있다.

 

스트래지티 패턴 - 사용자가 쉽게 알고리즘 전략을 바꿀 수 있도록 유연성을 제공. 상속의 한계를 해결하기 위하여 나온 패턴

스테이트 패턴 - 한 객체가 동일한 동작을 상태에 따라 다르게 수행해야 할 경우 사용하는 패턴

posted by 여성게
:

 

어떤 클래스에 변화가 일어났을 때, 이를 감지하여 다른 클래스에 통보해주는 것이 observer 패턴이다. 그림을 보면 이 패턴은 외부 객체의 상태 변화에 따라(subject 클래스) observer 객체(observer 인터페이스 클래스)는 이에 상속되어 있는 다른 객체(concreteObserver)들에게 변화된 상태를 전달하고(notify 메서드) 상속된 객체들은 그에 맞게 기능을 수행하는 형태로 구성된다.

이 패턴에 해당되는 예는 관심 있는 어떤 블로그를 미리 등록해놓으면 그 블로그에 새 글이 올라올 때마다 자동으로 알려주는 것과 같다. 즉 observer 패턴은 어떤 일이 생기면 미리 등록한 객체들에게 상태 변화를 알려주는 역할을 한다.

 

다시 말하자면 Subject 객체의 상태에 변화가 있을 때, Observer들에게 상태변화를 알린다. 그 이후 Observer들은 상태변화에 맞게 행동을 취한다. 우리가 잘 알고 있는 신문사 구독과 비슷한 이론인 것이다. 어떤 고객이 신문사에 구독을 신청하면 신문사가 새로운 신문을 발행할때마다 고객에게 신문을 보내는 것과 같은 행동이다.

 

예를 들어 온도,습도,기압 등의 기상 상태를 센서로부터 수집하는 Subject가 있다고 생각하고, 해당 Subject는 기상 상태의 변화가 있을때, Observer들에게 변화를 알리고 Observer들은 기상 상태 변화에 따른 새로운 데이터를 디스플레이에 출력하는 것을 구현하는 상황이다.

 

여기서 적용할 수 있는 패턴이 옵저버 패턴이다. 바로 구현해보자.(예제는 단순히 온도 변화만 다루었다.) 그리고 구독을 해지하는 등의 코드도 구현하지 않았고 상태변화가 있을 때, 옵저버들에게 알려주는 기능만 구현하였다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Subject extends Observable{
    
    private int temp;
    
    public void changeTemp(int temp) {
        setChanged();
        notifyObservers(temp);
    }
    
    public void changeTemp() {
        setChanged();
        notifyObservers();
    }
    
    public void setChangeTemp(int temp) {
        this.temp = temp;
        changeTemp(temp);
    }
 
    public int getTemp() {
        return temp;
    }
 
    public void setTemp(int temp) {
        this.temp = temp;
        changeTemp();
    }
    
}
 
cs

 

Subject 클래스이다. 이미 자바에는 옵저버 패턴의 구현을 쉽게 해주는 API를 제공하고 있다. Subject를 구현하기 위해서는 Observable 클래스를 상속받아 확장하여 사용하면 된다.(사실 Observable을 확장하면 단점들이 존재한다. 반드시 상속을 강요하고 있으며 추상클래스가 아닌 일반 클래스로 되어 있기에 나중에 필요한 기능을 확장하기 위하여 상속을 받을 수 없다. 왠만하면 새로 구현해서 사용하는 것도 추천한다.)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Observer1 implements Observer {
    
    private Observable sub;
    private int temp;
    
    public Observer1(Observable sub) {
        this.sub=sub;
        sub.addObserver(this);
    }
 
    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof Subject) {
            if(arg != nullthis.temp=(int)arg;
            else this.temp=((Subject) o).getTemp();
        }
        
        display();
 
    }
    
    public void display() {
        System.out.println("Observer1 - "+temp);
    }
 
}
 
public class Observer2 implements Observer {
    
    private Observable sub;
    private int temp;
    
    public Observer2(Observable sub) {
        this.sub=sub;
        sub.addObserver(this);
    }
 
    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof Subject) {
            if(arg != nullthis.temp=(int)arg;
            else this.temp=((Subject) o).getTemp();
        }
        display();
    }
 
    public void display() {
        System.out.println("Observer2 - "+temp);
    }
    
}
cs

 

Observer라는 인터페이스를 상속한 Observer 클래스 2개를 구현하였다. 해당 인터페이스에는 Subject에서 호출할 update 메소드만 오버라이드해서 구현해주면 된다. 옵저버 구현체들에서는 push 방식으로 데이터를 매개변수(arg)로 받아도 되지만 Subject 클래스에서 상태값에 대한 getter를 열어주어서 Observer 구현체들이 pull(getter호출)해서 가져가도 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ObserverMain {
 
    public static void main(String[] args) {
        
        Subject sub= new Subject();
        
        Observer observer1 = new Observer1(sub);
        Observer observer2 = new Observer2(sub);
        
        sub.changeTemp(20);
        sub.setTemp(10);
    }
 
}
 
=>결과
Observer2 - 20
Observer1 - 20
Observer2 - 10
Observer1 - 10
cs

 

결과적으로 Observer 구현체들의 어떠한 메소드도 호출하지 않았음에도 불구하고 Subject 클래스의 상태가 바뀔 때마다 Observer의 메소드가 호출되어 동작한다. 이러한 패턴이 옵저버 패턴이다. 이러한 패턴을 다른 말로 pub/sub 패턴이라고 부르기도 한다. 이러한 옵저버 패턴을 자바는 내부적으로 비동기처리 API의 내부 구현로직으로 사용하기도 한다.

 

위의 구현을 보면 Observer 구현체들의 인스턴스 변수로 Observable 인터페이스가 선언되어 있는 이유는 우선 생성자가 호출될때 해당 Observable 변수가 초기화되고, 이후에 별로 쓰이지 않는 듯 보이지만, 필자가 이야기 했듯 해당 예제에는 구독을 해지하는 등의 로직이 포함되어 있지않다. 추후에 구독을 해지하는 로직을 Observer 구현체들에게 메소드 형태로 넣어주고 내부적으로 Observable의 deleteObserver를 호출하게 하면 된다.

posted by 여성게
: