'전체'에 해당되는 글 76건

  1. 2016.12.04 객체지향의 올바른 이해 : 12. 책임 기반 패키징(packaging) 1
  2. 2016.12.03 객체지향의 올바른 이해 : 10. 조립 책임 3
  3. 2016.12.03 객체지향의 올바른 이해 : 9. 생성 책임 2
  4. 2016.11.29 객체지향의 올바른 이해 : 8. 구현 책임과 계약 책임 3
  5. 2016.11.26 객체지향의 올바른 이해 : 7. 의존(Dependency)과 책임(Responsibility)
  6. 2016.11.18 package private class에 대한 unit test 방법
  7. 2016.11.06 객체지향의 올바른 이해 : 6. 객체지향 개념 관계도
  8. 2016.11.05 객체지향의 올바른 이해 : 4. 중간 정리
  9. 2016.11.04 객체지향의 올바른 이해 : 3. 유연성과 객체지향 4대 특성 3
  10. 2016.11.01 객체지향의 올바른 이해 : 2. 유연성
  11. 2016.11.01 객체지향의 올바른 이해 : 1. 객체지향 언어의 대두
  12. 2016.10.11 (우리가 미처 알지 못한) 소프트웨어 공학의 사실과 오해
  13. 2016.10.07 Java 라이브러리(.jar) 동적 로딩
  14. 2016.10.06 [Programming Challenges] 지뢰 찾기(Minesweeper)
  15. 2016.10.06 [Programming Challenges]Sample 및 표준 입력 데이터 이중화
  16. 2016.10.01 Tell, don't ask 원칙(TDA 원칙) 3
  17. 2016.10.01 Adaptive Object Model(AOM) 패턴 및 그 구현
  18. 2016.09.30 Actor Model 패턴의 구현(Java)
  19. 2016.09.25 인간은 왜 비합리적이 되는가?
  20. 2016.09.24 객체지향의 올바른 이해 : 5. 정보 은닉(information hiding) 9
  21. 2016.09.24 Property List 패턴
  22. 2016.09.23 객체지향 용어 정리
  23. 2016.09.21 좋은 메소드 이름 만드는 법
  24. 2016.09.20 실용적인 Eclipse 단축키 모음 (엑셀 표 포함) 1
  25. 2016.09.19 내가 하는 일을 가치 있게 만드는 방법
  26. 2016.09.18 Mediator 패턴
  27. 2016.09.18 Facade 패턴
  28. 2016.09.18 Command 패턴
  29. 2016.09.18 Flyweight 패턴
  30. 2016.09.17 Chain Of Responsibility 패턴

이제 책임을 분리함으로써 소프트웨어의 구조를 잘 만들 수 있게 되었으니 책임을 중심으로 패키지를 만드는 방식을 이야기 할 차례다.


<단일 패키지 내 클래스들 : 지금까지 만든 클래스들을 각각의 소스 파일로 분리하여 하나의 패키지에 넣으면 이와 같은 모양이 될 것이다.>


패키지(package)는 소프트웨어를 배포하는 물리적 단위다. 만약 잘 만들어진 소프트웨어를 재사용해야 하는 상황이라면 패키지 단위로 재사용하는 것이 가장 현명한 선택이다. 그리고 패키지 단위로 배포를 하기 위해서는 패키지 구조를 잘 만들어 두어야 한다. 다행히 지금까지 만들어 왔던 책임 중심의 구조는 패키지로 구조화 시키기에도 매우 적합하다. 잘 패키징 된 소프트웨어는 소스를 분석하기에도 편리하다. Java의 패키지 구조는 디렉토리 구조와 매치된다. 따라서 패키징을 잘 하면 디렉토리 별로 잘 분류된 문서들처럼 손쉽게 필요한 소스들을 찾아 볼 수가 있다. 여기서는 지금까지 만든 클래스들을 어떤 형태로 패키징 하는 것이 좋으며, 특히 구현 책임을 외부로 노출시키지 않기 위해서 어떤 형태로 소스 파일들을 분리하고 가시성을 설정하는 것이 좋은지 이야기 해보도록 하겠다.


가시성 조정

가장 간단한 가시성 부분을 먼저 해결해보도록 하자. 우리는 지금까지 여러 책임을 분리하면서 그 기저에 단 한가지 목표를 두었다. 그것은 Client와 구현 객체의 분리이다. 이를 위해서 계약 책임을 도입하고, 생성 책임을 분리하고, 조립 책임을 분리했다. 결국 Client 클래스 눈에 구현 객체들이 보이면 안되는 것이다. 이것을 아예 설계 단계에서 방지할 수 있는 방법은 구현 객체들의 가시성을 낮추는 것이다. 만약 구현 객체들을 패키지 가시성 수준으로만 낮춘다고 해도 다른 패키지에서는 구현 객체들이 존재하는지조차 알 수 없게 된다. Client 클래스와 구현 클래스들을 서로 다른 패키지에 넣고, 구현 클래스들의 가시성을 패키지 수준(즉 class 선언부에 아무런 가시성 키워드가 없는 형태)으로 떨어뜨려 놓기만 해도 실수로라도 구현 객체에 접근해서 의존성이 발생하는 것을 미리 막을 수 있다.

그 다음엔 Client와 Client와 구현 객체를 별도의 패키지로 분리해 둔다. 이 시기에는 다른 기준이 없기 때문에 구현객체와 Client만 떨어뜨려 놓으면 된다. 일단 Client는 사용자의 입장이고, 다른 모든 클래스들은 사용자가 사용할 대상에 해당된다. 따라서 Client 클래스만 별도의 패키지로 설정하도록 한다. 이제 남은 부분은 모두 MasterContract 클래스와 그 조립을 위한 클래스들이다. Java에서는 기본적으로 서브 패키지 개념이 없다. 사실 모든 패키지는 독립적이다. 하지만 아무래도 이것은 불편하다. 왜냐면 상위 패키지와 그 하위 패키지로 구분해 두면 어떤 패키지가 다른 패키지에 종속적인지, 반대로 이야기 하자면 어떤 패키지가 다른 어떤 패키지에 의존적인지를 알기가 수월할 수 있기 때문이다. 우리의 예제에서는 일단 오직 Client만 MasterContract 클래스를 사용하도록 되어 있기 때문에 패키지의 구조를 계층적으로 구성하도록 한다. 만일 패키지 간에 1:1의 의존 관계가 존재하지 않는다면 하위 패키지 형태로 분류하지 않는 편이 좋다. 아래는 client 패키지와 그 하위 패키지, 즉 master 패키지를 구분한 예이다.

<client 패키지와 master 패키지의 분리. master 패키지는 client 패키지에 의해서만 사용되고 있으므로 하위 패키지 형태로 구성했다.>


패키지 계층 구성

master 패키지 내부에 있는 클래스들은 아직도 정리가 안 된 느낌이다. Assembler나 MasterContract 클래스와 같이 좀 더 종합적인 구조를 가지고 있는 것과 Contract1, 2와 같이 하위 의존성을 위한 클래스나 interface들이 뒤섞여 있다. 이런 형태로 패키지가 구성되어 있으면 일단 소프트웨어의 구조를 분석하는데 불편하다. 어떤 클래스가 다른 어떤 클래스와 어떻게 의존하고 있는지를 알기가 어렵기 때문이다. 또 이런 형태는 배포의 기준점을 잡기가 힘들다. 예를 들어 Contract1에 해당하는 클래스들만 외부에 배포하고 싶다고 할 때 일일이 그와 연관된 클래스 파일들을 골라서 배포해야만 한다. 따라서 master 패키지 내의 소스 파일들도 하위 패키지들을 만들어 분리해 주는 편이 좋다.

master 패키지를 구분한다고 할 때 그 단위가 될 수 있는 기준을 먼저 살펴보도록 하자.

1. MasterContract를 구성하는데 필요한 클래스들.

2. 단일 계약 책임(Contract1, 2)과 그 구현 클래스들.


1번의 MasterContract를 구성하는데 필요한 클래스들은 MasterContract와 그 조립 책임을 담당하는 Assembler 클래스이다. 이들은 여전히 master 패키지에 존재할 필요가 있다. 2번에 해당하는 각각의 계약 책임 클래스들은 서로 분리할 필요가 있다. 첫째 이유는 master 패키지에 있을 클래스들과는 다른 패키지에 놓여져야 하기 때문이고, 두번째 이유는 Contract1과 Contract2는 서로간에 의존성이 없고, 따라서 서로를 모르는 편이 좋기 때문이다. 그리고 세번째로는 소스를 분석하는 입장에서 Contract1과 Contract2가 다른 패키지로 구분되어 있을 때 자연스럽게 체계를 파악할 수 있기 때문이다. 그렇다면 결국 패키지의 구분은 master 패키지의 하위 패키지로 contract1과 contract2를 위한 패키지를 도입하는 형태가 될 것이다. 아래는 이와 같은 형태로 분류된 패키지 구성이다.

<master 하위에 contract1과 contract2를 분류한 패키지 구성>


우선 contract1과 contract2에 대한 패키지를 master 패키지의 하위 패키지 형태로 구성한 이유는 master를 client 패키지의 하위로 분류한 이유와 같다. 만약 contract1이나 contract2가 외부의 다른 패키지와의 의존관계가 있다면 이렇게 종속적인 형태로 구성해서는 안될 것이다. contract1과 2를 구분하는 것은 이슈가 없을 정도로 합리적인 선택이다. 약간의 이슈가 될 수 있는 부분은 Contract1Factory나 Contract2Factory를 함께 패키지에 넣은 것이다. 이들을 master 패키지에 두는 것도 고려해 볼 수 있다. 하지만 패키지 구성을 조정해야 하는 상황이라면 생성 책임은 역시 계약 책임과 함께 존재하는 것이 나은 선택일 수 있다.

위의 패키지에서 주목할 부분은 Task 클래스와 Job 클래스이다. 이들은 각각 Contract1과 Contract2에 대한 구현 클래스이다. 앞서서 이야기 한 것처럼 Client 클래스가 이들 구현 객체는 몰라야 하는 상황이다. 따라서 Task 클래스와 Job 클래스의 가시성을 패키지 가시성 수준으로 떨어뜨려 두었다.


배포과정에서의 이슈

위의 구성에서 보면 client 패키지가 가진 의존성은 모두 하위 패키지 형태로 구성되어 있기 때문에 client 패키지를 배포하는데 전혀 문제가 없다. 또한 master 패키지를 배포한다고 해도 조립책임 및 하위 계약 책임, 그리고 그 구현 책임들을 포함한 패키지 계층이 배포될 수 있는 구조이므로 문제가 될 것이 없다. 한가지 생각해 볼 부분은 각 계약 책임의 구체 구현 객체들에 대한 처리이다.

첫번째 이슈는 배포 시 Task와 같은 구현 객체가 필요하지 않고, 따라서 Contract1Factory와 같은 생성 책임도 필요가 없는 경우이다. 즉 단지 계약 책임만을 가지고 가고 구현 객체는 필요 없을 경우에는 어떻게 하는 것이 좋을까? 잘 생각해 보면 Contract1Factory는 Task에 의존적이다. Contract1은 계약 책임만 가지고 있으므로 Task 클래스와 같은 구현 책임이나 Contract1Factory와 같은 생성 책임에 의존하지 않는다. 따라서 Contract1은 contract1 패키지 내에서 독립적이다. 만약 구현 객체가 필요 없는 경우를 대비한다면 생성 책임을 가진 Contract1Factory와 구현 책임을 가진 Task 클래스를 묶어 contract1의 하위 패키지로 만들어 구성할 수 있다.

<구현 책임과 생성책임을 별도의 하위 패키지로 구성한 경우>


위의 구성에서 만일 Contract1의 계약 책임만을 배포하겠다고 하면 하위 패키지를 제외하고 배포시키면 된다.

배포 과정에서의 또 한가지 이슈는 위의 구성에서 impl 패키지 하위의 구현 책임들을 모두 함께 배포하되, 배포를 받은 입장에서 새로운 구현 객체를 등록, 사용하려고 할 수 있다는 점이다. 이 경우는 Contract1Factory의 생성 책임이 추가되어야 한다는 의미이다. 이 때 만일 Contract1Factory가 컴파일 된 형태로 배포된다면 간단하게 Contract1Factory를 상속 받아서 추가된 구현 객체에 대한 생성 메소드를 추가하는 형태로 구현하면 된다. 대신 새롭게 구현된 구현 클래스나 생성 책임을 가진 클래스는 위의 패키지를 배포 받은 쪽 소스 상에 추가되어야 할 것이다.


단위 테스트(Unit Test) 이슈

위의 패키지 구성에서 또 한가지 고려해야 하는 부분은 단위 테스트이다. 우선 책임을 종류별로 분리하고 이를 각각 클래스화 시켰을 때 단위 테스트는 그렇지 않은 형태보다 훨씬 수월하다. 우선 조립 책임에 의해 의존 관계를 파악하기가 매우 쉽고, 필요하다면 계약 책임을 구현한 mock 객체나 spy 객체 등을 생성하여 재조립(reassemble)하기도 매우 편리하다. 또 각각의 책임이 명확하기 때문에 테스트 작성 계획을 세우거나 테스트 클래스들을 합리적으로 계층화 시키기에도 좋다.

단 한가지 이슈가 될 만한 부분은 구체적인 구현 객체의 가시성으로 인한 테스트 가능성이다. 구현 클래스들의 가시성을 패키지 가시성 수준으로 떨어뜨려 두었기 때문에, 테스트 클래스가 구현 클래스들과 같은 패키지에 있지 않으면 클래스가 존재하는지조자 알 수 없다. 이에 대한 해법은 이미 Spring 프레임워크 등에서 사용하고 있는 폴더 분리 방식을 통해 해결 할 수 있다. 이에 대해서는 아래 글을 참고하기 바란다.

package private class에 대한 unit test


Posted by 이세영2
,

한번 만들어진 소프트웨어는 거기에 그냥 머물지 않고 점차 발전한다. 더 많은 요구사항을 수용하면서 점점 더 복잡해진다. 처음에는 구현 - 계약 - 생성 책임을 가진 객체들을 분리시키는 것 만으로도 이미 충분하다고 생각할지도 모른다. Client가 겨우 몇 개의 계약 책임만을 다루는 형태로도 충분할 수 있다. 하지만 애초에 소프트웨어는 한계를 정해두고 성장하지 않는다. 단지 몇 개의 계약 책임만 가지고 있다가 어느 순간 그 복잡성이 폭발적으로 증가할 수도 있다. 지금 설명하려고 하는 조립 책임은 현대의 소프트웨어로 발전되는 과정에서도 가장 최근에 성립된 개념이라고 할 수 있다. 조립 책임은 소프트웨어의 규모가 충분히 커지지 않으면 그 필요성을 인지하기 힘들기 때문이다.

객체들은 서로 의존한다. 일단 계약 책임과 생성 책임을 분리함으로써 그런 의존 관계를 어느 정도 줄일 수 있다. 또한 Client가 간단히 나열할 수 있는 수의 계약 책임들만을 가지고 있다면 특별히 조립 책임에 관심을 가지지 않을 수도 있다. 하지만 계약의 종류가 점점 더 많아지게 되면 양상은 점차 복잡해진다. 대부분의 소프트웨어는 Client와 Contract interface만의 관계를 가지지 않는다. 계약들은 계약들간에도 관계를 가지게 된다. 예를 들어 A라는 계약을 Client가 사용하지만 A라는 계약은 B라는 계약에 의존할 수 있다. 그러면 A라는 계약을 사용하려면 B라는 계약을 구현한 객체에 대한 참조를 누군가가 만들어 주어야 한다. 이러한 참조의 연결은 계약의 개수가 증가하고, 계약 간의 의존 관계가 복잡해지면 함께 복잡해질 수 밖에 없다. 소프트웨어의 동작 시작 시점이나 동작 중에 Client 혹은 계약들 간에 필요한 의존성을 연결시켜주는 책임을 조립 책임이라고 한다.

만약 소프트웨어의 개발을 시작하는 시점에서 이와 같은 조립을 고려하지 않는다면 어떻게 될까? Client 클래스가 보통은 이 작업을 수행하게 될 것이다. Client 클래스 입장에서는 필요한 시기에 필요한 객체가 있기만 하면 되므로 생성의 위치나 다른 계약들의 관계에 대해서는 별로 신경쓰지 않을 수 있다. 이렇게 되면 시간이 갈수록 점점 더 사용해야 할 객체들이 늘어나면서 각 객체들간의 의존 관계에 의해서 조립 책임을 모아 정리하기가 어려워지게 된다. 그리고 어떤 계약은 다양한 구현 객체들을 가지고 있고, 이들 구현 객체는 사용자의 선택사항이나 요구에 따라서 동적으로 교체되어야 하는 경우도 있다. 이런 상황에서 객체를 잘못 교체하게 될 경우 소프트웨어의 동작은 예측하기 어려워진다. 따라서 여러 객체가 협력해야 하는 소프트웨어라면 초기부터 이런 조립 책임은 일찍 고려되어야만 한다.

그러면 조립 책임을 어떤 식으로 분리해야 하는지 알아보자. 이 조립 책임을 Client 클래스가 가지고 있었다고 가정해보자. 앞서 얘기했듯이 조립 책임에 대한 명확한 구분이 없을 경우 Client는 필요에 의해 언제든 자신이 사용할 객체를 생성하여 사용할 수 있었다. 따라서 조립 책임은 명확하게 나타나지 않고 코드 상에 분산되어 있을 가능성이 높다. 결국 Client가 직접 조립 책임을 지는 경우 Client는 본질적인 기능 뿐 아니라 조립 책임이라는 부수적인 책임도 지게 된다. 이는 단일 책임 원칙에도 어긋나고 코드는 쓸데없이 복잡해질 수 있다. 따라서 이 경우에도 별도의 클래스를 만들어서 조립 책임을 가지도록 하는 편이 좋다. 또한 Client가 개별 계약 구현 객체를 직접 사용할 수도 있고, 여러 계약 구현 객체들의 조합된 단일한 객체를 요구할 수도 있다. 전자의 경우 생성 책임과 조립 책임을 구분하기가 힘들기 때문에 예제에서는 조립 책임을 가진 클래스가 완성된(즉 잘 조립된) 객체를 제공해주는 것으로 가정한다.

interface Contract1{}              // 계약 책임1

class Task1 implements Contract1{} // 구현 책임1

interface Contract2{}              // 계약 책임2

class Job1 implements Contract2{}  // 구현 책임2

class Contract1Factory{            // 생성 책임1

    public static Contract1 createTask1(){ return new Task1(); }

}

class Contract2Factory{            // 생성 책임2

    public static Contract2 createJob1(){ return new Job1(); }

}

class MasterContract{              // 완성 객체

    Contract1 task;

    Contract2 job;

    public void setTask(Contract1 task) { // 계약1의 의존성 주입

        this.task = task;

    }

    public void setJob(Contract2 job) {   // 계약2의 의존성 주입

        this.job = job;

    }

}

class Assembler{ // 조립 책임

    Contract1 task;

    Contract2 job;

    MasterContract master;

    public MasterContract assemble(){ // 조립 메소드

        master = new MasterContract(); // 1

        reassemble(Contract1Factory.createTask1()); // 2

        reassemble(Contract2Factory.createJob1());  // 3

        return master;

    }

    public void reassemble(Contract1 task){ // 계약1의 재조립 메소드

        this.task = task;

        master.setTask(task);

    }

    public void reassemble(Contract2 job){ // 계약2의 재조립 메소드

        this.job = job;

        master.setJob(job);

    }

}

class Client{

    MasterContract master;

    public Client(){

        Assembler assembler = new Assembler();

        master = assembler.assemble();

    }

}

조립 책임을 보여주기 위해서는 적어도 두 종류의 계약이 필요하기 때문에 코드가 좀 길어졌다. 하지만 매우 단순한 코드이므로 천천히 살펴보도록 하자. 우선 두 종류의 계약 책임과 구현 책임을 가진 요소들을 선언했다. Contract1과 Contract2가 두가지 계약에 해당한다. Task는 Contract1의 계약을 구현하고 있고, Job은 Contract2의 계약을 구현하고 있다. 원래는 Task의 종류나 Job의 종류가 다양한 것을 가정하고 있지만 코드가 너무 길어질 것 같아서 제외하였다. 계약이 두 종류로 늘었으니 생성 책임도 둘로 늘어난다. 그리고 전체가 모두 조립된 객체를 내주도록 가정하였기 때문에 완성된 객체를 나타내는 MasterContract 클래스를 선언하였다. 이 MaterContract 클래스는 현재 일반 클래스이지만 만약 MasterContract 클래스 역시 다양한 변경이 가능하도록 하려면 역시 계약 방식(interface를 선언하고 이를 구현하는 방식)을 취해야 한다. 하지만 그런 관계에 대해서는 이미 다루었고 예제의 단순성을 위해 일반 클래스로 선언하였다. 

Assembler 클래스는 이 글에서 설명하고자 하는 조립 책임을 가진 클래스이다. 우선 가장 먼저 Assembler가 조립할 대상 객체들에 대한 참조 변수들을 모두 선언하고 있다. 이들 참조 변수들은 이후 유용하게 사용될 예정이다. assemble() 메소드는 완성 객체를 생성해내는 메소드이다. 이 메소드가 실행되면 MasterContract 객체가 완벽하게 조립되어 나오는 구조이다. 내부를 들여다보면 우선 MasterContract 객체를 생성한다(1). 이 시점에서는 객체는 생성되겠지만 MasterContract가 의존하는 다른 객체들은 아직 연결이 안된 상태이다. 이제 첫번째 계약 구현 객체를 연결한다(2). 이 과정에서 Assembler는 reassemble()이라는 메소드를 이용한다. reassemble()메소드는 메소드 오버로딩을 통해 총 두 개의 메소드로 구성되어 있는데, 각각 두 종류의 계약 구현 객체를 조립하는 메소드이다. 이들 메소드는 2와 3에 의해 각각 계약 구현 객체를 생성 받아 이를 MasterContract 객체에 설정 메소드를 통해 집어 넣는다. MasterContract 클래스는 setTask() 메소드 또는 setJob() 메소드와 같이 의존하는 객체를 외부에서 집어 넣을 수 있는 메소드를 제공한다. 이렇게 의존 객체를 설정하는 방식을 의존성 주입이라고 부른다. 계약을 구현한 구체 객체를 사용하는 객체 내부에서 직접 생성하지 않고 외부로부터 주입 받는 것을 디자인 패턴에서는 전략 패턴(Strategy Pattern)이라고 부른다. 이들 reassemble() 메소드들이 호출되면 전체 조립이 끝난다. 그런데 굳이 reassemble() 메소드를 추출하여 실행하게 된 이유는 무엇일까? 이는 Assembler 클래스 위쪽에서 MasterContract와 각 계약 구현 객체에 대한 참조 변수를 선언한 이유가 같다. Client 객체는 실행 도중 어떤 이유로든 MasterContract 객체가 의존하는 의존 객체들을 바꾸고 싶어 할 수 있다. 이런 경우 적절한 의존 객체를 생성하여 Assembler의 reassemble() 메소드를 호출하기만 하면 기존의 MasterContract 객체의 의존 객체가 변경될 것이다. 이런 방식으로 실시간으로 사용자의 요구가 바뀌거나 옵션에 따라 기능이 달리지는 등의 동작을 구현해 낼 수 있다.

이제 Client 클래스 내부에서는 Assembler를 통해 MasterContract 객체를 생성 받아 사용하기만 하면 된다.

그러면 이처럼 조립 책임을 가진 클래스를 별도로 유지하면 어떤 장점이 있는지를 살펴보자. 무엇보다도 우선 좋아지는 것은 Client 클래스의 코드이다. 조립 책임은 소프트웨어가 복잡해지면 복잡해질수록 더욱 복잡성이 증가한다. 최초에는 Client 코드에 별 문제가 없겠지만 소프트웨어의 확장성을 극대화하기 위해서는 Client 클래스의 코드가 단순해져야만 한다. 또 한가지 장점은 소프트웨어의 구조를 파악하기가 매우 수월해진다는 것이다. UML의 클래스 다이어그램과 같은 것이 없더라도 Assembler 클래스만 분석해보면 어떤 객체가 다른 어떤 객체와 의존관계에 있는지를 손쉽게 파악할 수 있다. 특히 Assembler 클래스는 오직 조립에 대한 책임만을 지고 있기 때문에 의존 관계를 파악하기에 가장 적절한 형태로 구현된다. 만약 조립 책임을 Client 클래스가 지고 있거나 여러 객체들이 분산해서 의존 객체를 생성, 조립하는 경우라면 서로의 의존 관계를 파악하기가 매우 어려울 것이다. 의존 관계를 파악하기 어려운 코드는 의존성 관리가 어렵기 때문에 조금만 수정이 가해지더라도 금새 관계가 복잡해지고 순환 참조와 같은 문제가 발생하게 된다. 조립 책임을 분리하는 것의 또 다른 장점은 새로운 객체에 대한 조립을 구현할 때 조립 과정을 바로 확인할 수 있기 때문에 의존 관계의 안정성 여부를 바로 파악할 수 있다는 점이다. 앞서 말했듯이 순환 참조는 의존 관계 분석이 어려울 때 발생한다. 만약 필요에 의해서 새로운 의존 관계를 만들고 싶을 때 바로 이를 조립 책임 객체에 구현해 보면 정상적인 의존 관계를 만들 수 있는지 여부를 바로 알 수가 있다.

