TDA 원칙이라고도 불린다.

우리 말로 번역해 보자면 "물어보지 말고 그냥 시켜라"가 될 수 있다.

이는 객체와 객체가 협력하는 경우, 다른 객체에게 정보를 요구하지 말고 그냥 행위하도록 시키라는 의미이다. 즉 정보 은닉의 중요성을 강조하는 원칙이라고 할 수 있겠다.


정보를 처리하는 소프트웨어 구현의 경제성 관점에서 보면 이렇다.

소프트웨어의 복잡성은 다루어야 할 정보의 양에 영향을 받는다. 다루어야 할 정보가 많아지면 더 많은 정보를 가공해야 하고, 정보의 값에 의한 제어 변경(보통 상태라고 부른다)이 더 자주 발생하게 된다. 

하지만 꼭 정보의 양만 복잡도를 가늠하는 척도는 아니다. 정보를 처리하는 단계가 짧고, 정보를 다루어야 할 객체가 적고, 정보에 대한 처리 과정을 중복되지 않고 간결하게 처리한다면 같은 데이터를 처리하더라도 훨씬 단순한 소프트웨어를 만들 수 있을 것이다. 이 과정에서 TDA 원칙의 중요성이 떠오르게 된다. 정보를 입수했을 때 그 정보를 한정적인 범위 내에서만 다루도록 하고(예를 들어 단일 객체), 혹시 외부에서 그 정보에 기반하여 동작을 수행해야 할 경우에는 정보를 가지고 있는 쪽에 동작을 요청하도록 하면 넓은 범위에서 데이터를 입수하여 처리하는 방식에 비해서 훨씬 복잡도가 줄어들게 될 것이다.


아래 그림을 살펴보자.




이 그림에서 데이터는 최초로 객체1에 전달된다. 그림 상에서는 객체1이 받은 데이터를 객체2와 객체3에 주고, 객체3은 이를 다시 객체 4에 준다. 이렇게 데이터가 전달되는 방식은 크게 두 가지가 있다.

1. getter를 통해 데이터를 요청하는 경우.

2. 다른 객체의 API에 데이터를 인자로 넣게 되어 있는 경우.


Tell, don't ask라는 것은 1번에 해당하는 말이다. 즉, 데이터를 getter로 요청하지 말 것을 의미한다. 하지만 데이터를 전파하는 방법은 2도 해당하므로 이 두가지 경우가 발생하지 않도록 설계해야 한다.

자, 그림 상에서 1번이든 2번이든 어떤 방식으로든 데이터를 전달하도록 설계했다고 하자. 그러면 저 그림의 모든 객체들은 데이터의 값에 영향을 받게 된다. 그것이 조건문으로 나타나든, 변수로만 나타나든 어떻게든 코드 상에 모습을 드러내게 된다. 이것은 다음과 같은 문제들을 발생시킨다.


코드가 복잡해진다

데이터를 가지고 있으면 데이터를 핸들링 해야 한다. 핸들링하는 코드는 단순히 전달하거나 저장하는데만 그치는 경우도 있고, 데이터 값의 범위에 따라서 조건문이나 제어문이 필요한 경우도 있다. 어떤 식으로든 코드가 늘어나면 문제가 생기는 것은 당연하다.


데이터의 변경에 다수의 객체가 영향을 받는다

일단 데이터를 가지고 있으면 더이상 쓸모없는 데이터여서 지우거나, 새로운 데이터가 더해지거나 데이터의 타입이 변경되는 등의 여러가지 변경사항에 영향을 받게 된다. 이는 OCP(Open Close Principle) 원칙을 위배하게 된다.


데이터의 무결성을 지키기 어렵다 

각 객체들이 가지고 있는 데이터 값이 시간에 따라서 달라짐으로써 관리가 어려워지게 된다. 특히 멀티 쓰레드 환경에서 여러 곳에 데이터가 흩어져 있으면 데이터의 무결성을 지키기는 더더욱 어렵고 복잡해진다.


중복 코드가 발생할 가능성이 높다

한가지 데이터는 보통 소프트웨어 전체에서 한가지 용도로 사용된다. 따라서 하나의 데이터를 다루는 코드들은 유사성이 매우 높다. 이 코드들은 애초에 한번만 작성되도록 만들어져야 하는데 데이터가 여러 객체로 전달되고 나면 중복 코드가 발생하는 것은 거의 필연적이다.


그렇다면 어떻게 이 문제를 해결할 수 있는지 한번 살펴보자.


간단히 이야기 하자면, 데이터가 입력된 이후에는 데이터를 핸들링하는 객체를 별도로 생성하여 관리하면 된다. 위 그림에서는 데이터 객체가 이에 해당한다. 객체1은 데이터를 받아서 데이터 객체를 생성하거나 데이터 객체에 전달해 주는 것으로 자기 일을 마친다. 그리고 기존에 데이터를 가지고 다루던 객체들은 모두 데이터 객체에게 일을 시키는( tell() ) 형태로 설계를 변경한다. 그러면 데이터와 관련된 모든 일은 데이터 객체가 수행하게 되면서 다른 객체들이 데이터에 의존하는 것을 막을 수 있다.

