오브젝트

코드로 이해하는 객체지향 설계

5 minute read

객체, 설계

설계 개선하기

  • 자율성을 높이자. 객체들이 스스로 정보를 감추고 자율적인 존재가 되어야 한다.
  • 일반적으로 절차적 프로그래밍은 우리의 직관에 위배된다. 절차적 프로그래밍의 세계에서는 관람객과 판매원이 수동적인 존재일 뿐이다.
  • 훌륭한 객체지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다. 객체지향 코드는 자신의 문제를 스스로 처리해야 한다는 우리의 예상을 만족시켜주기 때문에 이해하기 쉽고, 객체 내부의 변경이 객체 외부에 파급되지 않도록 제어할 수 있기 때문에 변경하기가 수월하다.
  • 객체지향 설계에서는 독재자가 존재하지 않고 각 객체에 책임이 적절하게 분배된다. 따라서 각 객체는 자신을 스스로 책임진다. 객체지향 설계의 핵심은 데이터와 프로세스를 하나의 객체 안으로 모으는 것을 넘어 적절한 객체에 적절한 책임을 할당하는 것이다. 따라서 객체가 어떤 데이터를 가지느냐보다는 객체에 어떤 책임을 할당할 것이냐에 초점을 맞춰야 한다.
  • Audience의 책임은 무엇인가? 티켓을 사는 것이다. Theater의 책임은 무엇인가? 관람객을 입장시키는 것이다. 적절한 객체에 적절한 책임을 할당하면 이해하기 쉬운 구조와 읽기 쉬운 코드를 얻게 된다.
  • 가방에서 돈을 꺼내는 것은 관람객이지 가방이 아니다. 판매원이 매표소에 없는데도 티켓이 저절로 관람객에게 전달되지는 않을 것이다. Rebecca Wirfs-Brock은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism)라고 부른다. 훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.

객체지향 설계

  • 설계란 코드를 배치하는 것이다. 어떤 사람들은 설계가 코드를 작성하는 것보다는 높은 차원의 창조적인 행위라고 생각한다. 하지만 설계를 구현과 떨어트려서 이야기하는 것은 불가능하다.
  • 단순히 데이터와 프로세스를 객체라는 덩어리 안으로 밀어 넣었다고 해서 변경하기 쉬운 설계를 얻을 수 있는 것은 아니다. 객체들 간의 상호작용을 통해 구현된다.

객체지향 프로그래밍

객체지향 프로그래밍을 향해

  • 클래스 기반의 객체지향 언어에 익숙한 사람이라면 설계시 가장 먼저 어떤 클래스가 필요할지 고민할 것이다. 대부분의 사람들은 클래스를 결정한 후에 클래스에서 어떤 속성과 메소드가 필요한지 고민한다. 안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 객체지향은 말 그대로 객체를 지향하는 것이다.
  • 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
  • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
  • 일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다. 우리팀 제발…
  • 클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것이다.
  • 클래스의 내부와 외부를 구분해야 하는 이유는 무엇일까? 그 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이다. 더 중요한 이유로는 프로그래머에게 구현의 자유를 제공하기 때문이다.
  • 수신된 메시지를 처리하기 위한 자신만의 방법을 메소드(method)라고 부른다. 메시지와 메소드를 구분하는 것은 매우 중요하다.메시지와 메소드의 구분에서부터 다형성(Polymorphism)의 개념이 출발한다.
  • ‘메소드를 호출한다’보다 ‘메시지를 전송한다’라고 말하는 것이 더 적절한 표현이다. 사실은 그곳에 그 메소드가 존재하고 있는지조차 모르는 것이 정상이다. 단지 메시지에 응답할 수 있다고 믿고 메시지를 전송할 뿐이다. 메시지를 수신한 객체는 스스로 적절한 메소드를 선택한다. 메시지를 처리하는 방법을 결정하는 것은 객체 스스로의 문제인 것이다. 이것이 객체가 메시지를 처리하는 방법을 자율적으로 결정할 수 있다고 말했던 이유다.

상속과 다형성

  • 부모 클래스와 다른 부분마을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.
  • 상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 이것은 상속을 바라보는 일반적인 인식과는 거리가 있는데 대부분의 사람들은 상속의 목적으로 메소드는 인스턴스 변수를 재사용하는 것이라고 생각하기 때문이다. 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하라. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. -> Duck Typing
  • 동일한 메시지를 전송하지만 실제로 어떤 메소드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라고 부른다. 하지만 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다.
  • 구현 상속 : 서브클래싱(subclassing)이라고 부른다. 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것이다. 하지만 재사용을 위해서라는 composition을 사용하라.
  • 인터페이스 상속 : 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속하는 것이다. 상속은 구현 상속이 아니라 인터페이스 상속을 위해서 사용해야 한다.

