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
,

인간의 두뇌에는 두가지 능력이 있다.

하나는 살아남기 위해서 사실을 객관적으로 기억하고 바라볼 수 있는 능력. 또 하나는 살아남기 위해서 자기의 생각과 행동을 스스로 합리화 하여 살아남을 가치가 있음을 스스로에게 납득시킬 수 있는 능력이다.

인간은 살아 남기 위해서 객관적인 세계로부터의 정보를 정확하게 해석하고 판단하고 기억하는 능력을 발달시켜 왔다. 이것이 인간이 과학을 만들어내고 세계를 향한 지적 탐구를 할 수 있게 된 배경이다. 이 능력은 순수한 지적 탐구로부터 시작하여 새로운 사실을 발견하고, 이를 증명할 수 있는 방법을 고안해 내고, 실험을 통해 검증해 내는 일련의 과정들을 거치면서 객관적인 지식들을 만들어 낸다. 이러한 과정들을 지켜보면 인간은 매우 합리적인 동물이라고 생각할 수 있다.

인간이 생존을 위해 기억하고 쌓아온 지식들을 생각해 보면 인간의 합리성은 당연한 것으로 보인다. 인간이 채집을 하고 농사를 짓고 가축을 기르면서 인류 스스로의 생존을 위해 기억해야 할 일들은 무수하게 많다. 그리고 그 기억들이 조금이라도 불합리하다면 생존은 매우 크게 위태로워진다. 

제레미 다이아몬드는 그의 저서 "총, 균, 쇠"에서 이런 일화를 전하고 있다. 파푸아 뉴기니 원주민들과 함께 국경을 넘어가려고 기다리던 때에 에피소드로 기억한다. 국경을 넘어가는데 시간이 지체될 것이 예상되자 원주민들은 먹을 것을 구해 오겠다고 하면서 주변을 뒤지기 시작했다. 그리고 한동안 시간이 흐른 후 원주민들은 두 손으로 꼽을 수 있을 정도로 많은 종류의 버섯을 채취해 온 후 이를 요리하기 시작했다. 다이아몬드는 그 모습을 보고 혹시라도 버섯에 독이 있을지 모른다고 생각하여 먹지 않겠다고 말했다. 그러자 원주민은 화를 내면서 "버섯에 독이 있는지 모르는지를 모르는 바보 같은 인간들은 미국인 밖에 없다."라고 말하고는 먹을 수 있는 버섯의 종류에 대해서 이야기 하기 시작했는데 총 27가지 식용 버섯의 종류와 모양, 그리고 어디에서 주로 채집할 수 있는지 등을 설명해 주었다.

또 같은 책에서 다른 에피소드가 나온다. 농경이 시작될 무렵, 그러니까 일반적으로 채집을 주로 하면서 이제 막 정착이 시작될 무렵으로 추정되는 집단의 주거지가 발굴되어 조사를 한 내용이 있었다. 이 주거지에서는 야생 곡물들의 종자를 가져와 심어 본 흔적들, 즉 농경을 할 수 있는 가능성을 찾아본 흔적들이 있었는데, 그 주거지에서 발견된 곡물의 종류만 100가지 정도가 되었다고 한다.(기억을 더듬어 적느라 정확한 숫자인지는 잘 모르겠다.)

어쨌든 인간은 이렇게 생존을 위해서 다양한 방법으로 자연을 시험하는 법을 일찍이 터득하고 있었다. 당연히 이들은 실험해 본 대상들의 특성들을 열심히 조사했을 것이다. 씨앗의 크기는 충분한지, 심어진 양 대비 산출량은 적절한지, 식용으로 사용하기까지 필요한 작업들은 어느 정도인지, 추수에는 어려움이 없는지, 그리고 적절히 저장하여 두었다가 다시 심어도 발아에 문제가 없는지 등을 조사했다. 이런 방법은 도구나 대상이 다른 점을 제외하곤 현대의 과학자들이 하는 일이나 크게 차이가 없어 보인다.

이런 관점에서 보면 인간은 당연히 합리적인 이성이 모든 정신을 지배하는 존재여야 할 것처럼 보인다.


하지만 안타깝게도 인간은 그렇지 않다. 인간은 그와는 정 반대의 특성을 가지고 있다. 그리고 이것 역시 생존을 위해서는 꼭 필요한 것이기 때문에 정말로 아이러니한 특성이 아닐 수 없다. 인간은 매우 이성적인 사고를 발달 시킨 것과 같이, 그리고 그 사고가 생존을 위해 꼭 필요했던 것과 같이 어떤 본능 하나를 생존을 위해 키워 나갔다. 그것이 바로 "나는 꼭 존재해야 만 한다"는 비 이성적인 전제, 즉 생존 본능이다.

생존 본능이 어떻게 발달되었는지는 이성적으로 추적하기는 힘들다. 다만 이것은 다른 동물들에게도 매우 강하게 발현되는 것이기 때문에 그 본류를 찾아보기에는 어렵지 않다. 그리고 인간처럼 일반적으로는 생존에 그다지 적합하지 않은 여러 조건들(유아기가 너무 길다든지, 체력이 다른 동물들보다 약하고, 강력한 무기가 될만한 신체 조건이 갖춰져 있지 않다든지 하는 것 들)을 생각해 보면 다른 동물들에 비해 이 생존 본능이 더욱 강하지 않으면 안 될 것이라고 판단된다. 다이아몬드의 책에 다시 넘어가보면 떠돌이 채집 생활을 하는 종족들은 적절하지 않은 시기에 아이를 낳게 되었을 때, 즉 이미 낳은 아이가 아직 어려서 혼자 걷지 못하는 시기에 다음번 아이가 태어났을 경우 어쩔 수 없이 살해해야 하는 경우가 있었다고 한다.

또한 다른 동물들과 달리 유독 자기 생존을 위해서 지금 당장은 필요하지 않고, 아무리 생각을 해봐도 평생 스스로에게 도움을 줄 것 같지 않을 정도의 재산이나 먹을 거리를 쌓아두려고 노력하는 것을 보면 생존 본능에 있어서는 인간이 단연 모든 동물 중에서 으뜸이 아닐까 생각한다.


인간이 왜 비합리적이 되는가? 하는 질문에 대한 답은 매우 합리적으로 사고 하는 부분과 "나는 꼭 존재해야 만 한다"는 본능이 만나는 지점에 있다. 세계를 객관적으로 봐야 하는 두뇌의 대부분은 매우 이성적이다. 세계는 내가 원한다고 원하는 대로 되는 것이 아니다. 사실이라는 것은 내가 부정한다고 해서 거짓으로 바뀌는 성질의 것이 아니다. 따라서 사실은 온전히 사실로만 받아 들여져야 한다. 그리고 이것이 긍정적인 생존 본능을 자극하는 경우, 즉 세계를 객관적으로 바라보고 오직 사실만을 인정하고 받아들이는 것이 나의 생존에 도움이 되는 경우에 인간은 매우 이성적이고 합리적인 인간이 된다.

예를 들어 내가 응용 물리학자라고 하자. 그러면 내가 알고 있는 물리적 지식이 실제 어떤 장치로 만들어 질 수 있어야 한다. 이 장치를 만들어 내는 과정에는 당연히 시간과 열정을 투자해야 한다. 하지만 만일 내가 알고 있는 객관적인 사실, 즉 물리학적인 지식이 잘못되었다면 나는 어떠한 노력을 들여도 성공하지 못할 것이다. 만약 내가 개인적인 사고 편향을 가지고 있어서 상대성 이론은 받아 들여도 양자 역학은 죽어도 못받아 들이겠다면(아인슈타인처럼) 내가 양자 역학에 기초한 장치를 만들어 내는 것은 불가능한 일이다. 즉 정말로 객관적인 사실을 그대로 받아 들이지 않으면 응용 물리학자로서의 내 삶은 매우 고달퍼질 것이다. 이 예에서의 인간의 합리성과 생존 본능은 그대로 이성적인 상태로 유지될 수 있다.


하지만 인간의 합리성과 생존 본능의 결합은 항상 이런 식으로 이루어지지 않는다. 인간의 합리성은 보통 충분히 성숙된 나이에 자리를 잡는다. 인간이 세상을 충분히 알고 이를 객관적으로 분석할 수 있는 시기가 될 때까지 두뇌는 이성적인 사고에 지배 받기 보다는 생존 본능에 더 크게 지배를 받는다. 이 시기에 만약 생존 본능을 크게 자극 받는 일들이 벌어진다면 어떻게 될까?

예를 들어 어린 시절 학대를 받았다든지, 살고 있는 사회가 비 이성적인 행위나 삶을 강요한다든지, 불건전한 사상이나 종교에 물든 어른들 틈에서 자란다든지 하는 상황이 되면 이성적인 사고의 영역은 제대로 발달하지 못하고 그 자리를 생존을 위한 본능이 자리잡게 된다. 하지만 이 부분은 근본적으로 합리의 영역이기 때문에 역시 합리적인 형태로 나타나게 되는데 이것을 보통 생존을 위한 합리성 영역, 즉 실제로는 합리적이지 않지만 생존을 위해서는 이것이 꼭 필요하고, 자신이 생존할 가치가 있음을 지속적으로 찾아 내려는 합리적인 노력의 영역으로 자리 잡게 된다. 이것이 자기 합리화이다.

이 자기 합리화 과정을 좀 더 고찰해보면 다음과 같다. 우선 생존을 위협하는 주변 인자들이 있다. 안타까운 가정이지만 생존을 위협받는 사람을 생각해 보자. 이것은 이 상황을 겪고 있는 사람에게는 지극히 객관적인 사실이다. 그리고 그 머리 속에는 지속적으로 "내가 생존할 만한 가치가 있는 존재"라는 생존 본능의 목소리가 들려 온다. 결국 생존 본능은 합리성 영역과 만나게 된다. 일반적인 경우라면 이 합리성 영역은 외부 세계의 객관적인 사실에 대한 해석을 하는 영역이 되어야 하지만 이 사람은 자꾸 생존 본능을 자극 받고 있기 때문에 외부 세계에 대한 해석을 본능적 해석으로 바꾸게 된다. 이 해석은 일반적인 사람의 해석과 달라지기 때문에 매우 비 상식적인 형태로 바뀐다. 어떤 사람은 "자신이 학대 받아 당연한 사람"이라고 생각하게 되거나, "모든 사람들이 학대 받는다"고 생각할 수 있다. 양상은 다양하지만 그런 생각이 객관적이고 이성적인 것은 아니라는 것은 확실하다.

그런데 문제는 이러한 자기 합리화 과정이 비단 이런 극단적인 상황에서만 발생하는 것이 아니라는데 있다. 아직도 수많은 사람들이 비 이성적인 사상이나 종교의 영향 아래 있다. 이들은 이미 이 사상이나 종교 아래에서 자기 생존을 합리화하는 과정을 거쳤기 때문에 스스로 비 이성적인 행동을 한다고 생각하지 않는다. 하지만 종교에 의한 전쟁은 수세기동안 계속되어온 문제이다.


그러면 이 자기 합리화 문제가 우리 사회에는 없을까? 이 문제에 답하기 위해서는 어떤 생각이 자기 합리화된 생각인지 아닌지를 어떤 기준으로 판단할 수 있을까를 먼저 생각해 봐야 한다. 이것은 지극히 간단한 일이다. 나의 행동이나 생각이 다른 사람의 생존을 위협해서는 안된다는 것이다. 누구나 객관적으로 합리성을 발휘하기 위해서는 서로의 생존을 위협해서는 안된다. 다른 사람들의 생존을 위협하기 시작하는 순간 위에서 이야기 했던 생존 본능을 자극하는 합리화의 기제가 동작하기 시작한다. 그리고 이런 사람들이 늘어나면 늘어날수록 사회는 비 이성적인 사회로 변화하게 된다.


인간의 합리성과 생존 본능에 대해 이해하는 것이 우리가 살고 있는 사회가 정상적인지 아닌지를 판단하는 기준이 된다. 그리고 모든 사람들은 스스로의 생각이 다른 사람의 생존 본능을 자극하는 것은 아닌지, 즉 자기 스스로 자기 합리화에 빠져 있어서 세계를 객관적으로 바라보지 못하고 있지는 않은지를 끊임없이 되물어야 한다. 일단 생존 본능이 자리잡은 이성은 쉽게 치유되지 않는다. 그리고 자기 정화적인 노력이 없이는 스스로 치유될 수도 없다. 내가 생존하고 있는 사회가 끊임없이 자기 정화를 요구하느냐 그렇지 않느냐도 사회를 판단하는 기준이 된다.

