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
,


지금까지 공부해 온 소프트웨어 영역의 기술들을 트리 형식으로 정리해 본 것이다.


소프트웨어 기술은 그 발전 속도가 너무 빠르다. 그리고 새로운 기술은 매일 매일 쏟아져 나온다. 이런 상황에서 이제 소프트웨어를 접한지 얼마 되지 않은 사람들은 어떤 것을 먼저 공부해야 할 지 갈피를 잡기 힘들 것이다. 개인적으로 이제껏 소프트웨어를 공부해 오면서 안타까웠던 점을 꼽자면 이런 급변하는 상황에서도 기술들 간에 어느 정도 줄기가 있다는 것, 그리고 줄기가 되는 기술들 간에 선후 관계가 있다는 것을 처음부터 알지 못했다는 것이다. 그런 관계를 알게 된 것은 이제 소프트웨어의 근간이 되는 기술들을 대부분 알게 된 이후였다. 개인적인 능력의 문제도 있겠지만 이 점을 미리 알았다면 그것들을 모두 익히는데 이렇게 오랜 시간이 걸리지는 않았을 것이다. 

그런 안타까움이 뭍어 있는 것이 바로 이 기술 트리다. 만약 이제 소프트웨어를 막 공부하기 시작한 사람이라면 이 트리에 맞춰 공부하기를 추천한다. 그리고 다른 수많은 기술들이 있지만 적어도 이 영역 내의 기술들은 소프트웨어를 하는 사람들이라면 거의 필수적인 기술들이라고 봐야 한다.

일부 개발 영역에 따라서는 더 중요한 것이 빠져 있을 수도 있다. 개발자라고 해서 모두 같은 영역에서 일하는 것이 아니기 때문이다. 웹 프론트, 백엔드, 임베디드, 데이터베이스 영역에서는 세부적으로 보다 더 중요한 기술도 있을 수 있다. 그래도 역시 위의 기술들이 뼈대를 이루는 것들이다. 그리고 그 중에서도 가장 중요하다고 생각되는 기술들은 볼륨 처리를 해 두었다. 저 중에서 볼륨 처리된 기술에 대해서 간략히 이야기해 볼까 한다.


객체지향(OOP, Object Oriented Programming)

현대 소프트웨어 개발에 있어서 가장 중요한 되는 개념이라고 생각하면 된다. 스크립트 언어나 함수형 언어를 접하게 되더라도, 그리고 구조적 언어를 통해 개발을 하게 되더라도 객체지향은 꼭 알고 지나가야 하는 개념이다. 트리에서도 보듯이 프로그래밍 언어의 기초 문법을 익히고 나서 소프트웨어를 구조적으로 작성하기 위해 배우는 첫번째 단계이며 이후 필요한 소프트웨어 기술들의 모태가 되는 기술이다. 즉, 객체지향을 모르고는 어떤 소프트웨어적인 개념도 제대로 이해하기 힘들고, 객체지향을 모르는 사람을 소프트웨어 개발자라고 말하기 어렵다.

불완전성의 관리 관점에서 보면 객체지향은 갈수록 대형화 되어 가는 소프트웨어를 작은 단위로 축소시켜 주는 역할을 한다. 하위 타입에 대한 은폐를 통해서 작성해야 할 코드의 양을 줄이면서도 수정 및 확장이 용이한 소프트웨어 구조를 만들어 준다. 상속을 통해서는 중복된 코드가 발생하는 것을 막아주고, 인터페이스와 타입의 개념을 통해서는 내부 구현에 대한 은폐를 가능하게 해준다. 변수 대신 객체를 바꿈으로써 조건문/제어문을 사용하는 대신 직접 행위를 변경할 수 있게 한다.

객체지향이 소프트웨어 영역에 가져온 영향력은 막대하다. 사실상 소프트웨어에 설계의 개념이 도입된 것이나 설계의 원칙이 도입되게 된 것, 올바르고 좋은 설계의 패턴, 소프트웨어의 가시화(UML) 등 거의 모든 소프트웨어 기술은 객체지향을 이용하거나 객체지향에서 파생된 것, 또는 객체지향을 개선한 것들이다. 현대의 대부분의 언어들은 객체지향을 온전히, 혹은 적어도 부분적으로 지원한다.


UML(Unified Modeling Language)

UML이 있기 전까지 소프트웨어는 비 가시적인 기술 영역이었다. 인간이 눈으로 얼마나 많은 양의 정보를 얻는지를 안다면 이것은 치명적인 문제였다. UML이 없었던 시절, 소프트웨어를 여럿이서 함께 개발한다는 것이 무척 어려웠을 것이다. 인간의 언어는 코드보다 부정확하다. 코드는 완벽하게 진실만을 이야기 하지만 구조를 이해하지 못한 상태에서의 코드는 줄거리를 모르는 대서사시처럼 장황하다. 인간의 언어로 대화하다가 서로 막히는 곳이 있으면 그 대서사시를 살펴봐야 한다. 이 와중에 일부 개발자들은 자신의 코드를 신성시 한다. 아마도 UML이 없던 시절에 소프트웨어를 바라보는 다른 엔지니어들의 시선은 그리 좋지 못했을 것이다. 소프트웨어 개발자 간에도 의사 소통이 신통치 않았을텐데 다른 분야의 사람들과 원활히 대화하기는 더욱 어려웠을 것이다.

