Adaptive Object Model(이하 AOM) 패턴은 간단히 이야기 하자면 동적 객체 생성 패턴이다. 그래서 Dynamic Object Model 패턴이라고도 불린다.

이 패턴은 몇 개의 다른 패턴들이 결합되어 생겨난 패턴이다. 이에 대한 자세한 설명은 넥스트리(Nextree)의 블로그에서 확인할 수 있다.(http://www.nextree.co.kr/p2469/)

그리고 이 패턴을 응용하여 다양한 시도들이 이루어지고 있는데 이에 대한 자료는 Adaptive Object Model 패턴 공식 홈페이지(http://adaptiveobjectmodel.com/)에서 확인할 수 있다.

이 패턴은 실제 제품에도 활용된 사례가 있다. Tridium 사에서 개발한 Niagara Platform이라는 빌딩용 네트워크 시스템에 탑재되는 Niagara 소프트웨어가 이 패턴을 통해 구현되어 있다.(https://www.niagara-community.com/Comm_Home)


이 패턴은 객체의 동적 선언과 동적 생성이라는 두가지 특성을 모두 지원해주는 패턴이다. 그 자체가 매우 다이나믹한 특성을 가지고 있고, 이에 따라 다양한 파생 구조들이 생겨날 수 있다. 여기에서는 가장 기본이 되는 형태인 동적 선언과 동적 생성에 대해서 초점을 맞춰 보고자 한다.


만약 어떤 어플리케이션이 매우 다양한 대상을 취급한다고 가정하자. 특히 공장이나 빌딩, 대규모 상업 단지와 같이 각종 설비들이 설치될 수 있고, 이에 대한 통합적인 관리가 필요하다고 가정하자. 이런 경우에는 특히 기존에 잘 사용하던 장비를 다른 회사의 장비로 교체할 수도 있고, 새로운 장비들을 추가할 수도 있다. 이런 경우 새로운 장치는 기존에 통합 관리 시스템이 만들어질 당시에는 존재하지 않는 것일 수도 있다.

이런 장비들을 소프트웨어로 매핑하기 위해서는 수많은 장비들을 포용할 수 있는 객체를 만들어야 한다. 즉 각 장비들은 그와 대응되는 객체가 정의 되어 있어야만 정확하게 정보를 얻고 저장할 수 있다. 하지만 이미 이야기 했다 시피 장비의 종류는 너무나도 다양하고, 매번 새로운 장비들이 개발되게 되어 있다. 이런 상황에서는 아무리 객체 설계를 잘 했다고 해도 언젠가는 그 객체로는 장비 정보를 제대로 표현할 수 없게 될 것이다.

또한 현장에서의 요구사항은 항상 바뀌게 마련이다. 현장마다 장비의 종류가 각각 다르다. 그런 현장들을 지원하기 위해 매번 소프트웨어를 다시 컴파일 할 수는 없는 노릇이다. 즉, 소프트웨어가 동작하는 동안에도 새로운 장비들의 정보를 보여줄 객체를 생성할 수 있어야 하며, 이 객체는 수 많은 장치들을 모두 커버할 수 있는 객체여야 한다.

즉 문제의 핵심은 어떻게 실시간에 새롭게 정의된 객체를 생성할 수 있느냐 하는 것이다.


실세계의 장치를 지원할 객체를 디자인 한다고 하면 우리는 다음과 같은 방식들을 사용할 수 있다.

- 우선 공통된 정보들을 모아서 상위 클래스로 선언한다.(예를 들어 모든 종류의 장치를 나타내는 Device 클래스를 선언한다.)

- 그리고 상위 클래스의 속성들을 선언한다.

- 개별 디바이스 정보를 담을 하위 클래스를 선언한다. 당연히 상위 클래스의 속성들을 상속 받아야 한다.

- 하위 클래스의 속성을 선언한다.

- 장치들은 모두 하위 장치들을 포함할 수 있다. 따라서 속성 중에는 다른 장치가 포함될 수도 있다. (일종의 부품이나 참조 객체와 같은 성격이다.)


이런 방식은 우리가 객체지향 언어들을 통해서 일반적으로 장치에 대한 클래스를 선언할 때 사용하는 모델링 방식이다. 따라서 동적으로 객체를 생성할 수 있으려면, 위에서 이야기 한 상속 개념, 참조 개념을 모두 구현할 수 있어야만 한다. AOM 패턴은 상속이나 참조 개념들을 포함한 동적 객체를 모사하여 생성해 줄 수 있는 패턴이다.


AOM 패턴의 클래스 다이어그램


AOM 패턴은 TypeSquare라는 사각형 형태의 다이어그램을 가지게 된다.

이 다이어그램에서 왼쪽은 객체화되고 시간에 따라서 가변적인 정보들을 담는 객체이다. 오른쪽 두 클래스는 타입에 관한 정보를 가지고 있게 된다. 오른쪽 클래스들이 가진 정보는 일반적으로 변경되지 않는다.

우선 Klass와 KlassType에 대해 알아보자.

이 둘의 관계는 객체지향 프로그래밍에서 말하는 객체(Klass) - 클래스(KlassType)의 관계와 같다고 생각하면 된다. 우선 KlassType은 클래스명에 해당하는 name을 가지고 있다. 그리고 클래스가 가질 수 있는 상위 클래스는 parent라는 변수를 통해 가지고 있게 된다. 당연히 parent도 KlassType 객체이다. 그리고 실제 클래스는 속성(attribute)을 선언하게 된다. 이 속성에 대한 선언이 AttributeType이고, KlassType은 이를 소유하고 있다.

Klass는 실제 객체로 생성되었을 때 가지는 정보들을 선언한 클래스이다. Klass 클래스를 기반으로 객체가 생성될 때에는 무조건 한 개의 KlassType 객체를 인자로 받아야 한다. 일단 생성 이후에는 객체의 타입이 바뀌길 원하지는 않기 때문이다. 이는 마치 클래스를 기반으로 객체를 생성하는 과정과 같다. Klass 객체는 KlassType에 선언된 AttributeType을 기반으로 Attribute 객체를 생성한다.

Attribute와 AttributeType 간의 관계는 클래스의 속성 선언과 객체가 가진 속성의 관계와 같다. AttributeType이란 우리가 속성을 선언할 때 int data; 와 같이 선언하는 것처럼 우선 속성의 타입을 나타내는 typeClass 변수를 가져야 한다. 이 다이어그램에서는 이 typeClass를 Java의 클래스 객체로 선언하여 가지고 있다. 다음으로 속성의 이름에 해당하는 name 을 가지고 있어야 한다. description은 이 속성에 대한 설명을 담는 변수이다.

Attribute는 AttributeType을 기반으로 생성되는 객체이다. 실제 객체에서 데이터를 담을 수 있는 공간으로 이해할 수 있다. 따라서 자신의 타입 정보인 AttributeType 객체를 하나 가지고 있어야 하고, 데이터를 담을 value 객체를 가지고 있어야 한다. 이 Attribute는 범용적으로 쓰일 수 있어야 하기 때문에 value는 Object 타입이다.


이제 AOM 패턴의 구현을 살펴보자.


KlassType 클래스

class KlassType{

    // 클래스 정보에 해당한다.   

    KlassType parent;

   

    public KlassType(String name, KlassType parent){

        this.name = name;

        this.parent = parent;

        addParentAttributeTypes();

    }

   

    private void addParentAttributeTypes(){

        if(parent == null) return;

        Collection<AttributeType> parentAttributeTypes = parent.getAttributeTypes();

        for(AttributeType each : parentAttributeTypes){

            attributeTypes.put(each.getName(), each);

        }

    }

   

    Map<String, AttributeType> attributeTypes = new HashMap<String, AttributeType>();


        public void addAttributeType(AttributeType attributeType){   

            attributeTypes.put(attributeType.getName(), attributeType); 

        }


        public AttributeType getAttributeType(String typeName){ 

            return attributeTypes.get(typeName); 

        }


        public Collection<AttributeType> getAttributeTypes() {

            return attributeTypes.values();

        }

       

    private String name;

        public String getName(){ return name; }   

}

KlassType은 객체지향 언어에서 클래스를 선언하는 것과 대응되는 클래스이다. 클래스는 클래스 이름과 상속받은 부모 클래스 명, 내부에 선언된 속성들로 구성된다.(행위는 AOM 패턴과 연관된 다른 패턴들에 의해 구현된다. 이 예제에서는 일단 배제되어 있다.)

- parent : 부모 클래스

- name : 클래스명

- attributeTypes : 선언된 속성들


맨처음 KlassType이 객체로 선언되었을 때에는 아무런 속성타입(AttributeType)을 가지고 있지 않으므로 이를 추가해 줄 수 있도록 addAttributeType()이라는 메소드를 제공해야 한다. 또한 생성자에서 부모 클래스를 나타내는 객체를 받았다면 부모가 선언한 AttributeType들도 상속 받아야 한다. 따라서 addPrentAttributeTypes() 메소드를 생성자에서 호출하게 된다.


Klass 클래스

class Klass{

    // 객체에 해당한다.

    public Klass(KlassType type, String name, String id){

        this.type = type;

        this.name = name;

        this.id = id;

        initAttributes();

    }

   

    private void initAttributes(){

        for(AttributeType attributeType : type.getAttributeTypes()){

            attributes.put(attributeType.getName(), new Attribute(attributeType));

        }

    }

   

    private KlassType type;

        public KlassType getType(KlassType type){ return type; }

   

    String name;

        public String getName(){ return name; }

   

    String id;

        public String getId(){ return id; }   

    Map<String, Attribute> attributes = new HashMap<String, Attribute>();

        public Object get(String name){

            Attribute attr = attributes.get(name);

            if(attr != null) return attr.getValue();

            else throwNoSuchAttributeException(name);

            return null;

        }


        public void set(String name, Object value){

            Attribute attr = attributes.get(name);

            if(attr != null) attr.setValue(value);

            else throwNoSuchAttributeException(name);

        }


        public Attribute getAttribute(String name){

            Attribute attr = attributes.get(name);

            if(attr != null) return attr;

            else throwNoSuchAttributeException(name);

            return null;

        }

   

    public String toIndentString(String indent){

        StringBuffer buffer = new StringBuffer();

        buffer.append(indent + "Class " + type.getName() + " " + name);

        if(type.parent != null) buffer.append(" extends " + type.parent.getName());

        buffer.append("{\n");

        for(Attribute each : attributes.values()){

            if(each.getValue() instanceof Klass){

                Klass inner = (Klass)each.getValue();

                buffer.append(indent + "   " + each.getType().getTypeClassName() + " " + each.getType().getName() + " = " + inner.toIndentString(indent + "   ") + ";\n");

            }

            else{

                buffer.append(indent + "   " + each.getType().getTypeClassName() + " " + each.getType().getName() + " = " + each.getValue() + ";\n");

            }

        }

        buffer.append(indent + "}");

        return buffer.toString();

    }

   

    private void throwNoSuchAttributeException(String attributeName){

        try {

            throw new NoSuchAttributeException();

        } catch (NoSuchAttributeException e) {

            System.out.println("Class \"" + name + "\" has no such attribute : \"" + attributeName + "\n");

            e.printStackTrace();

        }

    }

} 

Klass 클래스는 생성 이후에 객체 역할을 수행하는 클래스이다. 객체는 객체로서의 이름과 아이덴티티, 그리고 타입에 대한 정보 및 속성 값들을 가지고 있어야 한다.

- type : 객체의 타입 정보(AttributeType)

- name : 객체의 이름(변수명이라고 이해하면 된다.)

- id : 객체와 객체를 유일하게 구분해주는 구분자 역할. 구현에서는 UUID를 문자열화 해서 사용한다.

- attributes : 속성들을 저장한 Map. 속성명(변수명)을 키로 하여 Attribute를 저장하는 Map이다.


각 Klass 객체는 별도의 속성을 가지고 있어야 한다. 따라서 Klass 객체가 생성되면 우선 KlassType을 인자로 받은 다음, 이 KlassType에 선언되어 있는 속성 타입 정보(attributeTypes)를 얻어온다. 그리고 그 정보를 기반으로 초기화 되지 않은 Attribute 객체를 만들고 이를 attributes 맵에 저장한다.

객체는 외부에서 자신에 접근하는 연산들을 제공해 주어야 한다. 따라서 저장된 속성값을 제공해주는 get() 메소드와 속상 값을 설정할 수 있게 해주는 set() 메소드를 제공해준다.

추후에 테스트를 통해서 의도한 대로 Klass 객체가 잘 만들어졌는지 확인하기 위해서 toIndentString() 메소드도 구현해 주었다. 이 메소드는 마치 우리가 클래스를 코딩했을 때와 유사하게 내부 속성들과 클래스명, 그리고 상속 받은 클래스의 정보들을 표시하도록 되어 있다.


AttributeType 클래스

class AttributeType{

    // 클래스에 속성(변수)을 선언하면 int data; 와 같이 선언한다. 이 정보를 이 클래스가 가지고 있어야 한다.

    Map<String, KlassType> klassTypes = new HashMap<String, KlassType>();


    @SuppressWarnings("rawtypes")

    private Class typeClass;

    @SuppressWarnings("rawtypes")

    public Class getTypeClass(){ return typeClass; }

    public String getTypeClassName(){

        String typeName = typeClass.getName();

        typeName = typeName.substring(typeName.lastIndexOf(".") + 1);

        return typeName;

    }

   

    @SuppressWarnings("rawtypes")

    public AttributeType(Class typeClass, String name, String description){

        this.typeClass = typeClass;

        this.name = name;

    }

   

    private String name;

    public String getName() { return name; }

   

    private String description;

    public String getDescription(){ return description; }

}

AttributeType 클래스는 속성의 선언에 해당하는 클래스이다.

- typeClass : 속성의 타입을 나타낸다. 이 구현에서는 클래스 객체를 이용하고 있다.

- name : 속성의 명칭을 나타내는 정보이다.

- description : 이 속성에 대한 설명을 넣는 변수이다.


이 클래스는 생성자를 통해서 위의 정보들을 받아 저장한다. 이 정보들은 이 후 Attribute 객체를 생성하기 위한 정보로 활용된다.


Attribute 클래스

class Attribute{

    // Attribute = 속성, 필드. attributType value를 가져야 한다.

   

    private AttributeType type;

   

    public Attribute(AttributeType type){

        this.type = type;

    }

   

    public AttributeType getType(){ return type; }

       

    private Object value;

    public Object getValue() { return value; }

    public void setValue(Object value) {

        if(isSettable == false){

            throwOperationNotSupportException();

            return;

        }

        this.value = value;

    }

   

    private void throwOperationNotSupportException(){

        try { throw new OperationNotSupportedException(); }

        catch (OperationNotSupportedException e) {

            System.err.println("Attribute " + type.getName() + " is immutable.");

            e.printStackTrace();

        }

    }

   

    private boolean isSettable = true;

    public boolean isSettable(){ return isSettable; }

    public void setSettable(boolean isSettable){ this.isSettable = isSettable; }

} 

Attribute 클래스는 실제 객체에서의 속성을 나타낸다. 따라서 Attribute는 자신이 저장할 속성 값에 대한 타입 정보를 가지고 있어야 하고, 속성 값을 함께 가지고 있어야 한다.

- type : AttributeType

- value : 속성 값을 저장하는 변수


이 속성은 외부에서 조회가 가능하고, 값이 변경되면 저장이 가능해야 한다. 따라서 getValue() 메소드와 setValue() 메소드를 통해 이런 연산들을 지원하고 있다.


이제 이 클래스들을 통해서 새로운 클래스를 선언하고 객체를 생성하는 과정을 살펴보도록 하자.


실행 함수

public static void main(String[] args) {

    KlassType site = new KlassType("Site", null);

    site.addAttributeType(new AttributeType(String.class, "position", "위치"));

    KlassType house = new KlassType("House", site);

    house.addAttributeType(new AttributeType(String.class, "owner", "소유주"));

    house.addAttributeType(new AttributeType(Integer.class, "area", "면적"));

    house.addAttributeType(new AttributeType(Klass.class, "car", "차량"));

    Klass myHouse = new Klass(house, "우리집", UUID.randomUUID().toString());

    myHouse.set("position", "사당동");

    myHouse.set("owner", "홍길동");

    myHouse.set("area", "30");

    KlassType car = new KlassType("Car", null);

    car.addAttributeType(new AttributeType(String.class, "model", "모델"));

    car.addAttributeType(new AttributeType(Integer.class, "hp", "마력"));

    car.addAttributeType(new AttributeType(String.class, "type", "종류"));

    Klass myCar = new Klass(car, "내차", UUID.randomUUID().toString());

    myCar.set("model", "아우디");

    myCar.set("hp", "500마력");

    myCar.set("type", "세단");

    myHouse.set("car", myCar);

    System.out.println(myHouse.toIndentString(""));

}


이 테스트 코드들이 의도하는 바는 다음과 같다.

우선 Site 클래스를 선언한다. Site 클래스는 각종 위치를 나타내기 위한 상위 클래스 역할이 된다.

House는 Site 클래스를 상속 받아 구현되는 클래스이다.

myHouse는 House의 객체(인스턴스)이다.

Car 클래스는 자동차를 나타내는 클래스이다.

myCar는 Car 클래스의 객체(인스턴스)이다.

myCar는 myHouse 내부에 선언된 속성으로 들어가게 된다. 즉 참조 객체가 된다.


설명이 좀 많긴 하지만 실행해보면 쉽게 알 수 있다. 출력은 다음과 같다.


출력

Class House 우리집 extends Site{

   String owner = 홍길동;

   Integer area = 30평;

   Klass car =    Class Car 내차{

      Integer hp = 500마력;

      String model = 아우디;

      String type = 세단;

   };

   String position = 사당동;

}

출력된 내용은 마치 소스코드와도 같다. 사실 AOM 패턴이 해결하고자 하는 문제가 딱 이런 것이다. 우리는 마치 새로운 클래스를 선언하고 이를 생성해 낸 것과 같은 효과를 얻었다. 이 과정에서 상속이나 참조 객체와 같이 객체지향 언어로 모델링을 할 경우 필요한 특성들까지도 역시 동일하게 확보할 수 있게 되었다.

이러한 특성은 또한 데이터베이스 설계에도 매우 도움이 된다. 실행 함수에서 선언한 정보들을 각각 테이블로 만들어주면 Klass / KlassType / Attribute / AttributeType과 매핑되는 4개의 테이블 만으로도 원하는 기능들을 모두 구현할 수 있다.

'5.디자인패턴' 카테고리의 다른 글

Actor Model 패턴의 구현(Java)  (0) 2016.09.30
Property List 패턴  (0) 2016.09.24
Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Posted by 이세영2
,

Actor Model 패턴에 대한 설명은 몇몇 블로그에 나와 있으나, Java를 통해 구현한 예제는 찾기 어려워서 한번 작성해 보았다.

Actor Model 패턴에 대한 일반적인 설명은 다음과 같다.

- Actor Model 패턴에서 Actor 개념과 객체지향 언어에서의 객체 개념은 상당히 유사하다. 단지 다른 점이 있다면 객체지향에서의 메소드 호출은 메소드가 모두 실행될 때까지 기다려야 하는 동기식이다. 반면 Actor Model 패턴에서의 다른 Actor에 대한 메시지 전송은 기본적으로 비동기식이다.

- 이 비동기식 메시지 전송을 지원하기 위해서는 Actor들이 모두 Active 객체, 즉 쓰레드 기반으로 동작하는 객체여야 하고, 메시지의 전송/수신에 대한 동기화 관리가 이루어 져야 한다.

- 동기화 부분은 Actor 내부에 있는 Mailbox라는 객체를 통해서 해결되기 때문에 Actor들을 구현하는데 있어서는 동기화에 대한 고려를 전혀 하지 않아도 된다.

- akka 프레임워크가 대표적이며, Go 언어에도 시범적으로 구현된 바가 있다.


Actor Model의 단점

- 모든 Actor가 개별적으로 쓰레드를 기반으로 동작하므로 동작이 느리다.(Actor 내부에 쓰레드를 넣지 않고 구현할 수도 있겠으나 일단 이는 논의 밖이다.)

- Actor 간 주고 받는 메시지의 종류에 따라 메시지의 종류도 늘어나게 된다. 단순히 API만을 만들면 되는 것에 비하면 조금은 번거롭고, 메시지들에 대한 하위 타입 컨버팅이 필요하다.


Actor Model 패턴 클래스 다이어그램

전체적인 흐름을 설명하자면 다음과 같다.

일단 메시지를 주고 받는 주체인 IActor가 있다. IActor는 인터페이스이고, Actor 클래스는 이를 구현한 추상 클래스이다. 이 클래스 내부에서 대부분의 메시지 전달 및 수신에 필요한 동작을 수행한다. 그리고 각 개별 Actor들이 다른 동작을 할 수 있도록 processMail() 메소드와 doWork() 메소드를 추상 메소드로 제공한다.(Template Method 패턴)

Actor들이 서로간에 메시지를 전송하기 위해서는 두가지 요소가 필요하다. 우선 메시지를 받아 저장하고, Actor가 메시지를 처리할 때 저장하고 있던 메시지를 보내 주는 역할을 하는 Mailbox가 필요하다. 특히 Mailbox는 모든 동시성 문제를 처리하는 역할을 한다. 그리고 메시지를 담아 전달될 Mail 객체가 필요하다. Mail 객체 내에 실제 전달할 내용(content)이 들어가게 된다.


소스가 좀 길기 때문에 부분으로 나누어 설명하도록 하겠다.


IActor - Actor - TempControlActor/ThermostatActor/DatabaseActor

interface IActor extends Runnable{

    public void tell(IMail mail);

}

abstract class Actor implements IActor{

    private IMailbox mailbox = new Mailbox();

    @Override

    public void run() {

        while(true){

            receiveMail();

            doWork();

            try {

                Thread.sleep(0);

            } catch (InterruptedException e) {

               e.printStackTrace();

            }

        }

    }

   

    private void receiveMail(){

        while(mailbox.hasNext()){

            IMail mail = mailbox.next();

            processMail(mail);

        }

    }

   

    @Override

    public void tell(IMail mail){

        mailbox.receiveMail(mail);

    }

   

    abstract protected void processMail(IMail mail);

    abstract protected void doWork();

}

class TempControlActor extends Actor{

    IActor db = new DatabaseActor();

    IActor thermostat = new ThermostatActor();

   

    public TempControlActor(){

        db = ActorFactory.create(DatabaseActor.class);

        thermostat = ActorFactory.create(ThermostatActor.class);

    }

    protected void processMail(IMail mail){

        db.tell(mail);

        thermostat.tell(mail);

    }

   

    protected void doWork(){

        /* do nothing */

    }

}


class DatabaseActor extends Actor{

    protected void processMail(IMail mail){

        System.out.println("db:" + mail.getContent());

    }

   

    protected void doWork(){

        /* do nothing */

    }

}

class ThermostatActor extends Actor{

    protected void processMail(IMail mail){

        Integer temp = (Integer)mail.getContent();

        if(temp > 30) System.out.println("cooling");

        else if(temp < 10) System.out.println("warming");

        else System.out.println("stop");

    }

   

    protected void doWork(){

        /* do nothing */

    }

}

우선 IActor는 쓰레드로 구현된다. 따라서 쓰레드 구현이 가능하도록 Runnable 인터페이스를 기반으로 한다. IActor는 Actor의 인터페이스가 된다. Actor 클래스는 추상 클래스로써 쓰레드 기반 동작을 구현한 run() 메소드를 가지고 있다. 이 메소드는 무한 반복되는 메소드로써, 내부에서 메일의 처리(receiveMail())와 자기 할 일(doWork())을 처리한다. tell() 메소드는 외부로부터 메시지를 수신하는 메소드이다. 메소드를 수신하면 위임 객체인 Mailbox 객체에게 바로 전달한다.

실제 동작을 구현한 Actor 클래스는 모두 3개이다. TempControlActor가 주요 Actor로서, 외부로부터 메시지( 온도 )를 받아서 협력 Actor들인 DatabaseActor와 ThermostatActor에게 전달하는 역할을 한다.

DatabaseActor와 ThermostatActor는 받은 온도 정보 메시지들에 대해 간단한 연산(출력)을 수행하는 Actor이다.


ActorFactory

class ActorFactory{

    private static Map<Class, IActor> actorMap = new HashMap<Class, IActor>();

   

    public static IActor create(Class clazz){

        IActor actor = actorMap.get(clazz);

        if(actor != null) return actor;

        try {

           actor = (IActor)clazz.newInstance();

           new Thread(actor).start();

           actorMap.put(clazz, actor);

           return actor;

        } catch (InstantiationException e) {

            e.printStackTrace();

        } catch (IllegalAccessException e) {

            e.printStackTrace();

        }

        return null;

    }

} 

ActorFactory는 Actor를 생성하는 클래스이다. Actor에 대한 클래스 객체를 받아 Actor를 생성한다. 현재는 중복 생성이 안되도록 되어 있다. 각 Actor들은 모두 쓰레드로 동작해야 하기 때문에 new Thread(actor).start() 를 통해 쓰레드 메소드인 run() 메소드를 실행 시킨 후에 actor를 리턴한다.


IMailbox - Mailbox

interface IMailbox{

    public void receiveMail(IMail mail);

    public boolean hasNext();

    public IMail next();

}

class Mailbox implements IMailbox{

    private List<IMail> in = new LinkedList<IMail>();

    public synchronized void receiveMail(IMail mail){

        in.add(mail);

    }

    public synchronized boolean hasNext(){

        return !in.isEmpty();

    }

    public synchronized IMail next(){

        IMail mail = in.get(0);

        in.remove(0);

        return mail;

    }

}

Mailbox는 IMailbox를 인터페이스로 구현된다. Mailbox는 외부와 연동되는 직접적인 부분이므로 동기화 문제에 대한 고려가 필요하다. 따라서 IMailbox 인터페이스에 해당하는 구현 메소드는 모두 synchronized 키워드를 통해 동시성 문제가 생기지 않도록 한다.


IMail - Mail

interface IMail{

    public Object getContent();

}

class Mail implements IMail{

    private Object content;

    public Mail(Object content){

        this.content = content;

    }

   

    public Object getContent(){

        return content;

    }

} 

Mail 클래스는 IMail을 구현한 클래스이다. Mail은 생성과 함께 내부에 저장할 Object 타입의 content를 인자로 받는다. 그리고 Mail을 받은 Actor들은 getContent() 메소드를 통해 내부에 저장된 content를 꺼내서 처리하게 된다.


실행 방법

public static void main(String[] args) {

    IActor tempControl = ActorFactory.create(TempControlActor.class);

    Scanner scan = new Scanner(System.in);

    while(true){

        Integer temperature = scan.nextInt();

        tempControl.tell(new Mail(temperature));

    }

} 

main() 메소드에서는 TempControlActor 객체를 생성한다. TempControlActor 내부에서는 DatabaseActor와 ThermostatActor를 생성하여 참조 객체로 가지고 있다.

그리고 Scanner 객체를 통해 키보드로 숫자(온도)를 입력 받아서 TempControlActor에 넣어주면 DatabaseActor와 ThermostatActor에게 전달하여 처리한다.

Actor Model 패턴을 이용하여 Actor들을 새로 구현할 경우에, 이미 동기화 문제는 Mailbox에 의해 해결된 상태이므로 아무런 동기화에 관한 고려를 하지 않아도 된다.

'5.디자인패턴' 카테고리의 다른 글

Adaptive Object Model(AOM) 패턴 및 그 구현  (0) 2016.10.01
Property List 패턴  (0) 2016.09.24
Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Posted by 이세영2
,

참고 URL

https://webcourse.cs.technion.ac.il/236700/Spring2013/ho/WCFiles/pp.pdf


일반적으로 프로퍼티라고 하면 "특성" 정도로 번역할 수 있다. 특성이란 어떤 대상이 동 종의 다른 대상과 다른점을 말한다. 이것을 소프트웨어 용어로 표현하자면 대상이란 객체를 말하고, 특성이란 변수라고 이야기 할 수 있다. 그리고 변수는 이름과 값으로 나타내어 질 수 있다. 이렇게 이름과 값으로 나타내어 질 수 있는 것들을 클래스에 직접 선언하지 않고, HashMap과 같은 Collection에 저장하여 둠으로써 프로퍼티의 동적인 변화에 대응할 수 있도록 하는 것이 Property List 패턴이다.


자료에서는 다음과 같은 이름으로도 불릴 수 있다고 한다.

- Prototype

- Property List

- Properties Object

- Adaptive Object Model(이것은 이 프로퍼티 패턴을 확장한 패턴이다. 동일한 것으로 취급되기는 어렵다)


Property List 패턴의 클래스 다이어그램

Property List 패턴은 PropList 객체를 중심으로 구성된다. PropList 객체는 동일한 타입을 parent 참조 변수를 통해 가지고 있는 복합 객체이다. 이 다이어그램에서는 PropList와 그 인터페이스인 IPropList를 구분시켜 두었다. 이는 parent에 대한 null 체크 등을 방지하기 위해서 parent의 기본 참조를 Null 객체인 NullPropList로 가지고 가기 위해서이다. 이 패턴에서 구조적으로 중요한 부분은 PropList를 parent로 가지고 있다는 점이고, 사실 API의 이해에 더 집중해야 하는 패턴이다.


Property List 패턴의 구현

interface IPropList{

    public Object get(String key);   

    public void put(String key, Object value);   

    public boolean has(String key);   

    public void remove(String key);   

    public Collection<String> keys();   

    public IPropList parent();

}


class NullPropList implements IPropList{

    public Object get(String key){return null;}   

    public void put(String key, Object value){/* not use */}   

    public boolean has(String key){return false;}

    public void remove(String key){/* not use */}

    public Collection<String> keys(){return Collections.emptySet();}

    public IPropList parent(){return null;}

}

class PropList implements IPropList{

    private IPropList parent = new NullPropList();

   

    private Map<String, Object> map = new HashMap<String, Object>();

   

    public PropList(IPropList parent){

        if(parent != null) this.parent = parent;

    }

   

    public Object get(String key){

        if(map.containsKey(key)) return map.get(key);

        return parent.get(key);

    }

   

    public void put(String key, Object value){

        map.put(key,  value);

    }

   

    public boolean has(String key) {

        return map.containsKey(key) || parent.has(key);

    }

   

    public void remove(String key){

        map.remove(key);

    }

   

    public Collection<String> keys(){

        List<String> result = new ArrayList<String>();

        result.addAll(map.keySet());

        result.addAll(parent.keys());

        return result;

    }

   

    public IPropList parent(){ return parent; }

} 

이 구현은 참고 URL 자료에 나와 있는 코드를 거의 그대로 사용한 것이다. 다만, parent에 대한 설정이 생성 즉시 이루어지고 있고, 별도로 parent에 대한 의존성 주입 메소드가 없기 때문에 사실상 일부러 null을 넣지 않는 한 null이 발생할 수는 없다. 따라서 Null 객체 패턴을 적용하여 null을 체크하는 부분들을 모두 없앰으로써 전체 소스의 간결함을 유지하도록 하였다.


이 패턴의 동작은 PropList 객체를 생성하면서 시작된다. 생성자를 통해 객체를 생성할 때 인자로 IPropList 타입인 parent를 넣어 주게 되어 있다. 이는 어떤 프로퍼티 리스트가 다른 프로퍼티 리스트들을 메타 데이터로 가지고 있을 경우를 위한 것이다. 예를 들어 아이폰의 특성을 나타낸다면, 제품명은 모두 다 같은 아이폰이다. 개별 제품들의 시리얼 번호는 각각 다를 것이다. 그렇다면 모두 같은 값을 나타내는 제품명이라는 프로퍼티를 모든 개별 제품들에 넣게 되면 메모리 소모가 많아지게 될 것이다. 따라서 이를 방지하기 위한 목적으로 parent를 별도로 둔다.

이 parent는 필요에 따라서는 계층화 될 수도 있다. 즉 parent가 또 그 상위에 메타 프로퍼티들을 가지도록 구성할 수도 있다.

PropList의 생성 이후 동작은 대부분 프로퍼티의 삽입 / 조회 / 삭제에 관한 것들이다. 일반적인 객체들의 경우 변수에 대한 조회, 변경을 통해서 동작하듯이 Property List 패턴도 그런 목적에 맞도록 이들 연산을 지원한다.


자료에서는 이 패턴이 만들어지게 된 배경에 대해 이렇게 이야기 하고 있다.

- No SQL 데이터 베이스를 이용한 어플리케이션 구현

    관계형 DB의 확장성 문제를 해결하기 위해 No SQL 데이터베이스들을 이용할 경우 key - attribute - value 형식의 테이블을 사용하게 되는데 이런 경우 데이터 베이스와의 연동성이 좋다.

- 프로퍼티 리스트의 유연성이 좋다

- 비즈니스 로직이 프로퍼티의 특정한 값들에 대해서 그다지 관심이 없는 경우에 좋다


사용에 있어서 주의할 점이 있다면 다음과 같다.

- 아무래도 객체를 직접 구현하는 것에 비해서 프로퍼티의 조회는 좀 더 구현이 복잡하다. 만약 이 패턴을 사용하여 복잡한 계산 로직을 구현한다면 문제가 될 것이다. 비즈니스 로직은 매우 단순하면서 다루어야 할 객체의 종류가 많은 어플리케이션에 매우 적합한 패턴이다.

- 프로퍼티란 마치 변수와 같은 것이다. 이 패턴은 객체의 생성 없이 객체를 흉내내려는 패턴이라 할 수 있다. 이를 통해 유연성을 얻을 수는 있지만 캡슐화의 장점은 포기해야 한다.

- 특히 value에 해당하는 객체의 관리에 신중해야 한다. 접근 관리가 잘못되면 전역 변수처럼 사용되버릴 수도 있다.


이 패턴에 맞는 응용 분야는 다음과 같다.

- 동일한 프로토콜을 사용하는 여러 장치들의 데이터를 수집해야 하는 센서 관련 소프트웨어

- 다양한 제품군과 제품들을 취급하는 경우

- 사용자가 필요에 따라서 새로운 제품을 계속 추가해야 하는 경우

'5.디자인패턴' 카테고리의 다른 글

Adaptive Object Model(AOM) 패턴 및 그 구현  (0) 2016.10.01
Actor Model 패턴의 구현(Java)  (0) 2016.09.30
Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Posted by 이세영2
,

Mediator 패턴

5.디자인패턴 2016. 9. 18. 21:46

시스템을 설계하다 보면 이벤트가 발생하는 객체가 여러개(M)이고, 이들 이벤트를 받는 곳도 여러 곳(N)인 경우가 있다. 이런 경우에 모든 이벤트들을 주고 받기 위해서는 M : N의 관계가 생기게 된다. 이렇게 되면 전체 시스템이 복잡해지는 것은 당연하다. Mediator(중재자) 패턴은 이런 다 대 다 관계에 중간 객체를 도입하여 각각 일 대 다 관계를 만들어 주는 패턴이다.

우선 객체들 간의 관계가 아래와 같다고 하자.

각 이벤트 소스들은 모두 이벤트 수신자에게 이벤트를 보내 주어야 한다. 소스나 수신자의 개수가 1~2개 정도 일 경우에는 크게 문제가 없겠지만 그 개수가 늘어나게 되면 위와 같이 복잡한 관계가 만들어지게 된다. 이것은 모든 소스가 각각 모든 수신자들을 알고 있어야 하고, 자신이 알고 있는 모든 수신자에게 이벤트를 전달해 주기 때문이다. 이를 단순화 하기 위해서는 각 소스들은 각각 이벤트가 발생했다는 사실만 별도의 객체에 알려 주고, 이벤트 수신자에게 이벤트를 보내는 역할은 그 객체가 담당하도록 만들면 된다. 이것이 Mediator(중재자) 패턴이다.

위의 객체들 간의 관계를 중재자를 통해 단순화 하면 다음과 같다.

이처럼 소스와 수신자 간의 복잡한 관계를 단순화 시켜줄 수 있다.


Mediator 패턴 클래스 다이어그램


복잡한 관계를 단순화 하기 위해서는 소스와 수신자를 동일화 시킬 필요가 있다. 소스 측은 ISource 인터페이스를 통해서 구현하도록 만들고, 수신자 측은 IDestination 인터페이스를 구현하도록 한다. 그리고 소스 측 구체 클래스인 TcpComm과 SystemSignal을 만들어 주고, 수신자 측 구체 클래스는 Display와 Log를 각각 만들어 준다. 더 많은 소스와 수신자가 있을 때 Mediator가 더 유용해지지만, 복잡함을 피하기 위해서 각각 둘 씩 만 구현했다.

소스는 setMediator() 메소드를 통해서 외부로부터 Mediator 객체를 주입 받는다. 그리고 이벤트가 발생하면 Mediator 객체의 onEvent() 메소드를 호출하여 자신에게 발생한 이벤트를 전달해 주도록 한다. IDestination을 구현한 수신자 객체들은 생성된 후 Mediator 객체에 자신을 등록 시킨다. 이를 통해 Mediator 객체가 이벤트 발생 시 이벤트를 전달 받을 수신자들을 알 수 있게 된다.

아래는 Mediator 패턴의 구현이다.


Mediator 패턴의 구현

interface ISource{

    public void setMediator(Mediator mediator);

    public void eventOccured(String event);

}

class TcpComm implements ISource{

    Mediator mediator;

    public void setMediator(Mediator mediator){ // 중재자 설정

        this.mediator = mediator;

    }

   

    public void eventOccured(String event){ // 이벤트의 전달

        mediator.onEvent("TCP comm", event);

    }

}

class SystemSignal implements ISource{

    Mediator mediator;

    public void setMediator(Mediator mediator){ // 중재자 설정

        this.mediator = mediator;

    }

   

    public void eventOccured(String event){ // 이벤트의 전달

        mediator.onEvent("System", event);

    }

}

interface IDestination{

    public void receiveEvent(String from, String event);

}

class Display implements IDestination{

    public void receiveEvent(String from, String event){

        System.out.println("Display : from " + from + " event : " + event);

    }

}

class Log implements IDestination{

    public void receiveEvent(String from, String event){

        System.out.println("Log : from " + from + " event : " + event);

    }

}

class Mediator{

    List<IDestination> list = new ArrayList<IDestination>();

    public void addDestination(IDestination destination){ list.add(destination); }

   

    public void onEvent(String from, String event){

        for(IDestination each : list){ // 이벤트의 전송

            each.receiveEvent(from, event);

        }

    }

}


실행 방법

public static void main(String[] args) {

    Mediator mediator = new Mediator();

    ISource tcp = new TcpComm();

    tcp.setMediator(mediator);

    ISource system = new SystemSignal();

    system.setMediator(mediator);

    mediator.addDestination(new Display());

    mediator.addDestination(new Log());

    tcp.eventOccured("connected");

    tcp.eventOccured("disconnected");

    system.eventOccured("key input");

    system.eventOccured("mouse input");

} 

main() 메소드에서는 Mediator와 소스, 그리고 수신자를 생성하고, 각각의 관계를 설정해 준다. 이벤트는 소스에서 생성되어 중재자를 거쳐 수신자 쪽으로 흘러 가게 된다. 실행을 시켜 보면 어떤 소스로부터 이벤트가 발생하더라도 수신자들 모두에게 잘 전달됨을 알 수가 있다.

'5.디자인패턴' 카테고리의 다른 글

Actor Model 패턴의 구현(Java)  (0) 2016.09.30
Property List 패턴  (0) 2016.09.24
Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Posted by 이세영2
,

Facade 패턴

5.디자인패턴 2016. 9. 18. 15:12

Facade 패턴은 사용하기에 복잡한 라이브러리에 대한 간편한 인터페이스를 제공하거나 어떤 목적의 동작인지 이해하기 어려운 일련의 작업들에 대한 적절한 네이밍을 통해 사용자로 하여금 그 의미를 쉽게 이해 할 수 있는 인터페이스를 제공하기 위한 패턴이다.

Facade라는 단어의 의미는 잘 지어진 건축물의 정면을 의미한다. 건축물의 정면은 보통 건축물의 이미지와 건축 의도를 나타내기 때문에 오래 전부터 특별한 디자인을 적용하여 의미를 부여했다. 이와 마찬가지로 자칫 동작의 목적과 같은 중요한 사항을 놓치기 쉬운 경우, 이해하기 쉬운 인터페이스를 제공해주면 동작에 대한 이해도가 높아질 수 있다.


Facade 패턴의 클래스 다이어그램

Facade 패턴은 다른 패턴들처럼 일정한 구조를 가지고 있는 것이 아니다. 따라서 구현의 예는 매우 다양할 수 있는데 여기서는 자동차(Car)와 자동차에 대한 Facade(CarFacade)를 통해 Facade 패턴을 살펴 보기로 한다.


Facade 패턴의 구현

class CarFacade{

    Car car;

    public CarFacade(Car car){

        this.car = car;

    }

   

    public void drive(){

        car.enginStart();

        car.doorLock();

        car.wheelsRoll();

    }

   

    public void stop(){

        car.enginStop();

        car.doorUnlock();

        car.wheelsStop();

    }

      

    public void park(){

        car.enginStop();

        car.doorLock();

        car.wheelsStop();

    }

}

class Car{

    public void enginStop(){ System.out.println("engine stop"); }

    public void enginStart(){ System.out.println("engine start"); }

    public void doorLock(){ System.out.println("door locked"); }

    public void doorUnlock(){ System.out.println("door unlocked"); }

    public void wheelsRoll(){ System.out.println("wheels roll"); }

    public void wheelsStop(){ System.out.println("wheels stop"); }

}

Car의 경우 부품인 엔진, 문, 바퀴 등의 동작에 대해 구현되어 있다고 하자. 이들 기능은 자동차의 동작에 매우 중요한 부분이긴 하지만, 일반적인 운전자 또는 자동차의 상태를 쉽게 조작하고자 하는 사람들에게는 각 부품을 일일이 조작하기는 힘들다. 따라서 CarFacade 클래스를 통해서 사용자가 이해하기 쉽게 자동차의 상태를 변경할 수 있도록 한다. 예를 들어 일반적인 운전자는 자동차를 운전(drive) 정지(stop) 주차(park)와 같은 형태로 차의 상태를 조작하기를 윈한다. 따라서 CarFacade가 drive / stop / park와 같은 Facade 메소드를 제공하여 주면 자동차를 한결 쉽게 운전할 수 있게 될 것이다.


사용 방법

public static void main(String[] args) {

    CarFacade facade = new CarFacade(new Car());

    facade.drive();

    facade.stop();

    facade.park();

}


'5.디자인패턴' 카테고리의 다른 글

Property List 패턴  (0) 2016.09.24
Mediator 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Posted by 이세영2
,

Command 패턴

5.디자인패턴 2016. 9. 18. 11:19

프로그래밍을 하다 보면 종종 사용자가 입력한 명령들을 기억해야 하는 경우가 있다. 매크로 커맨드를 만들어야 한다거나 undo / redo와 같이 사용자의 요청에 따라서 이미 수행한 명령을 취소하거나 다시 수행해야 할 필요가 있기 때문이다. 이럴 때 명령들을 객체화 하여 저장해 둠으로써 사용자에게 필요한 기능들을 제공해 주는 것이 Command 패턴이다.

Command 패턴의 이해는 우선 명령보다는 실제 다루고자 하는 객체로부터 시작된다. 우선 Shape 타입이 있고, 하위 타입인 Circle과 Rectangle이 다루려는 대상이다. Shape 타입은 draw() 메소드와 undraw() 메소드를 제공한다. 실제 하려고 하는 일은 이 두 메소드를 이용해서 도형을 그리거나 지우는 일이다.

Command 패턴에서 필요한 것은 Command를 객체화 하는 일이다. Shape을 다루기 위한 Command이므로 ShapeCommand라는 것을 만든다. 이 ShapeCommand는 위에서 정의한 Shape 타입에 대한 연산을 수행하는 객체이다. 예를 들어 ShapeCommand의 execute() 라는 메소드를 호출하면 Shape의 draw()가 호출되고, undo()라는 메소드를 호출하면 undraw()를 호출하는 식이다.

그리고 만약 Command들을 이용하여 사용자의 요청을 처리하고자 할 경우, 즉 사용자가 새로운 도형을 그리거나(execute) 이미 그린 객체를 지우거나(undo), 지웠던 객체를 다시 그리고자 할 때(redo) 명령 객체들을 저장해 두었다가 사용자의 요청에 따라 동작을 수행하는 객체가 필요하다. 아래 예제에서는 이를 CommandManager라는 이름으로 구현하였다.


Command 패턴의 클래스 다이어그램

우리가 다룰 대상 객체는 Circle과 Rectangle 객체이고, 이들을 공통 타입으로 묶기 위해서 Shape 인터페이스를 선언하였다. ShapeCommand는 Command 객체인데, 다양한 Command 객체가 구현될 수 있도록 ICommand 인터페이스를 선언하였다. ShapeCommand 객체는 대상이 되는 도형을 가지고 있고, 사용자의 명령에 따라 대상 객체를 조작한다. CommandManager 클래스는 사용자의 요청에 따라 생성된 Command 객체들을 저장하고 있다가 전체 실행(executeAll()), undo / redo와 같은 Command 객체 핸들링을 지원해주는 클래스이다.


Command 패턴의 구현

class CommandManager{

    private List<ICommand> undo = new ArrayList<ICommand>();

    private List<ICommand> redo = new ArrayList<ICommand>();

   

    public void execute(ICommand command){

        command.execute();

        undo.add(command);

    }

   

    public void executeAll(){

        for(ICommand command : undo){

            command.execute();

        }

    }

   

    public void undo(){

        ICommand command = undo.get(undo.size() - 1);

        command.undo();

        undo.remove(command);

        redo.add(command);

    }

   

    public void redo(){

        ICommand command = redo.get(redo.size() - 1);

        command.redo();

        redo.remove(command);

        undo.add(command);

    }

}

interface ICommand{

    public void execute();

    public void undo();

    public void redo();

}

class ShapeCommand implements ICommand{

    Shape shape;

    public void setShape(Shape shape){ this.shape = shape; }

    public void execute(){ shape.draw(); }

    public void undo(){ shape.undraw(); }

    public void redo(){ execute(); }

}

interface Shape{

    public void draw();

    public void undraw();

}

class Circle implements Shape{

    public void draw() {System.out.println("\tdraw Circle"); }   

    public void undraw() {System.out.println("\tundraw Circle"); }

}

class Rectangle implements Shape{

    public void draw() {System.out.println("\tdraw Rectangle"); }

    public void undraw() {System.out.println("\tundraw Rectangle"); }

}


실행 방법

public static void main(String[] args) {

    Scanner scan = new Scanner(System.in);

    int cmd;

    CommandManager manager = new CommandManager();

    do{

        System.out.println("1.execute");

        System.out.println("2.undo");

        System.out.println("3.redo");

        System.out.println("8.execute All");

        cmd = scan.nextInt();

   

        if(cmd == 1){

            System.out.println("Which on?");

            System.out.println("1.Circle");

            System.out.println("2.Rectangle");

            cmd = scan.nextInt();

            if(cmd == 1){

                ShapeCommand command = new ShapeCommand();

                command.setShape(new Circle());

                manager.execute(command);

            }

            else{

                ShapeCommand command = new ShapeCommand();

                command.setShape(new Rectangle());

                manager.execute(command);

            }

        }

        else if(cmd == 2){

            manager.undo();

        }

        else if(cmd == 3){

            manager.redo();

        }

        else if(cmd == 8){

            manager.executeAll();

        }

    }while(cmd != 9);

}

main() 메소드를 통해서 Command 패턴을 테스트 하는 방식은 다음과 같다. 먼저 대상 객체를 생성하는 명령은 1번 exetue이고, undo는 2번, redo는 3번을 키보드로 입력하면 된다. 여태 실행된 명령어들을 보기 위해서는 8번을, 종료할 때는 9번을 입력한다. 1번을 입력했을 경우에는 어떤 도형을 생성할지를 선택해야 한다. 1번은 Circle을 2번은 Rectangle을 생성한다.

도형을 몇 개 생성하고 나서부터는 자연스럽게 undo와 redo를 수행해보고 결과를 execute all 명령을 통해 확인해 볼 수 있다.

CommandManager를 조금씩 확장시켜 나가면서 Macro(특정 시점 이후에 입력된 여러 명령어들을 모아서 연속으로 실행 시키는 것)를 만드는 것도 가능하다.

'5.디자인패턴' 카테고리의 다른 글

Mediator 패턴  (0) 2016.09.18
Facade 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Composite 패턴  (0) 2016.09.17
Posted by 이세영2
,

Flyweight 패턴

5.디자인패턴 2016. 9. 18. 07:48

Flyweight 패턴은 비용이 큰 자원을 공통으로 사용할 수 있도록 만드는 패턴이다. 자원에 대한 비용은 크게 두가지로 나눠 볼 수 있다.

1. 중복 생성될 가능성이 높은 경우.

중복 생성될 가능성이 높다는 것은 동일한 자원이 자주 사용될 가능성이 매우 높다는 것을 의미한다. 이런 자원은 공통 자원 형태로 관리하고 있다가 요청이 있을 때 제공해 주는 편이 좋다.

2. 자원 생성 비용은 큰데 사용 빈도가 낮은 경우.

이런 자원을 항상 미리 생성해 두는 것은 낭비이다. 따라서 요청이 있을 때에 생성해서 제공해 주는 편이 좋다.

이 두가지 목적을 위해서 Flyweight 패턴은 자원 생성과 제공을 책임진다. 자원의 생성을 담당하는 Factory 역할과 관리 역할을 분리하는 것이 좋을 수 있으나, 일반적으로는 두 역할의 크기가 그리 크지 않아서 하나의 클래스가 담당하도록 구현한다.


Flyweight 패턴의 클래스 다이어그램

Flyweight 패턴의 구현

class Flyweight{

    Map<String, Subject> map = new HashMap<String, Subject>();

   

    public Subject getSubject(String name){

        Subject subject = map.get(name);

        if(subject == null){

            subject = new Subject(name);

            map.put(name, subject);

        }

        return subject;

    }

}

class Subject{

    private String name;

    public Subject(String name){

        this.name = name;

        System.out.println("create : " + name);

    }

}


사용 방법

public static void main(String[] args) {

    Flyweight flyweight = new Flyweight();

    flyweight.getSubject("a");

    flyweight.getSubject("a");

    flyweight.getSubject("b");

    flyweight.getSubject("b");

}

구현의 내용은 단순하다. Flyweight 클래스는 관리해야 할 자원인 Subject에 대한 생성과 제공을 담당한다. 외부에서 특정 명칭(name)의 자원을 getSubject() 메소드를 통해 요청해 오면 우선 이미 생성된 자원인지를 검사한다. 그리고 이미 생성되어 있었으면 기존의 자원을 제공하고, 생성되지 않은 자원은 생성을 하여 자신의 map에 저장하고 난 후에 제공해 준다. 이 과정을 통해서 Flyweight 패턴이 중복된 자원의 생성을 관리할 수 있다.


또 다른 예제(Java 라이브러리 내의 Flyweight 패턴)

Flyweight 패턴은 실제 여러 곳에서 사용된다. 쓰레드 풀이나 객체 재사용 풀도 일종의 Flyweight 패턴이다. Java 라이브러리들 중에서도 이를 사용하는데, 매우 사용 빈도가 높은 Integer 클래스에도 이와 같은 패턴이 적용되어 있다. 아래는 Integer 클래스에서 사용하는 Flywight 패턴의 코드이다.

private static class IntegerCache {

    static final int low = -128;

    static final int high;

    static final Integer cache[];

    static { // static으로 실행되기 때문에 실행 이전에 생성이 완료됨.

        // high value may be configured by property

        int h = 127;

        String integerCacheHighPropValue =

            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");

        if (integerCacheHighPropValue != null) {

            try {

               int i = parseInt(integerCacheHighPropValue);

               i = Math.max(i, 127);

               // Maximum array size is Integer.MAX_VALUE

               h = Math.min(i, Integer.MAX_VALUE - (-low) -1);

           } catch( NumberFormatException nfe) {

               // If the property cannot be parsed into an int, ignore it.

           }

        }

        high = h;

        cache = new Integer[(high - low) + 1]; // Flyweight 생성 부분

        int j = low;

        for(int k = 0; k < cache.length; k++)

            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)

        assert IntegerCache.high >= 127;

    }

    private IntegerCache() {}

}

public static Integer valueOf(int i) {  // Flyweight 객체 제공 부분

    if (i >= IntegerCache.low && i <= IntegerCache.high)

        return IntegerCache.cache[i + (-IntegerCache.low)];

    return new Integer(i);

} 

이 소스에서는 IntegerCache라는 static 클래스를 통해서 Integer의 일정 범위를 미리 생성해 둔다. 전체 범위는 VM(Virtual Machine)에 따라서 달라질 수 있음을 알 수 있다. 하지만 보통은 -128에서 127까지 범위의 Integer 클래스를 배열 형식으로 만들어 둔다. 그리고 valueOf() 메소드가 호출 되었을 때 요청된 Integer 값이 -128에서 127 사이라면 이미 생성된 Integer 객체를 반환해 준다. 이 코드는 jre1.8.0_91 기준으로 Integer 클래스의 780 ~833 번째 라인에 들어 있는 코드이다.

'5.디자인패턴' 카테고리의 다른 글

Facade 패턴  (0) 2016.09.18
Command 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Composite 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Posted by 이세영2
,

Chain Of Responsibility 패턴은 책임의 사슬이라고 번역할 수 있는 패턴이다. 여러 객체가 각기 다른 객체의 맴버 객체로 연결이 되어 있고(그 구조는 상관이 없다. 선형일 수도 있고 트리일 수도 있다.), 어떤 작업에 대한 요청이 발생했을 때 스스로 해결할 수 있을 경우에만 그 작업에 대해 직접 수행하고, 그렇지 않은 경우 맴버 객체에게 작업을 넘긴다.

패턴의 구조가 명확하지 않은 만큼 구현의 방법 또한 다양하다. 일반적으로는 사슬을 구성하는 상위 타입은 동일하게 가져가고, 개별 동작은 하위 타입이 구현하는 형태를 띈다. 하지만 하위 타입 구현 없이 동일한 타입만으로 사슬을 구성하는 것이 가능하고, Composite 패턴과 같이 트리 형태의 사슬을 구성하는 것 또한 가능하다.


Chain Of Responsibility 패턴 클래스 다이어그램

Chain Of Responsibility 패턴의 구현

abstract class Boundary{

    protected int upper;

    protected int lower;

   

    protected Boundary nested = null;

    public void setNested(Boundary nested){ this.nested = nested; }

   

    public Boundary(int upper, int lower){

        this.upper = upper;

        this.lower = lower;

    }

               

    public void action(int value){

        if(isInBoundary(value) == true) individualAction();

        else if(nested != null) nested.action(value);

        else individualAction();

    }

    abstract protected void individualAction();

   

    private boolean isInBoundary(int value){

        if(value >= lower && value <= upper) return true;

        return false;

    }

}

class NormalVoltage extends Boundary{

    public NormalVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("normal operation");

    }

}

class WarningVoltage extends Boundary{

    public WarningVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("warning operation");

    }

}