사람의 두뇌는 나이가 들수록 굳어간다. 하지만 이것도 절반만 사실이다. 주위 세계가 자기 정화를 강요하는 수준이 생존 본능을 자극할 수준이라면 어느 누구도 현재 자기의 상태에 안주할 수 없게 된다. 인간의 두뇌에 부자연스러운 일이긴 하지만 충분히 훈련하면 할 수 있는 일이다. 이런 훈련이 두뇌를 깨어 있게 만들고, 항상 세계를 지속적으로 진지하게 바라볼 수 있도록 만든다.

Posted by 이세영2
,

우리가 어떻게 해서 유연성을 확보할 수 있었는가?

그것은 추상화(Abstraction)에서부터 시작되었다. 추상화를 통해 우리는 여러 요구사항들 중에서 공통점을 찾고, 이 공통점에서 목표한 것과 관련 없는 것들을 제거하였다. 이를 기반으로 공통점을 캡슐화할 수 있었고, 이 캡슐화된 대상에 타입을 부여할 수 있었다.

이 추상화의 과정을 비유적으로 이야기 해보면 이렇다. 개별적인 것들은 다들 개성이 강하고 다른 듯 하지만 멀리서 보면 대개 비슷하다. 우리 두뇌는 이런 일들을 잘 해낸다. 소위 "패턴"은 이와 맥락을 같이 하는 단어이다. 디자인 패턴이든 건축 패턴이든 패턴이라는 것은 개별적인 시도가 가지는 공통된 맥락을 의미한다. 추상화란 다들 서로 다른 듯 보이는 것들이 내제하고 있는 일반적인 모습, 바로 "패턴"을 찾아내는 과정이다. 사실 세상에는 "패턴"으로 정의할 수 있는 것들이 무수하게 많다. 어느 분야의 대가라고 불리는 사람들은 그 분야에서 벌어지는 다양한 시도들이 어떤 "패턴"을 가지고 있는지를 이해하고 있다. 그들은 그러한 패턴들을 알고 있기 때문에 새로운 시도를 할 때에도 마치 이전에 경험했던 일을 하는 것처럼 쉽게 해낼 수 있다. 

당대에 유명한 사랑꾼으로 통했던 카이사르는 자신이 어떤 후보를 추천하기 위해 추천서를 썼던 것처럼, 이미 작성해 놓은 똑같은 내용의 연애 편지에 이름만 다르게 써서 여자들에게 보냈을지도 모를 일이다. 안타깝게도 카이사르의 연애 편지들은 모두 그 후계자인 아우구스투스가 없애버렸기 때문에 이제는 확인할 길이 없지만 말이다. 어쨌든 똑같은 연애 편지에는 대상자 이름이 적혀 있지는 않았을 것이다. 그래야 연애 편지가 유연성을 가질테니까 말이다. 이것을 좀 더 일반적으로 표현해 보자면 공통된 정보를 모아 놓되 구체적인 정보는 숨겼다는 말이다. 이것을 객체지향에서는 정보 은닉(Information Hiding)이라고 부른다.

진짜 객체지향은 정보 은닉에서부터 시작된다.

객체지향 언어를 통해서 얻고자 하는 것이 유연성(기능의 확장, 교체, 변경)이라면 정보 은닉은 그것을 가능하게 하는 전략이다. 객체, 상속, 캡슐화 등은 정보 은닉의 수단에 불과하다. 그리고 좋은 정보 은닉은 잘 된 추상화를 통해 얻어진다.

많은 개발자들이 객체지향에 들어서면서 캡슐화를 정보 은닉이라고 배운다. 몇몇 훌륭한 블로그들을 제외하고는 대부분의 블로그들이 정보 은닉 = 캡슐화로 설명하고 있다. 매우 안타까운 일이다. 정보 은닉을 캡슐화로만 알고 있으면 아직 객체지향 입구에도 못들어 온 것이다.

정보 은닉과 관련하여 인터넷을 검색해 본 결과, 정확하게 정보 은닉을 설명한 것은 아래 글 밖에 없었다.

http://egloos.zum.com/aeternum/v/1232020


정보 은닉의 정의

- 모든 객체지향 언어적 요소를 활용하여 객체에 대한 구체적인 정보를 노출시키지 않도록 하는 기법.


소프트웨어의 유연성을 확보하는 단 한가지 방법만 있다면 그것은 무엇일까? 그것은 "객체(또는 클래스) 간에 서로를 모르게 하는 것"이다. 어떤 객체가 다른 객체를 생성하든, 다른 객체의 메소드를 호출하든, 다른 객체가 가진 정보를 조회하든, 다른 객체의 타입을 참조하든, 어떤 행위라도 상관이 없이, 안하는 것이 가장 좋다. 두 객체(또는 클래스)가 서로를 모른다는 것은 서로의 코드에 상대 객체나 클래스에 대한 코드가 단 한 줄도 없다는 의미이다. 만약 두 객체간에 전혀 관계가 없다면 두 객체 중 어느 하나가 수정되거나 사라지더라도 다른 객체는 전혀 영향을 받지 않는다. 따라서 두 객체간에는 유연성이 확보된다. 이것은 매우 자명한 이치지만 이런 원칙을 전체 시스템에 확장시킬 수는 없다. 객체지향 언어에서 어떤 목적을 달성하기 위해서는 필연적으로 다른 객체와의 협력이 있어야 하기 때문이다. 이 필연성은 "어떤 객체도 섬이 아니다"라는 워드 커닝헴과 켄트 벡의 말로 대변된다. 객체지향 시스템에 참여하는 모든 객체들은 어떤 형태로든 관계들로 엮여 있다. 객체를 노드로 하고 관계를 엣지로 나타내면 단 한 덩어리의 연결 그래프가 되어야만 한다. 만약 어떤 객체가 다른 어떤 객체와도 관계를 갖지 않는다면 그 객체는 별도의 시스템이다.

일단 발생한 관계는 유연성을 발휘하지 못하게 만든다. A가 B에 책임을 위임한 경우라면 B의 수정은 A의 위임 목적을 해칠 수 있다. 원래 B로 부터 얻고자 했던 결과를 더 이상 얻을 수 없을지 모른다. 같은 관계에서 A의 수정은 B의 책임을 더욱 강화 시킬 수도 있고, 반대로 전혀 필요 없는 객체로 만들어 버릴 수도 있다. 일단 관계가 발생하면 언제라도 관계가 있는 객체에 수정을 발생 시킬 여지가 있다. 그리고 어떤 객체든 적어도 하나 이상의 다른 객체와 관계를 맺어야만 한다. 이것이 어쩔 수 없는 현실이라면, 즉 어떻게든 관계가 있을 수 밖에 없다면, 똑같은 관계라도 더 좋은 관계로 변경해야 한다. 그렇다면 어떤 관계가 좋은 관계일까?


1. 자주 변경될 가능성이 있는 것에는 의존하지 않는다.

2. 외부로 노출된 메소드를 최소한으로 줄인다. 노출된 메소드가 최소인 객체는 노출된 메소드가 많은 객체에 비해 메소드가 적게 호출되고, 이는 다른 객체와의 관계가 발생할 가능성을 줄인다.

3. 객체의 책임을 최소한으로 줄인다. 책임이 작은 객체는 다른 객체와의 관계가 작아진다. 책임이 작아진 객체는 또한 수정될 가능성이 줄어든다. 따라서 다른 객체에 수정의 영향을 줄 가능성도 줄어든다.


정보 은닉의 종류

- 객체의 구체적인 타입 은닉(= 상위 타입 캐스팅)

- 객체의 필드 및 메소드 은닉(= 캡슐화)

- 구현 은닉(= 인터페이스 및 추상 클래스 기반의 구현)


정보 은닉의 목적

- 코드가 구체적인 것들(타입, 메소드, 구현)에 의존하는 것을 막아줌으로써 객체 간의 구체적인 결합도를 약화시켜 기능의 교체나 변경이 쉽도록 함.

- 동일한 타입의 다른 구현 객체들을 교체함으로써 동적 기능 변경이 가능함.

- 연동할 구체적인 구현이 없는 상태에서도 (인터페이스 만으로) 정확한 연동 코드의 생성이 가능함.


자 그러면 본격적으로 구체적인 구현을 통해서 정보 은닉 방법과 이점을 살펴보도록 하자.


"생성부터 은닉질이냐!?"

그렇다. 객체의 생성시부터 정보 은닉을 해야 한다. 아래 코드를 보자.

class Rectangle{

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

}


public static void main(String[] args) {

    Rectangle rectangle = new Rectangle(); // --- 1

    rectangle.rectangle(); // Rectangle 클래스에 의존적인 코드

}

위의 코드에서 1과 같이 객체를 생성했다고 하자. 객체는 생성 이후에 rectangle이라는 Rectangle 클래스 변수로 참조된다. 따라서 Rectangle에 선언된 모든 메소드를 사용할 수 있게 된다. 따라서 rectangle.rectangle()의 호출이 가능해진다. 이것은 Rectangle라는 객체에 전적으로 의존하게 되는 코드이다.

만약 우리가 좀 더 생각해서 Rectangle과 유사한 기능, 즉 Circle을 구현하게 될지도 모르고, 이에 따라서 Rectangle을 대신해서 Circle을 사용하게 될지도 모른다고 하자. 그래서 Rectangle과 Circle을 모두 지칭할 수 있는 상위 클래스인 Shape을 만들고, 각각의 모양을 그릴 수 있는 메소드(draw() 메소드)를 구현하도록 정의했다고 하자. 

그러면 아래의 2와 같은 코드를 만들 수 있다.

abstract class Shape{

    abstract public void draw();

}


class Rectangle extends Shape{

    public void draw(){ rectangle();}   

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

}


public static void main(String[] args) {


    Shape shape = new Rectangle();         // --- 2

    shape.draw();  // Shape 클래스에 의존적인 코드

}

코드 2는 동일하게 Rectangle을 생성했지만 곧바로 그 상위 타입인 Shape 클래스 참조 변수인 shape으로 객체를 참조한다. 따라서 이후에 shape 참조 변수를 통해 사용할 수 있는 메소드는 Shape 클래스의 메소드로만 제한된다. 그래서 생성 이후에는 Rectangle 클래스와 관련된 어떤 메소드도 호출되지 않는다. 이것이 구체적인 타입 은닉에 해당된다.

이를 통해서 얻을 수 있는 이점은 다음과 같다.

- Rectangle의 생성 코드 이후에는 어떤 코드도 Rectangle 클래스에 의존하지 않는다.

- 따라서 Rectangle 대신에 Circle을 사용하고 싶어졌을 때에는 Rectangle 대신 Circle을 생성하도록 변경하기만 하면 된다. 그러면 그 이후의 코드들은 전혀 수정될 필요가 없다.

- 만약 Rectangle을 사용하다가 Circle을 사용해야 할 경우도 발생할 수 있다. 바로 동적으로 기능을 교체해야 할 경우이다. 이 때에도 선언되어 있는 shape 참조 변수에 새로 생성한 Circle 객체만 참조로 할당해 주기면 하면 된다. 이런 방법으로 동적인 기능 전환도 쉽게 할 수 있다.


위의 코드에도 문제가 있다고 해서 다양한 디자인 패턴들이 생겨났다. Abstract Factory 패턴, Factory Method 패턴과 같은 경우가 바로 그것이다. 이들 생성 패턴들은 생성과 동시에 구체적인 타입 은닉을 수행하도록 되어 있다. 

- Abstract Factory 패턴

- Factory Method 패턴


아래는 각 생성 패턴들이 공통으로 추구하는 방향만을 간략하게 구현해 본 것이다.

class ShapeFactory{

    public Shape createRectangle(){ return new Rectangle(); }

    public Shape createCircle(){ return new Circle(); }

}


public static void main(String[] args) {

    ShapeFactory factory = new ShapeFactory();


    Shape shape = factory.createCircle();

    shape.draw();

}

위에서 구체적인 객체, 즉 Rectangle과 Circle 객체를 생성하는 책임을 담당하는 클래스가 ShapeFactory 클래스이다. 그리고 이 클래스는 객체의 생성과 함께 객체를 리턴하는데, 리턴하는 타입은 동일하게 Shape 타입으로 객체를 리턴한다.

이것이 어떤 효과를 가져 오는가? main() 메소드에서 ShapeFactory를 사용하게 되어 있는데, main() 메소드가 ShapeFactory의 createCircle() 메소드를 호출해서 객체를 받아 올 때 타입은 이미 Shape으로 변경되어 있다. 따라서 main() 메소드에서는 Rectangle이라는 클래스나 Circle이라는 클래스는 전혀 모르는 상태다. main()이 알고 있는 것은 오직 Shape 객체 뿐이다. 따라서 이 ShapeFactory를 이용해서 객체를 생성하면 생성된 이후의 모든 코드와 Rectangle 또는 Circle과는 전혀 무관한 코드가 된다. 오직 Shape만 이용하게 되기 때문이다.