이를 단계별로 설명해보면 다음과 같다.


데이터를 수신(생성) 단계

소프트웨어의 어떤 부분이든 데이터를 수신하거나 생성해 낸 곳이 있기 마련이다. 데이터는 발생 시점부터가 중요하다. 데이터를 최초로 수신한 객체는 일단 다른 곳으로 데이터를 전파시킬 수 없어야 한다. getter를 통해서 다른 객체들이 데이터를 가지고 가게 하거나, 다른 객체의 매개변수로 데이터를 전송해서는 안된다.


데이터 객체 생성 단계

데이터를 처리할 객체를 생성한다. 데이터는 생성된 이후에는 오직 이 객체에게만 전달된다. 데이터를 처리하는 방식은 두가지가 있다.

1. 데이터 처리를 전담하는 객체가 있다. 이 경우라면 최초로 데이터를 수신한 객체는 처리 객체에 데이터를 넘겨주기만 하면 된다.

dataProcessor.receiveData(data);

2. 데이터 처리를 전담하는 객체가 없고, 여러 객체들이 데이터에 대한 의존성을 가지고 있는 경우가 있다. 이런 경우에는 데이터 객체를 (필요시) 생성하고, 이 데이터 객체를 다른 객체에 전달하여 준다. 이 객체는 데이터가 변경되었을 때 다른 객체를 어떻게 변경시켜야 하는지를 알고 있다. 이렇게 데이터를 객체화 하여 전달하는 것은 State 패턴이나 Strategy 패턴과 유사한 모양이 된다.

// 데이터 객체가 생성된다

Data dataObject = new Data(data);

// 처리를 위해 다른 객체에 전달된다.

object.receiveData(dataObject);


데이터 처리 단계

데이터 객체 생성 단계에서 방법이 두가지가 있듯이 처리 방식도 두가지이다.

1. 데이터 처리 전담 객체의 경우에는 다른 객체들이 수시로 데이터 갱신이 이루어 졌는지를 데이터 처리 전담 객체에게 물어보는 방식이 있고, 다른 객체들이 데이터 처리 객체에 자신을 이벤트 리시버로 등록하는 경우가 있다. 후자는 Observer 패턴과 유사하다.

2. 데이터 객체가 생성되서 다른 객체로 전송되어 오면 각 객체들은 이 데이터 객체를 이용하여 변경된 데이터에 의한 동작을 수행해야 한다. 이 때 각 객체들은 데이터를 모르기 때문에 직접 자기 자신의 행위를 변경할 수는 없다. 따라서 데이터 객체에게 자신의 상태를 변경해 달라고 요청해야 한다. 따라서 이를 실행하면 아래와 같은 형식의 코드가 된다.

// 데이터 객체를 수신한 쪽 : Object2라고 가정했을 때

dataObject.doubleDispatch(this);


// 데이터 객체 쪽

public void doubleDispatch(Object2 object){

    object.doSomething();

}

이러한 방식을 켄트 벡의 구현 패턴에서는 "더블 디스패치"라고 한다. 데이터를 알고 있는 쪽에서 데이터에 종속적으로 동작하는 객체를 넘겨 받아서 자기가 알고 있는 데이터를 기반으로 넘겨 받은 객체의 행위를 호출하는 것이다. 이렇게 하면 데이터를 넘겨주지 않고도 데이터가 넘어 갔을 때 일어나야 하는 행위를 호출할 수 있게 된다.


"정보"의 전달을 금지하는 원칙

기본적으로 정보 은닉(information hiding)은 단순히 캡슐화만을 의미하는 것이 아니다. 정보 은닉(information hiding)에 대한 올바른 이해에서도 이야기 했듯이 생성된 객체의 구체적인 타입을 숨기는 것이나 구현을 숨기는 것도 정보 은닉에 해당된다. 또한 아무리 캡슐화를 잘 했다고 해도 getter를 통해서 데이터를 전달하거나 매개변수로 데이터를 다른 객체에 넘겨버리면 기껏 정보 은닉을 강조한 보람이 없어진다.

정보 은닉은 데이터의 종류를 막론하고 데이터 처리를 수행하는 전담 객체가 아니면 어떠한 객체도 데이터를 전달해주지 않아야 한다는 원칙으로 해석해야 한다. 이 정보에는 외부에서 받은 데이터도 포함되지만 생성한 객체의 구체적인 타입이나 구현부와 같이 프로그래밍 요소의 정보도 포함이 된다. 그리고 이런 정보들은 생성과 동시에 은닉 됨으로써 정보에 의존하는 코드들의 생성을 막아야 한다. 이것이 Tell, don't ask 원칙과 정보 은닉 원칙이 추구하는 방향이다.

Posted by 이세영2
,

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
,