class FaultVoltage extends Boundary{

    public FaultVoltage(int upper, int lower){

        super(upper, lower);

    }

   

    protected void individualAction(){

        System.out.println("fault operation");

    }

}

우선 책임의 사슬 패턴을 위해서 사슬을 구성하는 Boundary라는 상위 클래스를 선언한다. 이 클래스는 책임 사슬을 구성할 수 있도록 setNested() 메소드를 제공한다. 이 메소드는 동일한 Boundary 객체를 받아 맴버 객체로 설정해 준다. 만약 어떤 객체가 작업을 수행할 조건에 맞지 않으면 맴버 객체로 설정된 Boundary 객체에게 작업을 위임한다.

하위 클래스에는 3종류가 있다. 우선 정상 범위의 전압을 나타내는 NormalVoltage 클래스가 있고, 경고 상태와 고장 상태를 나타내는 WarningVoltage와 FaultVoltage 클래스가 있다. 이들 클래스는 각각 자신이 작업을 수행해야 할 경우에 호출될 individualAction() 메소드를 재정의 하고 있다.

아래 main() 메소드에서는 이들간의 관계를 설정하고 동작시키는 코드가 있다.


실행 방법

public static void main(String[] args) {

    Boundary voltage = new NormalVoltage(230, 210);

    Boundary warning = new WarningVoltage(240, 200);

    Boundary fault = new FaultVoltage(Integer.MAX_VALUE, Integer.MIN_VALUE);

    voltage.setNested(warning);

    warning.setNested(fault);

    voltage.action(220);

    voltage.action(235);

    voltage.action(245);

}