그러면 어떤 장점이 있을까? 당연히 객체의 교체나 변경이 쉬워지게 된다. 또 다른 Shape 타입을 추가하는 것도 손쉬워진다. ShapeFactory를 거친 이후에는 모두 다 같은 Shape으로 취급될 것이기 때문이다.


캡슐화를 통한 정보 은닉

이 부분에 대해서는 다른 여러 블로그나 책들에서도 언급을 하고 있다. 하지만 상대적으로 덜 강조되고 있는 부분은 짚고 넘어가야 겠다.

일단 변수(필드)에 private 키워드를 이용해서 외부 노출을 줄이는 부분에 대해서는 어떤 책이든 강조를 하고 있다. 이를 통해서 필드를 외부에서 임의로 접근해서 발생할 수 있는 문제들을 없앨 수 있다.

메소드에 대해서는 상대적으로 강조가 적은 편인데 아래 예를 보면서 이야기를 해보자.

class Process{

    public void init(){}

    public void process(){}

    public void release(){}

}

위의 클래스는 public 메소드가 모두 3개이다. 즉, 외부에서 이 클래스의 객체를 사용하는 코드들에서는 모두 3개의 메소드에 의존하게 된다. 이는 혹시라도 Process 객체를 수정하거나, 아예 제거를 하는 등의 수정이 발생했을 때 3개의 메소드보다 적은 수의 메소드에 의존하는 코드들에 비해 수정이 더 많이 되어야 함을 의미한다.

또한 불필요하게 많은 수의 메소드를 노출시키면 여러가지 나쁜 면이 있다. 첫째로 메소드의 호출 순서를 제대로 알지 못해서 발생하는 문제점이 있을 수 있다. 메소드들이 서로 시간적 연관 관계가 있어서 순서대로 호출되어야 하는데 여러 메소드로 나누어져 있을 경우 이를 알지 못해 오류가 발생할 수 있다. 둘째로 구현의 구체적인 사항을 외부에 노출시킨다는 점이다. 이 Process의 세부 단계에 대해서 외부 객체들이 알게 됨으로써 구현을 유추할 수 있거나 유추해야만 하는 문제가 발생한다. 세번째로 어떤 메소드가 중요한지를 알 수 없게 된다. 적절한 수준에서 정보를 숨겨줌으로써 객체를 이해하는 입장에 도움을 주어야 하는데 모든 메소드가 노출되어 있으면 무엇이 중요한 메소드인지를 알 수 없다.

그럼 아래 코드를 보자.

class Process{

    private void init(){}

    private void process(){}

    private void release(){}

    public void work(){

        init();

        process();

        release();

    }

} 

위와 동일한 기능을 하지만 좀 더 나은 모습이다. 일단 이전에 보여졌던 메소드들이 모두 비공개(private) 메소드로 변경되었다. 따라서 외부 객체에서는 이들 메소드를 호출할 수가 없다. 대신에 외부에서 호출이 가능하도록 work() 메소드를 공개하고 있다. 따라서 외부 메소드들은 work() 메소드만을 이용할 수 있다.

이를 통해 얻는 장점은 다음과 같다. 우선 적절한 수준에서 메소드들이 공개와 비공개로 나누어져 있기 때문에 어떤 메소드를 우선 살펴야 할지를 알 수 있다. 또한 개별 메소드들의 호출 순서를 work() 메소드에서 정해주고 있기 때문에 Process 객체 사용에 대한 정보를 더 적게 알아도 된다. 마지막으로 work()라는 메소드만 노출 되었을 때에는 Process 객체가 하는 일의 세부 내용을 덜 노출시킨다. 즉, 외부에서는 Process 객체가 init - process - release 단계를 거친다는 점을 알 수 없다.


오직 인터페이스에만 의존하도록 한다

만일 객체를 잘 설계하여 변수를 private으로 선언하고, 꼭 필요한 메소드만 외부로 공개하였다고 하자. 그러면 외부 객체와 잘 설계된 객체간에 의존성은 오직 공개 메소드에 의해서만 발생하게 된다. 그래서 공개 메소드를 비공개 또는 보호 메소드들과는 구분하기 위해서 인터페이스라는 별도의 용어를 부여하게 되었다. 그만큼 공개 메소드가 개념적으로 중요하기 때문이다.

JAVA 언어에서는 이 개념을 더욱 강화하여 클래스와 유사하게 상속 가능한 타입이면서 구체적인 구현을 배제한 interface 라는 개념을 만들어 냈다. 예제에서는 interface라는 추상화 요소를 사용하게 될텐데 혹시 JAVA가 아닌 다른 객체지향 언어를 사용하고 있다면 interface를 "공개 추상 메소드만을 가지고 있는 추상 클래스" 정도로만 이해하면 된다.

앞서 이야기 했듯이 객체와 외부와의 소통은 오직 공개 메소드만으로 이루어진다. 그렇다면 어떤 객체가 공개 메소드의 모양, 즉 프로토타입(= 공개 추상 메소드)만 가지고 있는 상위 클래스를 상속 받았고, 오직 그 상위 클래스가 제공하는 공개 메소드만을 외부로 공개하였다면, 이 클래스는 상위 클래스로 지칭될 수 있다. 이 때 상위 클래스는 오직 공개 메소드를 선언하는 선언부 역할만을 하고, 하위 클래스는 이를 구체적으로 구현하는 역할만 가지게 된다.

이를 코드로 나타내 보면 아래와 같다.

interface Interface{

    public void method();

}

class ConcreteClass implements Interface{

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

}

Interface는 오직 공개 추상 메소드만을 정의하고 있다. ConcreteClass는 이 Interface가 제공하는 공개 추상 메소드를 구체적으로 구현하고 있다.

그러면 객체를 사용하는 입장에서는 어떠한가? 객체를 사용하는 입장에서는 ConcreteClass를 Interface 타입으로 지칭할 수 있게 된다. 그리고 모든 기능이 Interface가 정의한 공개 메소드를 통해 실행 가능하므로 ConcreteClass를 이용하는데 전혀 문제가 없다.

이처럼 인터페이스와 구현을 분리하면 다음과 같은 이점이 있다.

- Interface만으로 객체를 다룰 수 있으므로 구체적인 구현에 대해서 전혀 모르더라도 동작이 가능하다. 즉 구현에 대해 관심을 둘 필요가 없다.

- 좀 더 나가보면 Interface만 알고 있어도 Interface에 의존하는 코드를 작성할 수가 있다. 즉, Interface를 상속 받는 임시 객체를 만들어 두고 이를 이용하는 코드들을 만들었다가 추후에 Interface를 구현한 구체 클래스가 완성되었을 때 연결만 시켜주면 된다.

- 모든 클래스들이 오직 Interface에만 의존하게 되므로 구체적인 의존 관계가 없어지면서 각 객체가 서로 분리되어 있기 때문에 구체적인 객체를 다른 객체로 교체한다거나 Interface를 구현한 새로운 객체를 만들어서 제공함으로써 기능을 확장하는 것이 가능해진다.

이러한 인터페이스의 장점을 이용한 디자인 패턴은 모두 열거하기 힘들 정도로 많다. 가장 대표적인 것만 꼽으라면 아래와 같다.

- State 패턴 : 상태를 객체화하고, 인터페이스를 통해 상태화 된 객체를 지칭하게 함으로써 상태가 추가되기 용이하도록 한다.

- Bridge 패턴 : 연관관계가 있는 두 부류의 객체들을 두 개의 인터페이스 간의 연관관계로 바꾸고, 구체적인 객체들을 인터페이스 상속을 통해 구현 함으로써 각 부류의 객체들에 추가/삭제가 발생하더라도 다른쪽 부류에는 영향을 미치지 않도록 한다.

- Stragegy 패턴 : 기능을 담은 객체를 인자로 넘겨 줌으로써 이를 받는 객체의 기능이 변경될 수 있도록 한다. 이 역시 인터페이스를 중심으로 기능을 담은 객체를 지칭함으로써 기능의 확장이나 변경이 용이하도록 한다.

- Observer 패턴 : 관찰자 객체들을 인터페이스로 추상화하고, 관찰 대상 객체에 이벤트가 발생했을 때 인터페이스만을 활용하여 이벤트를 전달함으로써 관찰자와 관찰 대상 간의 구체적인 결합을 제거한다. 이를 통해서 관찰자에 해당하는 구체적인 객체들의 종류가 늘어나더라도 같은 관찰 대상 객체의 구현에는 영향이 없다.


정보 은닉은 객체지향 언어의 목표이다

기능을 간편하게 수정할 수 있으며, 기능을 추가하기 용이하고, 언제든 기능을 교체하는 것이 가능한 소프트웨어를 구현하는 것은 모든 소프트웨어 개발자들의 꿈이다. 그런 꿈이 담겨 있는 것이 객체지향 언어이고, 이 객체지향 언어가 기능의 수정 / 추가 / 교체를 가능하게 하기 위해 세운 기초 전략이 정보 은닉이다.

객체지향 언어를 통해 만들어진 좋은 설계, 즉 디자인 패턴과 같이 좋은 설계를 대표할만한 것들은 모두 정보 은닉 기법을 적어도 일부를 활용하고 있거나, 전적으로 정보 은닉을 통해 이득을 얻기 위해서 만들어진 것들이다. 그리고 객체지향의 설계 원칙(SOLID), 각종 객체지향 설계에 관련된 격언들은 거의 모조리 정보 은닉에 관련된 것들이다. 또한 객체지향이 만들어낸 여러 개념들과 언어적 특성들 중 대부분은 정보 은닉을 위해 만들어진 것들이다.

(예를 들어 상속을 보통 메소드와 변수를 재사용하는 것이라고 이야기하는데 이는 틀린 말이다. 상속을 통해 받을 수 있는 것 중에서 가장 중요한 것은 타입이다. 이 타입을 내려 받을 수 있기 때문에 하위 객체가 상위 클래스로 지칭 될 수 있다. 이것이 정보 은닉을 가져오고 객체지향의 모든 이점들을 가져 온다.)

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
,

객체지향 용어에 대한 글들을 찾아보니 개념의 일부만 정리되어 있거나 매우 중요한 개념이 제대로 다루어지지 않은 경우가 많아 보인다. 특히 비 객체지향 언어와의 정합성, 동일하거나 유사한 용어들에 대한 명확한 정리가 없는 점이 안타깝다. 그래서 객체지향 및 그와 연관된 개념들을 생각나는 대로 모두 정리해 보았다. 이 글은 아주 기초적인 내용들을 미리 알고 읽어 보는 것이 좋긴 하다. 하지만 객체지향을 접한지 오래 되었다고 해도 제대로 이해하지 못하는 중요한 부분들에 대해서 강조해 두었으니 충분히 도움이 될 것이라 생각한다.

일부 코드 예제나 추가적인 용어는 시간을 내어 더 작성할 예정이다. 내용이 많으니 시간을 충분히 가지고 도전하기 바란다.


객체지향 기본 요소를 이용한 코드 예제

// interface

interface IShape{ // IShape 타입

    public void draw();// 공개 추상 메소드

}

// 추상 클래스

abstract class Base{

    abstract public void init();// 추상 메소드의 프로토타입

}

// 클래스

class Subject

    extends Base// 상위 클래스, 부모 클래스를 상속한다

    implements IShape// 인터페이스를 구현한다

    // SubjectSubject 고유 타입이면서 Base 타입이고, IShape 타입이기도 하다

{

    Color color;// 변수, 참조 변수

   

    int width;// 변수, primitive 변수

   

    public void setColor(Color c){// 공개 메소드

        // csetColor 메소드의 매개 변수이다.

        this.color = c; // 여기서 cSubject의 멤버 객체가 된다.

    }

   

    @Override

    public void init(){}// 재정의. 추상 메소드를 구현한다

   

    @Override

    public void draw(){

        System.out.println("draw subject");

        color.brighter();// 위임

    }

}

public static void main(String[] args) {

    Subject subject = new Subject();// 객체화.

    // subjectSubject 객체를 참조하고 있다.

    Base base = (Base)subject;// up casting

    Subject subject2 = (Subject)base;// down casting

}


[JAVA] 자바 언어에서만 사용하는 용어

[C++] C++에서만 사용하는 용어

[정적] 코드 상에 존재하는 개념. 정적인 코드를 통해 동적인 것들을 만들어 내게 된다.

[동적] 실제 소프트웨어 동작 시 작동하는 것에 대한 개념. 메모리 상에 올라가 있는 것.