그러면 이제 우리의 소프트웨어가 어떤 구조로 이루어져 있는지를 클래스 다이어그램으로 파악해 보자.

우선 Client 클래스는 MasterContract 객체와 Assembler 객체에 대한 의존 관계가 있다. 매우 단순하게도 단 두 개의 객체에 대한 의존관계만으로도 충분한 구조이다. 혹시 필요에 의해서 계약 객체를 변경해야 할 경우에는 Contract1Factory나 Contract2Factory에 접근하여 객체를 생성할 필요가 있을 수는 있다. 하지만 그렇다고 각 계약 객체나 계약 자체를 Client가 다루어야 할 일은 없다. 어떤 상황에서도 Client 코드는 개별 계약과는 상관 없다. 이를 통해 전체 소프트웨어의 유연성은 유지된다. MasterContract 클래스의 경우 역시 그 의존 관계를 명확히 알 수 있을 정도로 단순한 관계만을 유지하고 있다. 이를 통해 기능을 수행하는 객체들의 관계를 매우 명료하게 알 수 있다. Assembler 클래스는 이 클래스 다이어그램에서 가장 복잡한 요소이다. 다이어그램에 나타나진 않지만 Assembler는 Contract1과 Contract2, 그리고 MasterContract 객체에 대한 참조 변수를 가지고 있기 때문이다. 하지만 조립 책임만을 가진다는 점을 명확히 이해하고 각 객체들의 기능을 수행하는 작업을 함께 구현하지만 않는다면 코드는 전체적으로 매우 단순한 형태를 유지하게 될 것이다. 나머지 클래스들의 관계는 모두 이전의 예와 같기 때문에 별도로 설명하지는 않겠다.

조금만 욕심을 부리자면 Assembler가 모든 기능 객체들을 나열하여 가지고 있다는 점을 이용할 수도 있을 것이다. 즉, Visitor 패턴의 accept() 메소드를 Assembler 클래스가 구현하여 Visitor 객체를 받아 모든 구성 요소들을 돌면서 횡단 관심사들을 구현해 볼 수도 있겠다. 횡단 관심사를 Visitor 객체가 구현할 것이고, Assembler 클래스는 단지 Visitor 객체를 받아 호출만 해주는 것이므로 크게 부담이 될 것은 없다. 이에 대한 판단은 여러분들에게 맡기겠다.



Posted by 이세영2
,

이전 글에서 우리는 구현 책임을 가진 객체와 이를 사용하는 객체와의 의존 관계를 어떻게 더 좋은 관계로 바꾸는지를 알아 보았다. 구현 객체를 직접 사용하는 것은 위험하다. 구현 객체가 어떤 방식으로 수정될지 모르고, 만일 수정되면 사용하는 쪽의 코드 역시 수정되어야 하기 때문이다. 또한 다른 구현 객체를 사용하고자 할 경우에는 다른 구현 객체를 사용하는 코드를 중복으로 만들어 내야만 한다. 이렇게 중복이 많고 수정 가능성이 높은 코드가 좋은 품질의 소프트웨어를 만들어 낼 수는 없다. 그래서 중간에 계약 책임을 가진 Contract interface를 도입하였다. 이를 통해서 Client 객체는 직접 Task 객체에 의존하지 않도록 만들었다. 하지만 아직도 객체의 생성은 직접 생성자를 호출해야 하기 때문에 Task 객체에 대한 의존성이 완전하게 해결되지 못한 상태이다.

GoF의 디자인 패턴에는 이 문제를 해결할 수 있는 다수의 패턴을 포함하고 있다. GoF는 이들을 생성 패턴이라고 분류하였다. 생성 패턴은 GoF의 23가지 전체 패턴 중에서 5가지를 차지하고 있다. 단지 객체를 생성하는 것에 지나지 않는데도 이 같이 큰 비중을 차지한다는 것은 그만큼 객체 생성을 관리하는 것이 중요하다는 것을 의미한다. 이 글은 디자인 패턴을 소개하는 목적으로 쓰여진 것이 아니기 때문에 패턴에 대해서 자세하게 설명하지는 않겠지만, 이들 패턴이 추구하는 목표를 명확하게 하는데 주력해 보도록 하겠다. 여기에 가장 적합한 것이 Factory Method가 아닐까 한다.

디자인 패턴에는 동명의 디자인 패턴이 있다. 여기서 소개할 것은 디자인 패턴이 아니라 Factory Method라고 불리는 메소드, 즉 함수이다. "자바 API 디자인"이라는 책에서는 이 Factory Method를 활용하는 몇가지 예가 나와 있다. 우선 코드를 보면서 이야기 해보자.


class Base{}

class Subject extends Base{   

    // 기본형   

    private Subject(){}// 외부 생성 금지

    public static final Subject create(){ // Factory Method

        return new Subject();

    }

   

    // 구체적인 타입을 숨길 수 있다.

    public static final Base create2(){

        return new Subject();

    }

   

    // 인스턴스 캐싱(Flyweight)과 함께 적용이 가능하다.

    private static Subject instance = null;

    public static final Subject create3(){

        if(instance == null){

            synchronized(Subject.class){

                if(instance == null){

                    instance = new Subject();

                }

            }

        }

        return instance;

    }

   

    // 객체 생성 시점 이후 해야 할 일을 수행하기 용이하다.

    public static final Subject create4(){

        Subject s = new Subject();

        // 객체 생성 이후 초기화 작업

        // this 키워드를 활용하거나 다형성을 이용하는 위험한 작업을 수행

        return s;

    }

   

    // 객체 생성과 그 직후의 일을 동기화 할 수 있다.

    public static final synchronized Subject create5(){

        Subject s = new Subject();

        // 객체을 비롯하여 생성 이후 동작시까지 동기화가 필요한 작업을 수행할 수 있다.

        return s;

    }

}

위의 코드는 Factory Method를 활용하는 다양한 예들이다. 가장 기본적인 형태는 생성자를 private으로 바꾸고(외부에서 생성하는 것을 금지하기 위해서), create라는 자기 자신 타입을 리턴하는 정적 메소드(static)를 선언한 후, 그 안에서 자기 자신의 객체 하나를 생성하여 제공하는 것이다. 이 방법은 외부에서 자신의 생성자 메소드를 호출하는 것을 방지하고, 객체의 생성이 완료된 후부터 create() 메소드의 호출이 종료될 때까지, 즉 객체가 리턴될 때까지 외부에서 생성된 객체를 접근하지 못하도록 한다. 여기까지는 일반적인 생성자 메소드와 용도가 다르지 않다. 그 활용은 다음에 나오는 여러 create 메소드들이 보여주고 있다.

첫번째 용도는 구체적인 객체의 타입을 감추는 것이다. 위의 Subject라는 클래스는 Base 클래스를 상속 받는다. 즉, Subject라는 클래스는 다형성에 의해서 Base라는 타입도 가지게 된다. 유연성을 발휘하기 위해서는 구체적인 타입을 숨기는 것이 좋다는 점을 이미 알고 있다. create2() 메소드는 Subject라는 구체적인 객체를 생성하지만, 생성된 객체가 리턴될 때는 그 상위 타입인 Base 타입으로 리턴된다. 따라서 이 메소드를 통해 생성된 객체를 받는 쪽에서는 그 구체적인 타입을 알 수 없다.

두번째 용도는 인스턴스 캐싱이다. 어떤 객체는 소프트웨어 런타임 전체에서 단 한번만 생성되는 편이 좋을 수 있다. 그리고 그 객체가 이용될지 아닐지는 잘 모르는 경우가 있다. 이런 상황에서는 객체가 사용되는 시기까지 생성을 늦추는 편이 좋다. 즉, create3()과 같이 객체를 외부에서 요구하는 순간까지 객체의 생성을 미루다가, 일단 한번 객체가 생성된 이후에는 그 객체를 instance 참조 변수에 할당하여 두고, 이후 또다른 create3() 메소드 호출이 있으면 이미 생성된 객체를 내주도록 하는 것이다. 이러한 지연 로딩 방식을 인스턴스 캐싱이라고 부른다. 그리고 이렇게 늦게 객체를 생성하는 것을 디자인 패턴에서는 Flyweight 패턴이라고 부른다. 또한 위와 같이 객체 생성에 Flyweight 패턴을 적용한 것을 특별히 Singleton 패턴이라고도 부른다.

세번째 용도는 객체의 초기화 작업이다. 많은 객체들이 생성과 함께, 또는 생성 직후에 초기화 작업을 필요로 한다. 그래서 보통 생성자 내부에 여러 코드들을 집어 넣곤 하는데, 이것이 항상 안전하지는 않다. 어떤 상황에서는 메모리가 부족할 수도 있고, this 참조가 필요해서 사용해야 할 경우도 있다. 이런 작업은 객체가 생성된 이후에는 안전하지만 생성자 내부에서 사용하는데는 위험이 따른다. create4()와 같이 Factory Method를 활용하면 이 같은 문제를 해결할 수 있다. Factory Method는 객체의 생성을 안전하게 완료하고, 메소드가 종료되기 전까지의 시간을 벌어 준다. 만약 생성자 내부에서 해야 할 위험한 일들을 Factory Method의 객체 생성 이후에 수행한다면 객체에 대한 참조를 이용한다거나, 객체가 이용해야 할 다른 객체를 생성하는 등의 작업을 할 때 안전성을 확보할 수 있다.

네번째 용도는 생성의 동기화이다. create5() 메소드는 synchronized 키워드를 통해 Subject 클래스 전체에 대해 동기화 되어 있다. 만일 Subject가 멀티쓰레드 환경에서 동작하도록 설계 되어 있어야 한다면 객체의 생성 역시도 멀티쓰레드 환경에서 보호될 필요가 있을 것이다. 특히 생성자에서 여러 작업을 수행해야 하는 경우에는 더더욱 그렇다. 생성자는 synchronized 키워드를 붙일 수 없기 때문에 보다 안전하게 Factory Method를 이용할 수 있다.


자 우리는 이 중에서도 특히 객체들 간의 의존 관계를 끊는데 더욱 관심이 있다. 이와 관련해서는 구체적인 객체의 타입을 감추는 Factory Method의 특성을 이용할 수 있다. 하지만 이제와서 밝히는 것이지만 Subject 클래스 내부에서 Factory Method를 제공하면서 리턴 타입만 Base 타입으로 하는 형태는 사실 의미가 없다. 상위 타입을 리턴하는 Subject 클래스의 create2() 메소드를 사용하는 코드는 다음과 같다.

Base base = Subject.create2();

위 코드를 보면 알 수 있듯이 Subject 클래스라는 명칭(symbol)이 나온다. 명칭이 나오면 의존 관계가 성립된다. 이런 상황에서 위의 코드가 Subject라는 구체적인 객체에 대해 모른다고 말할 수 없다. 따라서 중요한 점은 Subject 클래스가 아니라 다른 클래스가 Subject에 대한 Factory Method를 제공해 주어야 한다는 점이다.

다시 Client - Contract - Task의 예로 넘어가보자. 우리는 Client와 Task간의 의존성을 끊고 싶다. 그럴려면 Factory Method를 통해 Task 객체를 생성하되, Contract 타입으로 리턴해 주도록 하면 된다. 그러면 Task에 대한 Factory Method를 어디에 구현해야 할까? 일단 Client 클래스는 탈락이다. Client와 Task간의 의존성을 끊는 것이 목적인데 Factory Method 내부에서는 Task의 생성자를 호출해야 하기 때문이다. Task 클래스도 탈락이다. Subject에서의 예와 같이 Task 클래스가 직접 Factory Method를 제공하게 되면 의존 관계가 없어지지 않기 때문이다. Contract interface도 역시 탈락이다. Contract는 interface이기 때문이다.(만약 계약 책임을 interface가 아니라 추상 클래스나 일반 클래스가 가진다면 Factory Method를 그 클래스에 구현하는 것이 가능하다. 이 방법은 "테스트 주도 개발(TDD)"이라는 책에 잘 나와 있으니 참고하기 바란다. 이 예에서는 책임을 분리하는 방법을 설명하는 것이 목적이므로 계약 책임과 생성 책임을 동시에 갖게 되는 방식을 사용하지는 않겠다.) 그러면 남는 방법은 별도의 클래스에게 생성 책임을 맡기는 것 뿐이다. 아래의 코드를 보자.

interface Contract{ // 계약책임

    public void method();

}

class Task1 implements Contract{ // 구현책임

    public void method(){}

}

class Task2 implements Contract{ // 구현책임

    public void method(){}

}

class ContractFactory{ // 생성 책임

    public static Contract createTask1(){ return new Task1(); }

    public static Contract createTask2(){ return new Task2(); }

}

class Client{

    Contract task = ContractFactory.createTask1();  // 1

    public void function(){

        task.method();

    }

}

위 코드에는 새롭게 CotractFactory 클래스가 추가되었다. 이 클래스에는 각종 Task 객체들을 생성할 수 있는 Factory Method들이 선언되어 있다. 잘 보면 Task1이나 Task2와 같은 객체를 생성하긴 하지만 Contract 타입으로 리턴하는 것을 알 수 있다. 따라서 ContractFactory를 통해 Task 객체들을 생성하면, 객체들을 리턴받은 쪽에서는 구체적인 타입을 알 수 없다. Client 클래스에서는 이전에 직접 Task1 또는 Task2의 생성자를 호출하는 대신 1의 코드와 같이 ContractFactory를 이용하여 Task 객체들을 생성하고 있다. 자세히 보면 이제 Client 클래스의 코드에는 어느 곳에서도 Task1이나 Task2를 찾아볼 수 없다. 즉 이제는 구체적인 구현 책임을 가진 객체들과의 의존이 완전히 끊어졌음을 알 수 있다.

현재의 상태를 클래스 다이어그램으로 나타내면 아래와 같다.

이전의 다이어그램과 비교해 보면 다음과 같은 부분이 달라졌다. 일단 ContractFactory라는 클래스가 추가되었다. Client 클래스는 ContractFactory를 통해 Contract 타입의 객체를 얻어가도록 변경되었다. 그리고 Client가 직접 Task1이나 Task2와 의존하던 의존성 연결이 사라졌다. 대신 ContractFactory가 의존성을 대신 가지게 되었다. 이제 전체적으로 보자면 Client 클래스는 Task1과 Task2에 대한 직접적인 의존 관계가 하나도 없다. 즉, Client 클래스는 구체적인 구현 책임을 가진 클래스와의 의존성이 없어진 것이다. 대신 Contract interface와 ContractFactory 클래스와의 의존성이 생겨났다. 일단 구체적인 구현 객체들과의 의존성을 없애겠다는 목표는 달성하긴 했지만, 구체적인 작업을 수행하는 것과는 별로 상관이 없어 보이는 Contract interface나 ContractFactory와 같은 부수적인 요소들과의 의존 관계가 생겨났다. 그러면 지금의 이 구조가 기존의 구조보다 더 낫다고 말할 수 있는 이유는 무엇일까?

가장 먼저 이야기 할 수 있는 것은 새로 발생한 의존은 구체적인 작업, 즉 기능과는 무관하다는 점이다. 기능은 요구사항의 변화에 민감하다. 요구사항이 변하면 기능도 변한다. 요구사항이 추가되면 기능도 추가 되어야 한다. 하지만 기능과 무관한 것은 그 요구사항도 잘 변하지 않는다. 간단하게 말해서 Client는 변화에 민감한 객체들과의 의존관계를 끊고 잘 변하지 않는 것들과의 의존관계를 맺게 된 것이다. 따라서 Client는 구체적인 구현 객체들의 변화에 의해 부수적인 변화가 발생할 가능성이 없어졌다. 또 다른 이점은 구체적인 구현 객체들의 수정도 자유로워졌다는 점이다. 구체적인 객체를 사용하는 곳이 많다는 것은 구체적인 객체를 수정할 경우, 그에 영향을 받아 수정되어야 할 객체들이 많다는 것이다. 이는 새로운 요구사항을 수용하는데 있어서 보수적이 되는 이유가 된다. 이런 형태로 구성된 소프트웨어는 사용자의 요구를 받아들이는데 보수적이 될 수 밖에 없다. 그리고 이런 소프트웨어는 사용자에게 인기가 없다. 또 한가지 이득은 계약에 의해 발생되는 것이다. 구체적인 구현 객체들은 계약을 무조건 따라야 한다. 따라서 계약을 수정할 경우 구체적인 객체들은 어쩔 수 없이 함께 수정되어야 한다. 이것은 interface를 수정하기가 어려워지는 이유이다. 하지만 반대로 이것이 이득일 경우가 있다. 즉, interface를 수정해야 할 중요한 요구사항이 들어왔다고 가정해보자. 그리고 잘 고려해 보니 유사한 요구사항들도 새로운 요구사항의 일부를 수용해야 할 것으로 판단이 되었다고 가정해 보자. 이 경우 기존에는 어떤 요구사항들이 어떤 구현 객체에 구현되어 있는지를 찾아보고 수정해야 한다. 그리고 이 수정에 따라서 구현 객체를 사용하는 Client 쪽도 함께 수정해 주어야 한다. 구체적인 구현 객체의 수만큼 말이다. 하지만 계약 책임을 지는 interface를 이용할 경우, interface를 수정하게 되면 자연스레 구체적인 객체에 대한 수정 사항이 발생한다. 구체적인 객체들은 interface를 제대로 구현하지 않으면 컴파일 자체가 안되기 때문이다. 따라서 수정하려는 입장에서는 문제가 매우 명확하게 보인다. 또한 Client 객체의 경우에는 계약 책임을 가진 interface를 이용하면 단지 interface에 의존하는 코드만 고치면 된다. interface를 이용하지 않았다면 구현 객체와 의존하는 코드들을 모두 고쳐야 했을 것이다. 마지막으로 ContractFactory를 이용하는 경우에는 생성이라는 책임이 명확하므로 생성에 대한 변경이 이루어졌을 때 그 책임이 어디 있는지 명확하고, 그에 대한 변경이 어디서 이루어져야 하는지를 정확하게 알 수 있다. 객체 생성은 어느 곳에서나 이루어질 수 있다. 참조 변수를 선언할 때도 생성할 수 있고, 생성자에서도 생성할 수 있으며 메소드 내에서도 생성할 수 있다. 이러한 자유도는 "관리" 측면에서는 매우 불편하다. 예를 들어 기존에는 객체에 대한 생성 이후 즉각 정상적인 동작이 가능했었는데, 어느 순간 초기화 작업이 필요해졌다고 가정하자. 그러면 이제 객체를 생성하는 코드들을 일일이 찾아가서 수정해 주어야 한다. 이것은 중복코드를 발생시킬 뿐더러 버그가 발생할 가능성도 높이게 된다. 만약 위와 같이 Factory Method를 구현한 별도의 생성 클래스를 두게 되면 생성 후 초기화 작업이 추가되는 경우에도 Factory Method에 단 한번만 초기화 작업을 넣어주면 모든 수정이 완료된다. 새로운 구현 객체가 추가되거나 기존의 객체가 사라진 경우 일단 ContractFactory의 Factory Method만 추가/삭제 해주고 나면 그 이후에는 다른 코드들이 모두 에러 처리되고 컴파일 불가능 해질 것이기 때문에 손쉽게 문제를 찾아 고칠 수 있게 된다.








Posted by 이세영2
,

다시 앞서의 예로 돌아가보자.

class Task{

    public void method(){}

}


class Client{

    Task task = new Task();    // 1

    public void work(){

        task.method();         // 2

    }

}

책임의 종류를 따르자면 Task 클래스는 구현의 책임을 가지고 있다. 현재 명시적인 기능을 구현하지는 않았지만, Client 입장에서 Task는 어떤 기능을 수행하길 바라는 대상이다. Client 클래스는 1에 의해서 Task 객체를 생성하는 책임을 가지고 있다. 그러면서 2와 같이 Task 객체의 메소드를 이용함으로써 의존하고 있다.

만일 만들고자 하는 소프트웨어가 간단히 구구단 정도 출력하는 일이라면 지금의 구조로도 충분히 훌륭한 소프트웨어를 작성할 수 있다. Task 객체에 구구단 출력 기능을 구현하고, 구현된 구구단 출력 메소드를 Client 객체가 호출해 주면 되기 때문이다. 그리고 위의 예에서는 다른 어떤 보호 장치도 없이 Task 객체에 Client가 직접 의존하고 있다. 이것은 마치 Task 객체가 절대 사라지지도, 변경되지도 않을 것임을 확신하고 있는 것이나 다름 없다. 아무래도 Task 객체는 Client 객체와 매우 밀접한 관계에 있는 듯 하다. 그렇지 않고서야 서로 어떤 안전장치도 없이 의존할 수 있겠는가?

이와 같은 상황을 조금 더 이해하기 쉽게 하기 위해서 현실 세계의 예를 하나 도입해 보도록 하겠다. 잠시 스스로가 작은 음식점 주인이 되었다고 가정해보자. 적은 자본을 투자한 관계로 우선 혼자서 가게를 내기로 한다. 혼자 가게를 낸 상황이라면 가게의 모든 일을 주인인 내가 다 해야 하는 상황이다. 위의 예에서 보면 Client가 Task 객체에 의존하지 않고 혼자서 일을 모두 처리하는 상황이다. 시간이 조금 지나서 가게를 찾는 사람도 늘고, 아무리 음식점이라도 전문성이 중요하기 때문에 나보다 더 나은 주방장을 구해 일을 해보기로 한다. 그러면 주인인 나는 고용된 주방장에게 일을 맡기는 형태로 가게를 운영하게 될 것이다. 위의 예에서 Task 객체를 생성하고 메소드를 호출하는 과정과 유사하다. 하지만 뭔가 좀 위태로워 보인다. 주방장을 고용하는데 있어서 어떠한 안전장치도 없다. 음식점의 예에서 보면 취직한 주방장이 가게를 갑작스레 그만 둔다거나, 성실성이 부족하여 종종 가게에 나타나지 않는다거나, 음식 솜씨가 좋다고 해서 고용했는데 맛이 형편 없다거나 하는 위험한 상황이 있을 수도 있는데 말이다. 이런 상황에서 주인인 내가 안전하게 주방장을 고용할 수 있는 방법은 무엇일까? 바로 주방장과 계약을 하는 것이다. 가게 주인인 나는 주방장을 고용하기 위한 일련의 계약서를 작성해 둔다. 출퇴근 시간, 월급여, 경력 사항, 업무 내역과 같이 이슈가 될만한 사항들을 고용의 조건으로 만들어 두는 것이다. 이렇게 되면 좋은 점이 두가지 생긴다. 하나는 고용 조건에 합의한 주방장과 업무 시 직접 주방장이 할 수 있는 일을 물어보고 일하지 않아도 된다. 단지 계약서에 나열된 업무를 중심으로 일을 시키기만 해도 계약에 합의한 주방장은 당연히 그 일을 할 수 있을 것이다. 또 만일 기존의 주방장이 일을 그만두고 다른 주방장을 고용하게 되더라도 계약서에 합의 하기만 한다면 실제 업무 지시 방식을 바꾸지 않아도 새로운 주방장과 일하는데 문제가 없을 것이다.