사람들이 소프트웨어를 (자기 나름대로의 방법으로) 가시화 하기 시작했을 때에도 그 가시적인 도안들을 통한 커뮤니케이션이 원활하지 않았다. 작은 그룹에서는 통용될지 몰라도 의사소통의 단위가 커지면 가시화의 방식이 달라 서로 이해하기 어려웠다. 

UML은 이런 가시적인 툴로서는 최초로 보편적인 표시 언어로 사용된 것이다. 개발자들은 UML을 통해 비로소 서로의 코드를 보지 않아도 소프트웨어의 구조를 이해하게 되었고, 코드를 먼저 만들지 않고도 구현을 이야기 할 수 있게 되었다. 

아직까지는 코드와 유사한 수준의 소프트웨어 이해를 가능하게 하는 언어는 UML이 유일하다. 


디자인 패턴

디자인 패턴이 탄생한 후부터 개발자들은 좋은 설계를 인간의 언어로 말할 수 있게 되었다고 할 수 있다. 아기로 비유하자면 이제 막 첫 마디 단어를 말하는 그 시점만큼 극적인 일이다. 디자인 패턴이 있기 전에는 어떤 설계가 다른 설계보다 어떻게 나은지를 설명하기 위해 코드를 작성하거나 UML을 그리거나 자신이 하려고 하는 일에 대해서 상대방에게 인간의 언어로 수 십 분에 걸쳐 이야기 해야 했다. 디자인 패턴이라는 것이 개발자들이 설계 문제를 해결하던 여러 방법들에 이름을 붙여 놓은 것이기 때문에, 설계에 대해 한참 이야기를 하다 보면 서로 같은 이야기를 하고 있었다는 것을 알게 되었을 것이다. 디자인 패턴은 이런 "같은 이야기"들에 이름을 붙였다. 그 이후부터는 같은 이야기를 지루하게 반복하는 일이 없어졌다.

사람들이 잘 된 설계에 대해 이름을 붙이기 시작하면서 대화는 짧아지고 정밀한 설계에 대해 집중할 수 있게 되었다. 그러면서 다른 디자인 패턴들도 많이 생겨나게 되었고, 대화는 더욱 풍성해졌다. 같은 설계 문제에 대해 어떤 패턴을 적용하는 것이 더 나은 설계인지를 이야기할 수 있게 되었다. 

디자인 패턴을 모르고는 설계를 이야기 할 수 없다.


Unit Test(단위 테스트)

단위 테스트는 소프트웨어의 안전망이다.

단위 테스트 이전의 소프트웨어는 주로 정밀한 설계를 통한 구현 상에서의 오류 감소, 그리고 통합 테스트를 통한 디버깅이 불안전성 제거를 위한 거의 유일한 방법이었다. 이 방법을 제외하고는 인간의 두뇌가 유일한 불안정성 관리 도구였다. 불안전성의 원리 때문에 직접적으로 소프트웨어의 완전성을 증명할 수 없지만 유닛 테스트는 간접적인 방법으로 안전망을 구축해준다.

유닛 테스트의 유용성을 이야기 해보면 다음과 같다. 

우선 직접 작성하지 않은 소스에 유닛 테스트가 있을 경우, 소스의 의도를 파악하는데 도움이 된다. 필요한 경우에는 리팩토링을 통해서 소스를 더욱 잘 이해할 수도 있고, 설계를 바꿈으로써 소스의 흐름을 더 원활하게 가져갈 수도 있다. 

유닛 테스트는 구현에서 발생한 버그를 테스트 단계에서 발견하게 됨으로써 생기는 디버깅의 어려움을 감소시켜 준다. 버그는 발생한 시점에 발견하여 즉각 수정하는 것이 손쉬운데 이는 버그가 발생한 시점이 코딩 시점과 가까울수록 해당 버그의 문제점을 짚어 내기가 용이하기 때문이다.(사실 이 부분은 불완전성 관리의 도구가 오직 두뇌임을 명시적으로 보여주는 대목이다) 그런데 프로젝트가 커지면 커질수록 전통적인 개발 프로세스에서는 구현과 테스트 간의 간격이 더 벌어졌다. 대형 프로젝트일수록 더 정밀한 관리가 필요하고 더 나은 방식으로 문제점을 해결해야 함에도 전통적인 프로세스는 이 문제를 더 키우기만 할 뿐이었다. 유닛테스트가 생겨남으로써 일시적인 버그는 즉시 판단하고 제거할 수 있게 되었다.

유닛 테스트의 또 다른 이점은 설계에 준하는 수준의 소프트웨어 동작 지침을 제공한다는 것이다. 이는 TDD(Test Driven Development)가 추구하는 방향인데, 테스트 코드를 구현 코드보다 먼저 작성함으로써 구현 코드가 작성되어야 할 방향을 정해주는 것이다. 이로써 설계 단계에서 미비했거나 요구사항의 불확실성 때문에 완벽하지 못했던 설계를 유닛 테스트를 통해 보충해 줄 수 있다.