기본적으로 NormalVoltage 객체가 가장 바깥쪽에 있고, WarningVoltage 객체가 그 다음, 가장 안쪽에는 FaultVoltage 객체가 있다. action() 메소드를 통해 입력되는 입력 값을 최초로 받는 것은 가장 바깥쪽 객체인 NormalVoltage 객체이다. 이 객체는 상위 객체인 Boundary 객체가 정한 바와 같이 우선 입력된 값이 자신의 범위에 맞는지를 확인한다. 만약 맞으면 individualAction() 메소드를 호출하여 자신이 작업을 수행한다. 만약 맞지 않는다면 nested 객체가 있는지를 확인하고 있으면 작업을 위임하기 위해 nested 객체의 action() 메소드를 호출한다. 만약 nested 객체가 없다면 자신이 최종 작업 수행자이므로 자신의 individualAction() 메소드를 수행한다.


다시 이야기 하지만 이 예제는 일반적인 형태이긴 하나 꼭 하위 객체를 생성해야 하는 것은 아니다. 오히려 이 예제와 같은 경우라면 하위 객체를 생성하는 것보다는 Boundary 객체를 범용적으로 활용할 수 있도록 만드는 편이 좋다. 그렇게 하면 책임 사슬을 좀 더 유연하게 늘이거나 줄일 수 있다. 경고 레벨을 늘리고자 할 경우 그냥 Boundary 객체를 중간에 하나씩 추가해 주기만 하면 된다. 그러면 좀 더 촘촘한 간격으로 경고와 고장을 나타낼 수 있게 된다. 이렇게 하는 것이 Chain Of Responsibility 패턴을 사용하는 목적인 유연성을 좀 더 반영할 수 있을 것이다.

'5.디자인패턴' 카테고리의 다른 글

Command 패턴  (0) 2016.09.18
Flyweight 패턴  (0) 2016.09.18
Composite 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Enum Abstract Factory 패턴  (0) 2016.09.16
Posted by 이세영2
,

Composite 패턴

5.디자인패턴 2016. 9. 17. 20:17

Composite 패턴은 "복합 객체(Composite Object)"와 "단일 객체(Leaf Object)"를 동일하게 취급하고 싶을 때 사용하는 패턴이다.

일반적으로 가장 이해하기 쉬운 비유는 디렉토리와 파일의 관계이다. 디렉토리는 복합 객체와 유사하고, 파일은 단일 객체와 유사한 특성을 가지고 있다. 디렉토리는 자기 내부에 같은 디렉토리 또는 파일을 가지고 있을 수 있다. 파일은 단일 객체이기 때문에 내부에 다른 객체를 가지고 있을 수 없다. 만약 디렉토리 - 파일 관계를 보다 단순화 시키고자 한다면 디렉토리와 파일을 동일한 타입으로 취급하는 방법을 사용할 수 있을 것이다.

물론 모든 동작들을 이와 같이 만들 수는 없다. 적어도 디렉토리와 파일이 유사한 동작을 할 수 있는 경우에만 가능하다. 아래의 예제에서는 파일의 갯수를 세는 동작과 파일의 라인 수(공백을 제외한)를 세는 동작에 대해서 정의하고 있다. 디렉토리는 파일이 아니고, 내부에 파일들을 가지고 있기 때문에 파일 개수를 세는 동작을 단일 객체인 파일에 위임할 수 있다. 또한 라인 수를 계산하는 동작에 대해서도 디렉토리는 항상 파일에게 이를 위임한다. 파일은 파일 갯수에 대해 물어보면 항상 1이라고 대답한다. 라인 수에 대한 응답은 파일을 열어 공백을 제외한 라인의 수를 제공하도록 한다. 만약 디렉토리와 파일이 동일한 상위 타입을 가지고 있다면 외부에서는 최 상위 디렉토리에게 파일의 수와 라인 수를 물어보는 것만으로도 전체 디렉토리 구조 상에서의 정보들을 얻어 낼 수 있을 것이다. 이런 복잡한 동작을 단순하게 만들어 줄 수 있는 것이 Composite 패턴이다.


Composite 패턴 다이어그램

Item 인터페이스는 디렉토리와 파일로부터 파일 갯수(getFiles()) 및 라인 수(getLines())를 얻어 올 수 있는 공통 인터페이스를 정의한다. 그리고 디렉토리를 구현한 DirectionItem과 파일을 구현한 FileItem은 외부에서 동일하게 취급될 수 있도록 Item 인터페이스를 구현한다. FileItem은 단일 객체로써, 자신의 파일 수( = 항상 1)와 라인 수를 계산하여 넘겨주도록 구현된다. DirectoryItem은 복합 객체로서 내부에 하위 디렉토리 및 하위 파일들을 담을 수 있는 List 를 소유한다.

DirectoryTree 클래스는 실제 디렉토리를 방문하면서 디렉토리 및 파일의 트리 구조를 만들고 최종 결과를 얻어 낼 수 있는 인터페이스를 가지고 있다. 이를 위해서 DirectoryTree 객체는 makeTree() 메소드를 통해서 Item 객체들을 생성하고 디렉토리 구조를 만드는 작업을 수행한다.

아래는 위의 클래스 다이어그램을 구현한 코드이다.


Composite 패턴의 구현

interface Item{

    public int getFiles();

    public int getLines();

}

class DirectoryItem implements Item{

    List<Item> children = new ArrayList<Item>();

    public void addChild(Item child){ children.add(child); }

    File file;

    public DirectoryItem(File file){

        this.file = file;

    }

    public int getFiles(){

        int result = 0;

        for(Item each : children){

            result += each.getFiles();

        }

        return result;

    }

    public int getLines(){

        int result = 0;

        for(Item each : children){

            result += each.getLines();

        }

        return result;

    }

}

class FileItem implements Item{

    File file;

    public FileItem(File file){ this.file = file; }

    public int getFiles(){

        return 1;

    }

    public int getLines(){

        Scanner scan;

        int result = 0;

        try {

        scan = new Scanner(file);

        while(scan.hasNextLine()){

            String line = scan.nextLine();

            if(!line.equals("")) result ++;

        }

        } catch (FileNotFoundException e) {

            System.out.println("File Not Found");

        }

        return result;

    }

}


Composite 패턴을 이용하여 디렉토리 트리를 만드는 DirectoryTree 클래스

class DirectoryTree{

    String path;

    public DirectoryTree(String path){ this.path = path; }

    Item items;

    public void makeTree(){

        File file = new File(path);

        items = tourDirectory(file);

    }

    private Item tourDirectory(File file){

        DirectoryItem directory = new DirectoryItem(file);

        File[] files = file.listFiles();

        for(File each : files){

            if(each.isDirectory()){

                Item childDirectory = tourDirectory(each);

                directory.addChild(childDirectory);

            }

            else{

                FileItem childFile = new FileItem(each);

                directory.addChild(childFile);

            }

        }

        return directory;

    }

    public int getFiles(){

        return items.getFiles();

    }

    public int getLines(){

        return items.getLines();

    }

} 


사용 방법

public static void main(String[] args) {

    DirectoryTree treenew DirectoryTree("C:/JavaDesignPattern/src/GofPattern/");

    tree.makeTree();

    System.out.println(tree.getFiles());

    System.out.println(tree.getLines());

} 


여러 다른 타입의 객체들을 동일한 타입으로 취급할 수 있게 되면 구현은 단순해지기 마련이다. Composite 패턴에서는 복합 객체와 단일 객체가 동일하게 취급된다. 그리고 자칫 복잡해질 수도 있는 복합 객체가 하위 객체들에게 작업을 위임하는 방식으로 동작함으로써 구현이 단순해졌음을 알 수 있다. 

'5.디자인패턴' 카테고리의 다른 글

Flyweight 패턴  (0) 2016.09.18
Chain Of Responsibility 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Enum Abstract Factory 패턴  (0) 2016.09.16
Proxy 패턴과 그 활용  (0) 2016.09.16
Posted by 이세영2
,

Iterator 패턴

5.디자인패턴 2016. 9. 17. 10:55

Java에서는 이제 언어적인 지원이 이루어지기 때문에 잘 쓰이지는 않지만 여전히 C++ 등에서는 자주 쓰이는 패턴이다. 보통 컬렉션들(C++에서는 컨테이너)에 쓰인다.

Iterator 패턴은 일련의 순서를 가진 데이터 집합에 대하여 순차적인 접근을 지원하는 패턴이다.


Iterator 패턴 클래스 다이어그램


Iterator 패턴의 구현

interface IIterator{

    public boolean hasNext();

    public Object next();

}

interface IContainer{

    public IIterator iterator();

}

class NameContainer implements IContainer{

    List<String> list = new ArrayList<String>();

    public void add(String name){

        list.add(name);

    }

    class NameIterator implements IIterator{

        String current;

        @Override

        public boolean hasNext() {

            if(list.size() == 0) return false;

            if(list.indexOf(current) != list.size()-1) return true;

            return false;

        }

        @Override

        public Object next() {

            if(list.size() == 0) return null;

            if(list.indexOf(current) != list.size()-1){

                current = list.get(list.indexOf(current) + 1);

                return current;

            }

            return null;

        }

    }

    @Override

    public IIterator iterator() {

        return new NameIterator();

    }

}


사용방법

public static void main(String[] args) {

    NameContainer names = new NameContainer();

    names.add("Nick");

    names.add("Ceasar");

    names.add("Augustus");

    for(IIterator iter = names.iterator(); iter.hasNext();){

        String name = (String)iter.next();

        System.out.println(name);

    }

}



우선 데이터 집합을 가지고 있는 Container는 모두 Iterator를 제공해 줄 수 있어야 한다. 따라서 Container의 인터페이스인 IContainer와 Iterator의 인터페이스인 IIterator를 선언한다. 이는 어떤 Container를 구현하더라도 같은 방식으로 Iterator를 사용할 수 있도록 하기 위함이다. 구체 클래스들은 선언한 인터페이스들을 구현한다.

Iterator가 직접 데이터에 접근할 수 있도록 하는 것이 구현이 단순해 지므로 보통 Iterator는 Container의 내부 클래스로 구현하는 경우가 많다. 위의 예제에서도 NameContainer 내부에 NameIterator를 선언하였다. NameContainer로부터 NameIterator를 받아 오는 메소드는 iterator()인데, 이 메소드가 호출되면 NameContainer를 새로 생성하여 넘겨준다. 이렇게 해야 Iterator를 가지고 데이터에 접근하는 쓰레드가 많아도 문제 없이 동작이 가능하다.(데이터 집합의 추가/삭제로 인한 쓰레드 문제는 별도로 처리해 주어야 한다.)

'5.디자인패턴' 카테고리의 다른 글

Chain Of Responsibility 패턴  (0) 2016.09.17
Composite 패턴  (0) 2016.09.17
Enum Abstract Factory 패턴  (0) 2016.09.16
Proxy 패턴과 그 활용  (0) 2016.09.16
Observer 패턴  (0) 2016.09.16
Posted by 이세영2
,

미리 봐야 할 것 : Abstract Factory 패턴

Abstract Factory 패턴의 경우도 Factory Method 패턴과 마찬가지로 Factory 객체 생성의 이슈가 있다. 가만히 놔두면 Factory 객체가 자주 생성될 수 있고, 이를 해결하려면 Singleton 패턴을 적용해 주어야 한다. 이러한 문제를 해결하는 방법으로 enum을 활용한 방법이 있다. 이를 Enum Abstract Factory 패턴이라고 한다.


우선 생성하고자 하는 대상을 알아보자. Abstract Factory 패턴은 일련의 연관성 있는 객체 군을 생성하는 패턴이다. Xp 객체 군과 Linux 객체군이 있다고 가정하자. 그리고 각 객체군은 Button과 Edit 객체를 포함하고 있다. 마지막으로 생성된 Button과 Edit 객체는 객체군과 상관 없이 동일하게 취급 되어야 한다. 따라서 IButton과 IEdit와 같이 인터페이스를 선언해 주고 Xp 타입과 Linux 타입에 해당하는 클래스들을 선언해 준다.