객체지향으로 넘어와서 음식점 주인과 주방장의 관계 사이에 있던 "계약서"라는 개념이 무엇일까? 바로 interface이다. 이와 관련해서는 정확한 개념이 필요하기 때문에 잠시 정확한 개념을 설명하고자 한다.
앞서 지휘자와 개별 연주자들 간의 관계에서 "연주자"라는 개념을 도입했었다. 연주자는 개별 연주자의 차이점을 배제하고 공통점만 취한 추상적인 개념이다. 이와 상응하는 개념이 객체지향 언어에서의 interface이다. 그리고 가게 주인과 주방장 사이에 "계약서" 개념을 도입했는데 이 역시 interface를 염두해 둔 개념이다. 이들 두 예와 interface는 어떤 관계가 있을까? 우선 "연주자" 개념 역시 "계약서" 개념과 동일하다는 점을 설명할 필요가 있다. 지휘자 - 개별 연주자 간에는 직접적인 계약 관계가 필요하지는 않지만, 적어도 연주가 성공할 수 있으려면 "연주자" 개념은 강제성을 띄어야 한다. 연주하세요라고 말했을 때 연주를 시작하고, 연주를 멈추세요라고 말했을 때 멈추지 않으면 연주는 아름답게 이뤄지지 않을 것이다. 그런 강제성을 직접적으로 명시해야 할 필요가 있다면 그것이 "계약서"가 되는 것이다. 자 그럼 interface와 계약서의 관계를 이야기해 보자.
interface는 추상 공개 메소드(abstract public method)들의 집합으로 이루어진 추상 클래스(abstract class)이다. 만약 Java처럼 interface라는 명시적인 대상이 없는 언어를 다루고 있다면 추상 공개 메소드만 선언된 추상 클래스를 떠올리면 된다. 추상 클래스란 인스턴스화(객체화)가 불가능한 클래스다. interface는 추상 클래스이므로 interface 역시 객체화시킬 수 없다. 추상 공개 메소드는 구현부가 없는 메소드이다. 구현부가 없으므로 당연히 동작시킬 수 없다. 자기 자신은 구현부가 없지만 만일 interface를 상속한 경우(Java에서는 이를 상속이라 부르지 않고 구현(implements)이라고 부른다. 앞으로는 Java의 개념에 따라서 구현이라고 부르도록 하겠다.) 추상 공개 메소드의 구현부를 구현해야 할 의무가 생긴다. 추상 공개 메소드를 구현하고 싶지 않은 하위 클래스는 추상 클래스여야만 한다.
이러한 개념들에 비추어 보면, interface는 이를를 구현하는 하위 클래스들에게 정해진 공개 메소드를 꼭 갖추도록 강제하는 역할을 한다는 점을 알 수 있다. interface 스스로는 직접적인 구현을 제공하지 않는다.
이런 interface의 특징은 두가지 특성을 가져다 준다. 우선 interface를 구현하는 구체적인 객체에게는 강제성을 주면서도 자율성을 보장한다. 강제성은 interface가 선언한 추상 공개 메소드를 꼭 구현하도록 한다는 점에서 그렇다. 대신 interface는 어떤 형태로도 구현을 가이드 하지 않기 때문에 구체적인 객체는 어떤 방식으로든 자유롭게 구현할 수 있다. 이는 연주자가 연주하세요와 멈추세요에 반응해야 하는 것은 강제이지만, 개별 연주자들의 실제 연주 내용(구현)은 서로의 악기에 맞게 자율적으로 할 수 있다는 것과 같다. 이를 통해서 얻을 수 있는 이점은 개별 연주자의 범위를 넓힐 수 있다는 것이다. 단지 연주하세요와 멈추세요에만 응답할 수 있다면 서양의 악기든 동양의 악기든 상관하지 않고 모든 악기 연주자들을 구현해도 좋다. 이는 다양한 요구사항을 반영할 수 있는 역할을 한다. interface가 주는 또 하나의 특성은 구체적인 구현 객체들을 모르더라도 interface를 구현한 객체들이라면 모두 사용할 수 있다는 것이다. interface를 구현한 모든 객체들은 interface가 제공하는 공개 메소드를 구현해야만 한다. 이것은 강제적이다. 따라서 사용하는 측에서는 interface만 보고 객체를 다루더라도 전혀 지장이 없다. 사용하는 측에서 매우 다양해질 수 있는 구체적인 구현 객체를 모르고도 그 객체들을 다룰 수 있다는 점은 유연성에 매우 큰 이득이다. 

계약 책임의 도입

이제 우리의 예제에 계약 책임을 도입해볼 차례다. 계약 책임은 interface가 가지고 있다. 따라서 interface를 선언할 필요가 있다. 그리고 구현 책임을 가진 Task 객체는 그 interface를 구현해야 한다. 마지막으로 사용자인 Client 객체는 Task 객체를 직접 사용하지 않고 interface를 이용해서 Task 객체를 다루어야 한다. 이와 같은 형태로 코드를 수정하면 아래와 같아진다.

interface Contract{

    public void method();

}

class Task implements Contract{

    public void method(){}

}

class Client{

    Contract task = new Task();    // 1

    public void function(){

        task.method();             // 2

    }

}


interface가 계약이라는 점을 명시하기 위해서 interface의 명칭(symbol)은 Contract라고 붙였다. 그리고 이 계약서에는 method()라는 공개 추상 메소드를 구현하도록 명시되어 있다. 따라서 Contract interface를 구현하는 클래스는 항상 method()도 구현해 주어야 한다. 위의 예제에서는 Task 클래스가 Contract interface를 구현하고 있다. 따라서 Task 클래스는 method()를 역시 구현하고 있다. Task 객체를 사용하고자 하는 클래스인 Client 클래스는 "계약"에 따라서 Task를 사용해야 한다. 따라서 기존에는 Task 참조 변수를 선언하였으나 이제는 Contract 참조 변수로 대체되었다.(1번 라인) 하지만 실제 사용할 객체는 Task 타입이므로 Task 객체를 생성하여 Contract 참조 변수로 참조하도록 되어 있다. 2번 라인의 메소드 호출은 일면 이전의 코드와 같아 보인다. 하지만 task 참조 변수는 그 타입이 Contract로 변경되어 있다. 따라서 2번 라인의 호출은 직접 Task 객체의 메소드를 호출하는 것이 아니라 Contract 타입의 method()를 호출하는 것이다. 이와 같은 관계를 이해하기 위해서 다음의 예제도 한번 살펴보자.

interface Contract{

    public void method();

}

class Task2 implements Contract{

    public void method(){ /* something else */ }

}

class Client{

    Contract task = new Task2();

    public void function(){

        task.method();            // 메소드 호출

    }

}


위의 예제에서는 Task 클래스 대신 Task2 클래스를 선언하고 사용하고 있다. Task2 클래스 역시 Contract interface를 구현하고 있으므로 method()도 동일하게 구현하고 있다. 하지만 실제 동작은 기존의 Task 클래스와는 다르게 선언했다고 가정해 보자.
이제 Task2를 사용하기 위해서 Client 클래스에서는 Task2 객체를 생성하여 Contract 타입의 참조 변수인 task에 할당한다. 그리고 메소드 호출 부분으로 넘어가보자. 이전의 예에서와는 다르게 task 참조변수에는 Task2 객체가 참조되어 있다. 따라서 메소드 호출 부분에서 메소드를 호출한 경우 Task2 객체의 메소드가 호출된다. 즉, 이전의 예에서 할당된 객체는 Task 객체였고, 이번읜 Task2 객체이므로 실제 호출되는 메소드는 달라지지만 둘 다 같은 형태로 호출이 된다. 이는 참조를 직접 Task 객체나 Task2 객체로 하지 않고 Contract 타입으로 참조했기 때문에 가능한 일이다.
이를 통해 얻은 것은 무엇일까? 만일 필요에 의해서 Task 객체나 Task2 객체 또는 또 다른 Contract interface를 구현한 객체로 바꾸게 되더라도 function() 메소드가 수정될 필요가 없어졌다. 즉 우리가 바라던 유연성이 확보된 것이다. Contract 타입의 객체는 그 "계약"만 제대로 지킨다면 무수히 늘어날 수 있으므로 무수히 많은 요구사항을 반영할 수 있는 구조가 된 것이다. 

클래스 다이어그램을 통한 이해와 여전한 문제점

그러면 이제껏 진행했던 내용을 UML의 클래스 다이어그램을 통해 이해해보자. 클래스 다이어그램은 실제 동작시 객체간의 유기적인 협력 관계를 표현하기에는 적절하지 않지만(유기적인 협력 관계는 UML의 시퀀스 다이어그램을 이용하는 편이 좋다), 우리가 하고 있는 작업, 즉 의존 관계에 대한 관리를 하는데는 매우 유용한 다이어그램이다. 클래스 다이어그램을 단순화해서 보면 클래스(+ interface)와 관계(association, dependency, generalization)로 나누어 볼 수 있다. 관계는 연관, 의존, 일반화 등으로 나누어질 수 있지만, 단순하게 보자면 어떤 클래스와 다른 클래스가 관계로 연결되어 있다면 그 둘 중 적어도 하나에는 상대 클래스가 코드 상에 등장한다는 것을 의미한다. 그래서 우리가 하는 작업을 통해서 다른 클래스와의 관계가 잘 정리되었는지를 확인하는데 매우 유용하다.

위의 다이어그램은 이 글에서 작성한 코드를 클래스 다이어그램으로 나타낸 것이다. Client의 입장에서는 여러 개별 Task들을 직접 다루기보다는 Contract interface를 도입해서 간접적으로 다루도록 만들었다. 여기서 가장 좋은 것은 Client가 Task들과 아무런 관계를 가지지 않는 것이다. 하지만 위 다이어그램에서도 보듯이 Client는 Task들의 생성자 메소드를 호출함으로써 생성(create)을 수행하고 있다. Task들을 전혀 모르고도 사용할 수 있어야만 의존관계를 완벽하게 끊어낼 수 있다. 더 나은 구조를 만들어내기 위해서는 별도의 작업이 필요하다.


Posted by 이세영2
,

우리의 목표는 객체지향 언어를 이용해서 유연성 있는 소프트웨어를 개발하는 것이다. 소프트웨어가 유연하다는 것은 다양한 요구사항 변화에도 대처가 가능하다는 의미이다. 요구사항 변화는 소프트웨어에 추가, 제거, 변경과 같은 수정을 가져온다. 즉, 유연성은 소프트웨어가 얼마나 손쉽게 수정될 수 있느냐에 달려 있다. 이미 요구사항의 변화를 쉽게 반영할 수 있는 객체지향의 4대 특성에 대해서 이미 살펴봤다. 그리고 객체지향의 4대 특성이 반영된 일반적인 구조에 대해서도 살펴봤다. 지휘자 - 연주자 - 개별 연주자로 연결되는 구조를 통해서 객체지향 언어가 어떻게 다양한 요구사항을 동일하게 취급하는지 살펴 보았다. 하지만 이제 객체지향 언어를 이용하여 완성된 형태의 유연성을 제공하기 위해 알아 두어야 할 요소에 대해 설명할 시간이다.


의존(Dependency)과 책임(Responsibility)

의존과 책임은 객체지향 4대 특성을 통해 확보한 유연성을 더욱 강화시키기 위해 필수적으로 알아 두어야 할 요소이다. 그리고 의존과 책임은 별도로 설명하기 곤란할 정도로 밀접한 관계가 있는 개념들이다. 이 개념들을 실제 코드를 통해 이해해 보도록 하자.

class Task{

    public void method(){}

}


class Client{

    Task task = new Task();    // 1

    public void work(){

        task.method();         // 2

    }

} 

위의 코드에는 두 개의 클래스가 있다. 하나는 Task 클래스이고, 다른 하나는 Task 클래스를 사용하는 Client 클래스이다. Client 클래스에서는 1 처럼 Task 객체를 "생성"하고 있다. 그리고 2번 처럼 그 객체의 메소드를 호출하고 있다. 메소드 호출을 객체지향 용어로는 "메시징"이라고 부른다. 그리고 메소드 호출을 통해 다른 객체를 "사용하는" 것을 객체지향 용어로는 위임(Delegation)이라고 한다. 본래 Client 객체가 해야 할 일이지만 (다양한 이유로 인하여) Task 객체에게 맡긴다는 의미이다. 이 전체를 한마디로 표현하자면 Client 객체가 Task 객체를 생성하고, 일부 기능을 위임한 것이다. 이 구조에서 구체적으로 어떤 기능을 위임했는지는 중요하지 않다. 다만 기능의 종류와 상관 없이 일반적으로 만들어지는 구조라는 것이 중요하다.

또한 Client와 Task 객체간의 관계를 의존관계라고 부른다. 좀 더 정확하게 말하면 Client 객체가 Task 객체에 의존하는 것이다. 위의 코드에서 의존은 두 번 발생한다. 우선 Task 객체를 생성하는 1번 코드에 의해 의존한다. 또 2번의 method() 호출을 통해서도 의존한다. A가 B에 의존한다는 말은 간단하게 이야기 하면 A의 코드 상에 B의 요소가 등장한다는 말이다. 1에서는 Task 객체의 타입이 등장하고, 생성자 메소드 호출이 등장한다. 2에서는 method()라는 Task 객체의 메소드 호출이 등장한다. 이것이 의존으로 불리는 이유는 다른 객체의 요소가 등장하는 순간부터 그 객체가 없어지면 안되기 때문이다. 만일 의존 대상인 Task 클래스가 사라지면 Client 객체는 바로 컴파일이 불가능해지고 동작할 수 없는 상태가 된다.

이 의존 관계가 내포하는 문제를 조금 더 깊이 다뤄보자. 위의 코드처럼 Client가 Task에 의존하는 상태, 즉 Client 클래스 코드에 Task 클래스 코드가 등장하는 상황에서는 Task 클래스의 변경이 Client 코드에 영향을 미치게 된다. 앞서 Task 클래스가 사라지게 되는 경우도 이에 해당한다. 만약 의미를 명확하게 하기 위해서 Task 클래스의 명칭을 바꾼다고 하자. 그러면 Client 코드에서도 Task의 변경된 명칭을 사용해야만 한다. Task의 method()라는 이름의 메소드 역시 의미를 명확하게 하려는 이유로 변경될 수 있다. 그러면 2번 코드 처럼 해당 메소드를 호출하는 코드 역시 변경해 주어야 한다. 만약 Task의 기본 생성자 메소드를 대신하여 인자를 받는 생성자 메소드가 추가되었거나, 기본 생성자 메소드의 가시성을 낮춰서 외부에서는 Task 객체를 생성하지 못하도록 만들었다면 1번의 코드도 그에 맞춰 수정되어야 한다. 위의 예에서는 Task 클래스에 의존하는 곳이 단 두 곳 뿐이지만, 상황에 따라서는 코드들을 모두 변경하기 버거울 정도로 많은 곳에서 의존이 발생할 수도 있다.

이것은 소프트웨어 개발자들이 바라지 않는 일이다. 우리가 Task를 수정해야겠다고 느끼는 시기는 Task를 수정해야 할 이유가 있을 때이다. Task라는 클래스명의 의미가 모호하다고 생각될 경우, method()라는 메소드 이름이 모호하다고 생각될 경우, Task 객체가 해야 할 일이 변경될 경우, 새로운 메소드를 추가하거나 기존 메소드를 제거할 경우 등이 Task 클래스를 수정하게 되는 상황이다. Task 클래스를 수정하는 이유는 이를 사용하는 Client 클래스와는 전혀 상관 없는 일이다. 하지만 위의 코드에서 보듯이 Client의 코드가 Task의 코드에 의존하고 있을 경우 Client의 코드가 수정되는 것은 자명하다. 즉, 수정을 해야할 이유는 Task 코드에서 발생했는데 엉뚱하게 이를 사용하는 Client 코드가 수정되어야 하는 상황이 된 것이다. 

만약 사용되는 객체(또는 요소)로 인하여 사용하는 객체가 수정되는 이와 같은 일이 발생하지 않도록 하려면 어떻게 해야 할까? 단 한가지 답만 존재한다면 사용되는 대상이 수정될 가능성을 낮추는 것이다. 그렇다면 이 "수정될 가능성"을 어떻게 판단할 수 있을까? 이 답은 로버트 C. 마틴이 밝힌 객체지향 설계의 5원칙(SOLID)에 들어 있다. 


책임(Responsibility)의 정의

객체지향 설계의 5 원칙(SOLID) 중에서 첫번째 원칙은 단일 책임의 원칙이다. 단일 책임의 원칙(Single Responsibility Principle)은 객체지향의 구성요소가 단 하나의 책임만을 가진다는 원칙이다. 그러면 책임(Responsibility)이란 무엇인가? 단일 책임 원칙에서는 책임을 다음과 같이 정의 한다.


"변경하려는 이유"


즉 우리가 찾던 "사용하려는 대상이 수정될 가능성"을 "책임"이라고 부를 수 있다는 것이다. 어떤 객체가 다른 객체에 의존하는 이유는 무엇인가? 의존의 대상이 되는 객체에 어떤 "책임"이 부여 되어 있고, 그 책임을 이용하고 싶기 때문이다. 그래서 책임에 대해 덧붙이자면 기능이라고 생각할 수도 있다. 이용해야 할 이유, 즉 기능이 있어야 이용하고 싶기 때문이다. 그리고 기능이 있어야 그 기능을 수정하려는 이유도 생긴다. 아무 기능도 없다면 의존할 필요도 없고, 수정할 필요도 없다. 


그러면 실제 구현 속에서 책임을 찾아보자.

구현의 대상이 필드라면 필드의 가시성, 타입 및 명칭이 변경의 대상이 된다. 또한 필드의 초기값도 변경의 대상이 될 수 있다. 필드 중에서도 객체 참조의 경우에는 참조할 객체의 할당 방법도 변경의 대상이 된다. 예를 들어 필드의 선언과 함께 객체를 생성해서 할당한 경우라면 참조할 객체의 종류 역시 변경될 수 있다.

메소드는 메소드의 시그니쳐(signature)와 메소드 내부 구현이 변경될 수 있다. 메소드의 가시성, 리턴 타입, 메소드 명칭, 파라메터, 메소드의 내부가 변경의 대상이다.

클래스는 타입, 필드, 메소드를 가지고 있다. 따라서 클래스는 (가독성에 따라) 자신의 타입명이 변경될 수도 있고, 다른 클래스를 상속 받거나 interface를 구현하게 됨으로써 자신이 가질 수 있는 타입이 변경될 수도 있다. 또 클래스는 필드가 변경 될 수도, 메소드가 변경될 수도 있다. 

구현의 대상이 interface라면 interface에 선언된 메소드가 변경의 대상이 될 수 있다.


단일 책임 원칙에서 정의하는 책임을 그대로 따른다면 위에서 열거한 변경의 대상은 모두 다 책임이 된다. 이들을 모두 개별적인 책임으로 정의하고 각 책임에 맞는 대응책을 모색하기에는 너무 많은 양이다. 별로 중요하지 않은 책임을 드러내놓고 논의하는 것은 시간 낭비에 불과하다. 적어도 객체지향을 통한 설계나 구현을 위해 의미 있는 정보를 제공해 줄 수 있는 수준이 되어야 한다. 또 너무 유사한 것들을 일일이 나열하면서 설명하는 것도 의미가 없는 일이다. 비슷한 것들은 하나의 개념으로 통합 시킬 필요가 있다. 그래서 중요도, 유사성, 실용성 등을 고려하여 책임을 적절히 분류해 보고자 한다.

책임을 종류별로 분류하고자 하는 목적을 이야기 할 필요가 있다. 다시 말하지만 우리는 객체지향 언어를 통해서 유연한 소프트웨어를 만들고자 한다. 유연한 소프트웨어를 만들기 위해서 추상화된 대상을 통해서 다양한 요구사항의 구체적인 차이점을 감추도록 했다. 이것만으로도 훌륭하지만 구성요소간의 의존성이 여전히 문제로 남아 있다. 다른 객체를 이용하는 객체는 이용되는 객체의 변경에 영향을 받는다. 이를 해결하는 방법은 객체들이 수정될 이유가 적어지게 만드는 것이다. 객체들이 수정될 이유가 바로 책임이다. 만약 어떤 객체가 너무 많은 책임을 가지고 있다면, 이 책임을 분할할 필요가 있다. 책임이 적어진다는 것은 변경할 이유가 적어진다는 것을 의미한다. 의존하는 대상이 변경될 이유가 적어지면 사용하는 쪽의 코드가 수정될 가능성도 함께 적어진다.

그래도 한가지 의문이 더 남는다. 결국 많은 책임을 분할하여 여러 객체나 요소로 분산하자는 얘기인데, 그렇게 되면 결국 의존이 여러 대상으로 분산될 뿐, 전체적인 의존의 복잡성은 그대로 유지되는 것이 아니냐는 것이다. 물론 상황에 따라서는 의존성이 줄어들기보다 객체나 요소가 늘어나는 이유로 인하여 관계가 더 복잡해 보일 수도 있다. 하지만 책임을 나눌 경우 크게 두가지에서 이득을 얻을 수 있다. 

첫째로 책임이 적절하게 여러 대상으로 분할된 경우가 여러 책임이 하나의 대상에 밀집해 있는 경우보다 덜 수정된다는 점이다. 만일 여러 책임이 하나의 클래스에 몰려 있다고 가정해 보자. 이 경우에는 종종 여러 책임별로 메소드 단위, 속성 단위로 잘 구분되지 않는 경우가 많이 있다. 즉, 하나의 메소드가 두가지 책임을 위해서 사용될 경우도 있고, 두가지 이상의 책임이 하나의 메소드 흐름 속에 뒤죽박죽 섞여 있을 수도 있다. 책임을 면밀히 생각해 보지 않았기 때문에 이들이 섞여 있어도 그렇다는 것을 잘 모르는 경우가 많다. 이 때 어떤 이유로 대상을 수정하게 된다면 어떤 일이 벌어질까? 하나의 목적으로 수정을 시작했는데, 그 목적과는 관계없는 코드들이 중간중간에 섞여 있을 것이다. 이는 수정에 집중하기 어려운 상황을 만든다. 그리고 하나의 책임만을 수정했음에도 불구하고 다른 책임을 잘못 건드려서 다른 목적에도 맞지 않게 수정될 가능성도 있다. 이것은 단일 책임 원칙에 의해 책임이 잘 분할된 여러 요소들을 개별적으로 수정할 때에 비해 매우 비효율적일 수 밖에 없다.

둘째로 책임이 분할되어 있을 경우 중복 요소의 확인이 쉽고, 이를 통해 한차원 높은 추상화를 발휘할 수 있다는 점이다. 책임을 분할해 보면 책임이 다른 객체로 분할되거나 별도의 메소드로 분할되게 된다. 이렇게 대상들이 작아지면 전체를 이해하기가 수월해진다. 그리고 작은 단위들은 서로 유사성을 띄는 경우가 많다. 완성된 자동차를 종류별로 비교하면 동일성을 찾기가 어렵지만, 부품단위로 뜯어보면 같은 것을 발견할 가능성이 높아지는 것과 같은 이치이다. 설령 조금씩 다르다고 해도 부품들의 목적이 같다면 둘을 통일 시켜 하나의 요소로 만들기도 쉽다. 같은 세단이라도 완성차끼리 호환시키기는 어렵지만, 세단과 SUV 사이라고 해도 부품별로 놓고 보면 나사, 프레임, 바퀴, 선루프 등 부품별로는 얼마든지 동일성을 유지시킬 수 있는 여지가 있다. 이와 같은 이유로 더 많은 요소의 동일성을 확보하고, 이를 통해 더 많은 유연성을 확보하는 것이 가능해 진다.

책임을 잘 분류하면 다음과 같은 효과를 얻을 수 있다. 우선 단일 책임 원칙을 어떻게 위반하고 있는지를 확인할 수 있다. 어떤 구현 요소(interface, 클래스, 메소드 등)가 여러 종류의 책임을 함께 가지고 있음을 분석해낼 수 있고, 어떤 책임을 분리하여 단순화할지에 대한 전략을 세울 수 있다. 또 책임의 종류에 따라 어떤 형태로 책임의 분할을 수행할 수 있을지를 알 수 있다. 그리고 이런 분할 방식을 이용하여 어떤 소프트웨어를 작성하더라도 정형화 된 형태의 설계나 구현을 할 수 있게 된다. 이것은 개발의 속도를 향상 시키는데 매우 큰 도움이 된다. 마지막으로 객체지향 소프트웨어가 어떤 형태로 기능을 확장해 나가야 할지를 가이드 할 수 있다. 기능이 확장된다는 것은 책임이 증가한다는 것을 의미한다. 책임이 증가되면 단일 책임 원칙을 지키기 위해 분할이 필요하다. 이 때 책임의 종류를 명확하게 이해하고 있으면 쉽게 책임을 분할 할 수 있고, 단일 책임 원칙을 유지시킴으로써 좋은 소프트웨어 구조를 유지할 수 있다.


생성 책임

객체의 생성은 매우 중요한 작업 중의 하나이다. 객체를 생성하는 과정의 중요성 때문에 GoF의 디자인 패턴에도 5가지 종류의 생성 패턴이 존재한다. 또한 생성 과정에서 만나는 다양한 문제점들을 해결하기 위해서 많은 소프트웨어들에서 Factory Method를 이용한다.

이 생성 책임은 세부적으로 다음과 같은 작업들이 정상적으로 이루어지도록 만든다. 첫번째는 적절한 구체 객체를 생성하는 것이다. 생성 패턴이나 Factory Method에서는 사용자가 사용하는 생성자 객체 혹은 생성 메소드에 따라서, 또는 생성 메소드에 의해 전달되는 파라메터에 따라서 다른 객체를 생성하고 제공한다. 생성 책임을 지고 있는 모듈로부터 적절한 객체를 제공 받음으로써 어플리케이션이 적절한 동작을 취할 수 있도록 한다. 두번째로 객체의 타입을 감춘다. 구체 객체를 생성했다고 해도 그 구체 객체의 타입으로 사용하지 않는 편이 좋을 경우가 있다. 대표적으로 다형성을 통해 구현된 일련의 객체들을 받아 사용하는 경우이다. 이 때 생성 책임을 지는 모듈은 구체 객체의 타입을 감추고 추상 타입으로 리턴해 줌으로써 객체를 사용하려는 쪽과 생성된 구체 객체간의 관계가 생성되는 것을 막아준다. 세번째로 객체가 생성되고 나서 사용되기 전에 해줘야 할 작업들을 실행해준다. 대표적으로 초기화 작업이나 이벤트 수신을 위해 생성된 객체를 다른 객체에 등록하는 작업 등이다. 네번째로 객체의 생성에서 사용 전까지 있을 수 있는 동기화 문제를 방지해 주는 역할을 한다. 다섯번째로 객체가 생성되면서 가져야 할 초기 값들을 할당해준다.


조립 책임