리팩토링

현대의 소프트웨어는 늘 수정된다는 특성이 있다. 그래서 요즘에는 완벽한 설계보다는 실행 가능하고 수정 가능한 설계를 추구하는 경향이 있다. 이에 따라 별다른 수정 사항이 없어도 구현 중에 일부 설계가 부적절한 것을 발견하게 되는 경우도 있고, 초기에는 잘 된 설계임에도 불구하고 기능적인 수정이 늘어나면서 설계의 효율이 떨어지는 경우도 있다. 이렇게 효율이 떨어진 설계를 널리 잘 알려진 좋은 설계, 즉 디자인 패턴을 중심으로 좋은 설계로 바꾸어 나가는 작업을 리팩토링이라고 한다.

이 과정은 근본적으로는 설계의 변경이지만, 이미 만들어진 기능에 대해 수행하는 작업이므로 실질적으로는 잘 동작하고 있는 코드를 수정하여 설계 맞추는 작업이라고 할 수 있다. 이 과정에서는 잘 동작하는 코드가 수정 중에 버그가 발생하지 않도록 안전장치를 해 둘 필요가 있다. 이 역할을 하는 것이 유닛 테스트이다. 리팩토링 과정은 어떤 경우에는 별다른 어려움 없이 끝날 수도 있지만 어떤 경우에는 상당한 시간 동안 진행 될 때도 있다. 이 때 리팩토링의 각 단계에서 기존 기능과 동일하게 동작함을 확인시켜주는 유닛 테스트는 필수적이다.

리팩토링은 디자인 패턴이 나온 이후에 생겨난 것이고, 유닛 테스트를 통해서 그 안정성을 보장 받게 되었다고 볼 수 있다. 또한 구현 이후에 설계를 변경한다는 점에서 정통의 소프트웨어 개발 프로세스와는 상반된 개념이기도 하다. 소프트웨어 분야는 아직도 한창 발전하고 있는 분야이기 때문에 혁신적인 사고가 언제든 기존의 사고를 제치고 자리 잡을 수 있다. 설계를 반영하여 코드를 작성하고, 이미 작성된 코드를 수정하고, 수정된 코드에 맞춰 설계를 변경하는 일련의 과정은 소프트웨어가 가진 유연성이라는 장점을 가장 잘 드러내는 과정이라고 볼 수 있다. 리팩토링은 개발자가 설계와 코드 안에서 자유로워 질 수 있음을 보여주는 기술이라 할 수 있다.


Agile

Agile은 전통적인 소프트웨어 개발 방법론의 단점을 보완하기 위해 생겨난 개발 방법론이다. 전통적인 개발 방법론은 철저한 요구사항 수집 및 분석, 이를 바탕으로 한 세밀한 설계, 설계에 딱 맞는 구현, 설계-구현에서의 부족한 점을 테스트를 통해 보완하는 구조로 되어 있다. 이는 개발 방법론이 정립되지 않았던 시기 보다는 나은 결과물을 내줄 수는 있었지만 현대의 소프트웨어 분야의 트렌드와는 잘 맞지 않는다. 현대에는 개발 시작 시점에 요구사항이 완벽한 경우가 별로 없고(거의 없다), 시장의 요구 변화에 맞춰 개발 진행 중에 상당 부분 변경이 이루어진다. 개발 중간에 수많은 요구사항들이 새로 생겨나고 없어지거나 수정된다. 또한 개발이 완료되었다고 해도 지속적인 수정 요청이 발생하기도 한다. 이러한 요구사항 변화를 기존 프로세스 상에 반영하는 것은 거의 불가능에 가깝다.

Agile은 현대 소프트웨어 개발 과정의 특성을 반영하고자 하는 프로세스이다. 시장은 항상 변하고, 이에 따라 요구사항은 항상 변한다. 시간이 지날수록 사용자의 요구사항은 더 많아지게 된다. Agile에서는 이러한 요구사항을 수용하기 위해서 요구사항들을 중요도, 개발 기간, 구체화 정도 등의 요소를 통해 순위를 매기고 이들 중 일부를 가지고 개발에 착수한다. 따라서 전체 요구사항을 모두 수집하는 방식에 비해 요구사항 분석이 짧다. 또한 요구사항의 개수가 적으므로 각 단계별 수행 시간도 짧아지게 된다. 이를 통해 프로세스의 기간을 단축시킬 수 있다.

이런 방식으로 1차 개발을 완료한 후 남아 있거나 새로 추가된 요구사항, 수정된 요구사항들을 모아 다시 같은 과정을 반복한다. 그리고 이 과정에서 소프트웨어 결과물은 항상 동작 가능한 상태를 유지한다.

Agile은 구현에서 테스트로 넘어가는 기간을 단축시켜 디버깅이 용이하게 해준다. 짧고 반복적인 개발을 통해서 전체 프로세스의 종료 시간을 예측하는데 도움을 준다. 새로운 요구사항이 나올 경우 다음번 주기에 바로 반영시킬 수 있으므로 고객 피드백이 빨라진다.

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
,