생성하고자 하는 객체들의 선언

interface IButton {};

interface IEdit   {};

class XpEdit implements IEdit {

    public XpEdit(){System.out.println("XpEdit()");}

};

class XpButton implements IButton {

    public XpButton(){System.out.println("XpButton()");}

};

class LinuxEdit implements IEdit {

    public LinuxEdit(){System.out.println("LinuxEdit()");}

};

class LinuxButton implements IButton {

    public LinuxButton(){System.out.println("LinuxButton()");}

};


이제 Abstract Factory를 enum을 이용해서 구현한다.


Enum Abstract Factory의 구현

interface IFactory

{

    public IButton createButton();

    public IEdit createEdit();

};


public enum EnumAbstractFactory implements IFactory{

    XP{

        public IButton createButton() {

            return new XpButton();

        }

        public IEdit createEdit() {

            return new XpEdit();

        }

    }

    ,LINUX{

        public IButton createButton() {

            return new LinuxButton();

        }

        public IEdit createEdit() {

            return new LinuxEdit();

        }

    }

    ;

}

각 Factory들도 사용하는 측에서는 동일한 타입으로 취급이 가능해야 한다. 따라서 Factory들에 대한 추상 타입인 IFactory를 선언해 준다. 그리고 개별 Factory들이 IFactory를 구현하도록 선언해 준다. enum은 실제로 abstract class이기 때문에 인터페이스를 구현하는 것도 가능하다.


사용 방법

public static void main(String[] args) {

    IFactory factory = EnumAbstractFactory.XP;

    IButton button = factory.createButton();

    IEdit edit = factory.createEdit();

    factory = EnumAbstractFactory.LINUX;

    button = factory.createButton();

    edit = factory.createEdit();

}

사용 방법은 일반적인 enum과 동일하다. 대신 Factory 객체의 생성 문제가 enum을 통해 해결되었다. enum의 각 하위 타입은 public static final 키워드를 암시적으로 달고 있다. 따라서 Singleton의 특성을 가지기 때문에 별도로 생성할 필요가 없고, 오직 한번만 생성이 된다.

'5.디자인패턴' 카테고리의 다른 글

Composite 패턴  (0) 2016.09.17
Iterator 패턴  (0) 2016.09.17
Proxy 패턴과 그 활용  (0) 2016.09.16
Observer 패턴  (0) 2016.09.16
Abstract Factory 패턴  (0) 2016.09.16
Posted by 이세영2
,

Proxy 패턴은 단순하면서도 자주 쓰이는 패턴이고 활용 방식도 다양하다. 이 글에서는 Proxy 패턴과 함께 그 다양한 활용 방법에 대해서 이야기 해 보도록 하겠다.


Proxy 패턴의 클래스 다이어그램


Proxy 패턴의 구현(기본형)

interface Subject {

    public Object action();

}

class RealSubject implements Subject {

    public RealSubject(){}

    public Object action() { /* do something */ return null;}

}

class Proxy implements Subject{

    private RealSubject realSubject;

    public Proxy(){

        realSubject = new RealSubject();

    }

    public Object action() {

        return realSubject.action();

    }

}


Proxy 패턴은 Proxy 패턴의 기본형을 어떤 방식으로 변형하느냐에 따라 달라진다.

Proxy 패턴의 변형에는 두가지 방향이 있다.


1. RealSubject의 생성 시점

2. Proxy 객체의 action()에서 하는 일


위의 두가지 변형을 조합함으로써 활용도를 다르게 가지고 갈 수 있다.


동적 생성 프록시

class Proxy implements Subject{

    private RealSubject realSubject;

    public Proxy(){}

    public Object action() {

        if(realSubject == null){

            realSubject = new RealSubject();

        }

        return realSubject.action();

    }

}

동적 생성 프록시는 프록시 객체가 실제 객체를 생성하지 않고 시작한다. 그리고 실제 요청(action() 메소드 호출)이 들어 왔을 때 실제 객체를 생성한다. 이 구현은 실제 객체의 생성에 많은 자원이 소모 되지만 사용 빈도는 낮을 때 쓰는 방식이다.


자원관리 프록시

class RealSubject implements Subject {

    private String fileName;

    @Override

    public void action() {

        System.out.println("Displaying " + fileName);

    }

    public void loadFromDisk(String fileName){

        this.fileName = fileName;

        System.out.println("Loading " + fileName);

    }

}

class Proxy implements Subject{

    private RealSubject realSubject;

    private String fileName = "";

    public Proxy(){

        realSubject = new RealSubject();

    }

   

    @Override

    public void action() {

        realSubject.action();

    }

   

    public void loadFromDisk(String fileName){

        if(this.fileName.equals(fileName)) return;

        this.fileName = fileName;

        realSubject.loadFromDisk(fileName);

    }

}

자원관리 프록시는 실제 객체가 비용이 많이 드는 자원에 대한 생성을 담당할 때 쓰인다. 실제 객체는 기본적인 자원 생성(위 예제에서는 loadFromDisk() 메소드)를 담당하고, 프록시는 이 자원이 이미 생성되었는지를 체크한다. 만약 이미 생성된 자원이라면 자원에 대한 생성을 스킵한다. 이를 통해서 불필요하게 자원을 중복으로 생성하는 것을 방지한다.


가상 프록시

class Proxy implements Subject{

//    private RealSubject realSubject;

    public Proxy(){}

    public Object action() {

        return virtualAction();

    }

   

    private Object virtualAction(){ /* do something */ return null; }

}

실제 객체가 여러가지 이유로 존재하지 않을 경우에 사용되는 방식이다. 특히 여러 단위의 조직이 협업을 할 때 인터페이스는 정의되어 있으나 실제 구현은 아직 되어 있지 않은 경우에 이 가상 프록시를 사용한다. 가상 프록시에 시뮬레이션 된 동작을 하도록 구현해 두고 개발을 진행한 후, 실제 객체가 완성된 이후에 프록시를 실제 객체로 대체 시킨다. 이런 방식을 통해서 실제 객체를 붙여 보기 전에 발생할 수 있는 문제점들을 미리 테스트 해 볼 수 있기 때문에 통합 작업에서 발생하는 문제점들을 줄일 수 있다.


원격 프록시

class Proxy implements Subject{

//    private RealSubject realSubject;

    public Proxy(){}

    public Object action() {

        return remoteAction();

    }

   

    private Object remoteAction(){

        connect();

        Object result = getData();

        disconnect();

        return result;

    }

   

    private void connect(){/* connect to remote */}

    private Object getData(){/* data request and wait response */ return null;}

    private void disconnect(){/* disconnect from remote */}

}

프록시가 마치 원격에 있는 실제 객체처럼 동작하도록 하는 방법이다. 프록시 객체를 사용하는 객체들은 실제 객체와 동일한 인터페이스를 통해 프록시를 사용하고, 데이터 통신이나 변환과 같은 작업은 프록시 객체 내부에서 수행하도록 한다.


AOP(Aspect Oriented Programming) 프록시

class Proxy implements Subject{

    private RealSubject realSubject;

    public Proxy(){

        realSubject = new RealSubject();

    }

    public Object action() {

        preProcess();

        Object result = realSubject.action();

        postProcess();

        return result;

    }

    private void preProcess(){/* 선행 작업 */}

    private void postProcess(){/* 후행 작업 */}

}

AOP는 모든 객체에 공통으로 적용되는 기능들을 구현하는 개발 방법을 말한다. 예를 들어 어떤 객체를 멀티 쓰레딩 환경에서 보호해야 한다거나, 어떤 메소드 호출이 걸리는 시간을 측정하거나, 어떤 동작에 대한 트랜젝션을 작성하는 등의 경우이다. 이런 것들은 실제 객체의 행위와 별개로 이루어질 수 있다.

위의 코드에서 보면 action() 메소드가 preProcess() 메소드를 먼저 호출 한 후 realSubject의 action()을 호출한다. 그리고 이후에 postProcess() 메소드를 호출하도록 되어 있다. 이 preProcess()와 postProcess()에 어떤 작업을 구현해 넣느냐에 따라서 관점(Aspect)이 달라진다. 만약 mutex.lock()과 mutex.unlock()을 넣는다면 멀티쓰레드 안정성을 제공할 수 있다. 만약 preProcess()와 postProcess()의 호출 시각을 저장하고 둘의 차를 계산한다면 action() 메소드 실행에 걸린 시간을 측정할 수 있다.

이렇게 실제 객체는 그대로 두고 모든 객체들이 공통으로 수행해야 할 일들을 프록시 객체를 통해서 구현할 수 있다.(실제 Spring과 같은 프레임워크에서 AOP를 구현하는 방식은 reflection을 이용하는 방식이다. 단지 여기서는 같은 형태의 구현을 프록시로 할 수 있다는 점 만 보여주는 것이다.)

'5.디자인패턴' 카테고리의 다른 글

Iterator 패턴  (0) 2016.09.17
Enum Abstract Factory 패턴  (0) 2016.09.16
Observer 패턴  (0) 2016.09.16
Abstract Factory 패턴  (0) 2016.09.16
Factory Method 패턴  (4) 2016.09.16
Posted by 이세영2
,

Observer 패턴

5.디자인패턴 2016. 9. 16. 12:38

Observer 패턴은 관찰 대상 객체에 변경 사항이 발생했을 때 이벤트 형태로 알림을 받는 패턴이다. Push / Pool 방식의 데이터 처리 방식 중에서 Push에 해당한다. 소프트웨어에서 데이터 처리에서는 너무 자주 나타나는 패턴이기 때문에 자주 쓰이고, 그렇기 때문에 사용에 주의해야 하는 패턴이다. Observer 패턴을 너무 자주 사용하면 구조와 동작을 알아보기 힘들어진다.

일반적으로 Observer 패턴에서 관찰 대상이 되는 객체(Observerable) 한 개에 대하여 다수의 관찰자 객체(Observer)가 존재한다. 다수의 관찰자 객체들은 모두 같은 타입으로 취급되어야만 구현이 단순해진다. 따라서 관찰자들은 모두 같은 인터페이스나 상위 클래스를 상속 받아 구현된다. 관찰 대상으로부터 이벤트가 발생하여 관찰자 객체에게 전달되었을 때 이 데이터를 처리하는 내용은 개별 관찰자 객체들이 자신에 맞게 구현한다.


Observer 패턴의 클래스 다이어그램


Observer 패턴의 구현

interface IObserver{

    public void notify(Object data);

}

class Observable{

    private List<IObserver> observers = new ArrayList<IObserver>();

   

    public void registObserver(IObserver observer){ observers.add(observer); }

   

    String []table = {"a","b","c","d","e"};

    public void update(String data, int index){

        table[index] = data;

        onUpdate();

    }

    public void onUpdate(){

        for(IObserver observer : observers){

            observer.notify(table);

        }

    }

}

class Graph implements IObserver{

    public Graph(Observable obervable){ obervable.registObserver(this); }

    public void notify(Object data){

        String[] table = (String[])data;

        System.out.println("Graph : ");

        for(int i = 0; i < 5; i++) System.out.println(table[i]);

    }

}

class Display implements IObserver{

    public Display(Observable obervable){ obervable.registObserver(this); }

    public void notify(Object data){

        String[] table = (String[])data;

        System.out.println("Display : ");

        for(int i = 0; i < 5; i++) System.out.println(table[i]);

    }

} 


실행 방법

public static void main(String[] args) {

    Observable observable = new Observable();

    IObserver graph = new Graph(observable);

    IObserver display = new Display(observable);

    observable.update("abc", 1);

    observable.update("def", 2);

    observable.update("ghi", 3);

}


위의 코드에서 관찰 대상은 Observable이고, 관찰자 인터페이스는 IObserver로 선언되어 있다. 그리고 관찰자 구체 클래스는 Graph와 Display이다. 위의 코드에서는 우선 관찰 대상인 Observable 객체가 먼저 생성되어야 한다. 그리고 관찰자 객체들은 생성자에서 Observable 객체를 매개변수로 받은 후, Observable 객체에 자신을 관찰자로 등록한다.(Observable 객체에 관찰자를 등록 시키는 방법은 꼭 생성자를 이용한 방법이 아니어도 좋다.) 이후 관찰 대상에 이벤트가 발생하면 각 관찰자 객체들의 notify 함수를 통해서 발생한 이벤트를 전달해 준다.

'5.디자인패턴' 카테고리의 다른 글

Enum Abstract Factory 패턴  (0) 2016.09.16
Proxy 패턴과 그 활용  (0) 2016.09.16
Abstract Factory 패턴  (0) 2016.09.16
Factory Method 패턴  (4) 2016.09.16
Memento 패턴  (2) 2016.09.13
Posted by 이세영2
,

Abstract Factory 패턴은 연관성이 있는 객체군이 여러벌 있을 경우 각각의 구체 Factory 클래스를 통해 동일한 객체군을 생성하는 패턴이다.

예를 들어 윈도우 화면을 만든다고 생각해 보자. 윈도우에는 버튼도 들어갈 수 있고 에디터 박스도 들어갈 수 있다. 실행 환경에 따라서 구분해 보면 Linux 환경, 윈도우 XP 환경 등 다양하다. 이 실행 환경에 따라서 윈도우의 모양(테마)은 바뀔 수 있다. 이 경우 버튼과 에디터 박스는 공통으로 생성될 수 있지만 테마는 Linux냐 Xp냐에 따라서 다르다. 이런 경우 Linux와 연관된 객체군들은 Linux 객체군을 생성하는 클래스가, Xp와 연관된 객체군들은 Xp 객체군을 생성하는 클래스가 생성하도록 할 수 있다. 이런 상황을 구현하는 패턴이 Abstract Factory 패턴이다.

Abstract Factory 패턴을 설명하기 앞서서 Factory Method 패턴과의 관계를 알아보자.

유사점 : 객체를 생성하고, 구체적인 타입을 감춘다.

차이점

1. Factory Method 패턴은 생성 이후 해야 할 일의 공통점을 정의하는데 촛점을 맞추는 반면, Abstract Factory 패턴은 생성해야 할 객체군의 공통점에 촛점을 맞춘다.

2. 따라서 Factory Method 패턴은 생성해야 할 객체가 한 종류이고, Abstract Factory 패턴은 여러 종류이다.


물론 유사점과 차이점을 조합해서 복합 패턴을 구성하는 것도 가능하다.


그러면 Abstract Factory 패턴의 구현을 알아보자.

Abstract Factory 패턴 클래스 다이어그램

Abstract Factory 패턴의 구현

interface IFactory

{

    public IButton createButton();

    public IEdit createEdit();

};

interface IButton {};

interface IEdit   {};

class XPFactory implements IFactory

{

    public IButton createButton() {/// XpButton을 생성하는 함수.

        return new XpButton();

    }

    public IEdit createEdit() {/// XpEdit를 생성하는 함수.

        System.out.println("XpEdit()");

        return new XpEdit();

    }

}

class LinuxFactory implements IFactory

{

    public IButton createButton() {/// LinuxButton을 생성하는 함수.

        System.out.println("LinuxButton()");

        return new LinuxButton();

    }

    public IEdit createEdit() {/// LinuxEdit를 생성하는 함수.

        System.out.println("LinuxEdit()");

        return new LinuxEdit();

    }

}

class XpEdit implements IEdit {

    public XpEdit(){System.out.println("XpEdit()");}

};

class XpButton implements IButton {

    public XpButton(){System.out.println("XpButton()");}

};

class LinuxEdit implements IEdit {

    public LinuxEdit(){System.out.println("LinuxEdit()");}

};

class LinuxButton implements IButton {

    public LinuxButton(){System.out.println("LinuxButton()");}

};


사용 방법

public static void main(String[] args) {

    IFactory factory = null;

    factory = new LinuxFactory();

    System.out.println("LinuxFactory()");

    factory.createButton();

    factory.createEdit();

    factory = new XPFactory();

    System.out.println("XPFactory()");

    factory.createButton();

    factory.createEdit();

}


위의 예제에서는 객체 군에는 Xp와 Linux가 있고, 객체의 종류에는 Edit와 Button이 있다. 그리고 각각을 구현한 XpEdit와 XpButton, LinuxEdit와 XpButton이 있다.

Abstract Factory 패턴의 목적은 연관성 있는 객체군을 생성할 수 있도록 제어하는 것이다. Xp용 윈도우를 만들고 싶다면 XpEdit와 XpButton을 생성할 수 있도록 하고, Linux용 윈도우를 만들고 싶다면 LinuxEdit와 LinuxButton을 생성할 수 있도록 하는 것이다. 이를 위해서는 연관성 있는 객체군을 생성할 수 있는 객체를 만들어야 한다. 그리고 또 하나 중요한 부분은 객체가 생성된 이후에는 Xp인지 Linux인지를 알 수 없도록 하는 것이다.

이를 위해서 객체군 생성을 담당하는 Factory를 선언한다. 객체군이 여럿(Xp, Linux)이기 때문에 IFactory라는 인터페이스를 먼저 만든다. 그리고 Xp 윈도우 용 객체들을 생성할 수 있는 XpFactory, Linux용 객체들을 생성할 수 있는 LinuxFactory를 만든다. 그런 다음 각각의 객체군들에 맞게 객체를 생성하는 메소드를 구현해 주면 된다.

이 과정에서 XpButton과 LinuxButton은 객체 생성 이후에 구분되어서는 안된다. 따라서 공통 인터페이스인 IButton을 구현하도록 한다. Edit쪽도 마찬가지로 IEdit를 구현하도록 만들어 준다.

'5.디자인패턴' 카테고리의 다른 글

Proxy 패턴과 그 활용  (0) 2016.09.16
Observer 패턴  (0) 2016.09.16
Factory Method 패턴  (4) 2016.09.16
Memento 패턴  (2) 2016.09.13
Bridge 패턴  (0) 2016.09.13
Posted by 이세영2
,

Factory Method 패턴은 객체의 생성과 관련된 패턴이다.

Factory라는 것은 생산품을 생산하는 생산자의 의미로 사용되는 단어이고, 객체지향 언어에서는 객체를 생성하는 생산자를 의미한다.

Method는 본래 Template Method 패턴에서 차용한 단어이다. Factory Method 패턴에서는 객체의 생성 직후에 해야 할 일을 순서대로 정의한 메소드를 제공한다. 그리고 구체적인 생성은 구체 클래스들에 위임한다.

이 Factory Method 패턴을 통해 얻을 수 있는 이익은 다음과 같다.

1. 객체의 생성 후 공통으로 할 일을 수행한다.

객체가 생성된 직후에 어떤 작업을 수행해야 하는 경우는 자주 나타난다. 특히 문제가 되는 경우는 객체가 이벤트를 수신하는 경우이다. 만약 객체를 이벤트 수신자로 등록하는 코드를 생성자 내부에 넣어 두게 되면 생성이 완료 되기 이전에 이벤트를 받게 되어 오류가 발생하는 경우가 생긴다. 이런 일들은 객체 생성 이후에 해주어야 하는데, Factory Method 패턴을 이용하면 이 문제를 해결할 수 있다.

2. 생성되는 객체의 구체적인 타입을 감춘다.

Factory Method 패턴은 그 구조상 생성된 객체의 타입을 구체적인 타입이 아닌 추상 타입(상위 타입)으로 리턴한다. 이를 통해서 객체를 사용하는 측에서는 구체적인 타입의 존재 조차 모르도록 할 수 있다.

3. 구체적인 Factory 클래스가 생성할 객체를 결정하도록 한다.

구체 Factory 클래스는 생성 메소드 내부의 구현을 책임진다. 구체 생성 메소드 내부에서는 필요한 동작을 자유롭게 구현할 수 있는데, 특히 인자를 받거나 상태에 따라서 생성할 객체를 바꿀 수도 있다. 이렇게 하면 좀 더 다양한 기능을 수행하거나 수정에 용이한 구조를 만들어 낼 수 있다.


본격적인 구현 예제에 앞서서, 매우 간단한 형태의 Simple Factory Method를 구현해 보고자 한다. 이 구현은 사실 Gof가 의도한 Factory Method 패턴과는 관계가 없을 수 있다. 생성을 담당하는 메소드이기 때문에 Factory Method라고 불리긴 하지만 Factory Method 패턴은 아니다. 하지만 위에서 언급한 1번의 경우를 구현한 것이기 때문에 봐 둘 필요는 있다.


Factory Method ( Factory Method 패턴이 아님 )

class Wheel{

    private Wheel(){}// 생성자를 감춤.

    public static Wheel create(){

        Wheel wheel = new Wheel();

        wheel.init();// 생성 후 꼭 해줘야 할 일

        return wheel;

    }

    public void init(){}

}

위의 클래스에서 create() 메소드가 이에 해당한다. create() 메소드는 객체가 생성된 직 후 해주어야 할 일(위에서는 init() 메소드를 호출하는 일)을 수행한다. 위에서 이야기 한 것처럼 어떤 작업은 객체의 생성자 내부에서 하지 못하는 일이 있을 수 있다. 그런 일들은 객체가 생성된 이후에 해주어야 하는데, 이런 Factory Method를 제공해 주지 않으면 객체를 생성 받은 쪽에서 그 일을 해야 한다. 만약 객체를 생성하는 곳이 여러 곳이라면 이런 작업을 여러 곳에서 해야 하고, 또 이를 수정해야 할 경우에는 여러 곳에서 이를 수정해야 한다. 이런 문제를 방지할 수 있도록 해주는 것이 Factory Method 이다.


Factory Method 패턴은 좀 더 많은 문제에 관여하는 패턴이다. 다음의 예제를 보자.


Factory Method 패턴 클래스 다이어그램

Factory Method 패턴의 구현

abstract class ShapeFactory{

    public final Shape create(Color color){

        Shape shape = createShape();

        shape.setColor(color);       // 1. 생성 후 공통으로 할 일

        return shape;                // 2. 구체적인 타입을 알 수 없음

    }

   

    // protected이기 때문에 외부에 노출 안됨.

    // 3. 매개 변수를 받아서 생성할 객체를 결정하도록 할 수 있음.

    abstract protected Shape createShape();

}

class RectangleFactory extends ShapeFactory{

    @Override

    protected Shape createShape() {

        return new Rectangle();

    }

}

class CircleFactory extends ShapeFactory{

    @Override

    protected Shape createShape() {

        return new Circle();

    }

}

interface Shape

{

    public void setColor(Color color);

    public void draw();

}

class Rectangle implements Shape

{

    Color color;

    public void setColor(Color color){ this.color = color; }

    public void draw() { System.out.println("rect draw"); }

}

class Circle implements Shape

{

    Color color;

    public void setColor(Color color){ this.color = color; }

    public void draw() { System.out.println("circle draw"); }

}

최초에 나오는 ShapeFactory가 Factory Method 패턴의 기본 클래스이다. 