[OOP] OOP 개념이 아닌 소프트웨어 용어. OOP 용어와의 대비를 위해 사용.

~ : 대체적으로 동의어로 사용 가능(비 객체지향 용어지만, 객체지향에는 없는 용어이므로 대충 객체지향 언어로는 이와 같다는 의미)

 

[OOP] 함수(function) = 기능 ~ 행위(Operation) = 메소드(Method) = 멤버 함수 = 공개(public) 메소드 + 비공개(private) 메소드

-       속성에 대한 조작을 수행하는 명령어들을 모아 놓은 단위. 소프트웨어가 동작한다는 의미는 이 메소드가 실행된다는 것을 의미한다.

 

[OOP] 변수(variable) ~ 속성(Attribute) = 필드(field) = 데이터 = 멤버 변수 = 참조 변수 + primitive 변수

-       변경 가능한 값(value)을 저장할 수 있는 프로그래밍 요소. 메모리 상의 공간을 확보하고 있으면서 정ㅇ해진 타입을 가지고 있는 것.


[OOP] primitive 변수

-       비 객체지향 언어에서 말하는 기본 자료형. char, short, int, long 등과 같이 객체가 아닌 멤버 변수를 의미.


[정적] 참조 변수

-       객체가 다른 객체를 참조할 수 있도록 선언된 변수.

-       이 참조 변수가 멤버 객체를 가리키게 된다. 참조 변수를 가지고 있는 객체와 멤버 객체 간의 관계를 “has-a” 관계라고도 한다. 마치 한 객체가 (참조 변수를 통해) 다른 객체를 내부에 가지고 있는 것처럼 보이기 때문이다.

 

[동적] 멤버 객체 = 위임(delegation) 객체 = 참조(reference) 객체 = 내부 객체 = 레퍼런스(reference)

-       다른 객체의 필드로 선언되어 있는 객체

-       멤버 객체는 참조 변수를 통해 사용된다.

 

위임(Delegation)

-       1. A라는 객체가 가진 책임(Responsibility)을 수행하기 위해서 멤버 객체에게 책임의 일부 또는 전부를 수행하도록 시키는 것.

-       2. 멤버 객체를 이용하기 위해서 멤버 객체의 메소드를 호출하는 행위

 

책임(Responsibility)

-       객체가 설계된 기능적 목적. 객체가 해야 할 일.

 

공개(public) 메소드 = 공용 메소드 ~ API(Application Programming Interface) = 인터페이스(interface)

-       1. 객체의 외부에서 호출이 가능하도록 노출된 메소드

-       2. 객체 외부로부터의 명령을 받는 메소드. 이 특성 때문에 모든 공개 메소드의 명칭은 명령형으로 작성되어야 하고 그렇게 해석되어야 한다.

 

비공개(private) 메소드 = 전용 메소드

-       객체 내부에서만 호출이 가능하도록 감춰진 메소드.


접근성(visibility)

-       공개(public) / 보호(protected) / 비공개(private)과 같이 객체의 내부와 외부 또는 상속 관계에 따라서 멤버 변수나 메소드에 대한 접근 권한을 제어하는 키워드.

 

인터페이스(interface)

-       1. 공개 메소드와 동일한 의미로 사용되는 용어.

-       2. [JAVA] 공개 추상(abstract) 메소드를 가진 객체화 불가능한 상위 타입

 

[JAVA] interface

-       1. 순수하게 공개 추상 메소드로만 이루어진 추상 클래스.

-       2. [JAVA] 공개 추상(abstract) 메소드를 가진 객체화 불가능한 상위 타입

-       (원래 1번의 의미가 가장 강하지만 최근에는 static 필드와 default 메소드(구현이 있는 메소드)가 추가되어 엄밀하게는 1번이 정확한 의미는 아니다. 하지만 클래스와 인터페이스를 비교하는 개념으로써는 1번이 맞다.)

-       3. 인터페이스끼리의 상속(extends)을 통해 확장이 가능하고, 하나의 클래스가 여러 인터페이스를 구현(implements)하는 것이 가능하다. 따라서 C++과 같은 언어의 다중 상속 문제를 회피할 수 있는 대안으로 이해 되기도 한다.

 

추상(abstract) 클래스 = [C++]가상(virtual) 클래스

-       객체화 될 수 없는 클래스.

-       보통은 적어도 한 개 이상의 추상 메소드를 가지고 있는 클래스를 의미한다.

-       ([JAVA] interface는 모든 메소드가 공개 추상 메소드인 추상 클래스이다.)

 

추상(abstract) 메소드 = [C++]가상(virtual) 함수

-       프로토타입(prototype)만 있고 동작을 정의한 구현부가 없는 메소드.

 

[OOP] 프로토타입(prototype)

-       [리턴타입][이름][(매개변수)]

-       형식의 예 : int add(int a, int b);

 

[OOP] 매개 변수(argument) = 인자 = [OOP]파라메터(parameter)

-       메소드에 input으로 들어가는 변수.

-       객체와 primitive 변수 모두 매개 변수가 될 수 있음.

 

타입(Type) *** 중요 ***

-       [OOP] 1. 변수의 자료형(char, short, int, long, struct )

-       2. 객체가 다뤄질 수 있는 형식명. 객체가 XX 타입이라는 것은 객체가 XX 클래스 인터페이스로 참조(reference) 가능하다는 것을 의미한다.

 

참조(reference)

-       객체를 지칭하는 행위. 객체를 참조 변수에 할당하는 행위

 

참조 변수(reference variable)

-       Object aObject; à 여기서 aObject Object 타입의 객체를 지칭하는 참조 변수이다.


[정적] 클래스(Class) = 타입(Type) + 공개 메소드 + 비공개 메소드 + 필드

-       객체에 대한 명세서. 청사진.

-       객체지향 소프트웨어를 사전으로 비유하자면 클래스는 단어에 해당한다. ‘단어는 그 의미를 나타내는 설명을 가지고 있다. ‘설명에 해당하는 것이 클래스를 구현한 코드이다. 그러면 객체는? 단어로 지칭할 수 있는 실제 세계의 존재이다. ‘사과단어’, 즉 클래스이고 실제 사과는 객체이다. 따라서 사과라는 클래스는 한 개 뿐이지만, ‘실제 사과는 여러 개가 될 수 있다.

(예제)

 

-       공개 메소드는 특별히 분리할 필요가 있다. 공개 메소드는 클래스가 객체화 되었을 때 외부와 소통할 수 있는 유일한 통로이다. 객체는 수동적인 식물과 같아서 외부에서 공개 메소드를 통해 자극을 주지 않으면 아무것도 하지 않는 것이 기본이다.(동적(active) 객체는 예외.)

-       클래스가 메소드와 필드로 이루어져 있다는 말은 거짓말이다. 클래스에게 가장 중요한 것은 타입이다. 따라서 클래스는 타입과 메소드(공개 + 비공개), 그리고 필드로 이루어져 있다는 것이 정답이다. 타입은 상속을 통해 다형성과 같은 객체지향의 가장 중요한 개념을 형성해 주기 때문에 클래스가 가지고 있는 것 중에서 가장 중요한 것이다.

 

[동적] 객체(Object) = 인스턴스 = 상위 타입 + 고유 타입 + 공개 메소드 + 아이덴티티(identity)

-       실제 메모리가 할당되어 동작하는 객체지향의 최소 단위.

-       클래스의 동적 형식. 클래스가 개념이라면 객체는 실제다. 사과가 모든 사과를 나타낼 수 있도록 정의된 단어, 즉 클래스라면 내가 들고 있는 진짜사과는 그 객체이다.

-       정적 객체의 동작은 오직 공개 메소드에 의해 일어난다. 따라서 공개 메소드는 명령으로 해석되어야 하고, 공개 메소드의 내부 구현은 공개 메소드 명칭에 맞는 명령이 전달되었을 때 수행되어야 할 일을 구현해야 한다.

-       객체는 객체화가 완료된 후, 소프트웨어의 동작에 따라 자신이 가질 수 있는 모든 타입들로 변경되어 지칭되고 사용될 수 있다. 하지만 잘 설계된 객체지향 소프트웨어 내에서 자신의 고유 타입보다는 상위 타입으로 지칭되어 사용되는 경우가 일반적이다. 특히 상위 타입으로 지칭되다가 다시 그 하위 탕입, 또는 자신의 고유 타입으로 지칭되는 것은 일종의 금기이다. 고유 타입을 사용하면 상위 타입으로 지칭됨으로써 얻을 수 있는 정보 은닉(information hiding)의 장점을 잃어버린다.

 

고유 타입

-       객체가 생성될 당시에 생성의 기반이 된 클래스 타입. new Object()라는 명령을 통해 새로운 객체가 생성되었다면 Object가 고유 타입에 해당한다.

-       고유 타입 개념은 중요하다. 어떤 객체가 어떤 타입으로 참조될 수 있는지를 결정하기 때문이다.

 

아이덴티티(identity) = 고유성

-       어떤 객체를 다른 객체와 구분 짓는 고유한 특성.

-       모든 객체는 고유성을 확인할 수 있는 공개 메소드를 가지고 있어야 한다.([JAVA] equals() 메소드)

 

정적(passive) 객체 = 일반 객체

-       내부에 쓰레드를 선언하고 있지 않은 객체.

-       공개 메소드를 통해 명령하지 않으면 아무 일도 하지 않는 객체.

-       일반적으로 객체라고 하면 보통 정적 객체를 의미한다.

 

동적 객체(active) 객체

-       내부에 쓰레드를 선언하고 있고, 쓰레드의 동작에 기반하여 작동하는 객체.

-       정적 객체의 반대말.

 

추상화(Abstraction)

-     개별적인 대상들의 차이점을 배제하고 동일한 점을 추출해 내는 것. 특히 동일한 점을 모아 클래스 또는 인터페이스화 하는 것.

-    "모델링(Modeling)" 이라는 말과 동일하게 쓰인다. "수학적 모델링"이라고 하면 현실 세계의 문제들의 개별적인 차이점을 배제하고 동일한 특성들을 파악해서 오직 수치와 공식으로 표현 가능한 요소들로 바꾸는 작업이다.

-    따라서 객체지향에서 추상화란 "클래스 모델링" 또는 "객체 모델링"이라는 말로 표현할 수 있다.

-    실세계의 예를 들면 철수, 영희, 희동이는 각자 고유한 특성을 가지고 있지만 이런 특성을 배제하고 나면 이름과 나이, 성별, 사는 곳과 같은 동일한 특성을 가진 '사람'이다. 이렇게 동일한 특성들을 모아서 '사람'이라는 이름의 클래스를 만드는 과정이 추상화(Abstraction) 과정이다.


정보 은닉(information hiding) ****** 매우 중요. 클래스, 객체보다 더 중요 ******

-       정보 은닉은 객체지향 언어가 만들어진 목적에 해당하는 개념이다.(모든 개념 중에서 가장 중요한 개념이다. 믿음 소망 사랑 중 사랑이며, good, better, best 중에 best이다. 정보 은닉만 알면 나머지 개념이 왜 생겨난 것인지를 알 수 있다.)

-       정보 은닉은 객체지향 언어를 설계한 모든 목적을 달성하기 위한 특성이다. “객체화한다는 의미로서의 캡슐화와 상속(그 중에서도 타입의 상속)은 정보 은닉을 가능하게 하기 위한 수단(일 뿐)이다.

-       객체의 고유 타입 은닉

n  객체가 생성된 이후, 고유 타입이 아닌 그 상위 타입으로 지칭되도록 함으로써 생성 이후에는 객체의 고유 타입을 모르도록 하는 것.

n  객체의 고유 타입을 모른 상태에서 구현할 수 있다는 것은 그 고유 타입에 의존하는 코드가 없다는 말이다. 이는 그 고유 타입 객체가 삭제되거나 수정되더라도 코드는 전혀 변경되지 않는다는 것을 의미한다.

n  다형성 = 서로 다른 객체들의 고유 타입을 은닉하고 동일한 상위 타입을 통해 다수의 객체를 동일하게 다루는 것.(객체의 고유 타입 은닉 중 특수 케이스에 해당함)

-       객체의 필드 및 메소드 은닉 = 캡슐화

-       타입 하위 캐스팅 금지 : 상위 타입으로 지칭된 객체를 하위 타입으로 바꿔 지칭하는 것을 금기시 함으로써 온전하게 정보 은닉을 달성할 수 있다.

-       정보 은닉의 장점

n  재사용성 : 객체가 다른 객체의 고유 타입에 의존하지 않도록 함으로써 다른 소프트웨어나 다른 모듈에서도 쉽게 이용할 수 있음.