현대 소프트웨어처럼 요구사항이 다양화 되기 전까지는 객체를 조립한다는 개념이 두드러지지 않았다. 프레임워크, 즉 어떤 어플리케이션으로 진화하든지 상관 없이 충분한 기능을 지원해 줄 수 있도록 설계된 소프트웨어 플랫폼들에서나 주로 고민하던 부분이다. 하지만 객체지향 언어에서 의존성 관리를 통한 유연성 확보 개념이 널리 퍼지고, 엔터프라이즈 애플리케이션이 더 많은 산업 분야에 적용되고, 같은 기능의 소프트웨어를 대량으로 판매하기 보다는 사용자의 요구에 맞춰 다른 기능들을 제공하는 방식을 더 많이 지원해야 하는 상황에서 이 조립 책임이 대두되었다. 아마도 앞으로 인공지능 분야가 더더욱 발전하고 소프트웨어가 더 많은 데이터를 손쉽게 수집할 수 있는 플랫폼을 개발하는 방향으로 나가게 된다면 이 조립 책임은 더욱더 비중이 커지게 될 것이다.

Spring 프레임워크의 의존성 주입(Dependency Injection), 넷빈즈의 룩업(Lookup) 서비스 객체, 서비스 로케이터(Service Locator) 등과 같이 객체의 조립이라는 과정은 독립적이고 핵심적인 요소이다.


구현 책임

구현의 책임은 구체 객체가 가진 책임이다. 즉 특정 요구사항에 맞게 실제 동작을 수행할 수 있는 메소드를 가지고 있는 것이다. 좀 더 구체적으로 보면 공개 메소드가 구현 책임의 대상이 된다. 이 공개 메소드는 interface나 상위 클래스를 상속함으로써 구현이 강제 되기도 하고, 자체적으로 공개 메소드로 설정하고 제공되기도 한다. 어쨌든 구체 객체는 이들 공개 메소드에 대하여 구현함으로써 실제 작업을 수행하는 책임을 가진다.


계약 책임

계약 책임은 interface에 대한 책임이다. 계약이라는 이야기가 매우 생소할 수도 있다. 하지만 어떤 interface를 구현하는 객체들이 구체적인 구현에 있어서 자율성을 가지고 있다는 사실은 이미 알고 있을 것이다. 객체를 사용하는 쪽에서는 구체적인 객체가 어떤 방식으로 동작하는지에 대해서는 관여하지 않는다. 이를 통해 다형성이 유지되고 유연한 구조의 소프트웨어가 탄생할 수 있다. 하지만 이와 함께 interface는 행위를 강제하는 역할을 한다. 이것을 현실 세계와 비교해 보면 "계약" 관계와 그 유사성을 찾을 수 있다. 계약서를 들이 미는 쪽은 사용자 측이고, 계약을 이행해야 하는 측은 구체적인 구현 객체이다. 그리고 계약서에 해당하는 것은 interface이다. 사용자 측에서는 계약서, 즉 interface를 구현하기만 한다면 구체적인 객체가 어떤 일을 하든지 상관하지 않는다. 대신 계약서(interface)는 반드시 지켜져야만 한다. 

이런 개념으로 interface를 이해하면 리스코프 치환의 원칙이 자연스럽게 지켜진다. 사용자 측에서는 언제나 interface를 통해서만 객체를 취급한다. 구체적인 객체들은 interface의 구현을 강제 당하는 셈이지만, 그 방법에 대해서는 최대한 자유를 누릴 수 있다. 이런 특성들을 개념적으로 잘 이해할 수 있도록 하기 위해서 interface에 계약 책임을 부여하고자 한다.


상태 책임

상태 책임은 필드와 필드의 가시성, 그리고 getter/setter에 대한 책임이다. 어떤 객체에 필드를 선언할 때는 메소드를 선언할 때보다 훨씬 신중해야 한다. 필드는 구현을 도와주는 도구가 아니다. 오히려 가능한 한 선언을 피해야 하고, 일단 선언 되어 있다면 가능한 한 가시성을 낮춤으로써 외부로 유출되는 것을 방지해야 한다. 일단 선언된 필드는 필드를 가지고 있는 객체에 책임을 뒤집어 씌운다. 필드는 필드 스스로 연산을 수행할 능력이 없기 때문이다. 다른 객체들은 특정 필드가 선언된 객체에게 해당 필드의 값을 요구하거나(getter), 어떤 필드 값을 저장하라고 시키거나(setter), 그 필드와 연관된 연산을 수행할 것을 요구할 수 있다. 그리고 특히 getter 메소드는 필드의 값을 외부로 누출시켜 필드에 의해 발생하는 로직들(특히 조건문과 같은)을 전파시키는 역할을 하게 된다. 따라서 단순히 필드를 노출시키는 것만 아니라 필드를 통해 다른 객체들이 하고자 하는 일들을 예상하고 그 일들을 미리 구현해야 한다.


이로써 책임의 분류가 끝났다. 이제 책임의 분류가 어떤 효과를 나타내는지 확인해 볼 차례다.

Posted by 이세영2
,

설계를 하다보면 종종 상위의 인터페이스나 상위 클래스는 외부에 public으로 노출을 시키되, 하위의 구체 클래스들은 외부에 노출시키지 않아야 하는 경우가 있다. 이는 정보 은닉을 위한 좋은 설계 방법 중 하나이다. 이를 위해서 보통 하위의 구체 클래스들을 package private class(JAVA에서 접근자 키워드가 없이 선언되는 클래스)로 선언한다. 이렇게 하면 같은 패키지 내에서는 해당 클래스에 접근할 수 있어도 다른 패키지에서는 package private 클래스를 접근할 수 없으므로 오직 인터페이스나 상위 클래스를 이용할 수 밖에 없다. 따라서 하위 클래스들의 변경이나 추가와 같은 유연성이 보장된다.

하지만 문제는 package private class에 대한 Unit Test를 작성하는 일은 쉽지 않다는 점이다. 일반적으로 제품 코드와 유닛 테스트 코드는 분리되는 것이 좋다. 제품 코드와 유닛 테스트 코드가 같은 곳, 즉 같은 패키지 내에 존재하면 제품의 출시를 위해서 유닛 테스트 코드를 골라내는 작업이 필요하다. 또 제품 코드를 이해하기 위해서 소스를 뒤적이다 보면 유닛 테스트 코드가 함께 섞여 있어서 불편함을 겪게 된다. 그래서 일반적으로 제품 코드와 유닛 테스트 코드를 분리하여 두는데, 이렇게 되면 제품 코드와 유닛 테스트 코드의 패키지가 분리되면서 package private인 요소들에 대해서는 유닛 테스트가 접근할 수 없게 된다. 이렇게 되면 간접적으로 해당 요소들을 테스트해야 하는데 이는 매우 번거로운 작업이 된다.

이클립스에서는 이 문제를 해결할 수 있는 좋은 솔루션을 제공해 준다. 바로 소스 폴더를 여러개 등록하는 것이다.

일단 해결하고자 하는 문제를 명확하게 보여주기 위해서 다음과 같은 예제를 작성해 보겠다.


Shape 및 ShapeFactory 소스 코드

// Shape.java

public interface Shape {

    public void draw();

}

class Triangle implements Shape{

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

}

class Circle implements Shape{

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

}

// ShapeFactory.java

public class ShapeFactory {

    public Shape createTriangle(){ return new Triangle(); }

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

} 

우선 외부에 노출시키고자 하는 인터페이스인 Shape을 정의한다. 그리고 이 인터페이스는 외부에 노출될 것이므로 public interface로 선언하였다. 그리고 하위 클래스인 Triangle과 Circle 클래스들을 구현한다. 이들 클래스는 외부에 노출시키지 않을 것이다. 따라서 접근자가 없는 클래스, 즉 package private 클래스로 선언이 된다.

그리고 외부에 Triangle 클래스와 Circle 클래스를 노출시키지 않을 수 있으려면 생성자 호출을 대신할 Factory 클래스를 제공해 주어야 한다. ShapeFactory 클래스는 Shape 클래스와 같은 패키지 내에 있으면서 외부의 요청에 따라 Triangle 객체와 Circle 객체를 생성하여 제공해 준다. 이 때 구체적인 타입을 감출 수 있도록 Shape 타입으로 상위 타입 캐스팅을 한 후에 제공한다. 이를 통해 Triangle과 Circle 클래스가 외부로 노출되는 것이 차단된다.


이제 유닛 테스트를 구현해야 할 차례다. ShapeFactory가 각 API에 따라서 제대로 된 Triangle 객체와 Circle 객체를 생성하는지를 테스트 해보고자 한다. 그렇다면 아래와 같은 테스트 클래스를 구현해 볼 수 있다.


ShapeFactoryTest.java

public class ShapeFactoryTest {

    ShapeFactory sut;

    @Before

    public void setUp(){

        sut = new ShapeFactory();

    }

    @Test

    public void testCreateRectangle(){

        Shape shape = sut.createTriangle();

        assertTrue(shape instanceof Triangle); // --- 1

    }

    @Test

    public void testCreateCircle(){

        Shape shape = sut.createCircle();

        assertTrue(shape instanceof Circle);   // --- 2

    }

} 

1과 2에서와 같이 Triangle 객체와 Circle 객체가 제대로 생성되었는지를 확인할 필요가 있다. 그런데 Triangle과 Circle은 모두 package private 클래스들이다. 따라서 이 유닛 테스트 코드와 제품 코드가 분리되어 있더라도 이들에 접근 할 수 있는 방법이 필요하다.


제품코드와 테스트 코드의 소스 폴더 분리하기

Proeject Explorer / New / New JavaProject를 클릭하면 아래와 같은 다이얼로그가 나올 것이다. 


여기서 바로 finish를 누르지 말고 Configure default 버튼을 누른다. 그러면 아래와 같은 다이얼로그가 생성된다.

붉은색 박스처럼 src 대신 src/java/main을 입력하고 확인을 누른다. 그리고 나서 Next 버튼을 누르면 아래와 같이 화면이 바뀌게 된다.


여기서 오른쪽 클릭을 하고 New Source Folder 버튼을 누른다. 그러면 아래와 같은 다이얼로그가 생성된다.


여기에 Folder name에 src/test/main을 입력하고 확인을 누른다.

그리고 나서 Finish를 누르면 프로젝트 생성이 완료된다. 폴더 구성이 아래와 같다면 제대로 생성이 된 것이다.


일반적으로는 src라는 폴더 하나만 있는데 우리가 만든 프로젝트는 src/java/main과 src/test/main 폴더가 더 추가되어 있다. 그리고 이들 모두가 새로운 패키지를 생성하는 것이 가능하다. 이 두 폴더 중에서 src/java/main 폴더에는 제품 코드를 추가하고 src/test/main에는 유닛 테스트 코드를 추가하면 된다.

이렇게 소스 폴더를 둘로 나눠 놓은 이유는 물론 제품 코드와 유닛 테스트 코드를 분리시키기 위해서이다. 그런데 다른 방법으로도 둘을 분리할 수 있음에도 굳이 방법을 사용한 이유는 저 두 폴더에서 생성한 동일한 경로의 패키지는 같은 패키지로 취급되기 때문이다. 같은 패키지로 취급된다는 말은 위에서 package private 클래스에 대한 유닛 테스트와 Shape 및 ShapeFactory에 대한 소스 코드를 각각의 폴더에 분리하여 넣어도 둘의 패키지 경로만 같게 설정하여 주면 테스트가 정상 동작한다는 것을 의미한다.

이를 확인하기 위해서 두 폴더에 각각 com 이라는 경로의 패키지를 만들어 본다. 그러면 아래와 같이 될 것이다.


이제 위의 패키지에는 Shape.java와 ShapeFactory.java를 넣고, 아래 패키지에는 ShapeFactoryTest.java를 넣는다. 그리고 유닛 테스트를 실행해 보면 된다.

같은 테스트 코드라고 해도 다른 패키지 경로에 넣으면 Triangle 과 Circle 클래스가 등장하는 곳에서 에러가 발생한다.


사실 이러한 소스 폴더 분리 방식은 이미 Spring Framework에서도 적용하고 있는 방식이다. 만일 이와 같이 package private class들에 대한 테스트가 필요하다면(일반적으로 모든 프로젝트에서 그렇겠지만) 꼭 위와 같이 같은 패키지 경로들을 만들어 테스트 코드를 넣는 것이 좋다. 그래야만 보다 손쉽게 테스트 코드를 작성할 수 있다. 만일 Spring이 아닌 일반 프로젝트를 생성한다고 해도 유닛 테스트를 작성해야 하는 경우라면 미리 위와 같은 형태로 소스 폴더를 구성해 두는 편이 유닛 테스트를 작성하기에 용이할 것이다.

한가지만 더 이야기 하자면, protected 메소드 역시 같은 방식으로 테스트가 가능하다. protected 메소드는 package private과 같이 같은 패키지 내의 다른 클래스에서도 접근이 가능하다. 따라서 위와 같은 형태로 소스 경로를 분리한 후 테스트를 수행해 볼 수 있다.



Posted by 이세영2
,

객체지향 개념들간의 관계 UML


간략히 설명하자면 다음과 같다.

- OOP : Object Oriented Programming

- OOP는 유연성(Flexibility)을 가진다.

- 유연성은 캡슐화와 추상화, 다형성을 이용해서 달성된다.

- 상속은 캡슐화를 활용하고, 다형성은 상속을 이용해서 만들어진다.

- 캡슐화를 통해서 가시성(Visibility) 개념이 만들어지고, 클래스 개념이 만들어진다.

- 클래스는 타입과 필드, 그리고 메소드를 가지고 있다.

- 클래스 개념은 인터페이스(Interface), 추상 클래스(Abstract Class), 구체 클래스(Concrete Class) 개념을 파생시킨다.

- 객체(Object)는 구체 클래스(ConcreteClass)가 가진 개념을 포함한다.

- 객체는 Identity를 가지고 있다.(구체클래스를 통해 클래스가 가진 개념도 가지게 되므로 Method, Field, Type도 가지게 된다.)

- 메소드는 프로토타입을 가지며, 추상 메소드는 메소드의 일종이다.

- 필드는 Reference와 Primitive로 나뉜다.

- 클래스와 메소드, 필드는 가시성을 가진다.

- 클래스는 상속 가능(Inheritable)하다.

- 메소드는 재정의 가능(Overridable)하고, 오버로딩 가능(Overloadable)하다.

Posted by 이세영2
,

중간 정리

이쯤 해서 왜 "객체지향의 올바른 이해"라는 포스팅을 만들게 되었는지 그 동기를 설명하고 넘어가는 편이 전체적인 이해에 도움이 될 것이라고 생각한다.

이전 포스팅들에서 객체지향 4대 원칙이 나온 배경을 유연성의 예를 통해 설명하였다. 개인적으로 여러 책을 읽어보고 검색을 해 봤지만 이와 같은 방식으로 객체지향을 설명한 글은 없었다. 하지만 포스팅의 내용을 이해했다면 현실 세계에서 나온 개념을 프로그래밍 언어에서 차용했다고 보는 편이 더 타당하다는 점을 알 수 있을 것이다. 개인적으로 이 부분은 나름대로 확신하는 편이다. 하지만 그렇다고 해서 굳이 이미 사람들이 객체지향을 이해하고 있는 방식을 바꿔서 설명해야 할 필요가 있겠는지 하는 의문은 생길 것이다. 나는 그것이 필요하다고 봤다.


객체지향을 설명하는 일반적인 방식은 이렇다.

추상화(Abstraction)에 대해서는 공통된 속성과 행위를 추출하는 것으로 주로 설명한다. 이 설명이 틀린 것은 아니다. 하지만 공통된 속성과 행위를 추출하는 과정의 목적이 명확하지 않다. 유연성을 확보하기 위한 목적으로 설명하지 않기 때문에 추상화 과정이 필요한 이유를 설명하기가 어렵다. 그래서 어떻게든 가져다 붙이는 설명이 "속성과 행위를 중복 구현하지 않기 위해서"라고 말한다. 만약 추상화의 목적이 중복 구현의 방지, 즉 재사용이라면 일반적인 언어에서 말하는 작은 규모의 재사용, 즉 라이브러리와 무엇이 다른지를 명확히 해야 한다. 하지만 애초에 목표가 제시되지 않은 상태에서 추상화의 목적을 설명하기 때문에 그 본질적인 용도가 불명확해지는 것이다.

애초에 설명이 이렇게 시작하다보니 다른 개념들도 점차 모호해지기 시작한다. 캡슐화는 속성과 행위의 묶음이고, 이들 중에서 일부는 외부에 노출시키지 않는 것, 소위 정보 은닉을 통해 객체 내부 변경이 외부에 영향을 적게 미치도록 만든다고 말한다. 그래서 객체지향이 다른 언어보다 좋다고 이야기 한다. 캡슐화의 가장 큰 이점은 추상화를 통해 만들어진 객체(추상 객체라고 하자)가 그 고유한 타입을 가지게 되고, 이 추상화된 객체의 타입(추상 타입이라고 하자)을 통해서 여러 개별 객체가 공통으로 다루어 질 수 있음으로써 유연성이 확보된다는 점이다. 이 과정에서 캡슐화의 의미가 "공통의 속성과 행위에 타입을 부여하여 다루어질 수 있는 대상으로 만드는 것"이라는 정의를 성립시킬 수 있다. 따라서 캡슐화에서 가장 강조되어야 하는 부분은 타입의 부여이다. 하지만 캡슐화를 단순한 속성과 행위의 묶음 또는 일부 속성이나 행위를 외부에 노출시키지 않는 것이라고 정의한 덕분에 다른 개념들의 설명은 더욱 모호해진다.

상속에 대해서는 다른 객체의 속성과 행위 "만"을 가져 오는 것으로 보통 설명한다. 앞서 강조했듯이 캡슐화된 객체에 있어서 가장 중요한 것은 타입이다. 상속은 타입을 상속 받음으로써 그 타입으로 다뤄질 수 있도록 만드는 것이 핵심이다. 그래야 구체적인 타입이 아닌 상위 타입, 즉 추상 타입으로 다뤄지면서 유연성을 확보할 수 있게 된다.

다형성(Polymorphism)의 경우 역시 왜곡이 심하다. 다형성을 메소드 오버로딩과 혼돈하는 경우가 많고, 기껏해야 상위 타입으로 호출된 메소드를 하위 타입의 메소드로 대체되어 호출되는 것 정도로 말하는 경우가 대부분이다. 다형성은 하나의 타입이 서로 다른 타입으로 지칭될 수 있는 특성이다. 이 다형성은 객체지향 언어에서 상속을 통해 확보된다. 상속이라는 특성은 속성과 행위 뿐 아니라 타입도 상속시킨다. 이를 통해 상속을 받은 클래스는 자기 고유 타입 뿐 아니라 상위 타입도 가지게 된다. 이러면 가지고 있는 타입이 두 개가 되는데 이것이 다형성(multiple type)이다. 따라서 다형성이라는 단어만으로는 유연성을 이해하기 어렵다.


많은 소프트웨어 개발자들은 새로운 언어를 빨리 배우는 것이 자신의 능력과 연결된다고 생각한다. 그것 자체는 틀린 것이 아니다. 하지만 C언어와 같은 절차지향 언어를 쓰다가 Java와 같은 객체지향 언어를 배우게 되는 경우에도 이런 생각에 사로잡힌다. 특히 개발자들은 자신이 하는 분야에 대한 자부심이 크기 때문에 다른 분야 업무를 경시하는 경향이 있다. C언어와 객체지향 언어는 사용되는 분야가 많이 다르다. 언어를 빨리 배워야 뛰어난 개발자라는 생각과 다른 분야에 대한 경시가 객체지향 언어에 대한 이해 부족으로 나타난다. 빨리 배워서 기능 하나 빠르게 개발해서 보여주고 "쉽네!" 라고 빨리 말할 수 있어야 한다는 강박관념에서 언어를 배우는 것이다. 일반적인 형식의 객체지향 설명이 추구하는 방향이 딱 이런 정도다.

객체지향 언어는 애초에 프로그래밍을 시작할 단계에서부터 유연성을 염두해 두고 시작해야 하는 언어다. 프로그램을 구성해야 하는 요소(즉 객체)가 둘이 되었을 때부터 둘 간의 관계에 대해서 생각해보고, 이들의 관계가 확장될 소지가 있는지, 즉 유연성을 확보해야 할 필요가 있는지를 고려해야 한다. 또 이미 구현되어 있는 프로그램이라고 해도 서로 유사성이 있는 요소는 없는지, 그리고 유사한 요소들을 함께 다룰 수 있도록 함으로써 유연하고 간결한 프로그램으로 만들 수는 없는지를 생각해야 한다. 이런 과정에서 유연성을 확보하는 방법에 대한 일종의 청사진을 가지고 있다면 그 일이 매우 수월해질 것이다. 이것이 현실 세계에서 오케스트라 지휘자의 문제를 보여주고, 이를 객체지향 4대 원칙과 비교하면서 설명하게 된 계기이다.


이것을 통해 얻은 것은 무엇인가? 

우선 객체지향 언어를 배우는 과정에서 많이 간과되거나 잘못 설명되었던 개념들을 정확하게 정립할 수 있었다. 특히 객체지향 언어에서 "타입"이 얼마나 중요한지를 재발견할 수 있게 해주었다. 이제껏 일반적인 객체지향 도서들이나 블로그 들에서는 타입에 대해 거의 언급하지 않았거나 단편적으로만 설명했을 뿐이다. 하지만 이 포스팅을 통해서 객체가 속성과 행위의 집합이 아닌 타입과 속성과 행위의 집합으로 이해될 수 있었다. 그리고 캡슐화가 타입을 부여하는 과정이라는 점, 상속은 속성과 행위만이 아니라 타입까지도 물려 준다는 점, 마지막으로 다형성의 의미가 여러 타입을 가질 수 있는 특성이라는 점까지 이해할 수 있게 되었다.

객체지향 언어의 4대 특성이 왜 도입되었는지를 알 수 있었다. 객체지향이 실제 세계의 반영이라는 점은 많은 글들에서 설명하고 있다. 그러나 단순히 실세계를 반영하기 위해서 4대 특성을 도입했다는 것은 설득력이 부족하다. 앞서서도 이야기 했듯이 객체지향의 개념은 절차지향에 비해 이해하기가 어렵다. 굳이 더 어려운 개념을 도입해서 언어를 개발하고, 그렇게 개발된 언어가 지금처럼 널리 쓰일 수 있게 된 이유가 있어야만 한다. 유연성이라는 목적은 그 이유에 부합한다. 그리고 이해하기 어려운 개념임에도 불구하고 유연성이라는 목적을 명확히 이해하고 사용하면 객체지향을 빠르게 익히는데 많은 도움이 된다.

객체지향 언어의 목적이 유연성이라는 점을 이해하고 있고, 이 목적에 맞게 4대 특성을 이해하고 있으면 이제 디자인 패턴과 같은 설계적인 특성에도 빨리 다가갈 수 있다. GoF의 디자인 패턴 23가지 중 약 10가지는 그 목적이 유연성 확보에 있고, 앞서 설명한 유연성 확보 방식을 기반으로 구현된다. 나머지 패턴들 중 일부는 일부 특수한 목적을 위한 것이거나 객체지향 4대 특성의 일부를 활용한 것들이다.


Posted by 이세영2
,

이제 현실세계에서 객체지향의 세계로 넘어갈 차례다. 아직까지도 왜 객체지향 언어가 다른 언어에 비해 더 유연한 언어인가에 대한 정확한 답을 얻지는 못했을 것이다. 객체지향 언어가 유연한 이유를 간단히 말하자면 현실세계의 유연성을 그대로 프로그래밍 언어에 접목시켰기 때문이다. 객체지향 언어가 가진 고유한 특성들을 이제부터 이야기 할텐데, 이 특성들과 유연성은 완벽하게 매치된다. 이제껏 객체지향의 특성들을 일단 공부하고 외우고 나서 어느 정도 객체지향 개념을 이해하게 된 사람들이나 이제 막 객체지향 언어에 발을 들여 놓은 사람들 모두 현실 세계의 유연성을 일단 이해하고 객체지향 개념을 접하게 된다면 그 개념이 왜 필요한지, 그리고 그것이 어떻게 유연성을 만들어 내는지를 정확히 이해할 수 있게 될 것이다.


객체지향 4대 특성

객체지향 언어는 다른 언어들과 차별화된 특성을 가지고 있다. 그리고 이러한 특성들 중에서 중요한 것을을 객체지향 4대 특성이라고 부른다. 


추상화(Abstraction)

유연성을 확보하는 각 단계 중에서 공통점을 추출하고 차이점을 감추는 작업에 대해서 이야기 했다. 이 단계에서 했던 일을 요약하자면 다음과 같다. 우리는 여러 요구사항들로부터

    1. 공통점을 추출하였고,

    2. 불필요한 공통점을 제거하였다.

오케스트라의 비유에서 우리는 연주한다는 공통점을 추출해냈고, 같은 국적이라는 공통점을 추출해 냈지만 국적은 연주와 상관없는 정보이기 때문에 제거하였다. 객체지향 언어에서 추상화(Abstraction)라고 부르는 특성은 우리가 유연성을 확보하기 위해 했던 과정과 동일하다. 추상화라는 말은 구체적인 것을 제거한다는 말이다. 이것은 여러 요구사항에 대한 공통 요약본을 만드는 과정이다. 따라서 무턱대고 추상화를 할 수는 없다. 우선 제거되는 대상은 공통되지 않은 것이어야 한다. 그리고 우리가 만드는 것은 요약본이기 때문에 공통점들 중에서도 목적한 바와 맞지 않는 것들은 다시 제거해야 한다. 이러한 과정을 통해서 문제와 밀접한 관계가 있고, 모든 요구사항들이 공통적으로 가지고 있는 부분들이 추출된다.