여기서 create() 메소드가 생성 메소드 역할을 담당한다. 이 메소드는 구체 메소드이므로 ShapeFactory를 상속 받은 클래스들은 모두 이를 이용할 수 있다. 또한 final 메소드이므로 재정의가 불가능하다. Shape을 리턴 타입으로 사용하기 때문에 ShapeFactory를 사용하는 곳에서는 리턴되는 Shape의 구체적인 타입(예제에서는 Rectangle인지 Circle인지)을 알 수 없다.

ShapeFactory는 createShape()이라는 추상 메소드를 선언하고 있다. ShapeFactory의 구체 클래스들은 이 메소드를 통해서 어떤 Shape 객체를 생성할지를 결정한다.

create() 메소드와 createShape() 메소드와의 관계는 Template Method 패턴과 같다.

createShape() 메소드는 protected로 선언되어 있어서 외부 클래스에서는 사용이 불가능하다. 즉, create() 메소드만을 이용하도록 해서 객체의 생성 이후 수행할 작업이 완료될 것을 강제하는 것이다. createShape() 메소드를 구현하는 구체 Factory 클래스들에서는 다양한 형태로 이를 구현할 수 있다. createShape() 메소드가 매개변수를 받는다면, 매개변수를 조건으로 하여 구체적으로 생성할 Shape의 종류를 바꿀 수도 있고, createShape() 메소드 내부에서 가지고 있는 정보들을 활용하여 생성 조건을 변경할 수도 있다.


Factory Method 패턴에서 문제가 될 부분은 구체 Factory들이 여러번 생성될 필요가 없다는 점이다. 따라서 Singleton 패턴을 이용해야할 경우가 있는데 이렇게 되면 패턴이 복잡해지는 문제점이 있다. 이 문제는 Enum 타입을 통해 Factory Method 패턴을 구현함으로써 해결할 수 있다. Enum Factory Method 패턴을 참고하기 바란다.

'5.디자인패턴' 카테고리의 다른 글

Observer 패턴  (0) 2016.09.16
Abstract Factory 패턴  (0) 2016.09.16
Memento 패턴  (2) 2016.09.13
Bridge 패턴  (0) 2016.09.13
Template Method 패턴  (0) 2016.09.10
Posted by 이세영2
,

Memento 패턴

5.디자인패턴 2016. 9. 13. 20:50

Memento 패턴은 특정 시점에 객체의 상태를 저장하고 복원하기 위해서 사용하는 패턴이다. 바둑이나 오목과 같이 일련의 진행 사항을 저장해 두었다가 다시 복구하기 위해서 사용할 수도 있고, 실패가 예상되는 작업이 있을 경우 복원 지점을 저장해 두었다가 실패했을 때 원 상태로 복원하기 위해서 사용할 수도 있다.


Memento 패턴 클래스 다이어그램

이 다이어그램은 User 클래스와 Memento 클래스의 관계를 표현한다. User 클래스는 id와 level, exp와 같은 필드들을 가진다. Memento 클래스는 User 클래스가 가진 필드들을 저장하는 역할을 한다. 따라서 User가 가지고 있는 모든 필드들을 가지고 있어야 한다. User 클래스는 다이어그램에서 save와 load 메소드를 제공하도록 되어 있다. 원래 Memento 패턴에서는 User 클래스의 데이터 저장과 복원을 별도로 관리하는 클래스를 두기도 하는데 이렇게 하는 경우 생각보다 구현이 복잡해 질 수 있다. 따라서 이 예제에서는 간단히 User 클래스 스스로가 저장과 복원을 관리하도록 하였다.

세부적인 구현은 아래 코드를 보고 이야기 하도록 하자.


Memento 패턴의 구현

class User

{

    private String id;

    private int level;

    private int exp;

  

    // 객체의 상태를 저장하기 위해 꼭 필요한 항목을 캡슐화

    class Memento

    {

        private String id;

        private int level;

        private int exp;

        public Memento(User user){

            id = user.id;

            level = user.level;

            exp = user.exp;

        }

    };

    Vector<Memento> backup = new Vector<>();

    public int save()

    {

        backup.add(new Memento(this));

        return backup.size() - 1;

    }

    public void load(int token)

    {

        Memento m = backup.get(token);

        id = m.id;

        level = m.level;

        exp = m.exp;

    }

}

특징적인 부분을 이야기해 보면, 우선 Memento 클래스가 User 클래스 내부에 선언되어 있다. 이것은 클래스가 단순할 경우 특히 유용한데, 다수의 Memento 패턴이 사용되어야 할 경우라면 Memento 클래스를 내부 클래스로 선언하는 편이 좋다. 

save와 load를 구현하기 위해서는 Memento 객체를 생성하여 저장해 둘 backup 리스트를 선언해 주어야 한다. save() 메소드에서는 새로운 Memento 객체를 생성하고, User 객체가 자기 자신을 인자로 넘겨 Memento 객체가 초기화 되도록 한다. 그리고 backup 리스트에 저장하여 복원에 대비한다. load() 메소드에서는 backup 리스트의 인덱스를 받아 Memento 객체를 꺼내고, 이 Memento 객체에 저장된 필드 값들을 User 객체의 필드 값으로 바꿔준다. 이를 통해 이전에 저장된 데이터를 복원할 수 있다.

이 구현 방식은 Memento 패턴의 간략화 된 버전이라 할 수 있다. 이 Memento 패턴의 save() load() 메소드를 이용하여 undo()나 redo()와 같은 기능들도 구현할 수 있다.

'5.디자인패턴' 카테고리의 다른 글

Abstract Factory 패턴  (0) 2016.09.16
Factory Method 패턴  (4) 2016.09.16
Bridge 패턴  (0) 2016.09.13
Template Method 패턴  (0) 2016.09.10
Singleton 패턴  (0) 2016.09.10
Posted by 이세영2
,

Bridge 패턴

5.디자인패턴 2016. 9. 13. 19:32

Bridge 패턴은 두 구체 클래스 간의 강한 결합을 제거하기 위해서 사용하는 패턴이다. 특히 두 클래스 모두 추상화된 상위 클래스 또는 인터페이스(= 타입)를 가지게 되고, 의존성은 상위 타입간에만 이루어지게 된다. 이를 통해 실제 의존성이 발생하더라도 서로의 구체 타입은 알 수 없도록 한다. 이렇게 되면 두 상위 타입을 구현하는 어느 쪽도 변경이 가능한 상태가 된다.

구체적인 예를 위해서 RPG 게임을 제작한다고 생각해 보자. 일단 우리에게는 각종 무기들(Bow, Sword 등)이 필요할 것이다. 그리고 무기를 다루는 전사들이 있을 것이다. 일단 Warrior가 있다고 가정하자. 그런데 무기를 다루는 것은 전사 뿐만 아니라 대장장이(Smith)도 있다. 대장장이도 어떻게든 무기를 다룰 수 있도록 해 주어야 한다.

만일 Warrior가 구체적으로 Bow와 Sword를 다루는 구현을 가지고 있다면 Bow와 Sword를 변경할 때마다 Warrior의 구현도 변경되어야 할 것이다. 또한 무기가 추가되면 또 해당 무기에 대한 구현을 해야 한다. 또한 Smith 역시 무기들에 대한 구체적인 구현을 가지고 있다면 같은 문제점을 가지게 될 것이다.

이러한 경우에 Bridge 패턴을 이용하면 구체적인 클래스들 간의 의존성을 배제시킬 수 있다.


무기(Weapon) - 무기 핸들러(WaponHandler) Bridge 패턴 Class Diagram


위 클래스 다이어그램에서 보는 바와 같이 Weapon과 WeaponHandler는 각각 인터페이스이다. 그리고 구체적인 의존 관계는 이 인터페이스들 간에만 존재한다. Weapon을 구현한 Bow 클래스와 Sword 클래스는 실제 자신들을 다룰 구체적인 클래스를 알지 못한다. 역시 WeaponHandler를 구현한 Warrior나 Smith도 구체적인 무기, 즉 Bow나 Sword를 알지 못한다. 따라서 Weapon이 Spear, Gun 등과 같이 늘어나더라도 WeaponHandler의 구체 클래스들에는 변경에 있어서 영향을 주지 않는다.

그러면 실제 구현을 살펴 보자.


Weapon - WeaponHandler Bridge 패턴 구현

interface Weapon{

    public void attack();

    public void repair();

}

class Bow implements Weapon{

    public void attack(){

        System.out.println("Bow attack");

    }

    public void repair(){

        System.out.println("Bow repair");

    }

}

class Sword implements Weapon{

    public void attack(){

        System.out.println("Sword attack");

    }

    public void repair(){

        System.out.println("Sword repair");

    }

}


interface WeaponHandler{

    public void handle();

}

class Warrior implements WeaponHandler{

    private Weapon weapon;

    public Warrior(Weapon weapon){

        this.weapon = weapon;

    }

    public void handle(){

        System.out.print("Warrior ");

        weapon.attack();

    }

}

class Smith implements WeaponHandler{

    private Weapon weapon;

    public Smith(Weapon weapon){

        this.weapon = weapon;

    }

    public void handle(){

        System.out.print("Smith ");

        weapon.repair();

    }

}

여기서 Weapon 클래스 군과 WeaponHandler 클래스 군 간의 구체적인 의존성이 나타나는 곳은 Warrior와 Smith 클래스의 생성자에서 Weapon을 인자로 받는 부분이다. 이 경우에서 보듯이 Warrior나 Smith 클래스는 인터페이스인 Weapon만 알 뿐, 구체적인 타입은 알지 못한다.

이러한 방식으로 무기의 종류를 늘리거나 무기를 다루는 사람을 늘리더라도 다른 한 쪽에는 변경에 영향을 미치지 않도록 만들어 준다.

'5.디자인패턴' 카테고리의 다른 글

Factory Method 패턴  (4) 2016.09.16
Memento 패턴  (2) 2016.09.13
Template Method 패턴  (0) 2016.09.10
Singleton 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Posted by 이세영2
,

Template Method 패턴은 실행 순서(sequence)는 고정하면서 세부 실행 내용(operation)은 다양화 될 수 있는 경우에 사용하는 패턴이다.

구현 시 자주 발생하는 문제들 중에서 실행 순서가 고정되어야 하는 경우가 있다. 예를 들어 input() -> calculate() -> output() 순서에 따라서 실행되어야 하거나 init() -> do() -> release(), 또는 lock() -> do() -> unlock()과 같이 정확한 순서에 따라 실행되어야만 올바른 결과를 얻을 수 있는 경우가 있다. 이럴 때 보통 사용하는 방법은 외부에는 상세 단계들을 정해진 순서대로 실행시키는 함수를 public으로 제공하고, 각 단계를 실행하는 함수는 private으로 감춰 두는 것이다.

Template Method 패턴은 이것과 매우 유사한 방식이다. 다만, 실행 방법만 정해져 있을 뿐 각 순서에 따라 해야 할 일들이 서로 다를 수 있는 경우에 이 세부 순서를 구현할 수 있는 추상 메소드를 제공한다. 따라서 보통 Template Method 패턴은 추상 클래스로 구현되는 경우가 많다.

아래는 Template Method 패턴을 구현한 상위 클래스의 형식이다.

abstract public class AbstractTemplate {

    public final void templateMethod(){

        sequence1();

        sequence2();

        sequence3();

        sequence4();

    }

   

    abstract protected void sequence1();

    abstract protected void sequence2();

    abstract protected void sequence3();

    abstract protected void sequence4();

}

먼저 templateMethod()를 살펴보자. 이 패턴의 목적은 일단 순서를 고정하자는 것이다. 따라서 하위 클래스가 이 클래스를 상속 받은 후 templateMethod()를 재정의 하는 것을 방지하기 위해서 final 키워드를 사용해 주어야 한다. 그리고 설계상에서 이미 정해 놓은 순서를 내부적으로 구현한다. 각 순서를 메소드화 한 후, templateMethod() 에서 이 메소드들을 순서에 맞게 호출하는 것이다.

세부 메소드는 하위 클래스들이 구현할 수 있도록 abstract 함수로 제공한다. 이들 abstract 함수들은 templateMethod()에서 호출되어야 하므로 private이 아닌 protected로 선언되어야 한다. 

세부 순서 함수 중 일부는 모든 하위 클래스에게 동일할 수 있다. 이 경우에는 상위 클래스가 이를 구현하고 하위 클래스들은 아예 볼 수 없도록 private으로 선언하는 편이 좋다.

'5.디자인패턴' 카테고리의 다른 글

Memento 패턴  (2) 2016.09.13
Bridge 패턴  (0) 2016.09.13
Singleton 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Posted by 이세영2
,

Singleton 패턴

5.디자인패턴 2016. 9. 10. 18:13

Singleton 패턴을 이야기 하기 전에 먼저 주의사항부터 이야기 할 필요가 있다.

1. 가급적 쓰지 마라.

이것은 실제로 자주 겪는 문제이기도 한데, Singleton으로 객체를 생성하는 경우, 이 객체는 "전역 변수"와 같다. 단 하나의 객체만이 생성되고, static 함수(getInstance() 함수)를 통해 접근이 가능하고, static 함수가 public 이기 때문에, 전역 변수와 같은 특성을 지니게 된다. 의도한 바가 아니더라도 말이다. 그래서 마치 전역 변수처럼 사용되어 버리는 문제가 생긴다. 테스트 주도 개발(켄트 벡)이라는 책에는 싱글톤에 대해 이렇게 나와 있다.

"싱글톤 : 전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 어떻게 해야 할까? 사용하지 마라. 프로그램은 당신이 전역 변수를 사용하는 대신 설계에 대해 고민하는 시간을 가졌던 점에 대해 감사할 것이다."

싱글톤은 전역 변수처럼 무분별하게 사용될 여지가 있고, 특히 Unit Test를 작성하기 어렵게 만든다. 전역 변수라서 이곳 저곳에서 접근이 가능하고 static final 변수로 선언되기 때문에 대체가 불가능하다. 비슷한 문제가 enum이나 Holder 패턴에서도 발생한다. 하지만 Singleton 객체는 이들 패턴보다 훨씬 무질서하게 사용되는 경향이 있다.

Singleton 패턴에는 크게 두가지 종류가 있다. 하나는 static 변수 선언 시 바로 생성하는 방식이고, 하나는 getInstance() 함수 호출 시 생성하는 방식이다.


static으로 생성하는 방식

public class StaticSingleton {

    private static final StaticSingleton instance = new StaticSingleton();

    private StaticSingleton() {}   // 규칙 1. private 생성자.

    // instance, getInstance, singleton, shared

    public static StaticSingleton getInstance()  // 규칙 2. 오직 한개만 만듦

    {

        return instance;

    }

   

    public static void main(String[] args) {

        StaticSingleton instance = StaticSingleton.getInstance();

    }

}

이 방식은 특별히 구현에 신경 쓸 것이 별로 없다. 생성자를 private으로 만들어서 객체를 외부에서 생성하지 못하도록 하는 것, 그리고 getInstance()라는 메소드를 static으로 제공해 줌으로써 외부에서 객체에 접근할 수 있도록 하는 것이다.

getInstance() 함수를 통해 생성하는 방식

public class Singleton {

    private volatile static Singleton instance = null;

    public static Singleton getInstance(){

        if (instance == null) {                     // A

            synchronized(Singleton.class) {

                if (instance == null) {             // B

                    instance = new Singleton();

                }

            }

        }

        return instance;

    }

    private Singleton(){} // 생성자를 통한 생성 방지

   

    public static void main(String[] args) {

        Singleton instance = Singleton.getInstance();

    }

} 

앞서 static 생성시와의 차이점 중 하나는 instance 변수 선언 시 Singleton 객체가 생성되지 않는다는 점이다. Singleton 객체가 매우 크고, 쓰일 가능성이 매우 낮은 경우에 보통 사용하는 방식이다. 객체의 생성은 getInstance() 함수가 호출 될 때 이루어진다.

이 경우 객체가 이미 생성되었는지 그렇지 않은지(null 여부)를 체크함으로써 중복 생성을 방지할 필요가 있다. 이 때 멀티 쓰레드 상태에서 발생할 수 있는 레이싱 문제도 고려해 주어야 한다. 

위에서 보면 instance 변수가 null인지 체크하는 부분이 두 곳(A와 B)이 있다. 

첫번째(A)는 일단 instance가 null이 아닐 경우 synchronized 블럭 내부로 들어가는 것을 미리 차단함으로써 Singleton 객체를 얻기 위해 발생하는 부하를 줄여 주는 역할을 한다. 

일단 null임이 확인된 이후에는 synchronized 블럭으로 넘어가게 되는데, 이 때는 이 블럭이 단 하나의 쓰레드만 내부로 접근할 수 있도록 막는다. 

이 후 instance 객체의 null 체크를 한번 더 한다(B). 이는 맨 처음 synchronized 블럭에 들어온 쓰레드를 위한 것이 아니고 그 다음에 들어오는 경우를 위한 것이다. 

즉, A에서 두 쓰레드가 instance를 null로 판단했고, 둘 다 synchronized 블럭에 접근했다고 하자. 첫번째 쓰레드는 일단 instance 객체를 생성하고 종료 한다. 

두번째 쓰레드가 B 구문에 도달했을 때에는 이미 첫번째 쓰레드가 생성을 마치고 난 다음이다. 따라서 두번째 쓰레드는 B에서 instance 객체가 null이 아니라고 판단하게 된다. 그리고 그냥 블럭을 빠져 나온다. 이를 통해서 instance 객체가 중복 생성되는 것을 막을 수 있다.


사실상 최근의 컴퓨팅 파워를 고려할 경우 두번째 방식은 실제로는 거의 의미가 없다시피 하다. 어쨌든 필요한 경우 사용해 보려면 정확한 구현 메카니즘을 이해하고 있는 것이 도움이 될 것이다.

다시 한번 강조하지만 가급적 Singleton은 사용하지 않는 편이 좋다.

'5.디자인패턴' 카테고리의 다른 글

Bridge 패턴  (0) 2016.09.13
Template Method 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Adapter 패턴  (0) 2016.08.23
Posted by 이세영2
,

Builder 패턴

5.디자인패턴 2016. 9. 10. 17:36

빌더 패턴은 객체의 생성과 객체 생성 시 필요한 매개 변수 설정 과정을 분리함으로써 다음과 같은 이점을 확보해 주는 패턴이다.

1. 매개 변수가 많은 경우(특히 연속된 동일 타입의 매개 변수를 설정할 경우)에 발생할 수 있는 설정 오류를 방지할 수 있는 가독성을 제공한다.

2. 디폴트 값이 존재하는 매개 변수를 생략할 수 있도록 한다.

3. (immutable 변수나 final 키워드가 있는 변수처럼) 꼭 생성자를 통해서 설정 되어야 하는 변수를 지원할 수 있는 방법을 제공한다.


최종 결과물

class Hero{

    private int exp;

    private int cash0;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수

    private Hero(Builder builder){

        this.exp = builder.exp;

        this.cash = builder.cash;

        this.name = builder.name;

        this.level = builder.level;

    }

    public void print(){

        System.out.println(exp + " " + cash + " " + name + " " + level);

    }

    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;

        public Builder(String name, int level){

            this.name = name;

            this.level = level;

        }

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

        public Builder cash(int cash){ this.cash = cashreturn this;}

        public Hero build(){

            return new Hero(this);

        }

    }

} 


실행 방법(객체 생성)

public static void main(String[] args) {

    Hero hero = new Hero.Builder("ceasar", 10).cash(20).exp(20).build();

    hero.print();

}


이 예제에서는 Hero라는 객체를 생성하고자 한다. Hero는 총 4개의 변수를 가지고 있고, 이 중에서 name과 level은 꼭 생성자를 통해서 설정 되어야 하는 변수이다. 그리고 나머지 exp와 cash 변수는 디폴트 값인 0을 가지고 있다.

따라서 다음과 같은 경우의 수를 가지고 있다.

1. name과 level만을 매개변수로 받아 생성하는 경우

2. name + level + exp

3. name + level + cash

4. name + level + exp + cash


만약 이러한 조합을 Telescoping Parameter Pattern으로 구현한다면 총 4개의 생성자를 구현해야 한다. 여기서 매개 변수가 더 추가된다면 생성자 수는 조합적으로 증가하게 될 것이다.

또한 level과 exp, 그리고 cash 변수는 모두 int 타입이다. 만약 단일 생성자에서 4개의 변수를 모두 설정한다면

    생성자("이름", 1, 2, 3);

과 같은 형식으로 변수 값을 할당하게 될 것이다. 하지만 이 경우 생성자의 변수 입력 순서를 정확히 알고 있지 않다면 여러 값을 넣으면서 착오에 의한 에러를 발생시킬 수 있다. 그리고 코드만으로는 쉽게 오류를 찾아내기가 힘들다.

이런 문제점을 해결하는 것이 빌더 패턴이다. 우선 Hero 클래스를 만들어 보자.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수

간단히 변수만 선언한 모습이다. 빌더 패턴에서는 생성자에 바로 매개 변수를 입력하도록 하지 않고, 내부 클래스인 빌더 클래스를 이용하게 되어 있다. 빌더 객체인 Builder는 Hero의 내부 클래스로 선언이 된다. 그리고 외부 클래스인 Hero 객체를 생성하는 것이므로 Hero 객체가 없이 Builder 객체를 생성, 이용할 수 있도록 static 클래스로 선언해 주어야 한다. 그 모양은 아래와 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수

    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;

    }

}

일단 Builder 클래스가 가지고 있는 내부 변수는 모두 Hero가 가지고 있는 것과 일치한다. 여기서 중요한 점은 Builder 객체가 가지고 있는 변수 값이 Hero 객체의 초기값이 될 예정이므로 Builder 객체가 가진 변수들의 초기값을 원하는 값으로 맞춰 주어야 한다는 점이다. 즉, exp와 cash는 0이라는 초기값을 가지고 있는데, 이 초기값을 Hero가 아닌 Builder 클래스의 초기값으로 설정해 주어야 한다. 그래야 나중에 Builder 객체의 초기값이 Hero에 덮어 씌워지면서 원하는 초기값을 가질 수 있게 된다.

다음으로는 생성자를 만들 차례다. 생성자는 Builder 객체가 제공해 준다. 이 때 Hero의 변수 중 name과 level이 final로 설정되어 있으므로, 이 두 변수를 필수적으로 설정해 주도록 생성자에 매개 변수를 선언해야 한다. 구현을 해 보면 다음과 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }

    }

}

위와 같이 Builder의 생성자를 선언하여 name과 level을 매개 변수로 입력하도록 강제 할 수 있다.

다음은 옵션으로 입력 받을 수 있는 exp와 cash 변수에 대한 setter를 선언한다. 일반적인 setter 함수는 set+변수명 형식이지만 Builder 패턴에서는 가독성을 좋게 하면서도 setter와의 다른 특성을 가지고 있는 점을 알리기 위해서 변수명 그대로를 setter 이름으로 사용한다.(Java에서는 이것이 가능하지만 다른 언어라면 언더 바('_') 등을 활용할 수 있다.) 이를 선언해 보면 아래와 같다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }


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

        public Builder cash(int cash){ this.cash = cashreturn this;}

    }

}