n  유연성 : 위임 객체의 고유 타입에 의존하지 않게 함으로써 위임 객체를 교체하기 쉽게 만들어 소프트웨어 기능을 교체/확장하기 용이하도록 함.

n  유지보수성 : 객체가 가져야 할 기능들을 각각 고유한 클래스에 구현하도록 함으로써 기능의 수정 시 다른 기능에 영향을 주는 것을 최소화 함. 객체가 가진 최소한의 공개 메소드만을 호출함으로써 설계 변경 시 변경할 코드의 양을 최소화 함.

-       객체지향에서 설계를 통해 좋은 특성을 얻는다고 하는 설명이나 좋은 설계를 대표하는 디자인 패턴, 아키텍쳐 패턴, 프레임워크 구조에서 볼 수 있는 패턴들은 모두 다정보 은닉 개념을 활용한 것들이다.

 

캡슐화(encapsulation)

-       1. 다루고자 하는 변수와 그 변수를 다루는 함수를 묶어 객체로 만드는 행위

-       2. 정보 은닉의 하위 개념 중 하나로써, 객체의 필드를 비공개(private)로 하고, 꼭 필요한 경우에만 메소드를 공개를 설정하는 것.

-       캡슐화의 장점은

n  필드에 대한 임의 접근을 방지하여 의도하지 않은 정보 변경을 막을 수 있다. 필드에 대한 접근을 하는 메소드에 대해서만 관리 하면 데이터의 동기화와 같이 구현이나 테스트가 어려운 특성도 상당히 구현하기 쉬워진다.

n  메소드 공개를 최소화하여 객체간의 연관 관계를 느슨하게 함으로써 잠재적인 변경 사항의 반영을 쉽게 한다.

n  객체의 동작을 이해하는데 필요한 정보를 계층화한다.(즉 더 중요한 공개 메소드를 우선 이해하고 비공개 메소드에 대해서는 그 다음 단계에 이해하는 식이다.)

 

 

상속(inheritance)

-       상위 클래스의 타입과 공개 메소드, 필드를 물려 받는 것.

-       상속에서 가장 중요한 부분은 타입을 물려 받는다는 것이다. 이 부분에 대한 강조가 부족하여 객체지향을 제대로 이해하지 못하는 경우가 너무나 많다. 상속을 통해 타입을 물려 받으면 하위 클래스는 상위 타입으로 지칭될 수 있다. 이것이 정보 은닉의 장점을 확보할 수 있도록 해준다. 공개 메소드와 필드를 물려 받아 중복 코드를 줄일 수 있다는 점이 객체지향의 장점이라면, 정보 은닉(여기서는 하위 타입 은닉)을 통해 재사용성, 유연성, 낮은 결합도, 유지보수성, 단일 책임 등의 특성을 얻을 수 있다는 점은 객체지향 언어가 만들어진 목적에 해당한다.

-       그다지 중요한 부분은 아니지만 어떤 경우에는 메소드의 구현이나 필드를 재 구현하지 않기 위해서 상속을 이용하기도 한다고 한다. “위대하신 타입 상속에 비해 중요도가 많이 떨어지는 정보이므로 참고만 하도록 하자.

 

다형성(polymorphism)

-    하나의 객체가 서로 다른 타입으로 지칭될 수 있음을 이르는 말. 클래스가 상위 클래스를 상속하면 상위 클래스의 타입까지 상속받게 된다. 이 때 상속을 받은 클래스는 상위 클래스 타입임과 동시에 자기 자신 타입이기도 하다. 이렇게 여러 타입(= 모양 = 형)을 가질 수 있는 클래스의 특성이 바로 다형성이다.(C언어의 struct와 비교해 보라. C 언어의 struct는 기존의 struct를 내부에 선언하여 사용할 수는 있어도 기존에 있는 struct 타입으로 사용 될 수는 없다.)

-       서로 다른 객체들의 고유 타입을 은닉하고 동일한 상위 타입을 통해 다수의 객체를 동일하게 다루는 것.(정보 은닉의 한가지)

-       다형성은 오케스트라 지휘로 비유할 수 있다. 각각의 고유 타입을 가진 객체를 오케스트라의 바이올린 연주자, 첼로 연주자, 플롯 연주자 등이라고 하면 상위 타입은 연주자이다. 지휘자 격인 객체는 이들 객체를 다룰 때 바이올린 연주자연주하세요, “첼로 연주자연주하세요와 같이 이야기 하지 않는다. “연주자 여러분 연주 하세요라고 한마디만 하면 끝난다. 이처럼 각각의 객체의 다른 점보다 같은점, 즉 상위 타입(여기서는 연주자)을 통해서 여러 다른 객체들을 동일하게 다루는 것이 다형성이다. 당연히 끊임없이 여러 연주자들를 외치는 것보다 단 한번만 연주자라고 부르는 것이 코드를 줄이고 객체를 대체하는데 유리하다.

-       메소드 재정의(overrinding)도 다형성의 일종이다.

 

메소드 재정의(overriding)

-       1. 추상 메소드의 구현부를 구현하는 것.

-       2. 이미 구현부가 있는 메소드의 구현부를 대체하여 구현하는 것. 보통 2의 의미로 더 많이 쓰인다.

-       3. 재정의된 메소드는 프로토타입은 같지만 동작이 다르다. 따라서 재정의된 메소드를 이용하는 객체는 재정의 되지 않은 메소드와 동일하게 취급할 수 있고, 따라서 다형성을 이용하는 수단이 될 수 있다.

-       가급적 구현부를 대체하는 방식보다는 추상 메소드를 제공하는 편이 코드를 이해하기가 훨씬 수월하다.

 

메소드 오버로딩(method overloading)

-       동일한 명칭에 다른 인자를 받는 메소드들을 여럿 구현하는 것.

-       메소드 오버로딩은 근본적으로 같지 않은 인자에 대한 취급 방법을 동일하게 하는 것이라고 생각할 수 있다.(근본적으로 같은 것이라면 여러 메소드를 만들 필요도 없다.)

 

상위 캐스팅(up casting)

-       하위 타입 객체를 상위 타입 변수로 지칭하는 것.

 

하위 캐스팅(down casting)

-       상위 타입 변수로 지칭되던 객체를 하위 타입 변수로 지칭하는 것. 객체지향 언어에서 하지 말 것으로 정해진 것 중의 하나.

 

Has-a 관계 = association = 연관 = 연관 관계

-       어떤 객체 A가 참조 변수를 가지고 있고, 그 변수를 통해서 다른 객체 B를 멤버 객체로 가지는 관계를 A has-a B 관계라고 한다.

 

Is-a 관계 = generalization(일반화) 또는 인터페이스에 대해서는 realization

-       어떤 객체 A가 다른 객체 B의 상위 타입이고, 다른 객체 B는 객체 A의 하위 타입일 때 B is-a A 관계이다.

Posted by 이세영2
,

기초적인 메소드 이름 만드는 법

일반적으로 메소드 이름을 아래와 같이 만들 것을 많은 곳에서 권하고 있다.

1. 동사 + 명사형 이름으로 만들 것 : getData() / set Data() / calculateData() / computeData


하지만 조금만 더 이해하고 나면 더 좋은 메소드를 만들 수 있다. 아래의 내용을 먼저 이해하고 넘어가도록 하자.


객체는 수동적(passive)이다

객체는 외부에서 메소드를 호출해 주지 않으면 아무 일도 하지 않는다. 객체는 완벽하게 수동적이기 때문이다. 따라서 메소드 명칭은 수동적인 객체에게 무언가를 시키는 형태의 이름이 되어야 한다. 즉 객체에게 명령하는 형식의 이름이어야 한다. 따라서 객체의 메소드 이름 만드는 규칙은 정확하게 말하면 아래와 같다.


메소드 명칭 = 명령형 동사 + 목적어

단순히 동사 + 명사형으로 생각하지 않기 바란다. 예를 들어 getData()라는 메소드 명칭은 "데이터를 가지고 온다."라는 의미로도 해석 할 수 있지만 좀 더 명확하게는 "데이터를 내놔!" 이다. 이렇게 해석하는 것이 좋은 이유는 객체를 구현할 때 좀 더 구현을 정확하게 할 수 있기 때문이다. 객체를 구현할 때 getData() 메소드를 구현하면서 "데이터를 가지고 온다" 라고 해석하고 구현하기 시작하면 구현이 꼬이기 시작한다. 반대로 객체가 "데이터를 내놔!"라는 명령을 받았을 때 해야 할 일을 getData() 메소드에 구현하면 된다고 생각하고 구현을 시작하면 구현이 매우 명료해 진다.


메소드 이름을 해석하는 관점

실제로 잘 되어 있는 오픈 소스들은 객체가 수동적이라는 관점으로 메소드 이름을 작성한다. 따라서 메소드를 해석할 때에도 이와 같은 관점에서 해석해야 한다. 객체가 sendEvent()라는 메소드를 가지고 있을 때 이를 "이벤트를 보낸다"라고 해석하지 말고 "이벤트를 보내라!"라고 명령하는 것으로 해석해야 한다. 유사하게 make(만들어라), add(더해라, 또는 집어 넣어라), remove(제거해라), initialize(초기화해라) 등으로 해석하는 편이 훨씬 소스를 이해하는데 도움이 된다.

Posted by 이세영2
,
Eclipse 단축키.xlsx


Java 프로그래밍 툴로 Eclipse를 많이 사용한다. Eclipse가 제공하는 단축키도 많고 단축키를 새로 바인딩 할 수도 있는데, 그 중에서도 매우 유용하게 사용할 수 있는 것들을 골라서 소개해 보도록 하겠다. 특히 단축키는 외우고 있을 때 더욱 위력일 발휘하기 때문에 나 같은 경우도 모니터 앞에 단축키 목록을 출력해서 복사해서 붙여 놓고 있다. 그럴때 사용하기 좋도록 엑셀파일로 만들어 첨부해 두었다.


이동 단축키

원하는 곳으로 이동을 쉽게 할 수 있는 단축키들이다.

Ctrl+객체클릭 변수나 클래스 등을 정의한 곳으로 이동

대부분 알만한 단축키인데 컨트롤을 누르고 객체명 또는 타입명을 클릭하면 해당 객체나 타입의 선언부로 이동한다.


Ctrl+Shift+G 변수나 함수 등을 레퍼런스 하는 곳으로 이동

    변수나 함수명을 드래그 또는 더블 클릭 해서 선택한 후 이 단축키를 누르면 아래와 같이 해당 변수나 함수를 레퍼런스 하고 있는 곳이 열거 된다. 클릭하면 해당 코드로 이동할 수 있다.


Alt+LEFT 이전 커서 위치로 이동

    매우 유용한 단축키인데, 한 곳에서 편집을 하고 있다 다른 곳으로 이동한 후, 다시 이전 위치로 이동하고 싶을 때 이 단축키를 누르면 된다. 여러번 반복해서 이전 위치로 이동하는 것도 가능하다.


Alt+RIGHT 다음 커서 위치로 이동

    위의 키와 함께 쓰이는 키로써, 편집을 하던 이전 위치로 이동했다가(Alt + LEFT) 다시 다음 위치로 이동하고 싶을 때 이 단축키를 누르면 다시 돌아간다. 역시 반복적으로 복귀하는 것이 가능하다.


찾기 단축키

Ctrl+Alt+G 전체 workspace에서 문자열 찾기

    문자열을 선택한 후 이 단축키를 누르면 아래쪽 Search 창에 해당 문자열이 들어 있는 모든 프로젝트의 모든 파일을 찾아서 보여 준다.


Ctrl+K 선택한 문자열을 파일 내에서 찾기

    문자열을 선택한 후 이 단축키를 누르면 파일 내에 있는 동일한 문자열을 "위에서 아래" 순서로 찾아서 커서를 이동시켜준다.


Ctrl+Shift+K 선택한 문자열을 파일 내에서 역순으로 찾기

    문자열을 선택한 후 이 단축키를 누르면 파일 내에 있는 동일한 문자열을 "아래에서 위" 순서로 찾아서 커서를 이동시켜준다.



주석 단축키

Ctrl+Shift+/ 블록을 블록 주석으로 처리

    화면에서 일부 블럭을 드래그 하여 선택한 후 이 단축키를 누르면 /*로 시작하여 */로 끝나는 블럭 주석으로 만들어준다.


Ctrl+Shift+\ 블록 주석 제거

    이미 블럭 주석이 되어 있는 부분을 선택한 후 이 단축키를 누르면 블럭 주석이 해제된다.


Ctrl+/ 한줄 주석 처리 또는 제거

    화면에서 일부 블럭을 드래그 하여 선택한 후 이 단축키를 누르면 각 줄이 "//"로 시작하는 한줄 주석들로 만들어준다.