추상화(Abstraction)의 과정



캡슐화(Encapsulation)

캡슐화는 "캡슐로 만들다"라는 의미이다. 우리는 이미 "대상화" 한다는 개념을 통해 이 특성을 익혔다. 캡슐화의 의미는 크게 3가지로 나눌 수 있다.

1. 공통점들의 묶음에 명칭을 부여한다. 이 "묶음"은 다룰 수 있는 "대상"이 된다.

2. 속성과 행위를 하나로 묶는다.

3. 실제 구현 내용 일부를 은닉한다.


일반적으로 캡슐화라고 하면 2번과 3번을 떠올린다. 그리고 그것 만으로도 어느 정도 이해하는데 도움이 되기는 한다. 하지만 정작 중요한 것은 1번이다.

우리는 유연성에 대한 설명 중 이상화 시인의 시 "꽃"에 대한 이야기를 했었다. 2번처럼 속성과 행위를 하나로 묶었다고 하더라도 거기에 "명칭"이 부여되지 않으면 다룰 수 있는 "대상"이 되지 못한다. 일반적으로 객체지향에서 이 "명칭" 이라는 것은 타입(Type)이라고 말하고, "대상"은 객체를 말한다. 즉, 다룰 수 있는 대상인 객체가 되려면 타입을 가져야 한다는 말이다. 아래 코드는 이 캡슐화의 예이다.


class Musician{ // 명칭을 부여했다.

    public void play(){ /* nothing to do */ }

    public void stop(){ /* nothing to do */ }

} 

Musician은 이제 우리가 추출한 공통점들의 명칭이 된다. 그리고 중괄호({})의 시작과 끝에 의해서 그 명칭이 의미하는 내용이 묶여진다. 중괄호를 통해 공통점들이 묶이고 Musician이라는 명칭이 부여된 것이다. 명칭, 즉 타입(Type)이 중요한 이유는 이후 다른 객체지향 특성을 알고 나면 매우 명확해지게 될 것이다. 일단 아래의 코드를 보고 한가지만 이야기 하고 넘어가겠다.

Musician musician = new Musician();

위의 코드 한 줄은 다음과 같은 의미를 지닌다.

    1. 등호( = ) 오른쪽은 Musician 타입의 객체를 생성하는 코드이다. 

    2. 등호 왼쪽은 musician이라는 참조를 선언하는데 그 타입은 Musician 타입이다.

    3. 이 한 줄의 코드를 통해 Musician 타입의 객체가 생성되고, 이 객체는 Musician 타입의 참조 객체를 통해 다루어진다.

객체를 생성하고 객체와 타입의 참조를 통해 객체를 다룬다는 말이다. 같은 타입이니 당연히 객체를 다루는 것은 문제가 없다. 이제 다음에 나올 개념들을 이해할 차례다.


상속(Inheritance)

이제 우리는 공통점에 대해 해야 할 일들을 마쳤다. 그러면 이제 다양한 요구사항들의 차이점들에 집중할 때가 되었다. 오케스트라의 비유에서 각 악기 연주자들은 서로 다른 악기를 다룬다. 따라서 이들 악기로부터 나오는 소리는 모두 다 다를 것이다. 하지만 이들은 모두 연주한다(play()), 멈춘다(stop())는 공통점을 가지고 있어야 한다. 특히 유연성의 관점에서 보면 각 악기 연주자들은 연주한다와 멈춘다를 통해서 다루어져야 한다.

이것은 이미 만들어져 있는 공통점을 개별 악기 연주자가 이용할 수 있어야 함을 의미한다. 객체지향 언어에서 이미 만들어져 있는 타입을 다른 객체가 물려 받아 이용하는 것을 상속(Inheritance)이라고 한다.

상속을 통해 다른 객체로부터 물려 받는 것은 세가지가 있다.

    1. 타입(Type)

    2. 필드(field, = 속성 = 변수)

    3. 메소드(method, = 행위, 함수)

이 중에서 가장 중요한 것을 고르라면 당연히 타입이다. 일반적으로 많은 객체지향 관련 자료들이 놓치는 것이 상속을 통해 타입을 상속받는다는 사실이다. 이 타입 상속에 대한 이해가 없이는 객체지향을 온전히 이해할 수 없다. 타입 상속은 다음에 나올 다형성의 중요한 기반이 된다.

일단 상속이 어떤 방식으로 이루어지는지부터 알아보자. 우리는 공통점을 추출해 낸 Musician이라는 타입을 이미 가지고 있다. 그리고 이제 우리는 구체적인 요구사항, 즉 개별 악기 연주자들을 정의해야 한다. 그리고 개별 악기 연주자는 이미 정의된 Musician으로도 다루어 질 수 있어야 한다. 따라서 Musician이라는 타입을 "상속" 받아야만 한다. 일단 하나만 해보도록 하자.

class FluteMusician extends Musician{

    public void play(){

        System.out.println("playing flute");

    }

    public void stop(){

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

    }

} 

FluteMusician 클래스는 Musician 클래스를 상속 받는다. 이를 통해 FluteMusician 클래스는 FluteMusician이라는 자신의 타입도 가지면서 동시에 Musician이라는 타입도 가지게 된다. 또한 Musician 클래스가 가지고 있던 공통점들, 즉 연주하다(play())와 멈추다(stop()) 역시 Musician 클래스로부터 상속을 받는다.(예제에서는 일단 이들 공통점들을 "재정의" 하고 있다.)

여기서 주목할 것은 타입과 함께 공통점들을 물려 받는 것이 왜 중요한가 하는 부분이다. 


다형성(Polymorphism)

다형성은 객체지향의 꽃이라고 불린다. 다형성이란 다(많은) + 형(타입) + 성(특성)이 합쳐진 말로, 하나의 대상이 여러 개의 타입을 가진다는 것을 의미한다. 하나의 대상이 여러개의 타입을 가진다는 것이 무슨 의미일까?

우리는 이미 다형성을 만들어내는 기법을 알고 있다. 바로 상속이다. 상속은 상속을 받는 클래스에게 자신의 타입과 필드와 메소드를 전달해 주는 역할을 한다. 앞서서 이 세가지 중에 가장 중요한 것이 타입이라고 말했다. 이 타입을 물려 줌으로써 물려 받은 클래스는 다형성을 가진 클래스가 되기 때문이다.

위의 예제에서 FluteMusician은 자기 자신의 타입을 가진다. 그와 함께 Musician을 상속 받음으로써 Musician 타입으로도 취급이 될 수 있다. 아래 코드를 보자.

FluteMusician fm = new FluteMusician();

위의 코드는 FluteMusician 클래스를 객체화 하고 FluteMusician 타입으로 다루겠다는 의미를 지닌다. 아래 코드를 한 번 더 보자.

Musician m = new FluteMusician();

위의 코드는 똑같이 FluteMisician 클래스를 객체화 하지만 Musician 타입으로 다루겠다는 것을 의미한다. 이처럼 똑같은 객체가 여러 타입으로 취급될 수 있는 특성이 다형성이다.


그렇다면 왜 다형성이 객체지향의 꽃이라고 불리는가?

우리는 유연성에 대한 예로부터 Musician 객체를 추출해냈다. 그리고 각각의 클래스가 가진 차이점을 나타내는 클래스들 중에서 FluteMusician이라는 클래스를 만들어 보았다. 이 FluteMusician 클래스는 Musician 클래스를 상속 받도록 되어 있다. 이제 하프 연주자, 피아노 연주자도 같은 방식으로 만들어 볼 수 있다.

class HarpMusician extends Musician{

    public void play(){

        System.out.println("playing harp");

    }

    public void stop(){

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

    }

}

class PianoMusician extends Musician{

    public void play(){

        System.out.println("playing piano");

    }

    public void stop(){

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

    }

}

이제 3명의 연주자로 구성된 오케스트라(좀 빈약 하지만)를 지휘한다고 생각해보자. 만약 다형성이 없었다면, 즉 FluteMusician은 오직 FluteMusician이라는 타입만 가질 수 있고, Musician 타입을 가질 수 없다면, 그리고 다른 클래스들도 이와 같다면 연주는 다음과 같이 이루어질 수 밖에 없다.

public void playAll(){

    FluteMusician fm = new FluteMusician();

    HarpMusician hm = new HarpMusician();

    PianoMusician pm = new PianoMusician();

    // ......

    fm.play();

    hm.play();

    pm.play();

    // ......

    fm.stop();

    hm.stop();

    pm.stop();

    // ......

}

개별 악기 연주자들은 서로 공통점으로 묶어 다룰 수 없다. 다형성이 없을 경우 각각의 클래스는 자기 고유 타입 한가지만 가질 수 있기 때문이다. 따라서 각각의 클래스를 통해 만들어진 객체들은 한번에 취급될 수 없고 각각 다뤄질 수 밖에 없다. 마치 플루트 연주하세요 / 하프 연주하세요 / 피아노 연주하세요 라고 지휘자가 따로따로 외치는 것과 같다. 객체지향에서 다형성이 없다는 것은 현실 세계에서는 마치 개별 연주자들을 공통된 명칭, 즉 연주자라고 부를 수 없는 것과 같다. 플루트 연주자는 현실 세계에서 플루트 연주자이면서 동시에 그냥 연주자라고 불릴 수 도 있다. 하프 연주자도 하프 연주자로 불릴 수 있지만 그냥 연주자라고 불릴 수도 있다. 따라서 현실 세계에서는 연주자 여러분 연주하세요라고 말하면 자연스럽게 동시에 모든 악기 연주자들이 연주를 시작할 수 있다.


객체지향에서는 다형성을 이용하여 현실 세계에서의 동시 연주 효과를 낼 수 있다. 다형성을 통해 개별 연주자 객체들은 Musician이라는 공통 타입을 통해 다루어질 수 있다.

public void playAll(){

    List<Musician> musicians = new ArrayList<Musician>();

    musicians.add(new FluteMusician());

    musicians.add(new HarpMusician());

    musicians.add(new PianoMusician());

    // ......

    musicians.forEach(each -> each.play());

    musicians.forEach(each -> each.stop());

}


개별 연주자들은 각자 고유한 타입을 가지면서도 Musician이라는 타입으로 다뤄질 수 있다. 따라서 Musician 타입 객체를 넣을 수 있는 musicians라는 List에 함께 집어 넣을 수 있다. 그리고 forEach와 같은 순회 메소드를 이용하여 Musician 클래스가 가진 play() 메소드나 stop() 메소드를 동시에 호출함으로써 동시 연주와 같은 효과를 낸다.


유연성과 객체지향 특성

자 이제 다시 한번 정리를 해보자. 우리는 현실세계에서 여러가지 방식으로 유연성을 확보하고 있다. 이미 익숙한 방식을 재활용하는 것은 훌륭한 전략이다. 따라서 그리고 객체지향 언어는 현실 세계에서의 유연성 전략을 그대로 따르고 있다.

현실 세계에서의 유연성은 4가지 단계로 나뉘어진다. 그리고 이 단계들은 객체지향의 4대 특성과 1:1로 매칭된다.

일단 다루어야 할 여러 요구사항의 공통점을 찾고 문제 해결과 관계가 없는 공통점들을 제거한다. 이 과정을 객체지향 언어에서는 추상화(Abstraction)라고 한다.

그런 다음에는 공통점을 묶어 대상화 한다. 객체지향에서는 이를 캡슐화(Encapsulation) 라고 한다.

이 다음에는 개별적인 차이점들을 구현해 주어야 한다. 이 때 이미 뽑아 놓은 공통점은 모든 차이점들에게도 포함을 시켜 줘야 하는데, 이 과정을 객체지향에서는 상속(Inheritance)이라고 한다.

이제 차이점 대신 공통점을 통해 대상을 다루어야 한다. 공통점을 통해 대상을 다루려면 각각의 요구사항들이 공통점을 통해 다뤄 질 수 있어야 하는데, 객체지향에서는 이를 다형성(Polymorphism)을 통해 지원한다.

Posted by 이세영2
,

소프트웨어 분야에서 유연성은 다양한 요구사항을 수용할 수 있는 소프트웨어의 구조적 특성이라고 정의할 수 있다. 소프트웨어는 다른 어떤 컴퓨팅 요소보다도 유연하다. 그리고 잘 만들어진 소프트웨어일 수록 더 유연하게 요구사항을 수용할 수 있다. 소프트웨어의 적용 분야가 확대되고, 소프트웨어 개발 업체들 간에 경쟁이 치열해지고, 더 많은 사람들이 소프트웨어를 사용하게 되고, 더 많은 기기들이 연결되고 더 많은 정보를 수집하게 되면서 유연성에 대한 요구는 점점 더 증가하고 있다. 이러한 요구사항을 반영하기 위해서 소프트웨어는 유연성을 다른 어느 요소보다도 높은 가치로 가지고 갈 수 밖에 없다. 이것이 현대에 객체지향 언어가 대두된 이유이다.

그렇다면 객체지향 언어는 어떤 방식으로 더욱 유연한 구조를 가질 수 있게 되었을까? 여기에 대한 답 역시 현실에서 찾아볼 수 있다.


어린 왕자의 유연성 : 양이 든 상자

소프트웨어의 유연성을 떠올리면 항상 어린 왕자의 일화가 떠오른다. 어린 왕자는 사막에 불시착한 조종사(화자)에게 양을 그려줄 것을 부탁한다. 조종사는 양을 그려본 적이 없기 때문에 거친 솜씨로 이런 저런 양을 그려 준다. 하지만 어린 왕자는 어떤 것은 병들어 보인다, 어떤 것은 숫양이다, 어떤 것은 너무 늙어 보인다는 이유로 퇴짜를 놓는다. 그러면서도 계속해서 양을 다시 그려달라고 말한다.(이쯤에서는 까다로운 요구사항을 늘어놓는 고객이 떠오르게 된다.) 그러자 조종사는 아무렇게나 쓱쓱 그린 상자를 주면서 네가 갖고 싶어하는 양은 그 안에 들어 있다고 말한다. 그러자 어린 왕자의 얼굴은 환하게 밝아진다. 어린 왕자가 어떤 양을 상상하든 그 양은 그 상자 안에 들어 있다. 그래서 어린 왕자는 상자에 만족하게 된다.

우리가 현실세계에서 말하는 유연성이란 이런 상자와도 같다. 구체적인 사항들을 세세하게 늘어 놓으면 정확하긴 해도 다른 것으로 대체하기는 힘들다. 좀 엉성하고 덜 구체적인 것은 이리저리 가져다 붙이면 이것도 되고 저것도 된다. 어린 왕자의 상자는 그런 엉성하고 덜 구체적인 것의 결정판 정도라고 할 수 있다. 엄밀히 말해서 상자는 양과는 너무 동떨어져 있다고 생각될 정도로 매우 추상적이다. 하지만 그렇기 때문에 어린 왕자는 그 상자에서 자신이 원하는 양의 모습을 마음껏 상상해 볼 수 있었다.

이와 비슷한 예를 하나 더 들어보자. 수잔 서덜랜드는 그의 저서 "밈"에서 다음과 같은 타로 카드 점괘를 이야기 한다.

"당신은 남들의 애정과 인정을 받아야만 하는 타입이면서도 스스로에 대해서는 엄격하네요. 겉으로는 절도 있고 자신을 잘 통제하는 것처럼 보이지만, 속으로는 걱정이 많고 불안감도 많이 느끼고요. 가끔은 내가 제대로 된 결정을 내렸을까 하고 심각하게 고민하는군요......"

이 점괘는 사실 트릭인데 이는 "바넘 효과"라는 것을 이용한 것이다. 바넘 효과는 거의 누구나 스스로에 대해서는 참이라고 생각하지만 남에 대해서는 그렇게 생각하지 않는 진술을 던지는 것이다. 이것은 심리적인 유연성에 해당한다. 즉, 대부분의 사람들이 자신에게 맞다고 생각하는 점괘를 말해 줌으로써 그 점괘를 믿도록 만든다. 이러한 점괘에서는 당연히 매우 구체적인 날짜나 시간, 구체적인 명칭이나 사건들에 대해서는 언급을 피하는 것이 상책이다. 그래야만 더 많은 사람들에게 들어 맞는 유연한 점괘가 될 것이기 때문이다.


율리우스 카이사르의 유연성 : 추천 편지


율리우스 카이사르는 기원전 1세기에 살았던 로마의 지도자이다. 율리우스 카이사르는 인물 사전에 보면 로마의 정치가, 장군, 작가라고 나온다. 대단히 다재다능한 사람으로써 생애 전체가 유연성의 표본이 아닌가 싶기도 하다. 그가 지휘해서 이긴 전투들은 대부분 너무 독창적이어서 안타깝게도 사관학교 교재로 쓰이지 못할 정도이고(사관학교에서 교육을 하려면 어느 정도 정형화되고 누가 하더라도 같은 효과를 낼 수 있는 전투 방식이어야 하는데 카이사르의 그것은 너무나도 창의적이었다는 의미이다.), 이런 능력은 여자를 홀리는데도 사용되어 유명한 난봉꾼으로 통하기도 했다.

어쨌든 카이사르의 이러한 재능의 원천은 철저히 합리적인 방식을 통해서 얻어진 것으로 생각된다. 그는 발명에도 일가견이 있었는데, 긴 두루마리에 글을 적고 읽었던 당시에 종이를 낱장으로 잘라 한쪽 면을 이어 붙이는 "책"을 발명하였다. 이것은 두루마리가 이전에 글을 읽었던 위치를 알기에는 적합하지만 원하는 글이 적힌 임의의 위치를 빠르게 찾는데는 불편하다는 점에 착안한 것이다. 또한 당시까지 사용되던 부정확한 로마력을 대신하여 율리우스력이라는 당시로서는 매우 정확한 달력을 사용하도록 했다. 이 달력은 기원전 1세기에 사용되기 시작한 이후로 1582년 그레고리력으로 바뀌기 전까지 사용될 정도로 매우 정확했다.

이런 합리적인 사고 방식은 그가 권력을 유지하기 위해서 지방의 선거에 자신이 원하는 인물들을 추천한 방식에도 나타난다. 카이사르는 다음과 같은 문장이 쓰인 편지들을 미리 만들어 두었다.

"카이사르는 선거구 (       )의 유권자 여러분이 던지는 표로써 후보자 (     )가 그들이 바라는 관직에 당선될 수 있기를 희망한다."

그리고 필요한 시기에 이 편지에다 선거구 이름과 후보자 이름만 적어 넣음으로써 필요한 모든 선거구에서 자신이 원하는 후보를 지지해 주도록 추천할 수 있었다. 카이사르의 합리적인 성격 덕분에 매우 유연성 있는 추천 편지가 만들어지게 된 것이다.

이러한 유연성의 예는 현대에도 자주 찾아 볼 수 있다. 이 글을 읽고 있는 사람들 중에서 이미 결혼을 한 사람이 있다면 결혼을 위해서 청첩장을 만들어 전달했을 것이다. 그리고 그 청첩장의 내용은 그 청첩장을 받는 사람들마다 모두 달랐을 것이다...... 라고 짐작한다면 아무래도 유연성이 떨어진 생각이 아닐 수 없다. 거의 모든 사람들은 청첩장의 내용물은 모두 같고, 단지 전달할 상대에 따라서 겉포장에 이름만 다르게 적어 보냈을 것이다. 이것이 인간미는 조금 떨어질지 모르지만 바쁜 현대인들에게는 상당히 합리적인 선택이라고 할 수 있다.


어린왕자의 상자, 유연한 점괘, 카이사르의 추천 편지, 청첩장의 예를 통해서 우리가 현실 세계에서 유연성이라는 것을 어떻게 이용하고 있는지 확인해 볼 수 있었다. 일단 여러 요구사항들(어린왕자가 원하는 그 양, 점을 보러 오는 다양한 사람들, 다양한 선거구에 다양한 후보)을 모두 만족시키려면 모든 요구사항이 포함하는 공통된 부분들을 먼저 알아야 한다. 그러기 위해서 서로 다른 점들을 제거하고 공통된 부분을 모아 필요한 대상을 미리 만들어 둔다. 그리고 나서 개별적인 요구사항들에 대해서는 차이점만 조금씩 더해서 대응한다. 이렇게 함으로써 여러 요구사항을 개별적으로 대응하는데 들어가는 수고를 최대한 덜어낼 수 있다. 요컨데 똑같이 할 수 있는 부분을 뽑아 내어 한번만 하고 다른 부분들에 대해서는 최소한으로 대응하는 방식이다.


유연성 확보 전략 : 오케스트라의 비유

이제 조금 더 객체지향에 가까운 유연성 비유를 이야기 해보도록 하겠다. 이번에는 오케스트라에 비유해 보도록 하겠다. 오케스트라는 매우 다양한 악기를 연주하는 연주자들과 지휘자가 함께 음악을 연주한다. 악기는 바이올린, 피아노, 플루트, 하프, 비올라, 바순, 트럼펫, 트럼본, 첼로, 콘트라베 등 매우 다양하다.


이제 우리는 오케스트라 지휘자의 행동을 유추해 보자. 일반적으로 지휘자는 특별히 말을 하지 않고 지휘봉을 이용하겠지만 목적한 바를 명확하게 보여주기 위해서 지휘자가 말로 지휘를 한다고 생각해 보겠다. 지휘자는 이제 연주가 시작되었으면 좋겠다고 생각한다. 그래서 이제 각각의 연주자들에게 연주를 부탁해야 한다. 이 상황에서 지휘자가 이렇게 이야기한다.

"바이올린 연주하세요. 피아노 연주하세요. 플루트 연주하세요. 하프 연주하세요......"

이렇게 각각의 연주자들에게 개별적으로 연주를 부탁한다면 어떻게 되겠는가? 일단 연주를 시작하는 시점이 다르기 때문에 아름다운 연주가 되지도 않을 뿐더러 연주를 다 시작하게 될 때까지 시간이 매우 많이 걸리게 될 것이다. 이것은 일반적이고 합리적인 방식이라고 할 수 없다.

그러면 이제 다음의 방법을 생각해 보자.


이런 난감한? 상황을 타개하기 위해서 지휘자는 기지를 발휘한다. 지휘자는 각각의 악기를 다루는 사람들을 한꺼번에 다룰 수 있는 방법을 모색한다. 바이올린을 연주하는 연주자, 피아노를 연주하는 연주자, 플루트를 연주하는 연주자...... 이들은 모두 다른 악기를 연주하지만 연주자라는 공통점을 가지고 있다. 따라서 각각의 악기 이름으로 대응하기 보다는 공통된 부분, 즉 연주자라는 호칭으로 연주를 부탁하면 된다.

"연주자 여러분, 연주하세요."

이렇게 되면 각 연주자들은 동시에 연주를 시작할 수 있고, 지휘자는 단 한번만 요청하면 모든 악기가 연주 되도록 할 수 있다.

이 과정에서 지휘자는 매우 구체적인 대상, 즉 바이올린 연주자, 피아노 연주자 등을 대신하여 조금은 추상적인 대상, 즉 연주자라는 개념을 만들고 활용하였다. 이는 앞서 살펴본 현실 세계의 예들을 대표하는 방식이다. 실제적이면서 개별적인 대상 대신에 공통적이고 조금은 추상적인 개념을 대상화하고 이를 이용함으로써 개별적으로 대응하는 방식에 비해 훨씬 합리적이고 손쉽게 요청을 전달할 수 있었다.


우리는 이제 현실 세계에서 서로 다른 다양한 요구사항들을 어떤 방식으로 합리적이고 유연하게 대처하는지를 알게 되었다. 여러가지 요구사항들을 수집(각 악기 연주자들)하고, 구체적인 요구사항에 비해 다소 추상적인 공통점(연주자)을 찾아내고, 이를 대상화(연주자라고 부를 수 있도록)하고, 이들 공통점에 차이점을 조금 더하고(어떤 연주자는 바이올린을 연주한다), 공통점을 중심으로 대상을 다룬다(연주자 여러분 연주하세요). 이런 방식으로 유연성을 확보하는 것은 조금만 관심있게 찾아보면 늘상 있는 있는 일이라는 점도 알 수 있다. 동사무소에 준비된 각종 서류들은 이름과 서명란, 주소 등의 몇가지 구체적인 정보만 놔 둔체 이미 작성되어 있다. 여기에 자기의 정보만 적어 넣으면 곧바로 훌륭한 신청서가 된다. 보험을 가입할 때에도 약관은 이미 작성되어 있고, 가입자가 개인 정보를 기록하고 사인만 하면 가입이 완료된다. 이런 예를 더 찾아보자면 무수히 많을 것이다.

그러면 이제 우리가 현실 세계에서 유연성을 확보하는 과정을 단계별로 살펴볼 시간이다.


공통점을 추출한다.( = 차이점을 감춘다.)