여기서 주목할 부분은 각 함수 마지막 구문인 return this;이다. 여기서 this는 Builder 객체를 말한다. Builder 객체 자신을 리턴함으로써 생성자 호출 후 옵션 변수 setter 함수들을 연속적으로 호출할 수 있다. 가령

Builder("이름", 10).exp(값).cash(값).... 형태로 연속 호출이 가능하다는 말이다. 이를 통해 각 변수 값이 어떤 변수에 셋팅되게 되는지를 쉽게 알 수 있게 된다.

Builder 클래스에서는 최종적으로 build() 함수를 제공해 주어야 한다. build() 함수는 생성자와 setter를 통해 설정된 매개 변수들을 이용하여 Hero 객체를 생성하는 함수이다.

class Hero{

    private int exp;

    private int cash;

    private final String name; // 꼭 생성자를 통해 설정되어야 할 변수

    private final int level;   // 꼭 생성자를 통해 설정되어야 할 변수



    static class Builder{

        private int exp = 0;

        private int cash = 0;

        private final String name;

        private final int level;


        public Builder(String nameint level){

            this.name = name;

            this.level = level;

        }


        public Builder exp(int exp){ this.exp = expreturn this;}

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


        public Hero build(){

            return new Hero(this);

        }

    }

}

이제 build() 함수가 호출하는 Hero 클래스의 생성자를 만들어 주어야 한다. build() 함수 내에서의 Hero 생성자는 this, 즉 Builder 객체를 받도록 되어 있다. 따라서 Builder 객체를 받는 생성자를 선언해 주어야 한다. 그리고 Builder 객체가 가지고 있는 변수 값들을 모두 가지고 와서 자신의 변수 값으로 셋팅하는 과정을 포함해야 한다.

class Hero{

    private int exp = 0;

    private int cash = 0;

    private final String name; // 생성자를 통해 설정되어야 할 변수

    private final int level;   // 생성자를 통해 설정되어야 할 변수


    static class Builder{

        private int exp;

        private int cash;

        private final String name;

        private final int level;

        public Builder(String name, int level){

            this.name = name;

            this.level = level;

        }

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

        public Builder cash(int cash){ this.cash = cashreturn this;}

        public Hero build(){

            return new Hero(this);

        }

    }


    private Hero(Builder builder){

        this.exp = builder.exp;

        this.cash = builder.cash;

        this.name = builder.name;

        this.level = builder.level;

    }

    public void print(){

        System.out.println(exp + " " + cash + " " + name + " " + level);

    }

}

위와 같이 Hero 클래스의 생성자를 구현해 주었다. 그리고 추가로 내부 변수 값을 확인할 수 있는 print() 함수를 구현해 주었다. 실행은 글의 첫머리에 나오는 실행 함수를 실행해 보면 된다.

이처럼 Builder 패턴은 객체 생성시 초기 설정 값들에 의해 발생할 수 있는 여러 문제점들을 해결해준다. 종종 setter 함수의 경우에는 가독성 지원 문제를 해결하기 위해 생성 과정과는 별도로 구현해서 사용하기도 한다.

'5.디자인패턴' 카테고리의 다른 글

Template Method 패턴  (0) 2016.09.10
Singleton 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Adapter 패턴  (0) 2016.08.23
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
Posted by 이세영2
,

Holder 패턴

5.디자인패턴 2016. 8. 28. 22:03

Holder 패턴이 생소할지 모르니 좀 더 익숙한 FactoryMethod 패턴을 먼저 이야기 해보겠다.


FactoryMethod 패턴은 객체의 생성을 책임지는 패턴이다. 특히 상위 타입을 가지고 있는 여러 하위 객체들 중에서 외부에서 입력한 조건에 맞게 생성해 주는 역할을 한다. 그리고 생성된 객체를 전달해 줄 때는 상위 타입으로 캐스팅해서 리턴한다. 따라서 FactoryMethod 패턴을 구현한 클래스 외부에서는 객체의 구체적인 타입을 알 수 없다. 이것은 구체적인 타입에 의존하면서 발생하는 강한 결합과 코드의 중복 등 객체지향에서 지양하는 여러 문제들이 발생하지 않도록 방지해 준다.


Holder 패턴은 FactoryMethod 패턴과 유사점이 많다. 우선 객체의 생성을 담당한다. Holder 패턴을 구현한 클래스는 클래스 내부에서 각 하위 객체를 생성하여 가지고 있다. 그리고 외부에서 특정 조건에 맞는 객체를 요구할 경우 그에 해당하는 객체를 제공해 준다. 이 경우에도 FactoryMethod와 마찬가지로 하위 객체의 타입을 알리지 않기 위해서 상위 타입으로 캐스팅 해서 내보낸다. 기본적인 동작과 목적에 있어서 Factory Method 패턴과 Holder 패턴은 매우 유사하다.


다른 점이 딱 한가지 있다. Factory Method 패턴은 외부에서 객체를 요구하면 매번 생성해서 전달해 주는데 비해 Holder 패턴은 하위 객체 각각에 대하여 딱 한번씩만 생성한다는 점이다. 이 점이 FactoryMethod와 Holder 패턴을 구분하는 가장 큰 차이점이다.


그러면 이제 Holder 패턴을 구현해 보도록 하겠다. 우리는 이 예제에서 온도 상태를 나타내는 객체를 생성하고자 한다. 온도 상태 객체는 ITemperature라는 인터페이스 형태의 상위 타입을 가진다. 그리고 하위 객체들은 구체적으로 Hot / Normal / Cold 라는 상태를 표현하는 객체이다. Holder 객체는 외부에서 받은 온도 값이 30도 이상이면 Hot 객체를, 0도 미만이면 Cold 객체를, 나머지의 경우에는 Normal 객체를 외부에 제공하게 될 것이다.


우선 상위 타입을 나타내는 ITemperature 인터페이스를 정의하도록 한다.



interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

}


이 ITemperature 인터페이스는 하위 객체의 구체적인 타입을 감추도록 만들어 준다. 인터페이스에서 구현하고 있는 메소드들이 크게 중요하진 않지만 이해를 돕기 위해서 설명을 하자면 다음과 같다. 우선 toDbString() 메소드는 이 온도 상태 객체가 데이터베이스를 만났을 때 자신의 상태에 대한 문자열을 생성하여 제공해 주는 메소드이다. operateThermostat() 메소드는 각 온도 상태에 따라 온도조절기(Thermostat)를 동작 시킨다. 상태 객체별로 다른 동작을 수행해야 하므로 온도조절기 객체를 인자로 받도록 되어 있다.


다음은 Hot/Normal/Cold를 나타내는 구체 객체를 구현한 코드이다.


class HotTemperature implements ITemperature{

    String dbString = "HOT";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

}

class NormalTemperature implements ITemperature{

    String dbString = "NORMAL";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){thermostat.stop();}

}

class ColdTemperature implements ITemperature{

    String dbString = "COLD";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

}


Hot/Normal/Cold 상태를 표현하는 3개의 클래스가 정의되었다. 셋은 모두 ITemperature 인터페이스을 구현하고 있고, 모두 각자의 상태에서 해야할 인터페이스들을 구현하였다.


Holder 패턴을 구현하는 TemperatureHolder 클래스는 아래와 같이 정의 될 수 있다.


class TemperatureHolder{

    public static final ITemperature HOT = new HotTemperature();

    public static final ITemperature NORMAL = new NormalTemperature();

    public static final ITemperature COLD = new ColdTemperature();

    public static ITemperature getTemperature(int temperature){

        if(temperature > 30) return HOT;

        if(temperature >= 0) return NORMAL;

        else return COLD;

    }

}


우선 객체의 위쪽에 ITemperature 타입으로 Hot/Normal/Cold 총 3개의 객체를 선언하고 있음을 알 수 있다. 이 객체들은 단 한 개 씩만 생성될 것이므로 static final로 선언된다.


그리고 getTemperature() 라는 메소드가 있다. 이 메소드는 ITemperature 타입을 리턴하도록 되어 있고 온도 값(temperature)을 매개 변수로 받도록 되어 있다. Holder 객체는 외부에서 받은 온도 값이 30도 이상이면 Hot 객체를, 0도 미만이면 Cold 객체를, 나머지의 경우에는 Normal 객체를 외부에 제공하도록 구현되어 있다.



이해하기 쉽게 클래스 다이어그램으로 그려보면 다음과 같다.





이대로 사용하는 것도 괜찮지만 TemperatureHolder 클래스가 좀 더 수정에 닫혀 있도록 구현하는 방법이 있다.  TemperatureHolder 클래스는 코드의 다음과 같은 변경에 취약한 문제점을 가지고 있다.


1. ITemperature 하위 클래스가 늘어나면 TemperatureHolder 클래스가 생성해야 하는 클래스의 개수도 늘어난다.

2. 이와 함께 getTemperature() 함수도 변경된다. VERY_HOT이나 VERY_COLD가 추가될 경우를 생각해 보자. 이들은 단지 ITemperature 인터페이스의 하위 클래스일 뿐인데 이 클래스가 늘어난다고 해서 생성 및 전달의 책임만 가지고 있는 TemperatureHolder 클래스가 변경된다는 것은 문제가 있다.


이 두 가지 문제를 해결해 보도록 하자.


이 중에서 2번이 더 쉬운 문제니까 2번을 먼저 해결해보자. 알아 두어야 할 것은 두가지 문제를 모두 해결해야만 완벽한 해결이 된다. 따라서 2번만 해결하는 코드는 중간 산출물 정도로 이해해 주기 바란다.


getTemperature() 메소드 문제의 원인은 하위 객체들이 생성되는 조건을 TemperatureHolder가 알고 있다는 점이다. 하위 객체들은 특정 상태의 온도에 대한 정보를 가지고 있으므로 당연히 자기 온도 범위도 알고 있어야 한다. 이것을 객체가 가지고 있지 않기 때문에 TemperatureHolder 클래스가 가지고 있게 되고, 그것이 ITemperature 하위 객체가 변경될 때 TemperatureHolder 클래스도 변경되는 원인이다.


그래서 이를 해결하기 위해 ITemperature에 각 하위 객체의 온도 범위를 체크할 수 있는 인터페이스를 구현한다.


interface ITemperature{

    public String toDbString();

    public void operateThermostat(Thermostat thermostat);

    public boolean isInTemperatureRange(int temperature);

}



그리고 각 클래스들은 자신의 범위를 알려주도록 구현한다.


class HotTemperature implements ITemperature{

    String dbString = "HOT";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

    public boolean isInTemperatureRange(int temperature) { return temperature > 30; }

}

class NormalTemperature implements ITemperature{

    String dbString = "NORMAL";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){thermostat.stop();}

    public boolean isInTemperatureRange(int temperature) { return temperature <= 30 && temperature >= 0; }

}

class ColdTemperature implements ITemperature{

    String dbString = "COLD";

    public String toDbString(){ return dbString;}

    public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

    public boolean isInTemperatureRange(int temperature) { return temperature < 0; }

} 


무상태 프로그래밍을 지향하는 관점에서 상태 객체 생성시에 상태 객체를 생성하는 클래스 이외에 다른 클래스가boolean을 리턴하는 메소드를 구현하는 것은 상태 확인 메소드를 구현하는 것과 같지 않느냐고 질문할지 모르겠다. 변명이 통할지는 모르겠지만 일단 목적과 방법에 있어서 다른 점이 있어서 이야기를 하고 넘어가겠다.


1. 위의 isInTemperatureRange() 메소드는 자기 객체의 상태를 확인하는 것이 아니다. 오히려 인자로 들어온 temperature 값이 자기 기준에 맞는지를 확인해 주는 것이다.

2. isInTemperatureRange() 메소드가 어느 시점에 호출되든지 각 객체의 내부 상태에 대해 알 수 있는 정보는 하나도 없다.

3. 이 메소드는 TemperatureHolder 클래스에 의해 이용되는 것이다. 즉, 오직 생성 클래스에 의해서만 사용된다.

4. 이 메소드는 객체의 특성을 나타내주는 메소드이다. 따라서 객체 스스로 그 특성 정보를 가지고 있는 것이 타당하다.


일단 이렇게 수정한 후에는 TemperatureHolder 클래스를 수정해야 한다.


class TemperatureHolder{

    private static ITemperature HOT = new HotTemperature();

    private static ITemperature NORMAL = new NormalTemperature();

    private static ITemperature COLD = new ColdTemperature();

    private static List<ITemperaturetemperatures = new ArrayList<ITemperature>();

   

    static{

        temperatures.add(HOT);

        temperatures.add(NORMAL);

        temperatures.add(COLD);

    }

   

    public static ITemperature getTemperature(int temperature){

        for(ITemperature each : temperatures){

            if(each.isInTemperatureRange(temperature)) return each;

        }

   

        return null;

    }

} 


코드를 더 간결하게 만들어 보고자 약간의 무리수를 두었다. HOT, NORMAL, COLD가 모두 같은 타입인데 따로 떨어져서 사용되는 것을 방지하기 위해 List를 만든 것이다. TemperatureHolder가 생성되지 않고 사용될 수 있게 하기 위해서는 이 과정을 생성자가 아닌 static {} 구문 안에 넣어 주어야 한다. 그래야 실제 사용되기 전에 리스트에 객체들이 들어간 상태가 된다. 어쨌든 이 코드는 중간 결과물이라서 결국에는 모두 제거될 것이다.


자 이제 getTemperature() 메소드를 보자. 이전에는 if문들이 나열되어 있었는데 이제 단순히 각 객체에게 온도 범위를 물어보는 형태로 변경되었다. 적어도 이 상태라면 새로운 온도 상태 객체가 추가되더라도 getTemperature() 메소드가 변경되는 일은 없을 것이다.





이제 온도 상태 객체들의 생성 문제로 넘어가보자. TemperatureHolder 클래스는 Holder로서의 역할 때문에 각 하위 객체를 생성하여 가지고 있어야 한다. 이 점 때문에 새로운 온도 객체가 생성되면 TemperatureHolder 클래스도 변경이 되어야 한다. 이것을 자동화 할 수 있는 방법이 없을까?


1. 자 몇가지 특성을 먼저 이야기 해보겠다. 각 온도 상태 객체들은 타입이 ITemperature이다. 그리고 생성 이후 변경되지 않고 계속 유지되기 때문에 "static으로 선언"된다. 즉 소프트웨어가 실행되기 전에 모두 생성이 완료 된다.

2. 모두 ITemperature를 구현하고 있으므로 모두 "같은 상위 타입"을 가진다.

3. 온도 상태 객체는 "여러개" 이다.


위의 1~3에서 ""로 묶인 특성들을 잘 살펴보자. 어떤 프로그래밍 요소가 떠오르지 않는가? 바로 enum 타입이다. enum 타입은 enum 상위 클래스와 하위 클래스의 집합이다. 또한 static으로 선언되고, 여러 하위 클래스가 나열된다. 따라서 저 특성에 매우 적합한 형태이다. 자 enum을 가지고 각 상태 객체를 구현해 보자.


enum Temperature implements ITemperature{

    HOT{

        public String toDbString(){ return "HOT";}

        public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

        public boolean isInTemperatureRange(int temperature) { return temperature > 30; }

    },

    NORMAL{

        public String toDbString(){ return "NORMAL";}

        public void operateThermostat(Thermostat thermostat){thermostat.stop();}

        public boolean isInTemperatureRange(int temperature) { 

            return temperature <= 30 && temperature >= 0; 

        }

    },

    COLD{

        public String toDbString(){ return "COLD";}

        public void operateThermostat(Thermostat thermostat){ thermostat.cooling(); }

        public boolean isInTemperatureRange(int temperature) { return temperature < 0; }

    }

    ;

}


우선 이 enum 클래스는 ITemperature 인터페이스를 구현하고 있다. 따라서 외부에서는 자연스럽게 ITemperature로 사용이 가능하다. 또 enum 타입이므로 중복 생성되는 경우가 발생하지 않는다. 그리고 각 하위 클래스는 자신의 특성에 맞게 인터페이스를 구현하고 있다. 따라서 외부에서 사용할 때에는 이전에 개별적으로 구현되었던 방식과 전혀 다를 바 없이 사용이 가능하다.


그러면 우리가 목적했던 TemperatureHolder 클래스는 어떻게 바뀌는지 보자. 당연히 ITemperature 하위 객체를 생성하는 부분은 수정이 되었겠지만 우리가 선언한 enum 기반의 객체들이 어떻게 사용되는지 확인이 필요할 것이다.


class TemperatureHolder{

    public static ITemperature getTemperature(int temperature){

        for(ITemperature each : Temperature.values()){

            if(each.isInTemperatureRange(temperature)) return each;

        }

        return null;

    }

}


뭔가 구현을 덜 해놓은 느낌까지 준다. 하지만 있을 것은 다 있다. 이미 enum을 통해 하위 객체들의 생성은 완료된 상태이다.(enum은 static 이므로 별도의 생성이 필요하지 않다.) 그리고 이전 중간 단계에서 static {}으로 List를 만드는 부분도 없어졌다. 


왜 인지는 getTemperature() 메소드를 보면 알 수 있다. 이전에 리스트를 통해 for문을 돌던 것이 Temperature.values()를 통해 for문을 도는 것으로 변경되었다. enum의 values() 메소드는 enum 클래스 하위 객체들을 배열로 만들어 제공하는 메소드이다. 따라서 for문에 사용될 수 있다. 그리고 이 점이 특히 enum을 사용할 때 좋은 점인데, 만약 새로운 하위 객체가 생성되었다 하더라도 values() 메소드는 새로 추가된 하위 객체를 포함한 배열을 리턴해 준다. 따라서 TemperatureHolder 객체와 같이 enum.values() 메소드를 사용하는 곳에서는 코드를 수정할 필요가 없어진다.


이로써 TemperatureHolder 클래스가 가지고 있던 설계적인 문제들이 모두 해결 되었다.


'5.디자인패턴' 카테고리의 다른 글

Singleton 패턴  (0) 2016.09.10
Builder 패턴  (0) 2016.09.10
Adapter 패턴  (0) 2016.08.23
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
Pluggable Selector 패턴  (1) 2016.08.21
Posted by 이세영2
,

Adapter 패턴

5.디자인패턴 2016. 8. 23. 21:50

Adapter 패턴은 이미 구현 되어 있는 객체를 이용하여 다른 기능을 구현하는 패턴이다.


코드는 가능한 한 적은 것이 좋다. 이미 구현되어 있는 코드가 있다면 최대한 그 코드를 이용하는 것이 좋다. 중복은 코드를 만들어 내고 관리에 필요한 비용을 만들어 낸다. 그렇다고 해서 아무렇게나 기존의 코드를 이용하는 것은 문제가 있다.

객체지향에서 대표적으로 기존에 구현된 코드를 이용하는 방식에는 상속이 있다. 하지만 조금만 공부해 보면 상속을 통해서 상위 클래스의 기능을 물려 받은 후 하위 클래스로 다른 기능을 구현하는 것은 객체지향에서 금기시 되고 있다. 이는 다형성을 위배하고, 불필요한 인터페이스를 전파하며, 클래스 계층관계를 이해하는데 혼돈을 준다.


말로만 해서는 잘 이해가 안될 수도 있으니 하지 말라는 것을 한번 해보도록 하겠다. Stack을 구현하는 과정을 예로 들어 보겠다. Stack은 데이터를 push()와 pop()으로 넣었다 뺐다 하는 자료 구조이다. 익히 알다시피 Stack 뿐만 아니라 다른 대부분의 컬렉션들도 비슷한 기능은 이미 지원하고 있다. 그렇다면 다른 컬렉션 중 하나를 상속 받고, Stack이 지닌 고유한 특성, 즉 push()와 pop() 기능을 구현해 주면 될 것이다.


그러면 간단히 Vector를 상속 받아서 Stack을 구현해 보자.


상속을 통해 구현한 Stack(안티 패턴)

class Stack<T> extends Vector<T>{

    public void push(T t){ add(t); }

    public T pop(){

        if(size() == 0) return null;

        T t = get(size() - 1);

        remove(size() - 1);

        return t;

    }

}


그리고 테스트 코드를 작성해서 실행을 시켜보자.

테스트 코드

public static void main(String[] args) {

    Stack<Integer> stack = new Stack<Integer>();

    stack.push(100);

    stack.push(200);

    stack.push(300);

    System.out.println(stack.pop());

    System.out.println(stack.pop());

    System.out.println(stack.pop());

}


실행 결과


300

200

100



원하는 대로 매우 잘 동작하는 것을 알 수 있다. 하지만 여기에 아무 문제도 없을까?

이제 테스트 코드에서 선언한 Stack 인스턴스에 인터페이스를 살펴 보자. 이클립스라면 stack 옆에 "."을 찍으면 이와 같은 화면이 뜰 것이다.




위의 그림에 보면 우리가 직접 선언한 push()와 pop() 조차 보이지 않을 만큼 많은 API가 선언되어 있음을 알 수 있다. 이것들은 대체 어디서 온 것일까? 그렇다. 우리는 Vector를 상속 받았다. 이 메소드들은 모두 Vector에서 온 것들이다. 우리는 Vector 구체 클래스를 상속 받았기 때문에 그 API를 그대로 물려 받은 것이다.


이것은 우리의 의도와는 다른 결과를 만들어 낸다. 우리는 분명 이 Stack 클래스를 Stack으로 사용할 것을 기대하고 배포하였다. 하지만 이 코드를 받아 본 다른 개발자는 이 클래스가 가진 다른 API들을 보고 자신에게 필요한 용도로 활용할 수 있을 것이라 생각했다. 그래서 add() 함수를 통해서 Stack에 저장되는 순서와 다른 형태로 저장도 하고, remove() 함수를 통해서 역시 순서와 상관 없이 데이터를 제거하는 코드를 만들었다. 이렇게 만들어진 코드에 대해서 Vector를 상속 받아 Stack을 만든 사람은 책임을 피하기 어려울 것이다.


문제는 또 있다. Vector 클래스에 대한 설계가 변경되어 기존의 API들을 고쳐야 하는 상황이 온 것이다. Stack 클래스에서 새로 구현한 API들은 그대로 사용해도 문제가 없겠지만 Vector 클래스의 API를 사용해버린 경우 Vector 클래스의 수정에 영향을 받게 된 것이다. 단지 기존의 기능을 좀 이용하려 했을 뿐인데 상위 클래스의 변경에도 취약한 코드가 되어 버린 것이다.