자동화 단축키

Alt+Shift+R 변수나 클래스 등의 리팩토링

    변수나 클래스명 등 모든 명칭에 마우스를 올리거나 드래그 해서 선택 한 후 이 단축키를 누르면 아래와 같이 명칭에 박스가 생긴다. 그 후 명칭을 편집하여 변경하면 그 명칭을 사용하는 곳 전체에서 명칭이 한꺼번에 변경된다. 종종 변경된 명칭이 기존의 명칭과 충돌되면 에러가 발생한다.


Ctrl+Shift+O 자동으로 import

    외부 패키지나 라이브러리에 있는 클래스를 사용하게 되면 참조 오류가 발생한다. 만약 패키지나 라이브러리가 이미 프로젝트에 등록이 되어 있다면 이 단축키를 눌렀을 때 자동으로 import 코드를 생성해 준다.


Ctrl+I     들여쓰기 자동 수정

    일부 블럭을 드래그 하여 선택한 후 이 단축키를 누르면 들여쓰기를 설정된 포맷에 맞게 수정해 준다. 외부 소스를 복사해 왔을 때 종종 들여쓰기 단 수나 스페이스바 들여쓰기가 안맞는 경우가 있는데 이때 사용하면 알아서 사용하는 포맷에 맞게 들여쓰기를 해준다.


구조 보기 단축키

Ctrl+T(또는 F4) 클래스 Hierarchy 보기

    클래스의 계층이 복잡할 경우, 또는 인터페이스가 정의되어 있는데 인터페이스를 구현한 구체 클래스를 찾기 힘든 경우에 클래스 명 또는 인터페이스 명 위에 커서를 놓고 이 단축키를 누르면 그 클래스의 계층도를 보여 준다. 아래는 IDestination이라는 인터페이스에 이 단축키를 눌렀을 때 보여지는 화면이다. 인터페이스는 I 모양의 아이콘, 클래스는 C 모양의 아이콘으로 나온다.


Ctrl+O 클래스 멤버 함수 보기

    이 단축키는 파일 내에 선언되어 있는 모든 클래스와 모든 인터페이스들에 내부에 선언된 모든 멤버 함수들을 보여준다. 아래는 그 예시이다.



편집 단축키

Alt+Shift+A 상하 편집 모드로 전환

   종종 한 줄이 아니라 여러줄에 걸쳐 선언되어 있는 클래스 명이나 변수명만을 선택적으로 복사하고 싶을 때가 있다. 이럴때 이 단축키를 누르고 블럭을 선택한 후 Ctrl+C를 하고, 다시 이 단축키를 눌러서 상하 편집 모드에서 나온 후에 붙여 넣기를 하면 블럭 내에 선택되었는 부분만 복사가 된다.



Ctrl+Shift+X 선택된 문자열을 대문자로 전환

    선택한 문자열을 대문자로 전환해 주는 단축키이다. 보통은 일반 변수로 선언했다가 enum 타입 또는 상수로 선언을 바꾸고자 할 경우에 유용하다.


Ctrl+Shift+Y 선택된 문자열을 소문자로 전환

    선택한 문자열을 소문자로 전환해 주는 단축키이다. Java의 일반적인 명명법으로는 클래스는 대문자로, 인스턴스명은 소문자로 시작된다. 그런데 보통은 클래스명의 첫머리를 소문자로 한 명칭을 인스턴스명으로 사용하는 경우가 많다. 가령 TcpCommunication 클래스의 인스턴스명은 보통 tcpCommunication이라는 식이다. 이런 경우 클래스명을 복사하여 붙여 넣은 후, T를 선택하고 이 단축키를 누르는 식으로 사용한다. 이 단축키는 위에서 소개한 상하 편집 모드로 변환 단축키(Alt+Shift+A)와 함께 사용했을 때 더 강력하다. 즉 상하 모드에서 여러줄에 걸쳐 선언된 클래스명을 복사해서 붙인 후 이 단축키로 첫머리를 소문자로 변환하면 금새 인스턴스명으로 변환된다.



리팩토링 단축키

Alt+Shift+S R Getter/Setter 자동 생성 창 열기

    Getter / Setter를 자동 생성해주는 창을 여는 단축키이다. Getter와 Setter를 여럿 만들어야 하는 경우에 유용하게 사용할 수 있다.

Alt+Shift+M Method로 추출

    소스 코드를 블럭 선택 한 후 이 단축키를 누르면 메소드 생성 창이 뜬다. 메소드 명과 변수명을 적절히 입력하고 나면 입력한 메소드 명의 메소드가 생성되고, 선택한 소스는 그 메소드 내로 이동하며, 기존 소스가 있던 자리는 메소드 콜로 대체된다.


Alt+Shift+I Method를 인라인 하기

    Method로 추출 단축키의 반대이다. 메소드에 커서를 놓고 이 단축키를 누르면 이 메소드를 사용하고 있는 모든 곳에 메소드 내의 소스 코드가 삽입되고, 메소드는 삭제된다.


기타 단축키

Ctrl+W 현재 파일 닫기

    편집하고 있는 파일을 닫는다.

Ctrl+Shift+W 열린 파일 모두 닫기

    종종 편집하기 위해 열어 둔 파일이 너무 많은 경우가 있다. 이 때 이 단축키를 누르면 모든 창이 닫힌다. 닫히기 전에 저장이 안된 파일에 대해서는 저장하라고 경고 창을 띄워 주기 때문에 안전하게 사용할 수 있다.

Ctrl+F11 최근 실행 파일 실행

    최근에 실행했던 프로그램 실행 파일을 실행해 준다. 만약 현재 편집 중인 파일에 main() 메소드가 있을 경우 현재 파일을 실행한다.(최근 실행 파일과 현재 파일 중 어떤 것을 실행할지를 선택할 수 있는 옵션이 있다.)

Alt+Shift+ X T Unit Test 실행

    유닛 테스트를 작성하여 사용하는 경우에 유용한 단축키이다. 이 단축키는 여러모로 유용한데, 테스트 파일 전체를 실행하고 싶으면 그냥 이 단축키를 누르면 된다. 만약 특정한 한 개의 유닛 테스트 함수만을 실행하고 싶다면 함수를 드래그 해서 선택하고 이 단축키를 누르면 그 테스트 함수만 실행된다. 만일 특정 패키지를 실행하고 싶다면 Package Explorer 창에서 패키지를 선택하고 이 단축키를 누르면 된다. 또 전체 프로젝트에 대한 테스트를 하고 싶다면 프로젝트를 선택한 후 단축키를 누르면 된다.


Ctrl+ + / - 텍스트 에디터 폰트 크기 조절

    이 단축키는 내 경험상 Eclipse 최신 버전인 Neon에서만 동작한다. 종종 텍스트 폰트 크기를 손쉽게 변경하고 싶을 때가 있다.(세미나나 강의를 위해서 프로젝터를 사용하게 될 경우 특히 그렇다) 이 때 이 단축키를 사용하면 폰트 크기가 조절된다. 텍스트 에디트 창에서만 실행 가능하다.


사용자 지정 단축키

Eclipse에서는 다양한 커맨드에 대해서 사용자가 직접 단축키를 지정할 수 있다. 아래는 단축키로 지정해 두면 도움이 되는 것 들이다.

Ctrl+Shift+P 새로운 패키지 생성( New (Package)에 대해서 )

    새로운 패키지를 생성하는 단축키이다. 기본적으로는 지정이 안되어 있다. 따라서 Preferences / General / Keys 에 들어가서 검색창에 package를 입력한다. 그러면 그 중에 창 모양 아이콘과 함께 "New (Package)" 라는 것이 있을 것이다. 그것을 선택한 후 Binding 입력란에 이 단축키를 입력한다. 그리고 When 란에는 In Windows를 선택한다. 그러면 이후 새로운 패키지를 추가할 때 이 단축키만 누르면 패키지 생성창이 뜬다.


Ctrl+Shift+M 새로운 클래스 생성

    위와 비슷하게 새로운 클래스를 생성하기 위한 단축키이다. Preferences / General / Keys에 들어가서 검색창에 class를 입력한다. 그 중에서 "New (Class)"를 선택한다. 만약 Java와 C/C++을 동시에 사용하는 경우라면 Java 클래스를 생성하고 싶다면 "New (Class (org.eclipse.jdt.ui.....)"로 되어 있는 것을 선택하고, C++ 클래스를 생성하고 싶다면 "New (Class)"를 선택하면 된다. 그리고 위와 비슷한 방법으로 단축키를 입력하고 When란에 Java는 Editing Java Source를, C++은 In C/C++ Views를 선택한다. 이후에 클래스를 생성할 때 이 단축키를 입력하면 클래스 생성창이 뜰 것이다.


Posted by 이세영2
,

내가 하는 일을 가치 있게 만드는 방법은 무엇일까?

정말 특별한 소수의 사람을 제외하고 삶이란 겉으로 보면 모두 다 똑같아 보인다. 누구나 다 반짝반짝 빛나는 삶을 꿈꾸지만 현실적으로 그런 사람은 드물고, 나는 남들보다 특별하게 살려고 노력하는 것 같은데 사실 겉에서 보면 다른 사람들과 똑같은 일을 하는 평범한 사람일 뿐이다.

이것은 어떤 관점에서는 너무나도 당연한 일이다. 내가 살고 있는 곳에서 조금씩 멀어져 보자. 조금씩 멀어지다 보면 점점 더 넓은 세상이 보인다. 더 넓은 세상을 바라보다 보면 사람들의 개성은 점점 희미해지고, 종교와 같이 사람들을 죽음의 갈등으로 몰아 넣는 것들도 시시해지고, 피부색은 보이지 않으며 이념은 자취를 감춘다. 지구에서 조금만 벗어나더라도 누군가가 거기에 무언가 있다고 말해주지 않는다면 찬란하게 빛나는 인류 문명조차 눈에 들어오지 않을 것이다.

그렇다면 내가 무슨 일을 하든 가치가 없다고 생각하고 사는 것이 바람직한 일일까? 내가 지금까지 겪어온 바를 바탕으로 생각해보면 모든 것을 의심해도 단 한가지 의심하지 말아야 할 것이 있다. 데카르트 같은 사람은 자기 존재에 대한 확신을 회의적인 철학적 사유로 풀어 냈지만, 내 경험상 자신의 존재를 의심하는 것은 사유적 금기이다. 그 이유는 그 의심이 어떠한 가치도 만들어 주지 못하기 때문이다. 그런 사유는 다른 사람들과 공유될 수도 없고, 이득이 될만한 무언가를 만들어 낼 수도 없다. 그리고 다른 사람에게 그것을 이야기 한들 들어줄 사람도 없다.

자기 존재에 대한 의심은 끊임 없는 자기 검증과도 같다. 어떤 기계가 자기가 해야 할 일은 하지 않고 계속 자기 자신이 정상인가를 확인한다고 하자. 그 기계는 과연 쓸모 있는 기계인가? 그리고 결국에는 자기가 정상이라는 것을 확인하고 다른 사람에게 그것을 알려준다고 해서 그 기계의 가치가 달라지는가? 기껏해야 그건 그냥 정상인 기계에 불과하다. 의심해야 할 것은 의심하되 의심하지 말하야 할 것은 관심도 두지 말아야 한다. 영국의 경험론은 "물의 깊이는 알 필요가 없다. 배를 띄울 수 있는지 없는지만 중요할 뿐이다"라는 생각에서 출발한다. 공자는 죽음 이후의 세계가 있는지를 묻는 제자에게 "사람이 사는 것에 대해서도 다 알지 못하는데 죽은 이후에 대해서 어찌 알겠는가"라고 말하면서 사후 세계에 대한 생각을 일축시켰다. 그래서 유교에는 사후 세계가 없다.

자, 다시 원래 질문으로 돌아가보자. 그럼 일단 내가 존재한다고 치고, 내가 하는 일을 가치 있게 만들어야 존재 가치도 있는게 아니겠는가? 그러면 내가 하는 일을 어떻게 가치 있게 만들 수 있는가? 아래 보기가 있다.


1. 열심히 한다.

2. 돈을 벌기 위해서 한다. 밥 벌어 먹기 위해서 한다.

3. 가족을 위해서 한다.

4. 회사를 위해서 한다.