현실 세계에는 마치 아날로그와도 같은 수준의 서로 다른 요구사항들이 있다. 아날로그 수준이라는 말은 요구사항이 하나하나 떨어져 있지 않고 서로 연속적으로 이어진 것처럼 다양하다는 의미이다. 연속적으로 이어진 요구사항들은 그 사이사이에 또 무수히 많고 다양한 요구사항들을 품고 있다. 이렇게 생각해야 하는 이유는 이미 밝혀진 요구사항들보다도 더 많은 요구사항이 앞으로 생겨날 수 있기 때문이다. 사람들은 종종 한가지를 보면 다른 한 가지를 떠올린다. 요구사항 하나는 그와 유사한 생각을 만들어 내고 이것은 다시 구체적인 요구사항이 되어 돌아오기도 한다. 소프트웨어 분야에서 유독 웹 개발자들이 힘들어지는 이유가 바로 이것 때문이다. 소프트웨어 시스템 내부가 어떻게 동작하는지는 쉽게 알 수 없지만 브라우저 상에서 표현되는 그림이나 글자는 쉽게 눈에 보이고, 눈에 보이는 순간 생각을 하게 되고, 생각이 다시 요구사항으로 돌아 오면서 이미 만들어 놓은 것을 고치게 하기 때문이다.

공통점을 추출하는 과정은 이 아날로그 수준의 서로 다른 요구사항들을 모두 수용할 수 있는 공통된 특징을 모으는 과정이다. 이 과정은 당연히 서로 다른 부분은 제거한다. 오케스트라의 비유에서 각 악기 연주자들은 서로 다른 악기를 가지고 있다. 서로 다른 악기를 가지고 있다는 점을 포함하고 있으면 모든 연주자들을 지칭할 수 있는 공통점을 만들어 낼 수 없다. 따라서 개별적인 악기들은 공통점에서 제거한다.

이와 함께 제거 되어야 하는 것은 또 있다. 각 악기 연주자들은 서로 다른 개인이기도 하다. 거주하는 곳이 다르고, 식성이 다르고, 평소 즐겨 입는 옷도 다르다. 이름, 전화번호, 주민번호, 나이, 헤어스타일 등 다른 점을 찾아보자면 무수하게 많다. 또 공통점도 있을 수 있다. 모두다 한국 사람일 수도 있고, 모두 다 악보를 볼 두 눈을 가지고 있을 수 있다. 이렇게 나열하다 보면 공통점과 차이점은 무수하게 많아지게 될 것이다. 그래서 공통점만 모으는 과정에서 너무 많은 공통점이 모일 수 있다. 그렇기 때문에 공통점을 추출하는 과정에서는 목적에 맞지 않는 불필요한 공통점은 제거되어야 한다. 오케스트라 지휘자는 현실 세계에서 연주자들과 친하게 지내기 위해서 이런 저런 정보들을 알아야 할 수도 있다. 하지만 지금은 지휘에 집중해야 할 때이다. 따라서 지휘하는데 꼭 필요한 공통점만을 추출해 내면 된다. 국적이나 악보를 볼 수 있는 눈 따위는 이 기준에 의해서 제거 되어야 한다.

이처럼 차이점들을 제거하고, 공통된 부분들 중에서 우리의 목적과 상관이 없는 정보들 역시 제거되고 나면 이제 공통점만 모은 어떤 것이 된다.


"대상화" 한다.

어떤 공통점이 모였을까? 우리는 연주를 할 것이기 때문에 각 악기 연주자들은 연주를 할 수 있어야 한다. 그리고 필요한 시점에 연주를 멈춰 줄 줄도 알아야 한다. 더 많은 공통점이 필요할 수도 있지만 이 정도만 모였다고 가정하자. 

그러면 이제 "대상화"를 할 시간이다. 대상화란 모아 놓은 공통점을 다룰 수 있게 만드는 과정이다.


(이상화)

  내가 그의 이름을 불러주기 전에는 
  그는 다만 
  하나의 몸짓에 지나지 않았다


 
내가 그의 이름을 불러주었을 때 
  그는 나에게로 와서 
  꽃이 되었다

  ......
 
 
우리들은 모두 
  무엇이 되고 싶다

 
나는 너에게 너는 나에게 
  잊혀지지 않는 하나의 의미가 되고 싶다
.


이 대상화를 떠올리면 항상 이상화 시인의 꽃이라는 시가 떠오른다. 이 시의 아름다움도 좋지만 대상화라는 과정이 어떤 것인지를 이해하는데도 도움이 된다. 우리가 어떤 대상에 대한 개념을 모았다고 하자. 하지만 이것에 정확한 명칭을 부여하여 다룰 수 있도록 만들기 전에는 그것은 "하나의 몸짓"에 지나지 않는다. 하지만 공통점들을 명료하게 만들고 명칭을 부여하면 그것은 "꽃"이 된다. 오케스트라의 비유에서 "꽃"은 바로 "연주자"라는 개념이다. 이 연주자라는 개념은 구체적이지 않고 추상적인 개념이다. 따라서 구체적인 대상을 현실 세계에서 찾을 수는 없다. 하지만 반대로 연주자라는 개념을 다룰 수는 있다. 연주자는 연주를 할 수 있어야 해, 연주자는 필요할 때 연주를 멈출 수 있어야 해 와 같은 방식으로 다소 추상적인 방식이긴 하지만 다룰 수 있는 대상이다. "대상화"의 개념을 명쾌하게 이해하지 못하겠다면 일단 "명칭"을 부여했다는 정도만이라도 이해하면 되겠다.


차이점을 더한다.

이제 남은 것은 차이점이다. 차이점은 공통점을 추출하는 과정에서 남은 것들이다. 그렇다고 남은 모든 것들은 아니다. 우리의 목적은 오케스트라를 지휘하는 것이다. 차이점을 더하는 과정도 목적에 맞는 것에만 해당한다. 여기서 각 악기 연주자들의 개인정보나 취향은 제거된다. 대신 오케스트라의 목적에 맞게 악기는 다들 가지고 있어야 하겠다. 오케스트라 지휘에 앞서 필요한 악기들이 모두 준비 되었는지를 확인하는 과정도 필요할 것이다. 따라서 차이점에는 악기가 포함된다. 이제 우리는 연주자라는 개념에 개별적인 차이점이 더해진 개별적인 악기 연주자를 설정한다. 대략 플루트 연주자, 하프 연주자, 피아노 연주자 등이 되겠다.

이 차이점을 더하여 개별적인 악기 연주자를 만드는 과정은 한가지 암시적인 가정을 포함한다. 플루트 연주자는 매우 구체적이다. 하지만 동시에 좀 더 추상적인 개념인 연주자와 동일시 될 수도 있다. 즉 플루트 연주자는 다른 연주자들과 구분되는 플루트라는 악기 속성을 가지면서도 동시에 연주자가 가진 연주하는 속성, 연주를 멈추는 속성도 가지고 있어야 한다. 이렇게 되어야만 다음 단계가 가능해진다.

현실에서라면 이와 같은 과정을 겪어야 할 것이다. 아래는 지휘자가 플루트 연주자에게 하는 가상의 대화이다.

"플루트 연주자님, 플루트 연주자님은 다른 악기 연주자들과 다르게 플루트를 가지고 계셔야 합니다. 그리고 저는 연주를 효율적으로 하기 위해서 플루트 연주자님을 그냥 연주자라고 부를 것입니다. 연주자란 연주하세요 하면 자기가 가진 악기를 연주하고, 멈추세요 하면 연주를 멈추는 사람입니다. 이해되셨죠?"

현실 세계에서는 이미 각 악기 연주자가 연주자 개념을 알고 있기 때문에 이런 일이 일어나진 않는다.


공통점을 통해 대상을 다룬다.

이제 지휘자가 오케스트라 지휘를 시작할 시점이다. 앞서의 지휘자와 플루트 연주자와의 대화와 유사한 대화들이 각 악기 연주자들과도 오고 갔다고 가정하자. 그러면 이제 지휘자는 연주를 시작할 수 있는 상태가 된 것이다. 각 악기 연주자들은 우리가 정의한 "연주자"의 개념을 알고 있다. 따라서 각 악기 연주자들에게 개별적으로 연주를 부탁하지 않아도 된다. 단지 다음과 같이 말하기만 하면 된다.

"연주자 여러분, 연주하세요"

그러면 플루트 연주자는 자기가 가진 악기인 플루트를, 하프 연주자는 하프를, 피아노 연주자는 피아노를 연주하게 될 것이다. 이렇게 열심히 공통점을 대상화하고 이용할 수 있도록 만들어 놓고서도 각각의 악기 연주자에게 일일이 연주를 지시하는 것은 비 합리적인 일이다.


자 이제 현실세계에서의 유연성이라는 것은 무엇이고, 이것이 수많은 요구사항들을 어떻게 수용하는지 알 수 있게 되었으며, 유연성을 확보하는 전략을 단계별로 이해할 수 있게 되었을 것이다. 이제 현실 세계에서의 유연성 확보 방법이 우리가 알고자 하는 객체지향 언어와 어떻게 연관되는지 살펴볼 차례다.

Posted by 이세영2
,

현실과 소프트웨어 세계에서의 객체

우리는 현실에서는 객체의 세계, 소프트웨어 세계에서는 객체지향의 세계에서 살고 있다고 해도 과언이 아니다.

현실에서의 객체란 독립적이면서 (분리되지 않고) 하나로 취급되는 인식의 단위를 말한다. 인간은 대상을 정확히 인식하기 위해서 목적한 대상과 다른 대상들과의 개념적인 차이들을 필요로 하고, 그 차이들이 명확해지고 나면 그 대상에 대해 명칭을 부여한다. 이 과정을 통해 불분명했던 대상은 정확한 명칭을 지닌 객체가 된다. 우리의 인식 속에서 일단 객체로 자리잡은 대상은 고유한 인식적 위치를 점유하게 되고 이는 다른 대상을 판별하는데 이용됨으로써 인식의 세계가 점점 더 넓어지는데 일조하게 된다. 대상을 다른 대상들과 분리하여 인식하는 이와 같은 방식은 현실에서의 인간이 살아가는데 큰 도움을 준다.

근본적으로 프로그래밍 언어로서의 객체지향 개념은 위에서 본 현실에서의 객체에 대한 인식과 크게 다르지 않다. 프로그래밍 언어의 세계는 관념적인 세계이다. 인간은 현실을 객체 단위로 이해하고 있지만 실제 세계는 객체적인 세계가 아니다. 개와 고양이는 (관념적으로는) 같은 동물이긴 하지만 개와 고양이는 같은 개념으로 묶을 수 있는 단위가 아니다. 하지만 우리의 두뇌는 현실을 관념적으로 파악하기 때문에 거리낌 없이 개와 고양이를 묶어 동물이라고 부른다. 그리고 아이러니하게도 이렇게 비현실적인 관념으로 세계를 파악하는 방식이 전체를 분리하지 않은 체 이해하는 방식보다 거의 언제나 더 유용하다. 유용하다는 말은 더 빠르고 정확하게 이해할 수 있다는 말이다. 따라서 프로그래밍 언어에 객체지향이라는 개념이 도입된 이유는 인간이 객체를 중심으로 프로그래밍을 하는 편이 더 빠르고 정확하게 이해하면서 구현하는 것이 가능하기 때문이다. 이 점은 매우 중요한데, 프로그래밍의 도구는 오직 두뇌 뿐이고, 두뇌가 더 빠르고 정확하게 문제를 이해하는 것은 프로그래밍에 그만큼 유리하게 작용하기 때문이다.

2016년 6월 기준으로 Java, C++, Python, C#, PHP, Javascript, Ruby, Objective-C 등과 같이 굵직한 언어들 중에서 객체지향을 온전히, 혹은 상당 부분 지원하는 언어의 비중을 따져보면 약 44.5%를 차지했다. 만약 하드웨어적인 제약 사항을 만족시키기 위한 언어(C, Assembly)나 제한된 목적을 위한 언어(Cobol, Matlab, Delphi, Visual Basic 등)를 제외한다면 범용적이고 일반적인 어플리케이션의 대부분은 객체지향 언어로 만들어진다고 볼 수 있을 것이다.


프로그래밍 언어의 역사와 객체지향

우리가 현실 세계를 객체로 이해하고 있다는 점을 이해한다면 객체지향 언어가 프로그래밍 언어 영역에 도입된 시기는 매우 빨랐을 것으로 추측할 수 있다. 실제로도 그렇다. 이반 서덜랜드(1938~)는 1963년도에 "스케치패드 : 사람과 기계와의 그래픽 대화 시스템"이라는 논문을 통해 객체지향 개념을 기반으로 구현한 그래픽 프로그램을 발표한다.("누가 소프트웨어의 심장을 만들었는가") 이후 시뮬라67 언어가 만들어지고(1967), 앨런 케이에 의해 스몰 토크라는 언어가 만들어짐으로써 온전한 객체지향 언어가 탄생하게 된다. 시기적으로 보면 현대에 가장 많이 사용되고 있는 비 객체지향 언어인 C 언어가 1972년에 만들어졌으니 객체지향 언어는 완성된 비 객체지향 언어보다도 개념적으로 더 오랜 역사를 가지고 있는 셈이다.

하지만 이율배반적이게도 많은 사람들이 객체지향 언어를 접한 시기는 상대적으로 최근의 일이다. 여기에는 여러가지 이유가 있다. 첫째 객체지향 언어는 컴퓨팅 파워가 뒤쳐졌던 70년대에는 다른 언어들에 비해 상대적으로 비인기 언어였다. 낮은 하드웨어 성능을 최대한 끌어낼 수 있는 절차지향 언어가 먼저 인기를 끌었다. 둘째, 우리에게는 객체를 중심으로 현실을 이해하는 것 만큼 익숙한 이해의 방식이 하나 더 있기 때문이다. 이것은 어떤 문제(또는 일)를 시간과 인과 관계의 순서로 이해하는 방식이다. "처음엔 A였다가 B라는 이유로 C가 되었다가 결국엔 D로 끝났어"와 같은 서술 방식은 시간과 공간 속에서 살아가는 인간에게 매우 익숙한 상황 인식 및 설명의 방식이다. 이러한 방식을 프로그래밍 언어에 도입한 것이 절차지향 언어이다. 첫번째 이유로 인해 기피되던 객체지향 프로그래밍 방식이 초반부터 매우 강력한 경쟁자를 만난 셈이다.

세번째 이유는 객체지향 언어를 이해하는 것이 절차지향 언어에 비해 상대적으로 어렵다는 점이다. 객체를 중심으로 이해하는 것이 현실을 이해하는데 유용하다는 점에서 볼 때는 매우 아이러니한 일이 아닐 수 없다. 우리는 매우 자연스럽게 세계를 객체 중심으로 이해한다. 이 과정은 너무나도 자연스럽기 때문에 그 메커니즘에 대해서는 잘 인지하지 못한다. 우리는 개와 고양이를 개와 고양이라고 부르면서 구분하고 있고, 동시에 동물이라는 카테고리 안에 이 둘을 자연스럽게 포함시킨다. 또 동물과 식물을 구분하고 있으면서 동시에 생물이라는 카테고리 안에 이 둘을 포함시킨다. 이것을 그림으로 그리면 트리 구조가 될 것이다. 하지만 우리는 이러한 구분을 트리 구조를 통해서 이해하고 있지 않다. 그냥 자연스럽게 그 인식 과정을 받아들인다. 하지만 이러한 메커니즘을 모사하여 프로그래밍 언어에 도입하는 과정은 그다지 순탄하지 않다. 개와 고양이를 구분하려면 개를 개념적으로 명확하게 정의하고 고양이를 개념적으로 명확하게 정의할 수 있어야 한다. 그리고 이들 둘을 동물이라는 같은 개념으로 묶을 수 있으려면 두 동물이 가진 개념 중에서 공통점을 추출할 수 있어야 한다. 또한 동물의 특성을 개와 고양이가 가질 수 있게 하려면 상속이라는 개념을 도입해야 하고, 동물이라는 개념으로 개와 고양이를 다룰 수 있으려면 다형성이라는 개념을 도입해야 한다. 요컨데 인간의 두뇌가 현실을 이해하는데는 엄밀한 정의도 필요하지 않고, 전체적인 논리적 완결성을 추구할 필요가 없지만 이를 매우 논리적인 완결성을 요구하는 프로그래밍 언어에 도입하는 데에는 다양한 개념의 정립이 필요하다.


객체지향이 대두된 이유

그렇다면 절차지향 언어가 계속 우위에 설 수도 있었을텐데 객체지향 언어는 왜 다시 대두되었고, 현재와 같은 주류 언어가 될 수 있었을까?

기초적이고 단편적인 이유들을 먼저 설명해 보자. 첫째로 컴퓨터의 연산 능력이 좋아졌다는 점이다. 객체지향 언어가 생겨난 70년대 이후 수십년에 걸쳐 하드웨어는 비약적인 발전을 거듭해 왔다. 그리고 하드웨어적/소프트웨어적 발전으로 인하여 하드웨어는 단일한 플랫폼을 넘어서 병렬화되면서 성능은 더더욱 좋아지게 되었다. 이것은 상대적으로 연산량을 많이 필요로 하는 객체지향 언어가 활동할 수 있게 된 배경이 된다. 둘째로 절차지향 언어의 한계점이 대두되었기 때문이다. 앞서 말했듯이 절차지향 언어는 시간과 공간 상에서의 인과 관계를 중심으로 현실을 이해하는 인간의 인식 방식을 모방한 것이기 때문에 이해하기 쉽고 다루기 쉬운 프로그래밍 언어이다. 하지만 절차지향 언어로 개발된 소프트웨어는 어느 일부분을 떼어 내어 다른 소프트웨어를 만드는 것이 어렵다. 절차지향 언어가 함수라는 독립된 단위로 소프트웨어를 분할하고 있지만 각 함수는 연쇄적으로 호출되면서 동작하기 때문에 어떤 부분만을 별도로 나누어 다른 시스템에 적용시키기가 어렵다. 또한 절차 전체에서 메모리를 공유하거나, 특정 절차가 어떤 데이터와 연관되는지를 쉽게 알 수 없기 때문에 전체 동작을 이해하기 위해서는 각 절차들의 내부를 자주 들여다 봐야 되고, 이런 방식으로는 큰 소프트웨어를 작성하기가 어려워진다.

하지만 보다 근본적인 이유는 따로 있다. 1930년대 후반에 소위 "컴퓨터"라는 것이 발명된 직후, 컴퓨터의 용도는 주로 암호 해독이었다. 일련의 덧셈, 뺄셈, 곱셈, 나눗셈 등을 이용하여 인코딩된 정보를 디코딩하는 용도라고 할 수 있다. 1940년대에는 군사 분야쪽으로 용도가 넓어진다. 포탄 궤적용 표, 폭발 계산(원자폭탄 개발), 탄도 미사일 계산과 같이 군사 물리적인 용도가 주를 이루었다. 1950년대에는 FORTRAN 언어가 개발되면서 과학 연구 분야에 컴퓨터가 활용되기 시작한다. 초신성 폭발, 수치해석, 일기예보 등이 그 예이다. 1960년대에는 운영체제와 COBOL 언어가 등장함으로써 고등 제조 기술 분야와 비즈니스 분야에 소프트웨어가 활용된다. 항공기 설계(특히 엔진 설계), 우주선의 제작 및 우주선 궤도 계산 분야, 비즈니스 소프트웨어 분야에 활용되었다. 1970년대에 이르러서는 PC가 출현하고 유저 인터페이스 및 인터넷이 탄생한다. 이와 함께 PC용 소프트웨어가 개발되면서 소프트웨어는 산업 전반에 걸쳐 사용된다. 애플I이 개발된 것이 1976년의 일이다. 1980년대는 PC 시대가 활짝 열리게 된다. MS-DOS가 발명되고(1981), PC의 대중화를 주도한 맥킨토시(1984)가 발명된 것도 이 시기이다. 특히 맥킨토시는 최초의 GUI와 마우스를 채용함으로써 그래픽 기반의 소프트웨어 시대를 열었다. 1990년대는 뭐니뭐니해도 WWW, 즉 월드 와이드 웹의 시대이다. HTML과 웹 브라우저로 인하여 전 세계의 컴퓨터가 서로 정보를 주고 받을 수 있는 세상이 되었다. 이로 인하여 소프트웨어는 닫힌 공간을 위한 용도에서 연결된 공간을 위한 용도로 활용되게 된다. 2000년대는 소셜 네트워크 및 집단 지성의 시대이다. 이 시기에 구글이 기존과는 완전히 다른 개념의 검색 기술을 개발(2003)하였고, Wikipidia(2001)와 같이 집단 지성을 활용하는 시대가 되었다. Facebook(2004)은 SNS의 시대를 열었고, 아이폰(2007)은 이 연결을 통한 가치 창출을 모바일의 영역으로 확대하였다. 2010년대는 빅데이터와 인공지능의 시대이다. IBM은 왓슨(2011)을 통해 빅데이터 기반으로 훈련된 인공지능을 선보였고, 2016년에는 이세돌과 알파고의 대결을 통해 인간의 능력을 넘보는 인공지능의 시대가 시작되었다.

소프트웨어의 역사에서 가장 주목할만한 것은 소프트웨어 활용 영역이 지속적으로 확대 되었다는 점이다. 소프트웨어는 컴퓨터의 초기부터 지금까지 그 적용 분야가 계속 확대되어 왔다. 그러면 이제부터는 그 적용 분야가 점차 축소될 것인가? 인공지능 분야는 현재 제한적인 부분에서만 인간의 능력을 뛰어 넘는 수준에 머물러 있지만 앞으로 적어도 모든 분야에서 인간의 능력을 뛰어넘기 전까지는 계속 발전하게 될 것이다. 이를 위해서는 더 많은 정보가 수집되어 분석되어야 한다. 당연히 데이터는 계속 수집될 것이고, 인간의 능력을 뛰어넘는 분야는 점점 넓어질 것이다. 사람들이 가지고 있는 모든 지식과 컴퓨터 뿐만 아니라 모든 센서가 연결되고 그 데이터가 수집될 때까지 이 과정은 지속될지 모른다. 즉, 소프트웨어는 적용 분야, 크기, 연산량이 확대되는 방향으로 발전해 갈 수 밖에 없다.

이 과정에서 소프트웨어가 갖추어야 할 기능의 필요성, 즉 요구사항의 증가는 필연적이다. 요구사항은 계속 증가한다. 단순히 양만 증가하는게 아니고 적용 분야가 증가하면서 종류도 다양해진다. 또한 소프트웨어와 인간이 만나는 횟수가 증가하면서 요구사항은 점점 더 다양해지고 또 자주 바뀌게 된다. 개별적인 인간의 개성이나 변덕의 수준을 맞추기 위해서는 소프트웨어가 더 많이 바뀌고 더 자주 바뀌고 더 빨리 바뀔 수 있어야 한다. 이렇게 잘 바뀌는 특성, 즉 유연성(flexibility)이 소프트웨어의 최고 덕목이 된다. 그래서 프로그래밍 언어 중에서도 가장 유연성이 뛰어난 객체지향 언어가 새로운 강자로 떠오르게 된 것이다.


소프트웨어가 가져야 할 가치

TDD, xUnit 테스트, 애자일의 창시자로 유명한 소프트웨어 프로그래머 켄트 벡은 "켄트 벡의 구현 패턴"에서 소프트웨어가 가져야 할 가치를 유연성, 단순성, 커뮤니케이션으로 정의하였다. 커뮤니케이션은 갈수록 커져가는 소프트웨어의 규모를 맞추기 위해서는 여러 사람들의 협력이 필요하고, 이를 위해서는 소프트웨어가 자기 스스로를 설명할 수 있는 능력, 즉 커뮤니케이션 도구로서의 기능을 가져야 한다. 또한 사용자의 요구사항은 예측이 불가능하게 변화하거나 추가되기 때문에 소프트웨어는 유연성을 꼭 가져야만 한다. 이들 덕목을 갖추면서도 기능을 구현하는 가장 단순한 방법을 추구하는 단순성을 가지고 있어야 한다.

이들 중에서 소프트웨어의 유연성은 소프트웨어의 존재 이유에 해당하는 가치라고 볼 수 있다. 소프트웨어는 하드웨어와 같은 다른 요소가 감당하지 못하는 기능 변경 요구사항을 수용하기 위해서 만들어진 요소이다. 이로 인하여 소프트웨어의 생애 주기 중 대부분은 수정과 개선 기간이 된다. 로버트 L. 글래스는 (우리가 미처 알지 못한) 소프트웨어 공학의 사실과 오해라는 책을 통해서 소프트웨어의 유지보수가 보통 소프트웨어 비용의 40~80%를 차지한다고 말한다. 이와 함께 60/60 법칙을 제시하는데 소프트웨어 비용의 60%는 유지보수에 사용되고, 유지보수 비용의 60%는 소프트웨어 기능의 개선을 위해 사용된다고 한다. 이는 소프트웨어가 얼마나 자주 변경되는지, 요구사항이 얼마나 자주 바뀌는지를 말해준다. 더불어 유연성이 소프트웨어의 가치 중에서 얼마나 큰 비중을 차지하는지를 알 수 있게 해준다.

객체지향은 소프트웨어의 유연성이라는 측면에서 가장 뛰어난 접근 방식이다. 객체지향 언어는 소프트웨어에 새로운 기능을 추가하거나 기존의 기능을 변경하는 것, 또는 더 이상 필요 없는 기능을 제거하는 유지 보수 작업에 가장 유리한 언어이다. 다음 글에서는 객체지향 언어가 이 유연성을 어떻게 확보하는지를 알아보겠다.