자 이런 경우에 활용할 수 있는 것이 바로 Adapter 패턴이다. 일단 Adapter 패턴은 상속을 받아 구현하지 않는다. 따라서 상위 클래스의 변경 문제로부터 자유로울 수 있다. 대신에 Adapter 패턴에서는 기존의 클래스를 맴버 객체로 가지고 있다가 자신이 해야 할 일을 맴버 객체에게 시키는 방식으로 동작한다. 자기가 해야 할 일을 맴버 객체에게 시킴으로써 목적을 달성하는 것을 위임이라고 한다. 이를 통해 기존의 코드를 이용하면서도 상속 문제와 상위 클래스의 API 문제를 해결할 수 있다.


그러면 이제 Adapter 패턴으로 구현된 Stack 코드를 보자.


Adapter 패턴으로 구현한 Stack 클래스

class Stack<T>{

    private Vector<T> vector = new Vector<T>();

    public void push(T t){ vector.add(t); }

    public T pop(){

        if(vector.size() == 0) return null;

        T t = vector.get(vector.size() - 1);

        vector.remove(vector.size() - 1);

        return t;

    }

}


우선 우리가 이용하고자 하던 기존 코드, 즉 Vector는 내부 맴버 객체로 선언이 되었다.(더 좋은 코드라면 강한 의존성 문제를 해결하기 위해서 의존성 주입 형태를 사용해야 한다.) 그리고 마찬가지로 push()와 pop()을 구현하는데, 이 때 기존 코드에서는 상속을 받았기 때문에 직접 호출했었던 API들을 이제는 vector 맴버 객체의 API를 호출하고 있다.


이렇게 구현된 경우, Stack 클래스는 Vector가 가지고 있는 API들은 하나도 노출시키지 않고 Vector의 기능을 이용할 수 있다. 만약 Vector 클래스의 API가 변경된 경우라면 Stack 클래스를 사용하는 코드에는 수정이 가해질 필요가 없고 단지 Stack 클래스 내부 코드만 수정하면 된다.

'5.디자인패턴' 카테고리의 다른 글

Builder 패턴  (0) 2016.09.10
Holder 패턴  (0) 2016.08.28
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Posted by 이세영2
,

Decorator 패턴은 동일한 타입의 객체를 품고 있는 패턴이다.

Decorator 패턴은 기본적인 기능을 구현한 클래스를 인자로 받아서 추가된 기능을 구현한 객체가 이용함으로써 기능의 확장이나 변경을 수행하는 패턴이다. 이 패턴의 장점은 동적으로 기능의 추가 제거가 가능하고, 기능을 구현하는 클래스들을 분리함으로써 수정이 용이해진다는 점이다. 마치 기본 제품에 포장지나 외부 디자인을 살짝 변경해 줌으로써 새로운 기능을 부여하는 것과 같다고 해서 이 명칭이 붙었다.


해결하고자 하는 문제

일단 다음과 같은 데이터가 있다고 가정하자.

int data;

개발자들에게 주어진 미션은 다음과 같다.

    1. data를 멀티쓰레드 환경에서 사용할 수 있도록 구현할 것. 즉 동시성을 만족할 것.

    2. data의 사용 환경이 싱글쓰레드일 경우 성능을 우선시 할 것.(즉 동시성 만족을 위한 코드를 제거할 것)


이런 문제를 처음 접한 사람들은 상당한 난감함에 빠질 것이다. 데이터의 동시성 코드를 구현함과 동시에 동시성을 구현하지 않은 코드도 구현하라니. 특히나 C언어를 중심으로 공부한 사람들은 사실 이 문제가 더더욱 어렵게 느껴질 것이다. 전역 변수 문제로 데이터를 다루는 함수를 특정짓기가 어렵기 때문이다.


하지만 일단 객체지향으로 넘어오고 나면 개념적으로 이 문제를 해결할 수 있는 실마리가 있다. 클래스의 정의로부터 클래스는 속성(데이터)과 행위의 집합임을 알 수 있고, 속성은 정보 은닉(infomation hiding)을 통해서 외부에 노출시키지 않을 수 있다. 그리고 데이터를 다루는 것은 데이터를 포함하고 있는 클래스의 메소드들로 한정 지을 수 있다.


먼저 해결의 단계를 밟기 전에 Decorator 패턴을 적용하기 용이하도록 interface를 하나 정의하도록 하겠다. 물론 int data를 보호하기 위해 만들어질 클래스를 위한 인터페이스이다. 단순히 데이터를 꺼내 가고 집어 넣는 get/set 함수들이다.

interface IData{

    public void setData(int data);

    public int getData();

}



이제 위의 인터페이스를 구현하면서 int data를 선언하고 그에 필요한 행위를 정의하는 클래스를 선언한다. 이 클래스를 선언하는 순간 해결의 첫 단계를 밟게 된다.

class Data implements IData{

    private int data;

    public void setData(int data){

        this.data = data;

    }

    public int getData(){

        return data;

    }

}


위의 클래스를 선언함으로써 얻은 효과는 다음과 같다.

1. data를 private으로 선언함으로써 외부에서 임의로 접근하여 생기는 동시성 문제를 차단하였다.

2. data를 다루는 행위들을 모두 한 클래스 안에 모아 둠으로써 동시성 문제가 확산되는 것을 방지하였다.


이제 적극적으로 동시성 문제를 해결해 볼 차례다. Java 뿐 만 아니라 다른 객체지향 언어를 사용하는 사람들도 익숙한 형태로 먼저 문제를 해결해 보도록 하겠다. 현재 data를 다루는 메소드는 getData() 함수와 setData() 함수 둘 뿐이다. 그리고 여러 쓰레드에서 이들 함수를 호출할 때 동시성 문제가 발생하게 된다. 이를 방지하려면 데이터를 다루는 코드 영역을 동시에 여러 쓰레드에서 접근하지 못하도록 제한하면 된다. 즉, Lock 또는 Mutex를 이용하는 것이다.


동시 접근을 제한해야 할 영역은 이미 위의 두 메소드로 한정되어 있으니 적용도 역시 간단하다.

class Data implements IData{

    private int data;

    private Lock mutex = new ReentrantLock();

    public void setData(int data){

        mutex.lock();

        this.data = data;

        mutex.unlock();

    }

    public int getData(){

        mutex.lock();

        int backup = data;

        mutex.unlock();

        return backup;

    }

}


ReentrantLock 클래스는 Java에서 사용하는 Lock의 구체 클래스이다. 다른 언어에서 사용하는 Mutex나 Lock이라고 생각하고 보면 된다. setData()와 getData()를 보면 data를 다루는 영역이 Lock으로 잠겨 있는 것을 알 수 있다. data를 사용하는 영역은 저 두 영역 뿐이고, 두 영역이 같은 Lock 객체를 통해 잠겨 있으므로 동시에 저 영역이 접근되는 것은 차단되어 있다.


자 이제 Java 사용자들이 익숙한 synchronized 키워드를 이용한 접근 제한을 구현해 보도록 하겠다. 다른 언어 개발자들은 그냥 위의 코드와 아래 코드가 동일한 기능을 한다고 이해하면 되겠다.

class Data implements IData{

    private int data;

    public void setData(int data){

        synchronized(this){

            this.data = data;

        }

    }

    public int getData(){

        synchronized(this){

            return data;

        }

    }

}


바뀐 부분을 보면, 기존에 Lock 객체에 의해 상하로 막혀 있던 것이 synchronized 키워드를 통해 감싸져 있고, this 객체, 즉 자기 자신에 의해 동기화 되어 있음을 알 수 있다. this 객체에 의해 동기화 되어 있으므로 같은 객체 안에서 synchonized 키워드로 둘러 싸인 영역은 중복 접근이 불가능하다.


이것으로 일단 1번 요구사항을 만족시켰다. 하지만 2번 요구사항을 만족시키는 것은 간단해 보이지 않는다. 2번 요구사항을 좀 해석해 보자면, 동시성을 만족시키는 구현과 동시성을 만족시키지 않는 구현을 바꿔치기 하기 용이하도록 구현하라는 것이다.


이제 위에서 선언한 interface가 사용될 때가 되었다. 일단 동시성을 만족시키지 않는 클래스는 그대로 사용하도록 한다. 그러면 동시성을 만족하는 클래스를 만들어야 한다. 하지만 이미 구현된 동시성 코드와 그렇지 않는 코드는 중복이다. 이러면 한 클래스가 수정되었을 때 다른 클래스는 수정되지 않을 수 있다. 따라서 중복 코드는 제거 되어야 한다. 이 때 사용하는 것이 Decorator 패턴이다. 아래는 Decorator 패턴을 통해 구현한 동시성 만족 클래스이다.

class SynchronizedData implements IData{

    private IData data;

    public SynchronizedData(IData data){

        this.data = data;

    }

    public void setData(int data){

        synchronized(this){

            this.data.setData(data);

        }

    }

    public synchronized int getData(){

        synchronized(this){

            return data.getData();

        }

    }

} 


위의 코드는 우선 IData 인터페이스를 구현하고 있다. 따라서 Data 클래스와 외부적 관점에서는 동일한 타입이 된다. 내부에는 첫째로 IData 객체에 대한 레퍼런스를 선언해 두고 있다. 그리고 생성자를 통해서 IData 타입의 객체를 입력 받도록 되어 있다. 우리가 선언한 Data 클래스가 외부적 관점에서는 IData 타입이다. 이것을 객체로 생성하여 집어 넣을 것이다. 그리고 data를 다루는 메소드들에서는 생성자를 통해 들어온 참조 객체를 그냥 이용하기만 한다. 대신 synchronized 블럭을 이용함으로써 동시성을 만족하도록 구현 되어 있다. 이렇게 하면 동시성을 만족하면서도 중복 코드가 없어서 수정에 닫혀 있는 형태의 구현이 완성된다.


그러면 이제 결과물을 종합해 보자.


클래스 다이어그램


최종 결과물

interface IData{ // 동일한 타입으로 만들어 주기 위한 인터페이스

    public void setData(int data);

    public int getData();

}


class Data implements IData{ // 동시성을 구현하지 않은 Data 클래스

    private int data;

    public void setData(int data){

        this.data = data;

    }

    public int getData(){

        return data;

    }

}


class SynchronizedData implements IData{ // 동시성을 구현한 클래스(Decorator 패턴)

    private IData data;

    public SynchronizedData(IData data){

        this.data = data;

    }

    public void setData(int data){

        synchronized(this){

            this.data.setData(data);

        }

    }

    public synchronized int getData(){

        synchronized(this){

            return data.getData();

        }

    }

} 


이 패턴을 사용하는 코드를 보자.

public static void main(String[] args) {

    IData data = new Data(); // 동시성이 필요없을 때

    IData data = new SynchronizedData(new Data()); // 동시성이 필요할 때


}


동시성이 필요하지 않은 경우에는 그냥 new Data()를 호출해서 바로 Data 클래스를 사용하면 된다. 동시성이 필요한 경우에는 같은 코드에서 new SynchronizedData()를 호출한 후 Data 클래스를 생성해서 넣어 주기만 하면 된다. 즉, 동시성을 만족하고 안하고는 저 코드 한 줄만 수정하면 되는 문제이다.


사실 이 구현 방식은 특별한 것이 아니다. 이미 Java 라이브러리에서는 동시성을 이런 방식으로 구현해 두었다. 다음의 코드를 보자.

사용 방법

    List<String> list = new ArrayList<String>();

    List<String> list = Collections.synchronizedList(new ArrayList<String>());


Java에서는 Collection을 자주 사용하게 되는데, 사용 빈도가 높은 만큼 Collection의 동시성 문제는 상당히 해결하기 어려워 질 수 있다. 하지만 두번째 줄에 보면 우리가 구현한 내용과 유사한 코드가 보인다. 저것이 Decorator 패턴을 이용해 구현한 동시성 지원 Collection 클래스 사용 방법이다. Collections.synchronizedList와 우리의 SynchronizedData가 얼마나 유사한지 알아보기 위해 라이브러리 소스를 살펴 보도록 하겠다.


Collections.synchronizedList 내부 소스 코드

static class SynchronizedList<E> implements List<E> {

    final List<E> list;

    SynchronizedList(List<E> list) {

        super(list);

        this.list = list;

    }

    public E get(int index) {

        synchronized (mutex) {return list.get(index);}

    }

    public E set(int index, E element) {

        synchronized (mutex) {return list.set(index, element);}

    }

    public void add(int index, E element) {

        synchronized (mutex) {list.add(index, element);}

    }

    public E remove(int index) {

        synchronized (mutex) {return list.remove(index);}

    }

    ......

}


일단 잡다한 코드들은 일부 지웠다. 눈여겨 볼 것은 데이터를 다루는 메소드들이다. 보면 mutex 객체를 통해 모두 synchronized 블럭으로 막혀 있고, 그 안에서 일반 List 객체를 다루고 있는 것을 볼 수 있다. 그리고 생성자에서는 당연히 일반 List 객체를 인자로 받도록 되어 있다. 구조적으로 위에서 구현 내용과 완전히 같음을 알 수 있다.

'5.디자인패턴' 카테고리의 다른 글

Holder 패턴  (0) 2016.08.28
Adapter 패턴  (0) 2016.08.23
Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
Posted by 이세영2
,

켄트 벡의 TDD("테스트 주도 개발")에 보면 Junit 구현에 Pluggable Selector라는 패턴을 사용했다는 내용이 나온다. 책에서 언급된 내용은 코드보다는 말로 설명되어 있고, 아래 링크에 가면 코드가 어느 정도 나와 있다.

JUnit A Cook's Tour


하지만 안타깝게도 테스트 케이스를 수행하기 위한 객체 생성 부분이 없어서 상상력을 발휘해서 Pluggable Selector 패턴을 구현해 볼까 한다. 혹시 이 글을 보고 실제 Junit이 이렇게 구현되어 있다고 생각하지는 말길 바란다. 여기에 나오는 것은 Pluggable Selector 패턴을 설명하기 용이하게 만들어진 예제에 불과하다.


Pluggable Selector 패턴이란?

첫번째 예는 Unit Test Framework인 Junit3에 관한 것이다(Junit4 부터는 annotation 기반으로 바뀌었기 때문에 외부 구현 상에서 유사점을 찾기 힘들 것이다.) Junit3에서 Unit Test를 작성하기 위해서는 TestCase 라는 클래스를 상속 받아야 한다. 필요한 경우 setUp() 함수나 tearDown() 함수를 재정의 해야 한다.(여기까지는 Pluggable Selector 패턴과 별 상관이 없다.) Pluggable Selector와 관련이 있는 부분은 테스트 함수 부분인데, Junit3에서는 void testNAME() 형식의 함수를 찾아 실행하도록 되어 있다. 더욱이 중요한 부분은 각 테스트 함수들을 실행 할 때 이전 테스트와의 의존성을 배제하기 위해서 테스트 객체를 매번 다시 생성해서 실행 시킨다는 점이다.

보다 정확한 설명을 위해서 우리가 동작 시켜볼 테스트 클래스를 살펴 보도록 하자. 문제를 명확하게 하기 위해서 불필요하게 TestCase를 상속 받는 부분을 제외하였다.

class UnderTest{

    public void setUp(){

        System.out.println("setUp()");

    }

   

    public void testCase1(){

        System.out.println("testCase1()");

    }

   

    public void testCase2(){

        System.out.println("testCase2()");

    }

   

    public void tearDown(){

        System.out.println("tearDown()");

    }

}

위와 같은 테스트 클래스를 Junit3에서 실행 시키면 아래와 같은 순서로 실행이 된다.

(UnderTest 클래스 객체의 생성)

setUp()

    testCase1()

tearDown()


(또 다른 UnderTest 클래스 객체의 생성)

setUp()

    testCase2()

tearDown()


() 안의 동작은 Junit3 프레임 워크 내부에서 수행해 주는 일이다. 반복적인 테스트가 수행되는데 다른 점은 첫번째에는 testCase1()이 실행되고, 두번째는 testCase2()가 실행된다는 점이다. 이 부분이 Pluggable Selector 패턴의 핵심이다. 클래스 상에서 test로 시작하는 테스트 함수들을 모두 찾고, 실행시켜 주는 것이다.


Pluggable Selector  패턴에 대한 좀 더 범용적인 이해를 돕기 위해 다른 예를 들어 보겠다. 상위 클래스를 상속 받은 여러 하위 클래스들이 있다고 하자. 이들 클래스는 상위 클래스와 대부분 비슷하지만 모두 각자 (상위 클래스로부터 상속 받지 않은) 다른 메소드 하나씩을 가지고 있다고 하자. 이 때 상위 클래스에서 제공하지 않는 메소드들을 한번씩 실행 시켜야 한다면 어떻게 해야 할까? 이럴 때 사용하는 것이 Pluggable Selector 패턴이다.


다시 Junit3 예제로 돌아가보자. Java에서 이를 구현하는 방법은 Reflection을 이용하는 것이다. 특히 Reflection을 이용하면 테스트 함수들을 찾고, 객체를 생성하고, 함수별로 실행 시켜주는 과정을 모두 자동화 할 수 있다.


테스트 대상 클래스는 이미 위에서 만들었으니 Pluggable Selector 패턴으로 구현된 JUnit 클래스를 만들어 보자. JUnit 클래스는 테스트 클래스 등록(addTest())과 테스트의 실행(runTest()) 단계로 크게 구분될 수 있다. 우선 addTest()부터 구현해 보자.

class JUnit{

    Map<String, Class> map = new HashMap<String, Class>();

   

    public void addTest(Class clazz){

        Method[] methods = clazz.getDeclaredMethods();

        for(Method method : methods){

            method.setAccessible(true);

   

            if(method.getName().startsWith("test")){

                map.put(method.getName(), clazz);

            }

        }

    }

Reflection으로 구현되어 있기 때문에 약간 생소할 수도 있다. 먼저 addTest() 함수의 매개변수는 클래스 객체이다. 여기에 우리는 테스트 대상 클래스인 TestClass.class를 매개변수로 넣을 것이다.

다음으로 Method[]를 추출해 내는 작업을 수행한다. 클래스 객체에서 getDeclaredMethods() 함수를 호출하면 해당 클래스가 선언하고 있는 모든 메소드들을 배열 형태로 리턴해 준다.

이제 각 Method를 대상으로 for 문을 수행하는데, 맨 처음 호출되는 것은 method.setAccessible(true)이다. 이것은 메소드가 public이 아닐 경우에도 접근이 가능하도록 설정해 주는 것이다.

그 다음에는 method.getName()을 이용해서 메소드의 이름을 체크한다. 여기서는 JUnit3과 마찬가지로 test로 시작하는 함수를 추출한다. 그리고 만약 test로 시작하는 함수가 있다면 이 메소드 이름을 key로 사용하는 map에 메소드 이름과 클래스 객체를 값으로 하여 집어 넣게 된다.

이 과정을 마치고 나면 모든 test로 시작하는 메소드 이름 : 클래스 객체의 맵이 만들어지게 된다. 이 맵은 이제 만들어 볼 runTest() 메소드에서 사용된다.

우선 runTest() 메소드에서 해야 할 일을 다시 떠올려 보자. 첫번째 테스트 함수를 테스트 하기 위해서 테스트 객체를 생성하고, setUp() 함수를 호출하고, test 함수를 호출하고 tearDown() 함수를 호출한다. 다음 테스트 함수에 대해서도 마찬가지이다.(이 중에서 setUp() 함수와 tearDown() 함수의 호출 이유는 그다지 중요하지 않으니 Unit Test를 모르는 분들은 무시해도 된다.)

그럼 구현을 살펴보자.

    public void runTest(){

        for(String testName : map.keySet()){

            Class clazz = map.get(testName);

            try {

                Object testObject = clazz.newInstance();


                Class[] parameterTypes = new Class[0];

                Method setUp = clazz.getDeclaredMethod("setUp", parameterTypes);

                Method tearDown = clazz.getDeclaredMethod("tearDown", parameterTypes);

                Method testMethod = clazz.getDeclaredMethod(testName, parameterTypes);

                Object[] parameters = new Object[0];

                setUp.invoke(testObject, parameters);

                testMethod.invoke(testObject, parameters);

                tearDown.invoke(testObject, parameters);

            } catch (InstantiationException e) {

            } catch (IllegalAccessException e) {

            } catch (SecurityException e) {

            } catch (IllegalArgumentException e) {

            } catch (InvocationTargetException e) {

            } catch (NoSuchMethodException e) {

            }

        }

    }

우선 맵의 키 값(여기서는 test 함수 이름을 나타내는 String 객체) 이용하여 for문을 수행한다. 실제 테스트를 수행하려면 우선 객체가 필요하다. 그래서 맵에서 test 함수의 클래스 객체를 가지고 온 다음, testObject라는 이름으로 newInstance() 함수를 호출해서 객체를 생성한다. 클래스 객체를 이용하면 이와 같이 객체의 생성도 수행할 수 있다.

그 다음에는 setUp(), tearDown() 메소드를 클래스 객체로부터 얻어 온다. 마지막으로 테스트할 메소드도 가지고 온다.

실제로 이 메소드들을 실행하는 방법은 Method 객체에 구현된 invoke() 함수를 이용하는 것이다. 이 때 invoke는 특정 객체(위 코드에서는 testObject)의 것을 호출하는 것이다. 따라서 Method 객체는 형식만 지정하는 역할을 하고, 실제 수행되는 객체인 testObject를 매개 변수로 넣어 주어야 한다. 파라메터는 없으므로 비어 있는 Object[] 객체를 넣어 주면 된다.

전체 구현 내용을 종합해 보면 다음과 같다.


최종 결과

class JUnit{

    Map<String, Class> map = new HashMap<String, Class>();

   

    public void addTest(Class clazz){

        Method[] methods = clazz.getDeclaredMethods();

        for(Method method : methods){

            method.setAccessible(true);

   

            if(method.getName().startsWith("test")){

                map.put(method.getName(), clazz);

            }

        }

    }

   