추상화와 유연성

  • 상속의 가장 큰 문제점은 캡슐화를 위반한다는 것이다. 상속을 위해서는 부모 클래스의 구조를 잘 알고 있어야 한다. 인터페이스만을 활용하여 확장하는 composition과는 다르다.
  • 두번째 단점은 설계가 유연하지 않다는 것이다. 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.

역할, 책임, 협력

객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration)이다. 클래스, 상속, 지연 바인딩이 중요하지 않은 것은 아니지만 다분히 구현 측면에 치우쳐 있기 때문에 객체지향 패러다임의 본질과는 거리가 멀다.

협력

  • 결과적으로 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화하는 것이다. (타동적으로 활용되는 것을 원천봉쇄)
  • 객체가 참여하는 협력이 객체를 구성하는 행동과 상태 모두를 결정한다. 따라서 협력은 객체를 설계하는데 필요한 일종의 문맥(context)를 제공한다.
    • 영화라는 단어를 들었을 때 대부분의 사람들은 극장에서 영화를 상영하는 장면을 상상할 것이고 자연스럽게 Play()라는 행동을 수행할 것이라 생각한다. 하지만 영화 예매 시스템 안의 Movie에는 영화를 상영하기 위한 어떤 코드도 포함되어 있지 않다. Movie에 포함된 대부분의 메소드는 요금을 계산하는 행동과 관련된 것이다. 이거은 Movie가 영화를 예매하기 위한 협력에 참여하고 있고 그 안에서 요금을 계산하는 책임을 지고 있기 때문이다.

책임

  • 협력에 참여하기 위해 객체가 수행하는 행동을 책임이라고 부른다.
  • Craig Larman은 책임을 크게 하는 것(doing)과 아는 것(knowing)의 범주로 나누었다.
    • 하는 것
      • 객체를 생성하거나 계산을 수행하는 등의 스스로 하느 ㄴ것
      • 다른 객체의 행동을 시작시키는 것
      • 다른 객체의 활동을 제어하고 조절하는 것
    • 아는 것(가시성)
      • 사적인 정보에 관해 아는 것
      • 관련된 객체에 관해 하는 것
      • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
  • Screening의 책임의 예
    • 하는 것
      • 영화를 예매할 수 있어야 한다.
      • 예매 가격을 계산할 수 있어야 한다.
    • 아는 것
      • 자신이 상영할 영화를 알고 있어야 한다.
      • 가격과 어떤 할인 정책이 적용됐짖느 알아야 한다.
  • 처음에는 단순한 책임이라고 생각했던 것이 여러 개의 메시지로 분할되기도 하고 하나의 객체가 수행할 수 있다고 생각했던 책임이 나중에는 여러 객체들이 협력해야만 하는 커다란 책임으로 자라는 것이 일반적이다.
  • 자율적인 객체를 만든느 가장 기본적인 방법은 책임을 수행하는데 필요한 정보를 가장 잘 알고있는 전문가에게 그 책임을 할당하는 것이다. 이를 Information Expert 패턴이라고 부른다. (GRASP)
    • 가격을 계산하기 위해서는 가격과 할인정책이 필요하다. 이 모든 정보를 가장 잘 알고있는 정보 전문가는 Movie다. 가격을 계산할 책임은 Movie에게 할당해야 한다.
  • 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법을 책임 주도 설계(Responsibility-Driven Design, RDD)라고 부른다.
    • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
    • 시스템 책임을 더 작은 책임으로 분할한다.
    • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
    • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
    • 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
  • 객체가 메시지를 선택하는 것이 아니라 메시지가 객체를 선택하게 해야 한다. 이를 통해 객체가 최소한의 인터페이스를 가질 수 있게 된다. 또한 객체는 충분히 추상적인 인터페이스를 가질 수 있게 된다. 인터페이스는 무엇을 하는지는 표현해야 하지만 어떻게 수행하는지를 노출해서는 안된다.

역할

  • 실제로 협력을 모델링할 때는 특정한 객체가 아니라 역할에게 책임을 할당한다고 생각하는게 좋다.
  • 동일한 책임을 수행하는 객체가 생긴다면 두 메시지에 응답할 수 있는 대표자를 생각해볼 수 있을 것이다. 이것이 바로 역할이다. (다형성)
  • 협력은 역할들의 상호작용으로 구성되고, 협력을 구성하기 위해 역할에 적합한 객체가 선택되며, 객체는 클래스를 이용해 구현되고 생성된다.

                                 reference                    select from                        instance of   협력(Collaboration) ———————> 역할(Role) ———————> 객체(Object) ———————> 클래스(Class)