눈치 챘겠지만 저 중에는 정답이 없다. 열심히 한다고 해서 가치 있어지는 일은 없다. 누가 부지런하고 성실한 사람이라고 자기를 소개한다면 충분히 의심해 볼만한 가치가 있다. 히틀러는 독실한 카톨릭 신자에 술과 담배를 하지 않았으며, 매우 성실한 사람이었다. 매우 성실하게 매일 같이 근력 운동을 하면 근육은 커지지 않는다. 마찬가지로 매일 같이 열심히 일을 하면 실력은 늘지 않는다. 근육은 쉴 때 커지고, 능력은 놀 때 자란다. 간단한 예로 내가 지금 이 시간에 회사에서 매일 같이 일을 하고 있다면 내가 쓴 블로그 글들은 한 개도 없을 것이며, 내가 블로그를 쓰면서 소프트웨어에 대한 개념들을 정리할 시간이 없었을 것이다. 수치로 계산해 볼 수는 없지만 내가 생각해낸 (내 생각에) 창의적인 발상이나 이해는 컴퓨터 앞에서 문제를 해결하기 위해서 열정을 불태우고 있을 때가 아니라 대부분 커피를 타러 갈 때와 화장실에 갔을 때, 자면서 꿈에 나타난 것들이다.

돈을 벌기 위해서 하는 것은 열심히 하는 것보다 낫다. 적어도 자기가 존재해야 가치가 있어지는 것이기 때문이다. 하지만 존재 하는 것만으로도 가치가 있는 것은 세상에 단 한가지도 없다.

가족을 위해서 일 하는 것. 일면 좋은 일일 수 있다. 분명한 목적이 있기 때문이다. 하지만 그 목적이 일을 가치 있게 만들어주지는 못한다. 내가 하는 일이 가치가 있으려면 실제로 가치를 만들어야 한다. 어느 누군가를 위해서 일하는 것은 숭고한 일일 수는 있어도 항상 가치 있는 일은 아니다. 일하는 스타일로 치면 겉은 번지르르 하지만 실속은 없는 일에 매달릴 수 있다. 일에 가치를 두지 않기 때문이다. 단지 일을 계속하거나 일에 대한 평판을 좋게 만드는데에만 신경을 쓸 수 있기 때문이다.

회사를 위해서 일하는 것. 말할 필요도 없이 가치 없는 짓이다.


사람들은 자기 일에 여러가지 방법으로 동기 부여를 한다. 자기 만족, 돈을 벌기 위해, 가족을 위해, 회사의 무궁한 발전을 위해 일한다고 생각할 수 있다. 실질적인 일의 내용이 바뀌지 않는데 그 일에 동기를 부여하는 것은 무가치에 가치를 부여하는 자기 기만이다. 즉, 속이는 것이다. 자기가 하는 일이 가치가 없어지는 때는 이 어설픈 속임수가 자기 스스로에게 들통났을 때이다. 열심히 일하고 있다고 자부하고 있다가 몸이 상해가는 것을 알게 된 순간 그 일은 가치가 없어진다. 스스로 속이는 일이 불가능해졌기 때문이다. 열심히 해서 가치가 있는 일로 만들어 보려 했지만 자기 스스로가 망가지는 것보다 가치 있지는 않다. 가족을 위해서 일하다 보니 회사와 동료들을 속여가면 자기가 하는 일을 포장하는 자기를 발견했을 때. 자기가 벌이던 사기 행각이 자기에게 발각되었을 때. 그 때처럼 자기가 하는 일이 가치 없음을 느끼게 되는 순간이 또 있을까?


자 그러면 어떻게 해야 내가 하는 일이 가치 있어 질까? 이것이 애초에 무가치에 가치를 부여하는 것, 즉 속임수라면 어설프게 속일 것이 아니라 제대로 속여야 한다. 한마디로 판을 키워야 하는 것이다. 그래야 자기 자신까지도 자기 일이 가치 있다고 믿을 수 있다. 자기 일을 가치 있게 만드는 방법은 자기가 자기 일을 바라보는 관점을 바꾸는 것 밖에 없다. 한마디로 영원히 자기를 속이는 것이다.

앞서서 시야를 넓혀 가면서 점점 내가 사는 곳과 멀어지는 것에 대한 이야기를 했었다. 이제 관점을 바꿔보자. 과거로 가보자. 까마득히 멀리 떠나서 약 7만년 전 쯤으로 가보자. 유발 하라리가 인지 혁명이 시작되었다고 하는 그 시점쯤 되겠다. 그 인지 혁명이 7만년전에 인류를 하나로 만들고 공통된 목적을 추구하며 보이지 않는 상상의 것들을 믿게 끔 만들었다고 해서 그들이 살던 삶이 그 순간 가치 있어진 것은 아니다. 그들은 여전히 원시인이었으며, 먹는 것, 자는 것, 생존 하는 것이 그들의 삶의 목표였다. 두뇌는 현대인들과 비슷했을지라도 그들의 삶은 매우 비 상식적이었을 것이다. 제레미 다이아몬드는 저서 "총, 균, 쇠"에서 지금도 파푸아 뉴기니의 서로 다른 부족 사람 둘이 만났을 때를 일어나는 일을 이야기 해주었다. 파푸아 뉴기니에서 다른 부족 사람들이 서로 만났을 경우 그들은 대부분 이웃 부족 사람들이다. 이웃 부족 사람들끼리는 서로 살인이 일어나기도 하고 혼인이 일어나기도 한다. 그리고 사냥을 나갔기 때문에 둘은 무기를 가지고 있다. 이들은 서로를 인지하면 조심스럽게 상대에게 다가간다. 그리고 위협이 없다고 생각하면 나무 아래에 나란히 앉는다. 그리고 서로 자기 가계도에 대해서 이야기 하기 시작한다. 둘이 서로 인척관계가 있는지, 서로 알고 친하게 지내는 사람들이 있는지를 확인하기 위해서다. 그리고 그 확인이 필요한 이유는 서로 죽이지 않아야 할 이유를 찾기 위해서다. 이러한 일이 7만년 전에는 없었을까? 지금의 우리가 낯선 이들에게 얼마나 호의적으로 대하는지를 생각해 본다면 답은 상당히 뻔할 것이다. 그리고 지금처럼 완성된 언어가 있고, 사회적으로 서로 잘 엮여 있어서 일말의 대화가 통하는 상대라면 모르겠지만 사냥을 위해 떠돌아 다니는 두 집단이 서로 같은 언어로 소통할 수 있을 것이라는 기대는 상당히 하기 어려운 일이다. 그리고 이들도 당연히 사냥을 나온 것이기 때문에 무장을 단단히 하고 있다. 이들이 서로 만났을 때 어떤 일이 벌어졌을지는 여러분들의 상상에 맡기도록 하겠다.

그럼 이제 현대로 돌아와 보자. 여러분이 방금 전까지 상상했던 일들과 현대 지금 시대를 살고 있는 우리들이 어떤 차이가 있을까? 두뇌가 7만년 사이에 개벽하듯 변화했을 것이라는 생각은 들지 않는다. 차이가 있다면 크게 말해서 문명 그 자체에 있다. 현재의 인류가 벌이는 행태에 분개하면서 문명을 저주하고 싶은 생각이 들 수도 있다. 그리고 최근까지 벌어졌던 수많은 전쟁과 인종학살, 혐오범죄, 차별들을 떠올리면서 인류는 7만년 전과 전혀 달라진게 없다고 믿고 싶을 수도 있다.

하지만 딱 한가지 사실만 이야기 해보겠다. 지금의 시대는 인류의 어느 시대에 비해서도 폭력이 가장 적은 시대이다. 인정하기 싫을지 모르지만 인류는 7만년 동안 서서히 나아지고 있었다. 그리고 이것이 추세라면 앞으로는 더 나아질 것이다. 물론 굴곡이 있고, 어느 순간 상상하지도 못할 엄청난 일이 발생해서 여태껏 쌓아 왔던 수많은 업적들이 대부분 사라져 버릴 수도 있다. 그러나 역사에는 언제나 전쟁과 살인이 있었듯이 항상 그 사이 사이에는 찬란한 문명들이 존재했다. 황금기의 로마, 페르시아의 다리우스 시대, 그리스의 황금기인 페리클래스 시대, 그리고 미처 열거하지는 못했지만 어느 왕조, 어느 나라, 어느 문명에서도 황금기가 있었다. 이것은 7만년전을 기준으로 보면 매우 기형적인 일이다. 과연 인류에게 평화를 사랑하는 마음이 있는가 싶은 시대에 살고 있으면서도 돌아보면 지금이 그래도 낫다는 아이러니 같은 존재가 인류이다.

현대의 인류가 가지고 있는 문제가 작은 것은 아니라는 점은 인정하겠다. 하지만 인류가 자기보다 높은 계급의 사람들에게 목숨을 내놓고 살아가는 것이 당연한 시대에서 자유와 평등의 사상을 기반으로 어느 누구에게도 충성을 맹세할 필요가 없는 (이론적으로는) 세상에 있는 것만으로도 인류의 지성은 크나큰 발전을 한 것이다. 적어도 이 자유와 평등의 사상 만큼은 인류 역사상 어느 시대에도 이렇게 보편적으로 적용된 경우가 없었다. 만약 7만년 전보다 지금이 어떤 면에서건 조금이라도 나아진 것이 있다고 믿는다면, 이제 자기를 속여 볼 시간이다.

스스로에게 이렇게 이야기 해보자. 내가 하는 일은 인류 전체에게 아무리 작으나마 기여를 하는 일이라고. 소프트웨어 개발자라면 (나도 실제로는 하고 있지 못하지만) 오픈 소스에 기웃 거려 보자. 오픈소스를 다운 받고, 분석해보고, 거기에 조금이라도 기능을 추가해본다면 이미 인류에 기여한 것이다. 나는 아직 그럴만한 능력이 안되서 시도를 못하고 있는 일이지만 지금은 블로그라도 써서 올리면서 지식을 공유해 보려고 노력하고 있다. 이미 발전을 해왔고, 앞으로도 발전할 가능성이 있는 인류에 기대하는 바가 있고, 그 인류가 하는 일에 내가 조금이라도 보탬이 되는것이 내가 나를 충분히 속일 수 있는 일이 아닐까? 내가 하는 일하고 오픈 소스에 기여하는 것은 다른가? 그러면 이렇게 생각해보자. 내가 하는 일로 실력이 늘고, 늘어난 실력이 오픈 소스에 기여하는데 도움이 된다면, 내가 하는 일이 인류에 기여하는 가치 있는 일이 되지 않는가?


자 이제 자신을 속이는 방법을 좀 더 단순한 단계로 정리해 보겠다.

1. 인류는 발전해 왔다.

2. 내가 (특정한 사람이 아닌) 보편적인 사람에게 작은 일이라도 기여하면 내 일은 가치 있는 일이다.

3. 그러면 인류는 더 발전할 것이다.


사실 내가 하는 일을 가치 있게 만드는 방법은 1번에서 시작한다. 역사를 이해하는 것이 시작이다. 단순히 믿는 것과 어떤 식으로든 확인해가면서 확신을 쌓아가면서 믿게 되는 것은 엄연히 다른 일이다. 따라서 끊임 없이 과거를 공부해야 한다. 과거 인류에게 있었던 어떤 일들이 지금 현대를 살아가는 우리들에게 어떤 영향을 미치고 있는지를 알게 되는 것. 그것이 인류가 발전해 왔다는 것을 믿게 하는 원동력이다. 그리고 이를 통해 나 스스로에게 인류라는 가치 있는 존재에게 기여하도록 하는 것이 스스로를 가치 있게 만드는 일이다. 

인정할 것은 인정하겠다. 이것은 믿음일 뿐이다. 믿음은 잘 알았을 때보다 잘 속았을 때 더 잘 생겨난다. 하지만 알고 있는지 모르겠다. "진리"라는 것은 "보편되게 믿어지는 사실"이라는 것을. 그 대단한 진리라는 것 조차 정의를 살펴보면 일개 믿음일 뿐이다. 그리고 어차피 믿음이 속는 것이라면 적어도 더 가치 있어 보이는 것에 속아 주어야 한다. 대충 "인류" 정도 되면 나 조차도 깜빡 속일 수 있지 않을까?

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
,

Java에서는 다음과 같이 가변 길이 파라메터를 지원한다. 이 가변길이 파라메터는 재밌는 특성들이 있는데 이를 활용하는 방법을 알아보자.


우선 기본적인 사용법 부터 알아보도록 한다.

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

이 함수의 사용법은 아래와 같다.

target("A", "B", "C", "D");

target();

실행 결과는 아래와 같다.

A

B

C

D


이와 같이 0~* 개의 동일한 타입을 파라메터로 받을 수 있다. 만약 또 다른 파라메터와 함께 사용하고 싶다면 가변길이 파라메터는 맨 뒤에 위치 시켜야 한다.