Posted by 이세영2
,

로버트 L.글래스의 우리가 미처 알지 못한 S/W공학의 사실과 오해

간만에 정신이 맑아지고 현실과 미래가 뚜렷히 보이는 명쾌한 작품을 읽은 느낌이다. 왜 이 책이 그렇게 많은 사람들에게 회자되고 인용되었는지 알 수 있었다. 이 책은 소프트웨어가 세상에 등장한 이후부터 이 책이 쓰여진 시점까지 소프트웨어 분야에서 일어났던 모든 일들을 집약한 책이라고 설명할 수 있겠다. 우리가 소프트웨어를 개발하면서 만났던 수많은 일들, 경험적으로는 알고 있었으나 증명하기 어려웠던 문제들, 소프트웨어 개발 과정에서 겪었던 어려움과 그 이유들, 수많은 문제들 중에서 어떤 문제에 집중하는 것이 옳고 어떤 자세로 임해야 하는지 등에 대한 명료한 답을 제시해 준다.

이 글은 이 책(원 제목은 Facts and Fallacies of Software Engineering)에서 얻은 내용들을 정리한 것이다. 하지만 이 글만 읽어보고 책을 읽지 않게 되는 것은 내가 원하는 것과는 정 반대의 일이다. 내가 이 책을 누군가에게 소개한다면 다음과 같을 것이다. 이 책은 소프트웨어 개발과 관련된 모든 사람들이 읽어야 할 책이다. 소프트웨어를 직접 개발하는 사람이라면 꼭 읽어야 할 책이다. 이 책을 모르고 소프트웨어 개발을 해왔던 시간들이 너무 아깝다.


1장  관리

사람

사실 1. 소프트웨어 직업에서 가장 중요한 요소는 프로그래머가 사용하는 도구나 기술이 아니라, 프로그래머의 자질이다.

-       사람이 (소프트웨어 개발) 성공의 열쇠다

-       엄격한 방법론을 적용한 프로젝트를 한꺼풀 벗기고 프로젝트의 성공 이유를 물으면 그 답은 사람이다.

-       소프트웨어 생산성에 있어 가장 중요한 요소는 소프트웨어 실무자 개인의 역량이다.

사실 2 최고의 프로그래머는 최하의 프로그래머보다 28배 뛰어나다.

-       뛰어난 소프트웨어 실무자가 (동료들보다) 5배에서 28배까지 뛰어나다는 사실을 알 수 있다면, 가장 뛰어난 사람들을 잘 돌보는 것이 소프트웨어 관리자의 가장 중요한 업무라는 것은 자명한 일이다.

-       우리의 연구에서 가장 중요한 실질적 발견은 프로그래머 실무 능력의 현저한 개인차다

-       개인간에 5배 정도의 생산성 차이는 흔한 것이다.”

사실 3 지체된 프로젝트에 사람을 추가 투입하면 프로젝트가 더 늦어진다.

-       사람이 많을수록 커뮤니케이션은 더욱 복잡해진다. 따라서 프로젝트가 지연될 때 인력을 투입하면 프로젝트는 더 늦어지는 경향이 있다.

사실 4 작업환경은 생산성과 품질에 지대한 영향을 미친다.

-       프로젝트에서 상위 25%와 하위 25%에 대해서(이들간에는 2.6배 생산성 차이가 났다) 작업환경을 조사한 결과, 상위 그룹은 1.7배 넓은 공간에서 일했고, 충분히 조용한 환경이라고 대답한 비율이 2배 높았고, 개인 공간이라고 말한 비율은 3배 이상 높았으며, 전화를 돌리거나 꺼놓을 수 있다고 답한 비율은 각각 4배와 5배 많았다.


도구와 기술

사실 5 소프트웨어 업계에는 과대선전(도구와 기술에 대한)이 만연해 있다.

-       소프트웨어 기술 각각으로 얻을 수 있는 생산성 향상은 기껏해야 5~35% 정도이다.

-       재사용(공용화)으로 얻을 수 있는 이득도 10~35% 정도이다.

사실 6 새로운 도구와 기술은 도입 초기에 생산성/품질 저하를 초래한다.

-       도구와 기술을 익히는데는 적어도 6개월 ~ 2년 이상이 걸리고, 이 기간동안의 생산성은 기존보다 저하된다.

사실 7 소프트웨어 개발자는 도구에 대해 많은 말을 하지만, 별로 사용하지 않는다.

-       좋은 개발 도구라는 것 중 대다수는 계속 사용되지 않고 폐기된다.

추정

사실 8 폭주하는 프로젝트의 가장 흔한 원인 두 가지 중 하나는 부정확한 추정이다.

-       아직까지 소프트웨어 프로젝트가 걸리는 시간을 추정할 수 있는 정밀한 방법은 없다.

-       다만 분야의 전문가가 하는 추정만이 좀 더 정확할 뿐이다.

사실 9 소프트웨어 추정은 보통 부적절한 시기에 수행된다.

-       소프트웨어 추정은 보통 요구사항이 구체화 되기 전에 이루어진다.

-       많은 프로젝트가 이미 완료 기한이 정해진 상태로 시작된다.

사실 10 소프트웨어 추정은 보통 부적절한 사람들에 의해 수행된다.

-       많은 추정은 경영진이나 마케팅에 의해 결정된다.

사실 11 프로젝트가 진행되면서 소프트웨어 추정을 수정하는 경우는 거의 없다.

-       NASA에서는 소프트웨어 추정 재평가를 주창하며 라이프 사이클 상의 재평가 시점까지 정의한 연구가 있으나 권고를 따르는 사람을 본 적은 없다.

사실 12 소프트웨어 추정이 부정확한 것은 별로 놀라운 일이 아니다. 그러나 우리는 추정이 죽고 산다!

-       일정에 대한 추정이 부적절하다고 해도 프로젝트는 대부분 정해진 일정에 의해 관리된다.

사실 13 경영진과 프로그래머 사이에는 단절이 있다.

-       (경영진이 정한 추정에 맞지 않아) 실패라고 평가된 프로젝트에 대해 기술자의 다수가 가장 성공적인 프로젝트로 평가했다. 애초에 불가능했던 일정과 예산을 포기하고 도전적인 목표를 성공적으로 완료 했다고 생각 했기 때문이다.

사실 14 타당성 조사에 대한 대답은 항상 타당하다이다

-       프로젝트 시작 전에 이루어지는 기술적 타당성(구현 가능성을 검증하는) 조사는 거의 항상 타당하다는 결론을 낸다.

재사용

사실 15 소규모 재사용은 잘 해결된 문제다.

-       소규모 재사용(보통 라이브러리라고 불리는 함수 단위 재사용) 1950년대부터 있어 왔으며 이미 증명된 문제다.

사실 16 대규모 재사용은 여전히 해결되지 않은 어려운 문제다.

-       대규모 재사용(일명 공용화”)은 일반적으로 다양해지는 요구사항에 의해 제대로 활용되기 어렵다.

-       매우 좁은 도메인(예에서는 항공 역학) 내에서는 70%까지 재사용 모듈로 구축될 수 있었다.

사실 17 대규모 재사용은 서로 관련 있는 시스템 사이에서 가장 잘 적용된다.

-       대규모 재사용은 도메인 종속적이다.

-       여러 프로젝트나 여러 도메인에 걸쳐 적용하려 한다면 성공할 가능성이 거의 없다.

사실 18 재사용 가능 컴포넌트는 만들기가 3배 어렵고, 3곳에 적용해봐야 한다.

-       3이라는 숫자는 모두 경험적인 숫자이다. 다만 재사용 가능한 컴포넌트는 일반적인 것보다 만들기가 훨씬 어렵고, 잘 적용되는지를 여러 번 검증해 봐야 한다.

사실 19 재사용된 코드를 수정하는 것은 특히 오류를 범하기 쉽다.

-       기존의 솔루션을 이해하는 것은 소프트웨어 작업 중에서 가장 어려운 일이기 때문에, 재사용을 위해서 기존의 컴포넌트를 수정하는 것은 새로 만드는 것에 비해 매우 어렵다.

-       20~25% 이상을 수정해야 할 경우 처음부터 다시 만드는 것이 더 효율적/효과적이다.

-       소프트웨어 작업은 인류가 지금까지 해온 것 중 가장 복잡한 작업이다.”

사실 20 디자인 패턴 재사용은 코드 재사용 문제에 대한 해결책이다.

-       이 사실은 디자인 패턴과 같은 잘 알려진 설계에 대한 지식이 매우 중요함을 드러낸다.

-       구현된 컴포넌트는 재사용이 어렵지만, 소프트웨어의 구조 및 설계의 재사용은 가능하다.

복잡성

사실 21 문제의 복잡성이 25% 증가하면 솔루션의 복잡성은 100% 증가한다.

-       왜 그렇게 사람이 중요한가? 복잡성을 극복하는 데는 상당한 사고력과 기술이 필요하기 때문이다.

-       왜 대규모 재사용은 성과가 좋지 않을까? (도메인의 차이로 인한) 복잡성이 증대되기 때문이다.

-       왜 코드 리뷰(inspection)가 오류 제거에 대한 가장 효과적, 효율적인 접근 방법인가? 그 모든 복잡성을 걸러내고 오류의 위치를 찾는 데는 결국 사람의 노력이 필요하기 때문이다.

-       기존의 제품을 이해하는 것이 소프트웨어 유지보수에서 가장 중요하고도 어려운 작업인가? 하나의 문제를 해결하는데 적용할 수 있는 접근방법이 매우 많기 때문이다.

-       왜 소프트웨어에는 그렇게 많은 오류가 있는가? 처음부터 소프트웨어를 올바르게 이해하는 것은 매우 어렵기 때문이다.(여기에는 다른 견해가 있다. 소프트웨어는 불완전성의 원리에 지배 받기 때문에 사람의 두뇌가 아니면 오류를 잡아 낼 수 없다. 그리고 그 사람들은 모두 전문가인 것이 아니다. 물론 전문가라고 해서 오류를 만들어 내지 않는 것은 아니지만.)

사실 22 소프트웨어 작업의 80%가 지적인 작업이다. 그 중 상당 부분이 창조적인 작업이다. 사무적인 일은 거의 없다.

-       소프트웨어 실무자가 하는 일에 대한 관찰 연구 결과 80%는 지적인 작업, 20%는 사무적인 작업으로 분류되었다. 이 중 창조적인 작업은 6%에서 29%에 해당한다.

2장  생명 주기

요구사항

사실 23 폭주하는 프로젝트에서 가장 흔한 원인 두 가지 중 하나는 불안정한 요구사항이다.

-       고객과 사용자는 (그리고 대부분의 관리자들도) 보통 어떤 문제를 해결해야 하는지에 대해 확실하게 알지 못한다.

-       프로토타이핑은 요구사항이 명확하지 않을 때 자주 사용한다.(AOM 패턴을 이용해 볼만한 과정이다.)

사실 24 요구사항의 오류는 생산 단계에서 수정하는데 가장 비용이 많이 든다.

(너무 당연한 일이다)

사실 25 누락된 요구사항은 가장 수정하기 힘든 오류다.

-       누락된 요구사항은 이미 만들어졌거나 만들어지고 있는 코드에 대한 설계 수준의 수정을 요구한다.

설계

사실 26 명시적 요구사항을 설계로 옮겨갈 때 파생 요구사항이 폭발적으로 증가한다.

-       (명시적) 요구사항은 실제 어떻게 구현해야 할지를 결정하는 설계 단계에 오면 암시적 요구사항을 파생 시키고, 이 파생 요구사항은 명시적 요구사항의 50배에 달한다.

사실 27 소프트웨어 문제에서 최적의 솔루션이 하나 존재하는 경우는 거의 없다.

-       소프트웨어적인 문제는 문제에 대한 해결책이 매우 많다. 그 중에서 최상의 해결책은 없거나 알아내기가 매우 어렵다.

사실 28 설계는 복잡하고 반복적인 과정이다. 초기 설계 솔루션은 보통 잘못 되었거나, 최적이 아닌 경우가 많다.

-       (이는 애자일 진영의 XP, TDD, 리팩토링과 같은 기술 및 개발 방법론의 정당성에 힘을 실어 준다.)

-       전문 설계자는 설계상 핵심적인 문제에 대해 직접 구현해 보거나 솔루션을 찾아 놓은 다음에야 전체 설계 문제로 넘어간다.(이는 아키텍쳐는 코딩을 해야 한다는 주장을 뒷받침한다.)

코딩

사실 29 설계자의 기본단위와 프로그래머의 기본단위가 일치하는 경우는 거의 없다.

-       설계자의 코딩 경험, 전문 코딩 분야, 역량 등에 의해 설계 기본 단위의 크기가 달라진다. 이와 함께 코딩 실무자 역시 경험, 전문 분야, 역량에 따라 이에 대응되는 기본 단위가 달라지게 된다. 따라서 이 둘이 매칭되기는 어렵다.

-       (이것은 코딩 실무자의 능력이 설계자 수준과 비슷하게 뛰어나야 하고, 설계 수준이 낮거나 설계자가 없을 경우에는 코딩 실무자의 능력이 (설계를 커버할 수 있을 만큼) 매우 뛰어나야 한다는 것을 의미한다.)

-       나는 이 사실 때문에, 설계와 코딩 작업을 분리하는 것은 좋지 않다고 생각한다.”

-       소프트웨어 개발에서 전통적인 작업 분할 방식(설계자, 구현자, 테스터와 같이 전문 담당 업무를 분할하는 방식)은 적절하지 않다.”

사실 30 COBOL은 별로 훌륭한 언어가 아니지만, (비즈니스 데이터 처리에 대해서는) 다른 언어도 마찬가지다.

오류제거

사실 31 오류 제거는 생명주기에서 가장 많은 시간을 소모하는 단계다.

-       일반적으로 요구사항분석(20%) – 설계(20%) – 코딩(20%) – 오류 제거(40%)의 시간을 소모한다.

테스트

사실 32 프로그래머가 완전하게 테스트 했다고 믿는 소프트웨어도 보통은 로직 경로의 55~60%만 테스트 된 경우가 많다.

사실 33 100% 테스트 커버리지도 결코 충분하지 않다.

-       대략 소프트웨어 결함의 35%는 누락된 로직 경로(구현하지 않은 로직)에서, 40%는 로직 경로의 특정 조합을 실행할 때(로직 실행 후 다시 실행할 때 일부 변수가 초기화 되지 않아서 발생하는 오류가 대표적인 예이다) 나타난다. 따라서 100% 커버리지로도 잡히지 않는다.

사실 34 테스트 도구는 꼭 필요하지만, 많은 경우 거의 사용되지 않는다.

-       이유는 테스트 단계 자체가 관심을 많이 받지 못하기 때문이다. 자동화 테스트 도구는 매우 유용하다.

사실 35 특정 테스트 프로세스는 자동화할 수 있고, 또 자동화해야 한다. 그러나 자동화 할 수 없는 테스트 작업도 많다.

-       요구사항 명세서로부터 코드를 자동 생성할 수 있다는 개념은 이미 사라졌다. 따라서 프로그래머 없는 프로그래밍프로그래밍의 자동화에 대한 아이디어 역시 이미 사라졌다.(따라서 코더 개념 역시 사라져야 할 개념이다.)

-       (소프트웨어 개발 단계에서 가장 단순해 보이는) 테스트 조차도 완전히 자동화 할 수 있는 방법은 없다.(사실 모든 소프트웨어 개발 단계 중에서 자동화 할 수 있는 것은 없다.)

사실 36 프로그래머가 작성한 디버그 코드는 테스트 도구에 대한 중요 보완 수단이다.

-       컴파일러 옵션이나 외부 파일을 이용하여 테스트 코드를 실행할 수 있도록 만들어 놓는 것도 테스트에 도움이 된다.(이 책이 쓰여질 당시에는 Unit Test가 널리 보급된 상태가 아니었음을 상기하기 바란다.)

검토와 검사

사실 37 엄격한 검사는 첫 번째 테스트 케이스를 실행시키기도 전에 소프트웨어 제품에 포함된 오류의 90% 까지 제거할 수 있다.

-       (엄격한 검사란 inspection이라는 것으로 우리가 보통 이야기 하는 코드 리뷰이다)

-       (이는 인간의 두뇌만이 소프트웨어를 관리할 수 있는 유일한 도구라는 내 생각을 뒷받침한다)

-       이렇게 좋은 방법임에도 잘 실행되지 않는 것은 오류 검사 단계에 대한 무관심과 엄격한 검사 과정의 어려움(“이미 작성된 코드에 대한 이해가 가장 어려운 것이라는 점을 상기하기 바란다.) 때문이다.

-       (여기에 덧붙이자면 많은 관리자들이 코드리뷰나 페어 프로그래밍을 지적 유희라고 생각하거나 베테랑이 초보자에게 가르치기 위해 하는 일이라고 생각한다. 우리나라의 많은 관리자들은 프로그래밍을 저급한 업무로 취급하면서 모든 개발자가 1~2년쯤 지나면 다들 베테랑이라고 생각하기 때문에 페어 프로그래밍 하려는 사람들을 야단친다.)

사실 38 엄격한 검사도 테스트를 대체할 수는 없다.

사실 39 출시 후 검토(회고라 부르는 사람들도 있다)는 중요하지만, 거의 실행되지 않는다.

사실 40 검토는 기술적 측면과 사회학적 측면을 모두 가지는데, 어느 쪽도 무시하면 안 된다.

-      동료 검토(peer review)에 의해 이성과 감정에 상처를 받지 않도록 해야 한다.

-      비자아적 프로그래밍(코드에 자신의 자존심을 투영하지 않는 것)을 당부하지만 대부분의 개발자들은 자신의 코드에 대해 자부심을 가지려고 한다. (이는 미묘한 문제이면서도 코드를 통한 커뮤니케이션이나 형상 관리를 방해하는 문제이기도 하다. 자신 있는 코드라면 공개하는 것이 마땅하고, 자신 없는 코드라면 공개하고 조언을 받아야 하는 것이 마땅하다. 어느 편이든 코드는 공개되어야 한다.)

사실 41 유지보수는 보통 소프트웨어 비용의 40~80%를 차지한다. 따라서, 유지보수는 소프트웨어 생명주기 중 가장 중요한 단계일 것이다.

-       (일단 다음 절에서 유지 보수란 단순한 오류 수정이 아니라 기능의 개선 및 추가까지 포함한 작업이라는 것을 기억하기 바란다.)

-       유지 보수 비용이 높은 이유는 이미 만들어진 기능을 이해하기가 어렵기 때문이다.

-       (따라서 가독성 높고 간결한 코드를 만드는 것이 개발자의 자질 중 가장 중요한 것이다.)

사실 42 유지보수 비용의 60%는 개선 작업에 소요되는 비용이다.

-       소프트웨어를 처음 개발할 때 고객과 사용자는 새로운 소프트웨어로 무엇을 할 수 있을지에 대해 단지 비전의 일부만 갖고 있을 뿐이다. 소프트웨어가 출시되어 한동안 사용해 본 후에야 사용자는 그 소프트웨어 시스템을 개선해 무엇을 더 할 수 있을지 깨닫기 시작한다.

-       60/60 법칙 : 소프트웨어 비용의 60%는 유지보수에 사용되며, 유지보수 비용의 60%는 개선에 사용된다. 따라서 기존 소프트웨어를 개선하는 것은 큰 일이다.

-       60%는 개선 17%는 오류 수정 18%는 포팅, 5%는 기타 작업

사실 43 유지보수는 문제가 아니라 해결책이다.

(다른 산업 분야와는 달리) 소프트웨어의 유지보수는 대부분 기능의 개선을 위한 것이기 때문에 문제가 아니라 해결책이다. 따라서 부정적으로 보지 않아야 한다.

사실 44 유지보수에서 가장 어려운 작업은 기존 시스템을 이해하는 것이다.

-       유지보수 작업에서 가장 중요한 요소는 이해력이다 –Ned Chapin, 유지보수 분야 개척자-

-       소프트웨어 업무 중에서 가장 어려운 일이 유지보수 작업이다. 보통은 (내가 만든 게 아닌) 다른 사람이 만든 소프트웨어를 다루어야 하기 때문이다.

사실 45 더 좋은 소프트웨어 공학 기술로 개발하면 더 많은(더 적은 게 아니라) 유지보수가 필요하다.

-       현대적 개발 방법론 및 소프트웨어 기술이 적용된 소프트웨어는 더 수정하기 쉽기 때문에 더 많은 수정이 가해지고 더 오랫동안 개선되면서 사용된다.

3장  품질

품질

사실 46 품질은 속성의 집합이다.

-       이식성, 신뢰성, 효율, 사용편의성, 테스트 용이성, 이해 용이성, 수정 용이성

사실 47 품질은 사용자 만족, 요구사항 충족, 비용과 일정 목표 달성, 또는 신뢰성이 아니다.

신뢰성

사실 48 대부분의 프로그래머가 흔히 범하는 오류가 있다.

-       인간은 하나씩 밀린 인덱스, 정의/참조의 불일치, 중요한 설계 항목 누락, 자주 사용하는 변수에 대한 초기화 실패, 일련의 조건 중 하나의 조건 누락 등 특정 종류의 작업에서 쉽게 실수한다.

사실 49 오류는 뭉치는 경향이 있다.

-       오류의 반이 모듈의 15%에서 발견된다.

-       오류의 80%가 단지 모듈의 20% 이내에서 발견된다.

-       대략 80%의 결함이 모듈의 20%에서 나오고, 모듈의 절반 정도는 오류가 없다.

-       특정 모듈이 특히 더 어렵기 때문에, 여러 개발자가 모듈 별로 개발하기 때문에(개인 능력차 때문에) 그럴 것이다.

사실 50 소프트웨어 오류 제거에 있어서 단 하나의 최상의 방법은 없다.

사실 51 오류는 항상 남아 있다. 심각한 오류를 제거하거나 최소화하는 것이 목표가 돼야 한다.

-       오류 없는 소프트웨어를 개발하는 것은 불가능하다.

-       CMM 레벨 4인 팀과 다른 정형방법을 사용하는 팀 둘이서 충분한 비용과 일정에도 불구하고 98% 신뢰성의 간단한 제품을 만들어 내지 못했다.

효율

사실 52 효율은 훌륭한 코딩보다는 훌륭한 설계에 더 많은 영향을 받는다.

사실 53 고급 언어 코드도 어셈블리어 코드의 90%에 가까운 효율을 낼 수 있다.

-       항공 애플리케이션에서 고급 언어의 비효율성(어셈블리 대비 C언어) 10~20% 정도이다.

>  최적화 컴파일러를 이용하면 10% 성능향상

> 튜닝을 통해 2~5% 더 향상 시킬 수 있다.

사실 54 크기와 속도 사이에는 트레이드오프가 있다.

4장  연구

연구

사실 55 많은 연구자들이 연구보다는 옹호에 치중한다.

 

2부 오해 5+5

5장  관리

관리

오해 1 측정할 수 없는 것은 관리할 수 없다.

-       측정할 수 없는 것은 통제할 수 없다는 말은 사실이지만 관리할 수 없다는 말은 아니다. 소프트웨어 설계라는 것은 측정 불가능하지만 관리할 수 있는 대상이다.

-       몇몇 회사에서는 메트릭을 통한 관리를 중요시한다. 그리고 자주 사용하는 소프트웨어 메트릭이 개발되어 사용되고 있다.

-       (우리나라에서 메트릭을 통해 소프트웨어를 관리하지 않는 이유는 관리자들의 소프트웨어에 대한 이해가 부족하고, 필요한 경우 외주를 통해 해결할 수 있는 수준의 저급한 업무로 폄하되고 있기 때문이다. 야구나 농구에서 데이터를 관리하거나 기업의 재무제표가 관리되는 이유와 정반대 이유다.)

오해 2 소프트웨어 품질은 관리로 해결할 수 있다.

-       소프트웨어 품질에는 기술적 요소가 많아 관리만으로 해결할 수는 없다.(이는 소프트웨어를 외주로 개발하는 행위에 대해 경종을 울린다. 자체 기술 부족으로 인하여 기술이 검증된 업체를 통해 개발하고 그 기술을 내제화 하기 위한 외주가 아니라 단지 시간이 부족하거나 허드레 업무라고 생각해서 외주를 주는 경우 관리만으로 품질을 확보하는 것은 불가능하다. 이로 인한 짐은 고스란히 개발자에게 넘겨진다.)

오해 3 프로그래밍은 비자아적이 될 수 있고, 또 되어야 한다.

-       (많은 개발자들은 자신의 코드가 자신의 지적 능력을 대변한다고 생각한다. 그래서 코드에 대해 자아를 투영하곤 한다. 하지만 이러한 자세는 원활한 시스템 통합, 오류 검사, 형상 관리를 방해한다.)

-       오류 없는 프로그램을 작성하는 것은 불가능하다는 것을 인정하고, 기술적 약점이 잘 발견될 수 있도록 열린 자세를 가져야 한다.

도구와 기술

오해 4 도구와 기술 : 한 가지로 모든 문제를 해결할 수 있다.

-       소프트웨어 문제를 해결할 완전한 한가지 기술이나 도구는 없다.

오해 5 소프트웨어 분야에는 더 많은 방법론이 필요하다.