    public void runTest(){

        for(String testName : map.keySet()){

            Class clazz = map.get(testName);

            try {

                Class[] parameterTypes = new Class[0];

                Object testObject = clazz.newInstance();

                Method setUp = clazz.getDeclaredMethod("setUp", parameterTypes);

                Method tearDown = clazz.getDeclaredMethod("tearDown", parameterTypes);

                Method testMethod = clazz.getDeclaredMethod(testName, parameterTypes);

                Object[] parameters = new Object[0];

                setUp.invoke(testObject, parameters);

                testMethod.invoke(testObject, parameters);

                tearDown.invoke(testObject, parameters);

            } catch (InstantiationException e) {

            } catch (IllegalAccessException e) {

            } catch (SecurityException e) {

            } catch (IllegalArgumentException e) {

            } catch (InvocationTargetException e) {

            } catch (NoSuchMethodException e) {

            }

        }

    }

}


이제 실행을 시켜 보도록 하자.

public static void main(String[] args) {

    JUnit junit = new JUnit();

    junit.addTest(UnderTest.class);

    junit.runTest();

} 

위와 같이 실행 코드를 작성하고 실행 시켜 보면 아래와 같이 출력된다.

setUp()

testCase2()

tearDown()

setUp()

testCase1()

tearDown()

의도한 대로 각 테스트 함수들이 잘 실행되는 것을 알 수 있다.

JUnit 처럼 테스트 대상 클래스가 어떤 명칭의 테스트 함수를 구현하게 될지 알 수 없을 때에 Pluggable Selector 패턴은 매우 적절한 선택이다. 소프트웨어를 개발하다 보면 향후에 확장을 대비해서 상당한 수준의 자유도를 미리 확보해야 하는 경우가 종종 있는데, 이런 경우에 사용하면 좋을 것이다.

'5.디자인패턴' 카테고리의 다른 글

Adapter 패턴  (0) 2016.08.23
Decorator 패턴(synchronizedList의 구현 패턴)  (0) 2016.08.23
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
Telescoping Parameter 패턴  (0) 2016.08.13
Posted by 이세영2
,

State 패턴

5.디자인패턴 2016. 8. 15. 18:24

참고

- 상태와 행위의 결별


상태 변수는 어떤 경우에도 좋지 않다. 상태 변수는 변수와 행위와의 결합을 만들어 내고, 이 과정에서 조건문들을 부수적으로 생산해 낸다. 따라서 언어적으로 허용되는 한 상태 변수는 최대한 없애주는 것이 좋다.


해결하고자 하는 문제

- 상태 변수에 의해 행위가 변경된다.
- 이 상태 변수가 행위를 변경하기 위해서 조건문을 사용한다.
- 상태 변수를 체크하기 위한 조건문이 너무 많다.

문제 코드

class Employee {

    public static final int ENGINEER = 1;

    public static final int MANAGER = 2;

    public static final int SALESMAN = 3;

    private int type;

    public void setType(int type){

        this.type = type

    }

    public Employee(int type){

        setType(type);

    }

    public int getAmount(){

        switch(type){

            case ENGINEER :

                return 100;

            case MANAGER :

                return 200;

            case SALESMAN :

                return 300;

        }

        return 0;

    }

} 

이 예제는 마틴 파울러의 "리팩토링"에서 사용된 예제이다. Employee 객체는 내부적으로 직원의 타입을 나타내기 위해 type 이라는 상태 변수를 사용하고 있다. 그리고 getAmount() 함수에서는 Employee의 type에 따라서 다른 결과값을 출력하도록 만들어져 있다.

이 소스에서 확인할 수 있는 것은 다음과 같다.

1. type 변수는 getAmount() 함수 내부에 switch - case의 각 구문과 1:1 매칭된다.

2. type 변수의 값에 따라서 다른 구문이 호출된다. ENGINEER 값일 경우 return 100, MANAGER일 경우 return 200이 실행된다는 말이다.


사실 이런 상황에서 type 변수는 오직 행위를 변경하고자 하는 목적에서 선언된 것이다. 만약 type을 통해 변경하고자 하는 행위를 직접 변경한다면 전체 소스는 매우 간결해지게 될 것이다.


이 목적을 달성하기 위해서는 일단 각 type 에 해당하는 객체를 구현하고, 각 객체별로 수행될 함수를 정의할 필요가 있다.


interface EmployeeType{

    public int getAmount();

}

class Engineer implements EmployeeType{

    public int getAmount() { return 100; }

}

class Manager implements EmployeeType{

    public int getAmount() { return 200; }

}

class Salesman implements EmployeeType{

    public int getAmount() { return 300; }

}

이와 같이 ENGINEER, MANAGER, SALESMAN에 대응하는 객체를 구현하였다. 이 객체들은 EmployeeType이라는 인터페이스를 상속 받고 있으므로 EmployeeType으로 선언된 멤버 변수에 자유롭게 변경하여 집어 넣을 수 있다.


이제 기존의 type  변수 대신에 새로 정의한 EmployeeType을 사용한다.

private EmployeeType type;

public void setType(EmployeeType type){

    this.type = type

}

public Employee(EmployeeType type){

    setType(type);

}

이제 getAmount() 함수 부분을 수정해야 한다. 이미 EmployeeType의 하위 객체들은 getAmount() 함수를 가지고 있다. 그리고 새로 정의된 type 변수에는 적절한 EmployeeType 객체가 할당되어 있다. 따라서 getAmount() 함수에서는 EmployeeType의 하위 객체가 가지고 있는 getAmount() 함수를 호출해 주기만 하면 된다.

public int getAmount(){

   return type.getAmount();

}

보면 알겠지만 기존의 int 타입 변수를 사용했을 때에는 getAmount() 함수 내부가 switch 문으로 구성되어 있어서 가독성이 많이 떨어졌다. 그럴 수 밖에 없었던 것은 type이 상태 변수 역할을 했기 때문이다. 상태 변수는 행위를 직접 변경시킬 수 없다. 행위와 상태 변수가 직접 대응될 수 없기 때문이다. 그래서 상태 변수의 값을 검사하는 조건문이 추가 될 수 밖에 없었다. 하지만 상태 변수 대신 행위를 직접 수행할 수 있는 객체에 대한 레퍼런스를 사용하면 조건문을 별도로 사용할 필요가 없다.


최종 결과

interface EmployeeType{

    public int getAmount();

}

class Engineer implements EmployeeType{

    public int getAmount() { return 100; }

}

class Manager implements EmployeeType{

    public int getAmount() { return 200; }

}

class Salesman implements EmployeeType{

    public int getAmount() { return 300; }

}

class Employee {

    private EmployeeType type;

    public void setType(EmployeeType type){ this.type = type; }

    public Employee(EmployeeType type){

        setType(type);

    }

    public int getAmount(){

        return type.getAmount();

    }

}


State 패턴 클래스 다이어그램


Posted by 이세영2
,

Strategy 패턴

5.디자인패턴 2016. 8. 15. 17:51

참고

- 상태와 행위의 결별


Strategy 패턴은 작업을 수행하는 대상 객체에 (변수 대신) 다른 객체를 인자로 넣어 줌으로써 대상 객체의 행위를 변경하는 패턴이다.


해결하고자 하는 문제

- 객체의 동작을 동적으로 변경하고자 한다.
- 객체의 동작이 다양하고 확장될 가능성이 있다.
- 상태 변수에 의해 조건문 중첩이 너무 많이 발생한다.

문제 코드

public static final int ADD_STATE = 0;

public static final int SUB_STATE = 1;

int static calculate(int state, int a, int b){

    if(state == ADD_STATE){

        return 5 + 10;

    }

    else if(state == SUB_STATE){

        return 5 - 10;

    }

    return 0;

}

public static void main(String[] args) {

    int result = 0;

    result = calculate(ADD_STATE, 5, 10);

    result = calculate(SUB_STATE, 5, 10);

}

위의 코드는 state 변수의 값에 따라서 간접적으로 calculate() 함수의 동작을 제어하도록 되어 있다. 상태 변수를 통해 행위를 변경시키는 코드는 좋은 코드가 아니다. 불필요한 상태 변수가 선언되고, if 문이나 switch 문과 같은 불필요한 제어문이 생성되기 때문이다. 가장 좋은 방법은 변경시키고자 하는 행위를 직접 넘겨주는 것이다.

우선 아래와 같이 변경하고자 하는 행위를 객체로 선언한다. 인자로 넘길 때 변경이 가능한 형태여야 하므로 동일한 인터페이스를 상속 받은 Add 클래스와 Sub 클래스를 선언해 준다.

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}


이렇게 선언된 클래스를 calculate() 함수가 인자로 받을 수 있도록 한다. 이 때 state 변수는 이제 필요 없으므로 제거한다.

static int calculate(IFunction function, int a, int b){

    return function.calculate(a, b);

}


public static void main(String[] args) {

    int result = 0;

    result = calculate(new Add(), 5, 10);

    result = calculate(new Sub(), 5, 10);

}


최종 결과

interface IFunction{

    public int calculate(int a, int b);

}

class Add implements IFunction{

    public int calculate(int a, int b){

        return a + b;

    }

}

class Sub implements IFunction{

    public int calculate(int a, int b){

        return a - b;

    }

}

public class Strategy {

    static int calculate(IFunction function, int a, int b){

        return function.calculate(a, b);

    }

    public static void main(String[] args) {

        int result = 0;

        result = calculate(new Add(), 5, 10);

        result = calculate(new Sub(), 5, 10);

    }

}


상태를 나타내는 state 변수 대신에 행위를 구현한 객체를 직접 넣어 줌으로써 코드가 더 간결해지게 된다.

Strategy 클래스의 calculate() 함수는 IFunction 타입의 객체를 인자로 받도록 되어 있다. main() 함수에서는 calculate() 함수를 통해서 실행하고자 하는 행위에 따라서 Add 클래스 혹은 Sub 클래스를 바꿔 넣어주면 행위가 변경된다.  



Posted by 이세영2
,

Telescoping Parameter 패턴은 "켄트 벡의 구현 패턴"에도 언급되었던 패턴이다.

기본적으로 이 패턴은 다수의 매개 변수를 가진 함수의 문제점을 해결하기 위한 패턴이다.


보통 생성자가 다수의 매개 변수 개수를 달리 하면서 생성이 가능한 경우에 주로 사용된다.


실제로도 많이 쓰이는 패턴이라서 Java 라이브러리의 ServerSocket 함수를 가지고 설명을 해볼까 한다.


우선 API를 기준으로 보면 Telescoping Parameter 패턴의 외형은 다음과 같다.


public ServerSocket(int port);

public ServerSocket(int port, int backlog);

public ServerSocket(int port, int backlog, InetAddress bindAddr); 


이처럼 매개 변수가 여럿이고 매개변수의 기본 값이 있는 경우에 인자 개수가 다른 API를 제공해 준다. 이렇게 하면 사용하는 입장에서는 필요에 따라 짧거나 긴 매개 변수를 가진 API를 호출할 수 있다. 이 모양이 마치 망원경을 접었다 폈다 하는 모양과 비슷하다고 해서 붙여진 이름이다.



내부 구현 시 고려 사항

이 패턴에 대해서는 다음과 같은 사항을 잘 생각해 봐야 한다. 이는 내부 구현에 있어서 지켜야 할 중요한 부분이다.


저 함수들은 모두 backlog 변수나 bindAddr 변수에 대한 기본 값이 있다는 전제 하에서 작성되었다. 따라서 결과적으로는 세 함수 모두 세 개의 파라메터 모두에 대한 설정을 하게 될 것이다.


이런 상황에서 매개 변수로 하려는 일은 동일할텐데 이를 각 함수에 구현하면 중복 구현의 문제가 발생한다. 따라서 아래와 같은 형태로 내부를 구현해 주어야 한다.


public ServerSocket(int port){

    this(port, 50, null);

}


public ServerSocket(int port, int backlog){

    this(port, backlog, null);

}


/* 결국 어떤 함수를 사용해도 아래 함수가 호출되게 된다 */

public ServerSocket(int port, int backlog, InetAddress bindAddr){

    setImpl();

    bind(new InetSocketAddress(bindAddr, port), backlog);

} 


실제 코드보다는 좀 더 간단하게 변경하였다. 매개 변수가 한 개인 경우 남은 두개의 기본 값을 채워 3개짜리 함수를 호출한다. 두 개짜리고 마찬가지로 3개짜리 함수를 호출하는 것으로 할 일을 마친다. 3개짜리 함수만 실제 필요한 동작을 수행하게 된다.



코드 중복의 방지

객체의 외부에서 객체로 변수 값을 직접 전달하는 기본 방식은 setter를 이용하는 것이다. 하지만 종종 생성자를 이용하여 변수를 초기에 셋팅하게 하는 것이 좋을 때가 있다. 이렇게 되면 생성자와 setter에 의해 변수가 셋팅되게 된다. 변수 셋팅은 아주 중요한 작업이다. 이 경우 생성자 내부에서는 setter를 호출해 주는 것이 좋다.


안티 패턴

class Example {

    int data;   

   

    public void Example(int data){

        this.data = data;

    }

   

    public void setData(int data){ this.data = data; }

}


이렇게 구현했을 경우 setData 내부에서 data가 새로 설정된 이후 동작을 변경해버리면 생성자를 통해 data를 변경했을 때에는 이것이 반영되지 않는다. 따라서 아래와 같이 구현하는 것이 좋다.


좋은 구현 방식

class Example {

    int data;   

   

    public void Example(int data){

        setData(data); // 이렇게 해야 setData() 함수 내부의 변경에 안전하다.

    }

   

    public void setData(int data){ this.data = data; }

}



이 문제에 대한 고려가 Telescoping Parameter 패턴의 구현에도 고스란히 반영되어 있다.


단순히 자꾸 함수를 호출하려는 것으로만 보일지 모르겠지만 실제로는 모든 함수들은 모든 매개 변수가 설정되는 것을 기대하고 있다. 따라서 매개 변수로 내부 변수를 설정하는 코드(예제에서는 bind() 함수를 호출하는 부분)는 한 곳으로 몰아 주기 위해 this 함수를 호출하는 것이다. 


Telecoping Parameter 패턴을 구현할 때에는 이 점을 꼭 염두해 두어야 한다.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
interface -abstract class - concrete class 패턴(인터페이스 구현 중복 해결 패턴)  (2) 2016.08.10
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

interface - abstract class - concrete class 패턴은 인터페이스 구현 시 자주 발생하게 되는 중복 구현을 방지하는 패턴이다.


해결하고자 하는 문제

- 구현해야 할 클래스에 대한 인터페이스가 이미 정해진 상태이다.

- 정해진 인터페이스를 통해 구현해야 할 클래스가 여러개이다.

- 인터페이스 API 중 일부가 모두 같은 구현부를 같게 된다. 이 구현부의 중복을 없애야 한다.


해결 방법

인터페이스 구현 시 구현해야 할 함수 중에서 중복되는 함수들을 abstract class에 넣음으로써 곧바로 인터페이스를 구현하려고 할 때 발생할 수 있는 중복 구현을 방지할 수 있다.


간단한 예제를 통해 interface - abstract class - concrete class가 어떻게 쓰이는지 살펴보자.


우선 각종 도형들을 그리는 소프트웨어를 개발한다고 하자. 삼각형 사각형 원 등 다양한 도형이 있는데 이들 도형은 모두 표면(surface)과 라인(line)으로 그려진다고 가정해 보자. 이런 경우 모든 도형의 공통 요소인 표면 색깔 지정, 라인 색깔 지정, 도형 그리기와 같은 API를 생각해 볼 수 있다. 이들은 모든 도형에 공통이므로 공통 인터페이스를 선언하는 것으로 구현을 시작해 보겠다.


interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}



그러면 인터페이스가 정의 되었으니 도형을 구현해 볼 차례이다. 먼저 Rectangle을 만들어 보자.


class Rectangle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}



현재까지는 크게 문제는 없어 보인다. 표면 색깔과 라인 색깔을 지정할 수 있는 인터페이스를 구현했고, 도형을 그리는 draw() 함수도 구현했으니 실제로 잘 그려지게 될 것이다. 이렇게 IShape 인터페이스가 제공하는 모든 API를 구현했으니 이제 다른 도형도 만들어 보겠다. Circle을 만들어 보자.


class Circle implements IShape{

    private Color surfaceColor;

    private Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


이제 문제점이 눈에 보일 것이다. draw() 함수는 각 도형이 다르겠지만 setSurfaceColor()와 setLineColor()는 서로 동일하다. 하지만 도형이라면 위의 두 인터페이스도 제공해야 하는 것이 맞다. 그러면 계속 중복된 코드들을 만들어 가면서 구현을 완료하는 것이 옳을까?


이런 문제점을 해결할 수 있는 방법이 인터페이스(interfac)와 구체 클래스(concrete class) 중간에 추상 클래스(abstract class)를 하나 두고 공통되는 부분을 모아 두는 것이다. 위의 예제에서 공통된 부분을 추상 클래스로 뽑아 내면 다음과 같아질 것이다.


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

} 


우선 우리가 구현하고자 하는 구체 클래스를 외부에서 사용할 때는 IShape 타입이어야 한다. 따라서 일단 추상 클래스가 IShape을 구현하도록 선언한다. 그리고 구체 클래스에서 발생한 중복 코드들을 추상 클래스로 이동시킨다. 주의할 것은 private 변수들을 protected로 바꾸어 주어야 한다는 것이다. 그렇게 해야 구체 클래스들이 이 Shape 추상 클래스를 상속 받았을 때 그 변수들을 사용할 수 있게 된다.


그리고 한가지 주목할 것은 IShape이 제공하는 인터페이스 중에서 void draw() 인터페이스를 구현하지 않았다는 점이다. 추상 클래스의 경우 상속 받은 인터페이스의 일부만 구현해도 컴파일에러가 발생하지 않는다. 그 이유는 인터페이스에서 선언한 API의 타입은 항상 abstract public 타입이기 때문이다. 잠깐 옆길로 새서 interface의 실제 타입을 밝혀보면 다음과 같다.


interface Example{

    void api();

}

abstract class Example{

    abstract public void api();

}


위의 두 선언은 선언적으로는 동등하다. interface는 실체화 할 수 없는 추상 클래스(abstract class)와 같고, api()는 실제로는 abstract public 타입의 함수이다. 다만, 인터페이스는 다중 상속이 가능하지만 추상 클래스는 단 한 개의 클래스만 상속 가능하다는 점에서 실질적으로는 같지 않다. 어쨌든 개념적으로 보면 인터페이스는 추상 클래스의 "특수 케이스"라고 이해할 수 있다.


그러면 이제 본론으로 다시 넘어가서 draw() 함수를 추상 클래스에서 구현하지 않아도 에러가 나지 않은 이유를 알 수 있을 것이다. 추상 클래스는 추상 메소드를 선언할 수 있는 클래스이다. IShape에서 선언된 draw() 함수는 추상 메소드이고, Shape 클래스가 이를 상속 받았으므로 draw() 추상 메소드가 선언된 셈이다. 추상 클래스가 추상 메소드를 선언하는 것은 문법에 위배되지 않기 때문에 구현체가 없어도 전혀 문제가 없는 것이다.


그럼 이제 Rectangle 클래스와 Circle 클래스가 어떻게 바뀌었는지 보자.


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


자 일단 중복된 부분이 모두 제거되었다. 그 이유는 IShape을 implements 하던 것을 Shape을 extends 하는 것으로 바꿈으로써 setSurfaceColor() 함수와 setLineColor() 함수의 구현부를 상속 받았기 때문이다. 이를 통해서 두 클래스는 서로 다른 부분인 draw() 함수만을 구현하도록 바뀌었다.


그러면 최종적인 모습이 어떤지 한번에 살펴보자.


구현 결과

interface IShape{

    public void setSurfaceColor(Color surfaceColor);

    public void setLineColor(Color lineColor);

    public void draw();

}


abstract class Shape implements IShape{

    protected Color surfaceColor;

    protected Color lineColor;

    public void setSurfaceColor(Color surfaceColor){

        this.surfaceColor = surfaceColor;

    }

    public void setLineColor(Color lineColor){

        this.lineColor = lineColor;

    }

}


class Rectangle extends Shape{

    public void draw(){

        System.out.println("draw Rectangle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

}


class Circle  extends Shape{

    public void draw(){

        System.out.println("draw Circle with");

        System.out.println(surfaceColor.toString());

        System.out.println(lineColor.toString());

    }

} 


위와 같이 되었다 중복 코드가 없는 깔끔한 모습이다. 그러면 사용 방법에 있어서는 어떨까? Rectangle 클래스와 Circle 클래스를 외부에서는 IShape 타입으로 잘 인식 할 수 있을까? 다음과 같이 테스트를 구현해 보겠다.


테스트 함수

public static void main(String[] args) {

   IShape shape = new Rectangle();

   shape.setSurfaceColor(Color.BLACK);

   shape.setLineColor(Color.WHITE);

   shape.draw();


   shape = new Circle();

   shape.setSurfaceColor(Color.WHITE);

   shape.setLineColor(Color.BLACK);

   shape.draw();

} 


모든 API를 한번씩 호출해보도록 작성했고, 각 구체 클래스들을 IShape 타입으로 지칭하도록 했다. 물론 오류 없이 잘 동작하고 다음과 같은 결과를 출력해 냈다.


출력 결과

draw Rectangle with

java.awt.Color[r=0,g=0,b=0]

java.awt.Color[r=255,g=255,b=255]

draw Circle with

java.awt.Color[r=255,g=255,b=255]

java.awt.Color[r=0,g=0,b=0]


이처럼 아주 잘 동작하는 것을 확인 할 수 있다.


실제로 외부에서 제공된 인터페이스를 이용하여 구현을 하다보면 중복 코드가 자주 발생하게 된다. 같은 인터페이스를 상속 받는다는 것은 상속 받아 구현될 구체 클래스들이 유사점을 많이 가지고 있다는 것을 암시한다. 따라서 구현을 진행하다 보면 자연스럽게 중복된 코드들이 자주 만들어지게 된다.


이런 경우에 이 패턴 처럼 중간에 추상 클래스 하나를 만들어 상속 받도록 하면 중복 코드들을 제거할 수 있다. 중복된 부분들이 제거된 구체 클래스들은 구체 클래스들 간에 서로 다른 부분들만 구현하여 가지고 있게 되므로 코드에 대한 이해 속도도 빨라진다는 장점이 있다.


혹시라도 인터페이스 구현으로 인해 중복이 많이 발생하게 되었다면 이 패턴을 이용해 보자.

'5.디자인패턴' 카테고리의 다른 글

Pluggable Selector 패턴  (1) 2016.08.21
State 패턴  (0) 2016.08.15
Strategy 패턴  (0) 2016.08.15
Telescoping Parameter 패턴  (0) 2016.08.13
Enum Factory Method 패턴  (0) 2016.08.07
Posted by 이세영2
,

Enum Factory Method 패턴은 Factory Method 패턴의 단점을 보완하기 위한 패턴이다.

기본적으로 Factory Method 패턴과 마찬가지로 객체의 생성을 담당하는 메소드를 구현하는 패턴이다. 이와 함께 Factory Method를 구현한 객체를 생성하기 위해 Singleton을 사용해야 하는 문제점을 Enum의 특성을 이용하여 해결한다.


Enum Factory Method 패턴(Java에서만 가능)

public enum EnumFactoryMethod {

    RECTANGLE{

        protected Shape createShape(){return new Rectangle();}

    }

    ,CIRCLE{

        protected Shape createShape(){return new Circle();}

    }

    ;

    public Shape create(Color color){

        Shape shape = createShape();

        shape.setColor(color);

        return shape;

    }

    abstract protected Shape createShape();

    public static void main(String[] args) {

        EnumFactoryMethod.RECTANGLE.create(Color.BLACK);

        EnumFactoryMethod.CIRCLE.create(Color.WHITE);

    }

}



Enum 타입 자체가 public static final 이기 때문에 생성을 위임 받은 객체에 대한 중복 생성이 불가하고, Singleton을 굳이 구현하지 않아도 단일한 객체만 생성됨이 보장된다.


Enum의 이러한 특성은 다른 패턴들에도 응용이 될 수 있는데 이는 이후 포스팅을 통해 살펴보도록 하겠다.

Posted by 이세영2
,