자 여기서부터 재밌는 실험을 해보도록 하자. 혹시 가변길이 파라메터로 받은 인자를 가변길이 파라메터 함수에 다시 넣을 수 있을까? 코드는 아래와 같다.

public static void wrap(String...strings){

    target(strings); // 받은 가변 인자 파라메터를 그대로 넘김

}

      

public static void target(String...strings){

    String[] array = strings; // 배열화 된다.

    for(String str : strings) System.out.println(str);

}

다음과 같이 실행해 본다.

wrap("A", "B", "C", "D");

wrap();

실행 결과는 위와 같다. 즉, 가변인자 파라메터로 받은 인자를 다시 가변인자 파라메터로 넘기는 것이 가능하다.

재밌는 것은 가변길이 파라메터에 집어 넣은 파라메터들이 무엇으로 변환 되는지이다. target() 함수 두번째 줄에 보면 가변길이 파라메터들이 배열로 변환됨을 알 수 있다. 이것이 의미하는 바는 무엇인가? String...strings와 같은 가변인자 파라메터 형식은 여러 String 객체가 나열된 형식("a", "b", "c"와 같은)도 인자로 인정해 주고, String[] 형식 즉, 배열 한 개도 인자로 인정해 준다는 의미이다. 따라서 아래와 같은 사용도 가능하다.

String[] array = new String[]{"a", "b"};

target(array);

결과는 예상할 수 있을 것이다.


이 특성을 이용하면 몇가지 유틸리티 성 함수를 만들어 볼 수 있다. 우선 여러 개별 객체를 받아 배열로 변환해 주는 toArray(String...strings)와 같은 함수를 만들어 볼 수 있겠다.

public static String[] toArray(String...strings){

    return strings;

}


또한 객체의 배열을 인자로 받는 함수를 이용하기 쉽게 만들 수도 있다. 만약 제공된 API가 다음과 같은 모양이라고 가정하자.

public void api(Object[]);

이런 경우 가장 손쉽게 사용하는 방법은 api(new Object{......})와 같은 형식이다. 하지만 위의 toArray() 함수와 같이 한번 가변인자 파라메터로 통과 시켜 주면 더 간편하게 api() 함수를 사용할 수 있다.

Posted by 이세영2
,

복사해서 붙여 넣으면 끝!

중복 코드가 나쁘다는 것은 익히 알고 있지만 문제는 중복 코드가 자주 나타난다는 점이다. 여러가지 이유가 있겠지만 가장 큰 이유는 복사해서 붙여 넣기가 너무 쉽기 때문이다. 조금 다른 코드를 작성하는 경우에도 복사해서 붙여 넣고 일부만 수정해 주면 끝이다. 즉, 중복을 만드는 것은 매우 쉬운 일인데 상대적으로 중복을 없애기는 어렵다. 중복은 머리가 없어도 만들 수 있지만 중복을 없애는 것은 머리가 없이 할 수 없는 일이다.


중복에 대한 인식

중복에 대해서 크게 문제 삼지 않는 경우이다. 중복 코드에 대한 인식 문제는 프로그래밍에 대한 자세와 연관이 깊다. 코드를 만들고 돌아가면 끝이고 다시는 그 코드를 들여다 보고 싶지 않은 개발자들이 보통 이런 사고를 가지고 있다.


객체지향 설계 및 디자인 패턴에 대한 지식 부재

요컨데 중복 코드를 제거하는 도구와 정형화 된 방법들에 대한 이해가 부족하기 때문이다. 객체지향의 겉모습은 온톨로지(Ontology)를 기반으로 한 실제 세계에 대한 인식을 컴퓨터 상에 모사한 것으로 보이지만, 깊숙히 들여다 보면 중복을 효율적으로 제거하는 도구들로 채워져 있다. 문제는 객체지향을 이해하는 것보다 코드를 만드는 일이 더 선행된다는 점이다. 즉, 중복을 제거할 수 있는 도구는 모른체 코드를 만들기 시작한다. 당연히 코드는 중복으로 넘쳐날 수 밖에 없다.


발견하기 어려운 경우

중복 코드가 너무 멀리 있는 경우, 중복 코드를 만든지 너무 오래된 경우, 중복 코드가 너무 짧은 경우, 중복 코드가 다른 코드에 뭍혀 있는 경우, 중복 코드 블럭 중간에 다른 코드가 들어가 있는 경우 등이다. 아래 코드를 보자.

class Example{

    private int data;

    public Example(int data){

        this.data = data; // 중복

    }

    public void setData(int data){

        this.data = data; // 중복

    }

}

이 코드는 중복이 있다. 아래와 같이 고쳐 줘야 한다.

class Example{

    private int data;

    public Example(int data){

        setData(data); // 중복 제거

    }

    public void setData(int data){

        this.data = data;

        /* data 갱신시 수행할 일들 추가 */

    }

}

얼핏 보면 우스운 일이다. 단 한 줄의 코드고, 직접 수행하던 코드를 함수까지 써가면서 수정했다. 코드는 줄지도 않았고 오히려 함수 콜에 의한 연산만 증가했다. 하지만 기존의 코드는 명백한 중복 코드이다. 만약 data 값이 변경 되었을 때 해야 할 일이 생겼다면 어떻게 할 것인가? 기존의 코드에서는 생성자와 setter 함수 모두 그 일을 수행하도록 변경해야 할 것이다. 하지만 아래 코드에서는 setData() 함수 내부만 수정해 주면 된다. 코드는 한 줄 바뀌었지만 "data가 갱신 되었을 경우 해야 할 일"에 대한 코드가 들어가야 할 위치가 setter 함수 쪽으로 단일화 되었다. 단 한 줄의 코드가 중복인 경우라도 수정에 닫혀 있지 않다면 중복이다. 그리고 중복을 해결하면 코드가 짧아질 것이라는 선입견을 버려야 한다. 중복 코드를 없애는 것은 코드를 짧게 줄이는 것이 아니고 중복 코드가 발생시킬 수 있는 문제를 차단하는 것이다.


다 똑같은데 일부만 다른 경우

대표적인 예가 순회(방문) 코드일 것이다. 즉, 여러 객체들을 방문해서 어떤 작업을 수행하는 코드이다. 이 때 방문 코드가 매우 길어지면 상대적으로 방문해서 할 작업 코드는 짧아진다. 그러면 다른 작업이 추가되면 방문 코드는 복사해서 붙여 넣게 된다. 이런 형태의 코드들은 발견해도 바로 수정하기는 어렵다.


다른 코드와 섞여 있는 경우

중복 코드가 블럭 A와 블럭 B의 연속이라고 하자. 그리고 이 중복 코드가 두 군데 이상 존재하는데, 어느 한 쪽에서 블럭 A와 블럭 B 사이에 흐름과 관계 없는 코드를 집어 넣었다고 가정하자. 이런 경우에 중복 코드를 발견해 내기가 어려울 수 있다. 그리고 상황에 따라서는 삽입된 코드가 중복 코드들과 유사하거나 어떤 영향이 있는지를 알기 힘들어서 분리해 내기가 어려울 수도 있다.

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

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

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

public void example(){

    for(int i = 0; i < list1.size(); i++){

        /* list1에 대한 연산 */

        /* list1 + list2 + list3에 대한 연산 */

    }

} 

list1에 대한 연산이 중복 코드일 경우, 아래에 있는 코드와의 관계를 재빨리 파악하기는 힘들다. 왜냐하면 둘 모두 같은 for 문에 묶여 있기 때문이다. 이 경우 코드를 유심히 들여다 보지 않고는 중복 코드가 있는지 발견하기 어렵다.


매개가 필요한 경우

이 경우가 해결하기 어려운 문제 중 하나이다. 분명 코드는 중복인 것처럼 보이지만 다들 조금씩 다르고 해결하기에는 쉬워 보이지 않는다. 아래 코드를 보자.

class Boundary{

    int northLimit = 100;

    int southLimit = 50;

    int eastLimit = 20;

    int westLimit = 10;

   

    public int getNorthLimit() { return northLimit; }

    public int getSouthLimit() { return southLimit; }

    public int getEastLimit() { return eastLimit; }

    public int getWestLimit() { return westLimit; }

    public void setNorthLimit(int northLimit) { this.northLimit = northLimit; }

    public void setSouthLimit(int southLimit) { this.southLimit = southLimit; }

    public void setEastLimit(int eastLimit) { this.eastLimit = eastLimit; }

    public void setWestLimit(int westLimit) { this.westLimit = westLimit; }

}

각 데이터들에 대해서 반복적인 패턴이 나타난다. 즉, 데이터 선언과 getter / setter 선언이 그것이다. 이러한 중복은 얼핏 해결이 불가능해 보인다. 완벽하게 동일한 코드가 아니고 유사한 코드들의 나열이기 때문이다. 이것을 해결하기 위해서는 매개체가 필요하다.

enum Direction{

    NORTH,

    SOUTH,

    EAST,

    WEST

    ;

    public static int size(){ return values().length; }

}

class Boundary{

    int[]boundaries = new int[Direction.size()];

    public int getLimit(Direction direction) { return boundaries[direction.ordinal()]; }

    public void setLimit(Direction direction, int limit) { boundaries[direction.ordinal()] = limit; }

}

이것이 중복의 해결책인 이유는 다음과 같다. 만약 Direction이 추가되었을 경우, Boundary 클래스는 수정이 전혀 필요하지 않게 된다. 기존의 코드에서는 새로운 데이터가 추가될 때 getter/setter가 추가 되어야 했다는 점을 주목하자. 이렇게 매개체가 필요한 형태의 코드들은 중복을 발견해 내기가 어렵다.

Posted by 이세영2
,

많은 책에서 코드 중복이 나쁘다는 말이 나온다. 이 글에서 코드 중복이 발생시키는 문제들을 정리해 보도록 하겠다.


완벽하게 논리적인 사고로 만들어진 버그

중복 코드가 있을 경우, 개발자가 완벽하게 논리적인 사고를 한다고 해도 버그가 발생하게 된다. 이 문제를 첫번째로 놓은 이유는 그만큼 이 문제가 심각한 영향을 미치기 때문이다. 개발자의 능력이 아무리 뛰어나고 논리적 사고를 잘 한다고 해도 중복 코드가 있으면 비 논리적인 코드를 만들어 내게 된다.

아래 코드를 보자.

public void function1(){

    task1();

}

public void function2(){

    task1();

} 

위와 같이 두 개의 함수가 있다고 가정하자. 편의상 두 함수를 나란히 배치했지만, 두 함수가 멀리 떨어져 있다고 가정하자. 중복된 코드는 task1()이다. 여기서 task2()를 추가한다고 가정해 보자. 논리적으로 task1() 이후에는 항상 task2()가 와야 한다. 그러면 각 함수의 task1() 이후에 task2()가 실행되도록 수정되어야 할 것이다.

그래서 개발자는 function1()에서 task1() 이후 task2()를 실행하도록 코드를 수정한다. 하지만 가정했듯이 두 함수가 아주 멀리 떨어져 있다면 function2() 함수가 있는지 모를 수도 있고, 그래서 task2()를 실행하도록 수정하지 않았다면 그 코드는 버그로 남게 된다.

이 문제는 중복 코드가 들어 있는 함수 간의 거리, 중복 코드를 만든 후 지나간 시간, 중복 코드의 개수, 중복 코드의 길이에 비례하여 커진다.


중복의 강요

중복된 코드는 중복을 강요한다. Unit Test를 한다면 중복된 코드에 대해서 중복으로 테스트를 만들어야 한다. 중복된 코드는 중복된 주석, 문서, 설명을 요구한다. 복사 해서 붙여 넣기는 쉽지만 그것을 계속 유지하기는 어렵다.


OCP(Open Close Principle) 위배

코드는 수정에 닫혀 있어야 한다는 SOLID 원칙에 위배 된다. 하나의 문제를 수정하기 위해 여러 코드를 수정해야 하기 때문이다.


코드량 증가

중복 코드는 코드의 양을 증가 시킨다. 코드의 양이 늘어나면 코드를 읽는데 걸리는 시간이 늘어나고, 수정이나 디버깅 이슈가 발생할 가능성이 높아진다. 같은 기능을 구현한다면 짧고 간결한 코드를 작성하는 것이 좋다.


중복 코드 동일성 검사

우스운 일일지 모르지만 중복 코드들은 모두 동일해야 한다. 그래서 개발자들은 종종 중복 코드가 완벽하게 동일한지를 검사한다. 이런 일이 발생하는 이유는 중복 코드를 발견하기는 쉽지만 생각보다 제거하기는 더 어렵기 때문이다. 어쨌든 이런 검사도 불필요한 비용을 발생시킨다.


Posted by 이세영2
,