-       많은 방법론이 교수들이나 대학원생 등 소프트웨어 비 실무자들에 의해 개발된다. 특히 엄격한 방법론(융통성을 거부하고 전체 개발 프로세스를 감시하려는 방법론, 예를 들어 전통 waterfall과 같은 방법론)을 경계해야 한다.

추정

오해 6 비용과 일정을 추정하기 위해서는 먼저 LOC(Lines of Code)를 추정해야 한다.

-       LOC 만으로는 부정확한 메트릭이다.(프로그래밍 언어, 도메인, 주석과 같은 요소를 고려해야 한다.)

-       (특히 이 절은 비용과 일정을 추정하기 위해 LOC추정하는 문제에 대해 언급하고 있음을 기억해야 한다. 추정을 위해서 부정확한 메트릭을 사용하는 것에 대한 주의이다.)

-       (LOC는 몇몇 부수적인 메트릭과 함께 사용되면 훌륭하게 기능할 수 있다. 특히 코드의 조건문 빈도, 중복 코드 비율, 모듈 특성 등과 함께 사용한다면 개발자의 생산성을 판단하는데 매우 유용할 수 있다. 같은 모듈이나 계층을 개발하는 개발자들간에도 LOC 10배 이상 차이 나는 경우가 흔하다. 2배 이하라면 큰 의미는 없다.)

6. 생명주기

테스트

오해 7 랜덤 테스트 입력은 테스트를 최적화 하는 좋은 방법이다.

-       소프트웨어의 복잡성이 늘어나면 늘어날수록 랜덤 테스트를 통해 찾아낼 수 있는 오류는 줄어든다.

검토

오해 8 “보는 눈이 많으면, 모든 버그는 그 깊이가 얕다.”

-       오픈소스에 대한 이야기인데, 많은 오픈소스가 다수의 눈을 거쳐 수정되는 과정을 거치지 않고, 수정되는 버그는 대부분 발견하기 용이한 것들이다. 중요하고 심각한 버그들은 여전히 숨어 있을 가능성이 있다.

유지보수

오해 9 과거의 비용 데이터를 살펴봄으로써 미래의 유지보수 비용을 예측할 수 있고 시스템 교체 결정을 내릴 수 있다.

-       다양한 연구가 있으나 (기본적으로 소프트웨어의 유지 보수가 대부분 새로운 기능을 추가하는 일이고, 이것은 이미 매우 어려운 일로 알려져 있으므로) 유지보수 비용을 예측하는 것은 매우 어려운 일이다. 따라서 이를 기반으로 시스템 교체 결정을 내리는 것은 불가능하다.

7장.  교육

테스트

오해 10 프로그래밍을 가르칠 때 프로그램을 어떻게 작성하는지 보여주며 가르친다.

-       잘 만들어진 코드를 읽어 보는 것은 직접 작성하는 것만큼(혹은 그보다 더) 중요하다. 하지만 잘 만들어진 코드 샘플을 찾아 내는 것은 어렵다.

-       다른 사람의 코드를 읽는 것은 소프트웨어 개발에서 가장 어려운 작업을 훈련할 수 있는 일이다.

-       (페어 프로그래밍이나 코드 리뷰에 힘을 실어 주는 사실이다. 다른 사람의 코드를 보지 않거나 고쳐보지 않은 개발자, 잘 만들어진 오픈소스나 라이브러리 코드를 보지 않는 사람의 개발 능력은 언제나 낮기 마련이다.)

Posted by 이세영2
,

 dynamicJarLoader.zip


라이브러리에 대한 동적 로딩은 사용자의 요구가 다양하게 변화하는 어플리케이션을 위한 기술이다. Java에서는 URLClassLoader라는 라이브러리 클래스를 통해서 이 기능을 지원하고 있다.

이를 이용할 상황을 하나 생각해 보도록 하자.

화면에 도형을 그리는 어플리케이션을 만들어 사용자에게 배포하였다고 가정하자. 이미 도형에 대한 클래스 모델링이 잘 되어 있기 때문에 Shape이라는 인터페이스는 이미 배포되어 있다. 그리고 몇몇 도형들 역시 배포된 상태다. 소프트웨어 내부에서는 이미 배포된 jar 파일을 통해 도형들을 생성해서 사용하고 있다.

이 상황에서 고객이 새로운 도형을 만들어 달라고 요청해 왔다. 필요한 도형은 Circle과 Triangle이다. 이 파일들은 개발자에 의해서 개발되었고 사용자에게 배포되었다. 이 때 이미 동작 중인 어플리케이션은 종료할 수 없다. 이런 경우에 어플리케이션을 중단시키지 않아도 클래스들을 로드할 수 있는 동적 로더가 활용될 수 있다.

아래 클래스 다이어그램을 보자.

위에서 설명했듯이 Shape 인터페이스는 이미 배포된 상태이다. 여기서 사용자가 원하는 Circle과 Triangle을 각각의 이름으로 된 jar 파일을 배포하였다.

여기서 사용자가 어플리케이션을 종료하지 않고 이들 jar 파일들을 로드하려면 동적 Jar 로드 기능이 탑재되어 있어야 한다. 이를 구현한 것이 DynamicJarLoader 클래스이다.


DynamicJarLoader 클래스 설명

이 클래스는 객체 생성자를 통해서 Jar 파일이 들어 있는 폴더 경로(String jarPath)를 입력 받는다.

이후 load(jarFileName : String) 메소드를 통해서 Jar 파일 이름을 입력해주면 라이브러리가 로드된다.

특히 DynamicJarLoader 클래스는 내부에 있는 loaderMap을 통해서 여러 이름의 라이브러리를 동시에 로드할 수 있다. 따라서 이 객체 하나 만으로도 여러 라이브러리 파일을 로드하고, 로드된 라이브러리들을 통해서 객체를 생성할 수 있다. 리턴되는 boolean 값은 라이브러리 로드가 성공했는지 여부를 알려준다.


unload(jarFileName : String) 메소드는 이미 로드된 라이브러리를 메모리에서 내리거나, 새로운 버전의 라이브러리를 열기 위해서 기존에 이미 열려진 라이브러리를 닫아야 할 경우에 호출하는 메소드이다.


이미 라이브러리가 로드되어 있다면 열린 라이브러리를 통해서 객체를 새로 생성할 수 있어야 한다. newInstance() 메소드는 클래스 이름(className)을 통해서 객체를 로드할 수 있도록 구현된 메소드이다. newInstance() 메소드는 두 개가 있는데, 하나는 className만으로 객체를 생성하도록 하고, 나머지 하나는 jar 파일 이름까지 입력해서 보다 정확한 객체를 생성하도록 한다.


첨부파일

맨 위쪽에 첨부된 첨부파일은 shape.jar / circle.jar / triangle.jar 파일과 DynamicJarLoader 클래스, 그리고 이들 라이브러리 동적 로딩을 테스트해 볼 수 있는  Use 클래스를 포함하고 있는 eclipse 프로젝트 파일이다.(JDK 1.7 이상을 사용하기 바란다.)


아래는 DynamicJarLoader 클래스의 소스이다.

public class DynamicJarLoader {

    private String jarPath;

    private Map<String, URLClassLoader> loaderMap = new HashMap<String, URLClassLoader>();

    public DynamicJarLoader(String jarPath){

        this.jarPath = jarPath;

        this.jarPath.replaceAll("\\\\", "/");

        if(this.jarPath.endsWith("/") == false) this.jarPath = this.jarPath + "/";

    }

    public boolean load(String jarFileName){

        if(loaderMap.containsKey(jarFileName) == true) unload(jarFileName);

        String jarFilePath = jarPath + jarFileName;

        File jarFile = new File(jarFilePath);

        try {

            URL classURL = new URL("jar:" + jarFile.toURI().toURL() + "!/");

            URLClassLoader classLoader = new URLClassLoader(new URL [] {classURL});

            loaderMap.put(jarFileName, classLoader);

            return true;

        } catch (MalformedURLException e) {

            return false;

        }

    }

    public boolean unload(String jarFileName){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            loader.close();

            return true;

        } catch (IOException e) {

            return false;

        }

        finally{

            loaderMap.remove(jarFileName);

        }

    }

    public Object newInstance(String jarFileName, String className){

        URLClassLoader loader = loaderMap.get(jarFileName);

        if(loader == null) return true;

        try {

            Class<?> clazz = loader.loadClass(className);

            return clazz.newInstance();

        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {

            return null;

        }

    }

    public Object newInstance(String className){

        for(String each : loaderMap.keySet()){

            Object object = newInstance(each, className);

            if(object != null) return object;

        }

        return null;

    }

}


Posted by 이세영2
,

*이 문제는 Programming Challenges : 알고리즘 트레이닝 북에 출제된 문제입니다.


지뢰 찾기(Minesweeper)

이 문제는 문제 자체가 어렵진 않지만 코드를 깔끔하게 작성하는 연습을 하기에는 좋은 문제이다. 코드를 깔끔하게 작성하기 좋은 팁은 다음과 같다.


1. 지뢰 찾기 맵을 char[][] map; 로 만들고, 지뢰가 아닌 곳은 아스키 문자 '0'을 넣는다.

    이렇게 하면 주위에 지뢰가 있을 때 map[i][j] ++를 호출해주면 자동으로 아스키 문자가 '1', '2', '3'.... 순서로 1씩 증가 된다.


2. 지뢰를 만나서 주변 8개 지뢰에 대한 카운트를 증가 시킬 때 해야 할 일을 함수로 빼낸다.

    "지뢰 주변"을 탐색하면서 맵을 벗어난 곳을 체크하기가 수월해진다.


3. 카운트 증가 조건에 맞으면 업데이트 하지 말고 조건에 맞지 않을 경우 리턴한다.

    리턴문(return)을 잘 활용하면 로직이 복잡해지는 것을 막을 수 있다.


아래는 직접 구현한 지뢰 찾기 소스이다.

class MineSweeper{

    Input input = new Input();

    private char[][]map;

    int n = 0;// row

    int m = 0;// column

    int iteration = 0;

    public void run(){

        while(true){

            readRowColumn();        // row(n)와 column(m) 값을 입력 받는다.

            if(isExit()) return;    // n == m == 0 이면 리턴한다.

            initMap();              // 맵을 생성한다.

            loadMap();              // 맵을 입력 받는다.

            calculateMap();         // 맵을 계산한다.

            printResult();          // 맵을 출력한다.

        }

    }

    private boolean isExit(){

        if(n == 0 && m == 0) return true;

        return false;

    }

    private void readRowColumn(){

        String line = input.readLine();

        StringTokenizer token = new StringTokenizer(line);

        n = Integer.parseInt(token.nextToken());// row

        m = Integer.parseInt(token.nextToken());// column

    }

    private void initMap(){

        map = new char[n][m];

    }

    private void loadMap(){

        for(int i = 0; i < n; i++){

        String line = input.readLine();

        for(int j = 0; j < m; j++){

        map[i][j] = line.charAt(j);

        if(map[i][j] == '.') map[i][j] = '0';

        }

        }

    }

    private void calculateMap(){

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                if(map[i][j] == '*') addCountAround(i, j);

            }

        }

    }

    private void addCountAround(int x, int y){

        for(int i = x - 1; i <= x + 1; i++){

            for(int j = y - 1; j <= y + 1; j++){

                addCount(i, j);

            }

        }

    }

    private void addCount(int x, int y){

        if(x < 0 || x >= n) return;

        if(y < 0 || y >= m) return;

        if(map[x][y] == '*') return;

        map[x][y]++;

    }

    private void printResult(){

        iteration++;

        if(iteration != 1) System.out.println();

        System.out.println("Field #" + iteration + ":");

        for(int i = 0; i < n; i++){

            for(int j = 0; j < m; j++){

                System.out.print(map[i][j]);

            }

            System.out.println();

        }

    }

}


이 구현에서 핵심 부분은 calculateMap() 메소드와 이 메소드에 호출되는 addCountAround() 그리고 addCount() 메소드일 것이다.

calculateMap()은 가로와 세로 방향으로 for문을 돌아서 모든 맵을 방문하는 일을 담당한다. 만약 이 과정에서 지뢰(*)를 만나면 addCountAround(i, j) 메소드를 호출하는 것으로 자기 임무를 마친다.

addCountAround()는 지뢰 주변의 9개 타일(자기 자신 포함) 타일들을 방문한다. 그리고 addCount() 메소드를 호출하는 것으로 자기 임무를 마친다.

addCount() 메소드가 실질적으로 카운트를 증가시킨다. 이 때 중요한 것은 예외 처리이다. 만약 타일이 지뢰이거나 맵의 경계를 넘어갔다면 카운트를 증가시키면 안된다. 따라서 미리 예외의 경우들을 체크하고 적절한 타일이 아니라면 리턴한다. 그리고 정상적인 타일인 경우에면 카운트를 증가시킨다.


또한 run() 메소드에서는 문제에서 주어진 절차를 모두 메소드로 만들고 절차에 맞는 적절한 이름을 할당해 줌으로써 전체 동작이 어떤지를 한눈에 알 수 있도록 만들어 주는 것이 코드를 깔끔하게 만들 수 있는 비결이다.


중간에 Input 클래스를 사용하는데 이에 대해서는 [Programming Challenges]Sample 및 표준 입력 데이터 이중화 글을 참고하기 바란다.


Posted by 이세영2
,

programmingChallenges.zip


당분간 알고리즘 공부에 전념해보고자 전에 사서 킵 해 두었던 Programming Challenges 알고리즘 트레이닝 북을 꺼내 들었다.


이 책은 여러 알고리즘 문제를 난이도별, 특성 별로 나누어 제공하고 있고, 이에 대한 해답도 소스로 첨부되어 있다. 특히 온라인 페이지를 통해서 자기가 작성한 답안을 올리고 채점을 받을 수 있도록 되어 있어서 좋다. 또한 로컬에서 테스트를 해 볼 수 있도록 샘플 입력 데이터들도 제공한다. 아래는 온라인 채점 사이트이다.


http://www.programming-challenges.com/pg.php?page=index


Sample 및 표준 입력 데이터 이중화

위에서 설명한 것처럼 온라인으로도 채점이 가능하고 로컬에다가 샘플 데이터를 받아서 테스트를 해 볼 수 도 있다.

문제는 온라인에서 채점할 경우 내가 제출한 소스로 동작한 결과를 확인해 볼 수 없다는데 있다. 따라서 로컬에서 샘플 데이터를 통한 테스트가 필수이다. 하지만 샘플 데이터는 파일 입력이고, 온라인에서 소스에 데이터를 입력 받을 때는 표준 입출력(일반적으로 키보드 입력이라 부르는)이다. 따라서 온라인에 답안을 올릴 때와 샘플 데이터를 이용할 때의 입력 방식이 달라서 불편한 점이 있다.


자동으로 Sample 데이터와 표준 입력을 전환

위의 첨부 파일을 받아 보면 이미 Programming Challenges 사이트에서 제공하는 샘플 데이터가 Samples 폴더에 들어 있다.

자동으로 Sample 데이터와 표준 입력을 자동으로 전환하는 방법은 단순하다. 로컬에 샘플을 저장한 폴더에서 원하는 샘플 파일을 열어보고 예외가 발생하면 표준 입력을 사용하도록 하는 것이다.

이에 대한 소스는 아래와 같다.

class Input{

    private Scanner scan;

    private boolean isSampleSource = true;

    public Input(){ initInputSource(); }

    private void initInputSource(){

        Problem problem = new Problem();

        File file = new File("samples/" + problem.pcId +".inp");

        try {

            scan = new Scanner(file);

        } catch (FileNotFoundException e) {

            isSampleSource = false;

        }

    }

    public String readLine(){

        if(isSampleSource) return readLineFromSample();

        return Main.ReadLn(255);

    }

    private String readLineFromSample(){

        if(scan.hasNextLine()) return scan.nextLine();

        return null;

    }

}

class Problem{

    public String pcId = "110101";// PC ID

    public String uvaId = "100";// UVa ID

} 

Input 클래스는 생성시 생성자에서 initInputSource() 메소드를 호출한다. 이 메소드에서는 일단 Problem 클래스를 생성한 후 그 안에서 pcId 값을 읽어 파일명으로 변환한 후 samples 폴더에서 파일을 열어본다. 만약 파일이 열리면 파일을 입력 소스로 사용한다. 만약 파일을 여는데 실패하면 FileNotFoundException이 발생하고 isSampleSource 값을 false로 함으로써 표준 입출력을 사용하도록 되어 있다.


사용법

첨부 파일은 Eclipse 프로젝트이다. samples 폴더에는 샘플 데이터들이 들어 있다.

programmingChallenges.submitForm 패키지 아래에 있는 Main.java 파일은 위의 소스를 포함하여 알고리즘을 작성할 부분과 필요한 클래스를 작성해 넣을 수 있는 부분을 비워 둔 소스이다. 따라서 이 소스에 알고리즘을 작성해 넣으면 된다. 이 때 소스 파일 지정을 위해서 Problem 클래스 안에 pcId는 책에 나오는 PC Id를 입력해 주어야 한다.


programmingChallenges.threeNPlusOne 패키지 아래에 있는 Main.java 파일은 책에 나오는 첫번째 문제인 3n + 1 문제를 풀어 채워 넣은 파일이다. 실제로 채점 사이트에 올려 보고 해결되는 것을 확인해 본 소스이다. 물론 로컬에서 실행을 시켜 보면 로컬의 samples 폴더에 있는 테스트 데이터를 입력 받아 동작함을 알 수 있다.


혹시 Programming Challenges 알고리즘 트레이닝 북을 가지고 알고리즘을 공부하는 사람 중에서 Java를 사용할 의향이 있다면 편리하게 사용할 수 있을 것이다.

'7.알고리즘' 카테고리의 다른 글

[Programming Challenges] 지뢰 찾기(Minesweeper)  (0) 2016.10.06
Posted by 이세영2
,

TDA 원칙이라고도 불린다.

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

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


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

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

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


아래 그림을 살펴보자.




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

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

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


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

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


코드가 복잡해진다

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


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

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


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

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


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

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


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


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

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


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

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


데이터 객체 생성 단계

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

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

dataProcessor.receiveData(data);

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

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

Data dataObject = new Data(data);

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

object.receiveData(dataObject);


데이터 처리 단계

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

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

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

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

dataObject.doubleDispatch(this);


// 데이터 객체 쪽

public void doubleDispatch(Object2 object){

    object.doSomething();

}

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


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

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

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

Posted by 이세영2
,

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

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

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

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


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


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

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

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

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


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

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

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

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

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

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


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


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


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

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

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

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

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

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

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


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


KlassType 클래스

class KlassType{

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

    KlassType parent;

   

    public KlassType(String name, KlassType parent){

        this.name = name;

        this.parent = parent;

        addParentAttributeTypes();

    }

   

    private void addParentAttributeTypes(){

        if(parent == null) return;

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

        for(AttributeType each : parentAttributeTypes){

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

        }

    }

   

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


        public void addAttributeType(AttributeType attributeType){   

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

        }


        public AttributeType getAttributeType(String typeName){ 

            return attributeTypes.get(typeName); 

        }


        public Collection<AttributeType> getAttributeTypes() {

            return attributeTypes.values();

        }

       

    private String name;

        public String getName(){ return name; }   

}

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

- parent : 부모 클래스

- name : 클래스명

- attributeTypes : 선언된 속성들


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


Klass 클래스

class Klass{

    // 객체에 해당한다.

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

        this.type = type;

        this.name = name;

        this.id = id;

        initAttributes();

    }

   

    private void initAttributes(){

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

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

        }

    }

   

    private KlassType type;

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

   

    String name;

        public String getName(){ return name; }

   

    String id;

        public String getId(){ return id; }   

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

        public Object get(String name){

            Attribute attr = attributes.get(name);

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

            else throwNoSuchAttributeException(name);

            return null;

        }


        public void set(String name, Object value){

            Attribute attr = attributes.get(name);

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

            else throwNoSuchAttributeException(name);

        }


        public Attribute getAttribute(String name){

            Attribute attr = attributes.get(name);

            if(attr != null) return attr;

            else throwNoSuchAttributeException(name);

            return null;

        }

   

    public String toIndentString(String indent){

        StringBuffer buffer = new StringBuffer();

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

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

        buffer.append("{\n");

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

            if(each.getValue() instanceof Klass){

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

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

            }

            else{

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

            }

        }

        buffer.append(indent + "}");

        return buffer.toString();

    }

   

    private void throwNoSuchAttributeException(String attributeName){

        try {

            throw new NoSuchAttributeException();

        } catch (NoSuchAttributeException e) {

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

            e.printStackTrace();

        }

    }

} 

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

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

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

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

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


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

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

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


AttributeType 클래스

class AttributeType{

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

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


    @SuppressWarnings("rawtypes")

    private Class typeClass;

    @SuppressWarnings("rawtypes")

    public Class getTypeClass(){ return typeClass; }

    public String getTypeClassName(){

        String typeName = typeClass.getName();

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

        return typeName;

    }

   

    @SuppressWarnings("rawtypes")

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

        this.typeClass = typeClass;

        this.name = name;

    }

   

    private String name;

    public String getName() { return name; }

   

    private String description;

    public String getDescription(){ return description; }

}

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

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

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

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


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


Attribute 클래스

class Attribute{

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

   

    private AttributeType type;

   

    public Attribute(AttributeType type){

        this.type = type;

    }

   

    public AttributeType getType(){ return type; }

       

    private Object value;

    public Object getValue() { return value; }

    public void setValue(Object value) {

        if(isSettable == false){

            throwOperationNotSupportException();

            return;

        }

        this.value = value;

    }

   

    private void throwOperationNotSupportException(){

        try { throw new OperationNotSupportedException(); }

        catch (OperationNotSupportedException e) {

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

            e.printStackTrace();

        }

    }

   

    private boolean isSettable = true;

    public boolean isSettable(){ return isSettable; }

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

} 

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

- type : AttributeType

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


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


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


실행 함수

public static void main(String[] args) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    myHouse.set("car", myCar);

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

}


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

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

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

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

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

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

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


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


출력

Class House 우리집 extends Site{

   String owner = 홍길동;

   Integer area = 30평;

   Klass car =    Class Car 내차{

      Integer hp = 500마력;

      String model = 아우디;

      String type = 세단;

   };

   String position = 사당동;

}

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

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

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

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

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

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

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

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

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

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


Actor Model의 단점

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

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


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

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

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

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


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


IActor - Actor - TempControlActor/ThermostatActor/DatabaseActor

interface IActor extends Runnable{

    public void tell(IMail mail);

}

abstract class Actor implements IActor{

    private IMailbox mailbox = new Mailbox();

    @Override

    public void run() {

        while(true){

            receiveMail();

            doWork();

            try {

                Thread.sleep(0);

            } catch (InterruptedException e) {

               e.printStackTrace();

            }

        }

    }

   

    private void receiveMail(){

        while(mailbox.hasNext()){

            IMail mail = mailbox.next();

            processMail(mail);

        }

    }

   

    @Override

    public void tell(IMail mail){

        mailbox.receiveMail(mail);

    }

   

    abstract protected void processMail(IMail mail);

    abstract protected void doWork();

}

class TempControlActor extends Actor{

    IActor db = new DatabaseActor();

    IActor thermostat = new ThermostatActor();

   

    public TempControlActor(){

        db = ActorFactory.create(DatabaseActor.class);

        thermostat = ActorFactory.create(ThermostatActor.class);

    }

    protected void processMail(IMail mail){

        db.tell(mail);

        thermostat.tell(mail);

    }

   

    protected void doWork(){

        /* do nothing */

    }

}


class DatabaseActor extends Actor{

    protected void processMail(IMail mail){

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

    }

   

    protected void doWork(){

        /* do nothing */

    }

}

class ThermostatActor extends Actor{

    protected void processMail(IMail mail){

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

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

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

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

    }

   

    protected void doWork(){

        /* do nothing */

    }

}

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

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

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


ActorFactory

class ActorFactory{

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

   

    public static IActor create(Class clazz){

        IActor actor = actorMap.get(clazz);

        if(actor != null) return actor;

        try {

           actor = (IActor)clazz.newInstance();

           new Thread(actor).start();

           actorMap.put(clazz, actor);

           return actor;

        } catch (InstantiationException e) {

            e.printStackTrace();

        } catch (IllegalAccessException e) {

            e.printStackTrace();

        }

        return null;

    }

} 

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


IMailbox - Mailbox

interface IMailbox{

    public void receiveMail(IMail mail);

    public boolean hasNext();

    public IMail next();

}

class Mailbox implements IMailbox{

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

    public synchronized void receiveMail(IMail mail){

        in.add(mail);

    }

    public synchronized boolean hasNext(){

        return !in.isEmpty();

    }

    public synchronized IMail next(){

        IMail mail = in.get(0);

        in.remove(0);

        return mail;

    }

}

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


IMail - Mail

interface IMail{

    public Object getContent();

}

class Mail implements IMail{

    private Object content;

    public Mail(Object content){

        this.content = content;

    }

   

    public Object getContent(){

        return content;

    }

} 

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


실행 방법

public static void main(String[] args) {

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

    Scanner scan = new Scanner(System.in);

    while(true){

        Integer temperature = scan.nextInt();

        tempControl.tell(new Mail(temperature));

    }

} 

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

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

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

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

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

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

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

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

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

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