오브젝트

2022-07-25

오브젝트

도서명 오브젝트 코드로 이해하는 객체지향 설계
지은이 조영호 지음
출판사 위키북스
ISBN 9791158391409
금액 38,000원
출판일 2019년 06월 17일 발행(개정판)
페이지수 656쪽
참고 Blog-Shine오브젝트

들어가며 프로그래밍 패러다임

  • 프로그래밍 언어는 일반적으로 어떤 패러다임의 사용을 권장하고 다른 패러다임의 사용을 막는다.
  • 기본 개념에 대한 노골적인 의견 충돌이 빚어지는 일이 드물어 지고,
  • 동일한 규칙과 표준에 헌신하게 된다.

01 객체, 설계

1. 티켓 판매 어플리케이션 구현하기

책에서 나온 티켓 판매 어플리케이션은 절차지향적이다. 이를 개선해 나가야 한다.

구현한 클래스 다이어그램은 다음과 같다.

그림1.2 너무 많은 클래스에 의존하는 Theater 그림1.2 너무 많은 클래스에 의존하는 Theater

우선 절차 지향방식의 Theater는 다음과 같다.

public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience){
        if(audience.getBag().hasInvitation()){
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        }else{
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

한눈에 봐도 매우 절차지향적이다.

로버트 마틴은 소프트웨어 모듈이 가져야 하는 3가지 기능에 관해 다음과 같이 설명한다.

  1. 모듈은 정상적으로 실행되어야 한다
  2. 변경에 용이해야 한다
  3. 이해하기 쉬워야 한다.

위 코드는 필요한 기능을 오류없이 정확하게 수행하고 있다. 제대로 동작해야 한다는 제약을 만족한다. 하지만, 변경 용이성과 이해하기 쉬워야 한다는 제약은 만족시키지 못하고 있다.

위 코드는 여러가지 문제가 있다.

  1. 소극장이 관람객의 가방에서 직접 돈을 가져간다. 관람객의 허락과는 상관없이 소극장이 관람객의 가방에 접근하고 있다.
  2. 소극장이 직접 매표소의 티켓과 현금에 접근한후, 관람객에게 티켓을 전달하고, 받은 돈을 관람객의 매표소에 적립한다.

=> 티켓판매원이 하는 일이 없다.

따라서 위 코드는 우리의 상식과는 너무 다르게 작동하고 있다. 또한 코드를 이해하기 위해 세부적인 내용을 한꺼번에 기억하고 있어야 한다.

가장 큰 문제는 Audience 와 TicketSeller를 변경할 경우 Theater를 함께 변경해야한다는 사실이다.

예를 들어 관람객이 가방을 들고있다는 가정이 변경된다고 해보자. Theater는 관람객이 가방을 들고 있고 판매원이 매표소에서만 티켓을 판매한다는 지나치게 세부적인 사실에 의존해서 동작한다.

이처럼 다른 Class 가 Audience의 내부에 대하여 더 많이 알면 알수록 Audience의 변경이 어려워 진다.

이것이 객체 사이의 의존성(dependency)와 관련된 문제이다. 의존성이라는 말 내부에는 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼 있다.

그렇다고 모든 의존성을 제거하자는 것이 아니다!, 최소한의 의존성만 유지하고, 불필요한 의존성을 제거해야 한다. 맨 위 다이어그램에서 살펴봤듯 Theater는 너무 많은 클래스에 의존하고 있다. 이렇게 객체 사이의 의존성이 과한 경우를 결합도(coupling)가 높다고 말한다.

2. 설계 개선하기

우리는 변경과 의사소통이라는 문제가 서로 엮여 있음에 주목해야 한다.

코드를 이해하기 어려운 이유는 Theater가 관람객의 가방과 판매원의 매표소에 직접적으로 접근하기 때문이다. 이는 Theater가 Audience 와 TicketSeller에 결합된다는 것을 의미한다.

이를 해결하기 위해, 정보를 차단해야 한다. 과연 Theater가 관람객이 가방을 갖고 있다는 사실과, 판매원이 매표소에서 티켓을 판매한다는 사실을 알아야 할까??

알 필요가 없다!

관람객이 스스로 요금을 계산하고, 판매원이 스스로 티켓을 판매한다면 모든 문제가 해결된다. 관람객과 판매원을 자율적인 존재로 만들어야 하는것 이다.

  1. Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를 TicketSeller 내부로 숨기는 것 이다.

    Theater 내부에 있던 enter 코드의 logic을 TicketSeller에게로 옮겨보자!

     public class TicketSeller {
         private TicketOffice ticketOffice;
        
         public TicketSeller(TicketOffice ticketOffice) {
             this.ticketOffice = ticketOffice;
         }
        
         public void sellTo(Audience audience){
             if(audience.getBag().hasInvitation()){
                 Ticket ticket = ticketOffice.getTicket();
                 audience.getBag().setTicket(ticket);
             }else{
                 Ticket ticket = ticketOffice.getTicket();
                 audience.getBag().minusAmount(ticket.getFee());
                 ticketOffice.plusAmount(ticket.getFee());
                 audience.getBag().setTicket(ticket);
             }
         }
     }
    

    이제 ticketOffice에 대한 접근은 오직 TicketSeller에서만 가능하다. TicketSeller는 ticketOffice에서 티켓을 꺼내거나 판매 요금을 적립하는 일을 스스로 수행할수밖에 없다. => 이를 캡슐화 라고 부른다. 캡슐화를 통해 변경하기 쉬운 객체를 만들게 된다.

    Theater의 enter 메서드는 다음과 같이 간단하게 변경된다.

     public class Theater {
         private TicketSeller ticketSeller;
        
         public Theater(TicketSeller ticketSeller) {
             this.ticketSeller = ticketSeller;
         }
        
         public void enter(Audience audience){
             ticketSeller.sellTo(audience);
         }
     }
    

    이제 Theater는 ticketOffice에 대하여 전혀 알지 못한다. Theater는 단지 ticketSeller의 sellTo 메서드를 통해 메시지를 이해하고 응답할수 있을 뿐이다.

    Theater는 오직 TicketSeller의 인터페이스에 의존한다. TicketSeller가 내부에 TickerOffice 인스턴스를 포함하고 있다는 사실을 구현의 역영게 해당된다.

  2. Audience 를 캡슐화 하자. TicketSeller는 Audience는 getBag 메서드를 호출해서 Audience 내부의 Bag 인스턴스에 직접 접근한다.

    캡슐화 하는 방식은 이전과 같다.

    Audience에서 buy() 라는 메서드를 만들어 줬다.

     public class Audience {
         private Bag bag;
        
         public Audience(Bag bag) {
             this.bag = bag;
         }
        
         public Long buy(Ticket ticket){
             if(bag.hasInvitation()){
                 bag.setTicket(ticket);
                 return 0L;
             }else{
                 bag.setTicket(ticket);
                 bag.minusAmount(ticket.getFee());
                 return ticket.getFee();
             }
         }
     }
    

    변경 된 코드에서는 Audience가 자신의 가방안에 초대장이 들어있는지 스스로 확인한다. Audience가 가방을 직접 다루기 때문에 외부에 더이상 getter를 노출시킬 필요가 없다. getBag()을 제거하고 결과적으로 Bag의 존재를 내부로 캡슐화할 수 있게되었다.

    Audience 와 TicketSeller를 자율적인 객체로 만들었다!

    그림 1.6 자율적인 Audience와 TicketSeller로 구성된 설계 그림 1.6 자율적인 Audience와 TicketSeller로 구성된 설계

캡슐화와 응집도

핵심은 객체 내부의 상태를 캡슐화 하고, 객체 간에 오직 메시지를 통해서만 상호작용 하도록 만드는 것 이다.

Theater는 
단지 TicketSeller 가 sellTo 메서드를 통해 응답할수 있다는 사실만 알고 있을 뿐 이다.
단지 Audience 가 buy 메시지에 응답할 수 있고 자신이 원하는 결과를 반환할 것 이라는 사실만 알고있다.

객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임지고, 외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력해야 한다.

절차지향 vs 객체지향

- 절차지향

    절차지향 적 관점에서는 Theater의 enter 메서드는 Process 이며, audience, ticketSeller, bag, ticketOffice 는 데이터 이다.
    이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차지향적 프로그래밍 이라 부른다.

- 객체지향

    데이터를 스스로 처리하도로고 하여 프로세스가 데이터를 소유하고 있는 Audience, TicketSeller 내부로 옮기는 방식을
    객체지향 프로그래밍 이라 부른다.
    
    즉, 객체지향은 책임을 분산시킨다. 이전의 절차지향 모델은 Theater에 책임이 몰려있다.
    하지만 객체지향은 책임을 적절하게 객체들에게 분산시키게 된다.
    
    ![그림 1.8 책임이 분산된 객체지향 프로그래밍](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv8N3e%2Fbtrr3n3dj5G%2FkBhMmDUL5POjUbI8JcFAyK%2Fimg.png)
    *그림 1.8 책임이 분산된 객체지향 프로그래밍*

그래 거짓말이다!

이전까지 객체는 스스로의 처리를 위해 Audience와 TicketSeller 역시 스스로 자신을 책임져야 한다 했었다.
하지만 Theater는? Bag? TicketOffice는? 이들은 실세계 에서는 자율적인 존재가 아니다.
가방에서 돈을 꺼내는것은 관람객이지 가방이 아니다.

하지만 이들을 생물처럼 스스로 행동하고, 자신을 책임지는 자율적인 존재로 취급했다.

이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 **의인화(anthropomorphism)**이라 부른다.
실세계 에서는 생명이 없는 수동적인 존재라고 하더라고, 객체지향 세계로 넘어오는 순간 그들은 생명과 지능을 가진
싱싱한 존재로 다시 태어난다.

02 객체지향 프로그래밍

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

진정한 객체지향 페러다임으로의 전환은 Class 가 아닌, Object에 초점을 맞출 때 에만 얻을 수 있다.

  1. 어떤 클래스가 필요한지가 아니라, 어떤 객체가 필요한지 고민해야 한다.

    클래스는 공통적인 객체들의 상태와 행동을 추상화 한 것 이다. 따라서 Class를 추상화 시키려면 어떤 객체가 필요한지 알아야 한다.

  2. 객체는 독립적인 존재가 아니다, 기능 구현을 위해 협력하는 공동체의 일원으로 봐야한다.

    객체를 고립된 존재로 바라보지 말고, 협력에 참여하는 협력자로 바라봐야 한다. 다른 객체에게 도움을 주거나, 의존하면서 살아가는 협력적인 존재이다. 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 Class를 구현해라.

  • 도메인의 구조를 따르는 프로그램 구조

    도메인(domain)이란 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라 부른다.

    객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터, 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할수 있기 때문이다. 요구사항과 프로그램을 객체라는 동일한 관점에서 보기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결된다.

    다음 도메인을 살펴보자.

    그림 2.3 영화 예매 도메인을 구성하는 타입들의 구조 그림 2.3 영화 예매 도메인을 구성하는 타입들의 구조

    일반적으로 클래스 기반의 객체지향 언어에서는 도메인 개념을 구현하기 위해 Class를 사용한다. Class의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다. Class 사이의 관계도 최대한 도메인 개념 사이에 맺어진 관계와 유사하게 만들어야 프로그램의 구조를 이해하고, 예상하기 쉬워진다.

    따라서 Class 다이어그램을 도메인 모델과 유사한 형태를 보이게 된다. 다음과 같이 말이다.

    그림 2.4 도메인 개념의 구조를 따르는 클래스 구조

  • 자율적인 객체

    객체에게는 중요한 2가지 사실이 있다.

    1) 객체는 상태(state) 와 행동(behavior)을 함께 가지는 복합적인 존재라는 것 이다. 2) 객체는 스스로 판단하고, 행동하는 자율적인 존재이다.

    객체지향 페러다임 에서는 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 캡슐화 시킬수 있다.

    또한 대부분의 객체지향 언어에서는 public, private 과 같은 접근 수정자를 제공한다. 이와 같이 접근 수정자를 통해 객체 내부에 대한 접근을 제어하는 이유는 객체를 자율적인 존재로 만들기 위해서이다.

    객체 스스로가 상태를 관리하고, 행동하는 지율적인 객체가 되려면 외부의 간섭을 최소화 해야한다. 외부에서 객체의 결정에 직접적으로 개입하려 하면 안된다. 객체에게 요청만 하고, 스스로 최선의 방법을 경정하도록 해야한다.

    캡슐화와 접근제어는 객체를 두 부분으로 나눈다.

    1) 외부에서 접근 가능한 부분으로 이를 public interface 라고 부른다. 2) 외부에서는 접근 불가능하고, 오직 내부에서만 접근 가능한 부분으로 이를 implementation 이라 부른다.

    일반적으로 Java로 생각해보면 클래스의 속성은 private로 감추고, 외부에 제공해야 하는 일부 메서드만 public으로 선언한다. public interface에는 public으로 지정된 메서드만 포함한다. 그 밖의 private 메서드나 protected 메서드, 속성은 implementation 에 해당된다.

  • 프로그래머의 자유
    • 클래스 작성자(class creator) : 새로운 데이터 타입을 프로그램에 추가
    • 클라이언트 프로그래머(client programmer) : 클래스 작성자가 추가한 데이터 타입을 사용한다.

    class creator는 client programmer 에게 필요한 부분만 공개하고, 나머지는 숨겨야 한다. class creator입장에서는 client programmer가 숨겨놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래밍에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. => 이를 구현은닉(implementation hiding)이라고 부른다.

    이를 통해 client programmer는 내부 구현은 무시한채 인터페이스만 알고 있어도 class를 사용할 수 있기 때문이다. 따라서 client programmer가 알아야 할 지식의 양이 줄어든다.

  • 협력에 관한 짧은 이야기

    객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response)한다.

    객체가 다른 객체와 상호작용 하는 유일한 방법은 메시지를 전송(send a message)하는 것 뿐이다. 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다. 이처럼 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.

    예를 들어 다음 코드의 일부를 살펴보자.

      public class Screening {
        
          private Money calculateFee(int audienceCount) {
              return movie.calculateMovieFee(this).times(audienceCount);
          }
      }
    

    위 코드는 ‘Screening이 Movie 에게 calculateMovieFee 메시지를 전송한다’ 라고 말하는것이 적절하다. 사실 Screening은 Movie안에 calculateMovieFee 메서드가 존재하고 있는지 조차 모른다. 단지 Movie 가 calculateMovieFee 메시지에 응답할 수 있다고 믿고 메시지를 전송할 뿐이다.

    메시지를 받은 Movie는 스스로 적절한 메서드를 선택한다.

2. 할인 요금 구하기

이번에는 할인 정책과 할일 조건을 구현해 보자.

부모 클래스인 DiscountPolicy 안에 중복 코드를 두고, AmountPolicy, PercentDiscountPolicy가 이 클래스를 상속한다. 실제 애플리케이션에서 DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 abstract class 로 구현했다.

public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    Money calculateDiscountAmount(Screening screening){
        for(DiscountCondition each : conditions){
            if(each.isSatisfiedBy(screening)){
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    protected abstract Money getDiscountAmount(Screening screening);
}

DiscountPolicy는 할인 여부와 요금 계산에 필요한 전체적인 흐름을 정의하지만, 실질적인 요금을 계산하는 부분을 추상메서드 인 getDiscountAmount() 메서드에 위임한다.

이처럼 기본적인 알고리즘의 흐름을 결정하고, 중간에 필요한 처리를 자식 class에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴 이라 부른다. 다음 글은 내가 정리해둔 템플릿 메서드 패턴에 관한 글 이다.

3. 상속과 다형성

우선 다음 코드를 살펴보자.

public class Movie {
    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.title = title;
        this.runningTime = runningTime;
        this.fee = fee;
        this.discountPolicy = discountPolicy;
    }

    public Money getFee() {
        return fee;
    }

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

Movie 클래스 어디를 봐도 할인 정책이 금액 할인 정책인지?, 비율 할일 정책인지? 판단하지 않는다. Movie 내부에 할인 정책을 결정하는 조건문도 없는데도 불구하고 어떻게 영화 요금을 계산할때 할인 정책을 선택할수 있을까?

이는 상속과 다형성을 통해 가능하다.

  • 컴파일 시간 의존성과 실행 시간 의존성

    그림 2.7 DiscountPolicy 상속 계층 그림 2.7 DiscountPolicy 상속 계층

    위 클래스 다이어 그램을 보면, Movie 클래스가 DiscountPolicy와 연관관계를 맺고 있다. 문제는 영화 요금을 계산하기 위해서는 abstarc class가 아닌!, AmountDiscountPolicy, PercentDiscountPolicy 와 같은 인스턴스에 의존해야 한다.

    하지만 Movie 클래스는 두 클래스 중 어떤것도 의존하지 않는다. 추상 클래스에만 의존하고 있다.

    그럼 Movie 코드를 작성하던 시점에는 존재조차 모르던 AmountDiscountPolicy, PercentDiscountPolicy의 인스턴스를 사용할수 있는 이유는 무엇일까?

    다음은 아바타 영화에 대한 코드이다.

      new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000),
              new AmountDiscountPolicy(Money.wons(800), ...));
    

    Movie의 인스턴스를 생성할때 생성자의 인자로 AmountDiscountPolicy 를 전달하면 된다.

    그림 2.8 실행 시에 Movie는 AmountDiscountPolicy에 의존한다. 그림 2.8 실행 시에 Movie는 AmountDiscountPolicy에 의존한다.

    실행시에 Movie 인스턴스는 AmountDiscountPolicy 클래스의 인스턴스에 의존하게 될 것 이다.

    여기서의 핵심은 코드의 의존성과 실행시점의 의존성이 서로 다를 수 있다는 점이다.

    => 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 이는 확장 가능한 객체지향 설계의 특징이다.

    설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다. 반면 유연성을 억제하면 코드를 이해하고 디버깅 하기는 쉬워지지만, 재사용성과 확장 가능성은 낮아진다.

  • 상속과 인터페이스

    인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억하자. 상속을 통해 자식은 부모 클래스가 수신할 수 있는 모든 메시지를 수신할수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.

      public class Movie {
          public Money calculateMovieFee(Screening screening) {
              return fee.minus(discountPolicy.calculateDiscountAmount(screening));
          }
      }
    

    Movie는 DiscountPolicy의 인터페이스에 정의된 calculateDiscountAmount 라는 메시지를 전송하고 있다. DiscountPolicy를 상속받은 AmountDiscountPolicy, PercentDiscountPolicy의 인터페이스에도 이 메서드가 포함되어 있다.

    Movie입장에서는 자신과 협력하는 객체가 어떤 클래스의 인스턴스인지가 중요한것이 아니라, calculateDiscountAmount라는 메시지를 수신할수 있다는 사실이 중요하다.

  • 다형성

메시지와 메서드는 다른 개념이다. Movie는 discountPolicy의 인스턴스 에게 calculateDiscountPolicy 메시지를 전송한다. 그럼 실행되는 메서드는 무엇인가? 이는 연결된 객체의 클래스가 무엇인지에 따라 달라진다.

다시말해, Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것 인가? 는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성 이라 부른다. (즉, 동일한 메시지에 대하여 객체의 타입에 따라 다르게 응답할수 있는 능력을 의미한다.)

이처럼 메시지와 메서드를 실행 시점에 바인딩 하는것을 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라 부른다. 반면에 전통적인 컴파일 시점에 실행될 함수나, 프로시저를 결정하는 것을 초기 바인딩(early binding), 정적 바인딩(static binding)이라 부른다.

4. 추상화와 유연성

지금까지 살펴본 코드들에서 DiscountPolicy는 AmountDiscountPolicy, PercentDiscountPolicy 보다 더 추상적이고, DiscountCondition 또한 더 추상적이다.

이는 인터페이스에 초점을 맞추기 때문이다. DiscountPolicy는 모든 할인 정책이 수신할 수 있는 calculateDiscountAmount 메시지를 정의한다. DiscountCondition은 모든 할인 조건들이 수신할 수 있는 isSatisfiedBy 메시지를 정의한다.

다음 다이어그램을 살펴보자.

그림 2.13 추상화는 좀 더 일반적인 개념들을 표현한다 그림 2.13 추상화는 좀 더 일반적인 개념들을 표현한다

위 그림은 추상화를 사용할 경우 2가지 장점을 보여준다.

  1. 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
  2. 추상화를 아용하면 설계가 좀 더 유연해진다.

위 다이어 그램을 하나의 문장으로 정리하면 “영화 예매 요금은 최대 하나의 할인정책 과 다수의 할인조건을 이용해 계산할 수 있다”와 같다. 위 문장은 “금액할인 정책과 두개의 순서조건, 한개의 기간 조건을 이용해 계산할 수 있다” 라는 문장을 포괄할수 있다는 사실이 중요하다.

이는 좀더 추상적인 개념들을 사용해 문장을 작성했기 때문이다.

추상화를 통해 상위 정책을 기술한다는 것은 기본적인 어플리케이션의 협력 흐름을 기술한다는것을 의미한다. 영화의 예매 가격을 계산하기 위한 흐름은 항상 Movie -> DIscountPolicy -> DiscountCondition 로 흘러간다. 할인 정책이나, 조건의 자식들은 추상화를 이용해서 정의한 흐름에 그대로 따르게 된다.

추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는것을 방지하기 때문이다. Movie는 특정한 할인 정책에 묶이지 않는다. 할인 정책을 구현한 클래스가 DiscountPolicy를 상속받고 있다면 어떤 클래스와도 협력이 가능하다.

DiscountPolicy 역시 특정 할인 조건에 묶여있지 않다. DiscountCondition을 상속받은 어떤 클래스와도 협력이 가능하다. 이 모든것이 추상화 덕분이다.

  • 추상클래스와 인터페이스의 트레이드오프

      public abstract class DiscountPolicy {
          private List<DiscountCondition> conditions = new ArrayList<>();
        
          public DiscountPolicy(DiscountCondition ... conditions) {
              this.conditions = Arrays.asList(conditions);
          }
        
          public Money calculateDiscountAmount(Screening screening) {
              for(DiscountCondition each : conditions) {
                  if (each.isSatisfiedBy(screening)) {
                      return getDiscountAmount(screening);
                  }
              }
        
              return Money.ZERO;
          }
        
          abstract protected Money getDiscountAmount(Screening Screening);
      }
    
      public class NoneDiscountPolicy entends DiscountPolicy {
        
          @Override
          protected Money getDiscountAmount(Screening screening) {
              return Money.ZERO;
          }
      }
    

    위 코드를 보면, NoneDiscountPolicy는 getDiscountAmount() 를 오버라이딩 하여 ZERO를 반환하도록 하고있다. 하지만 할인조건이 없는경우, calculateDiscountAmount 메시지 호출시 getDiscountAmount() 자체를 호출하지 않는다.

    부모 클래스인 DiscountPolicy는 할인 조건이 없는경우 getDiscountAmount()를 호출하지 않고, ZERO를 반환한다. 이는 DiscountPolicy내부에 NoneDiscountPolicy를 개념적으로 결합시켜둔 것 이다. 개발자는 DiscountCondition이 없으면 0원을 반환할 것이라는 사실을 가정하기 때문이다.

    이러한 문제를 해결하기 위해 DiscountPolicy를 인터페이스로 바꾸고, 기존의 DiscountPolicy를 DefaultDiscountPolicy 라는 abstract class로 만든다.

    그림 2.15 인터페이스를 이용해서 구현한 DiscountPolicy 계층 그림 2.15 인터페이스를 이용해서 구현한 DiscountPolicy 계층

    과연 어떤 설계가 더 좋은가? 구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다. 우리가 작성하는 코드는 모두 합당한 이유가 있어야 한다. 비록 아주 사소하더라도 트레이트오프를 통해 얻어진 결론은 그렇지 않은 경우와 매우 다르다.

  • 코드 재사용

    객체지향을 조금이라도 공부해봤다면 코드 재사용을 위해서는 상속이 아닌, 합성을 사용해야 함을 배웠을 것이다. Movie가 DiscountPolicy의 코드를 재사용 하는 방식이 바로 합성이다. 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

    다음은 이 합성 방식을 상속으로 변경한 다이어그램이다.

    그림 2.16 상속으로 구현한 할인 정책 그림 2.16 상속으로 구현한 할인 정책

    상속은 2가지 이유에서 설계에 좋지 못하다

    1. 캡슐화를 위반한다 : 결정적으로 부모클래스의 구현이 자식클래스에 노출되기 때문에 캡슐화가 약화된다.

    2. 설계를 유연하지 못하게 만든다 : 상속은 부모클래스와 자식클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것은 불가능 하다.

  • 합성

    상속은 컴파일 시점에 하나의 단위로 강하게 결합하는데(부모, 자식 코드) 반해, 합성은 클래스 간 인터페이스를 통해 약하게 결합된다.

    즉, 내부 구현에 대해서는 전혀 알지 못한다. Movie는 DiscountPolicy 가 외부에 calculateDiscountAmount 메서드를 제공한다는 사실만 알고 내부 구현에 대해서는 전혀 알지 못한다.

    이처럼 인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법을 합성이라 부른다.

03 역할, 책임, 협력

1. 협력

그림 3.1 영화 예매 기능을 구현하기 위한 객체들 사이의 상호작용 그림 3.1 영화 예매 기능을 구현하기 위한 객체들 사이의 상호작용

위 다이어그램 에서는 다양한 객체들이 영화 예매의 기능을 구현하기 위해 메시지를 주고 받으면서 상호작용 하고 있다. 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용을 협력이라고 부른다. 객체가 협력에 참여하기 위해 수행하는 로직은 책임이라고 부른다. 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할이 된다.

  • 협력

    협력은 객체지향의 세계에서 기능을 구현할 수 있는 유일한 방법이다. 즉, 두 객체가 상호작용을 통해 더 큰 책임을 수행하게 된다. 협력을 위한 두 객체 사이의 메시지 전송(message sending)은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다.

    이런 메시지를 수신한 객체는 메서드를 실행하여 응답하게 된다. 외부의 객체는 오직 메시지 만 전송하며, 메시지를 어떠한 방식으로 처리할지는 수신한 객체가 직접 결정하게 된다.

    위 다이어그램에서 예메 요금 계산을 위해 Screening은 Movie에게 calculateMovieFee() 메시지를 전송함으로써 1인 요금을 얻는다

    만약 메시지를 사용하지 않고, 요금을 계산하는 작업을 Screening이 수행하게 된다면 Movie의 인스턴스 변수에 직접 접근해야만 한다. => Screening은 Movie의 내부 구현에 결합되어 버린다. 가장 큰 문제는 Movie의 자율성이 회손된다.

    결과적으로 객체를 자율적으로 만들려면 내부 구현을 캡슐화 해야한다. 캡슐화를 하면 변경에 대한 파급효과를 제한할수 있기 때문에 자율적인 객체는 변경하기도 쉬워진다. Screening이 요금을 계산하기 위해 Movie의 내부에 직접적으로 접근하는 것은 캡슐화의 원칙을 위반하는 것 이다. 반면 calculateMovieFee와 같이 메시지를 사용하게 되면, Screening과 Movie 사이의 결합도를 느슨하게 유지할수 있다.

  • 협력이 설계를 위한 문맥을 결정한다.

    어떤 객체가 필요하다면 그 이유는 그 객체가 어떤 협력에 참여하고 있기 때문이다. 그리고 객체가 협력에 참여할 수 있는 이유는 협력에 필요한 적절한 행동을 보유하고 있기 때문이다.

    예를 들어 우리의 Movie 객체는 어떤 행동을 수행해야 할까? 우리의 Movie에 포함된 대부분의 메서드는 요금을 계산하는 행동과 관련되어 있다. 이것은 Movie가 영화를 예매하기 위한 협력에 참여하고 있고 그 안에서 요금을 계산하는 책임을 지니고 있기 때문이다.

    Movie의 행동을 결정하는 것은 영화 예매를 위한 협력이다. 협력이라는 문맥을 고려하지 않고 Movie의 행동을 결정하는 것은 아무런 의미가 없다. 협력이 존재하기 때문에 객체가 존재한다.

    객체의 행동은 협력이 결정하고, 상태는 행동이 경정한다.(협력 -> 행동 -> 상태)

    상태는 객체가 행동하는 데 필요한 정보에 의해서 결정된다고 할수있다. 행동은 협력 안에서 객체가 처리할 메시지로 결졍된다.

    결과적으로 객체가 참여하는 협력이 객체의 행동과 상태를 모두 결정한다. 협력이 객체 설계의 문맥(context)를 제공하는 것 이다.

2. 책임

협력에 참여하기 위해 객체가 수행하는 행동을 책임이라 부른다. 책임이란 객체에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장이다.

객체에게 얼마나 적절한 책임을 할당하느냐? 가 설계의 품질을 결정한다. 객체의 구현 방법은 책임을 결정한 후 에 고민해도 늦지 않다.

크레이그 리만(Craig Larman)은 객체의 책임을 ‘무엇을 알고 있는가’ 와 ‘무엇을 할 수 있는가’ 로 나누었다.

하는 것

  • 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
  • 다른 객체의 행동을 시작시키는 것
  • 다른 객체의 활동을 제저하고 조절하는 것

아는 것

  • 사적인 정보에 관해 아는 것
  • 관련된 객체에 관해 아는 것
  • 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것

ex) Screening의 책임은 무엇인가? 영화를 예매하는 것 이다. 또한 자신이 상영할 영화를 알고 있어야 한다. 영화를 예매한다 - 하는것 상영할 영화를 안다 - 아는것

협력 안에서 객체에게 할당한 책임이 외부의 인터페이스와 내부의 속성을 결정하게 된다.

객체는 자신이 맡은 책임을 수행하는 데 필요한 정보를 알고 있을 책임이 있다. (만약 자기가 할수 없다면 이를 도와줄 객체를 알아야 한다) => 어떤 책임을 수행하기 위해서는 그 책임을 수행하는 데 필요한 정보도 함께 알아야 할 책임이 있는 것 이다.

사실 협력이 중요한 이유도, 객체에게 할당할 책임을 결정할 수 있는 문맥을 제공하기 때문이다.

  • 책임 할당

    책임을 할당한다는 것은 메세지의 이름을 결정하고, 해당 메세지에 응답할 객체를 결정하는 것이다

    필요한 정보를 가장 잘 알고있는 전문가에게 그 책임을 할당해야 한다 => INFORMATION EXPERT(정보 전문가) 패턴

    객체에게 책임을 할당하기 위해서는 가장 먼저 협력이라는 문맥을 정의해야 한다.

    우리의 영화 예매시스템을 예로 들어보자. 시스템이 사용자에게 제공해야 하는 기능은 영화를 예매하는 기능이다. 이 기능을 시스템이 제공할 책임으로 할당하는 것 이다.

    • 객체가 책임을 수행하는 방법 => 메시지 전송
    • 책임을 할당 한다 => 메시지의 이름을 결정하는 것과 같다.

    우리의 예시 에서는 “예매하라” 라는 이름의 메시지로 협력(문맥)을 시작해 보자.

    예매하라1

    메시지를 선택했으면 메시지를 처리할 적절한 객체를 선택해야 한다. 정보 전문가 에게 책임을 할당하면 된다. 영화 예매와 관련된 정보를 가장 많이 알고있는 객체에게 책임을 할당하는 것 이 바람직하다. 영화를 예매하기 위해서는 상영 시간과 기본 요금을 알아야 한다. 누가 해당 정보들을 알고있는 정보 전문가 인가? => Screening 이다.

    예매하라2

    예매를 위해서는 예매 가격을 계산해야 한다. 하지만 Screening은 가격을 계산하는데 필요한 정보를 충분히 알고 있지 않다. Screening은 예매의 전문가 이지, 가격계산의 전문가는 아니다. => Screening 외부의 객체에게 가격 계산을 요청해야 한다. 새로운 메시지가 필요하다.

    예매하라3

    마지막으로 이 “가격을 계산하라” 라는 메시지를 처리할 적절한 객체가 필요하다. 마찬가지로 가격을 계산하는 데 필요한 정보를 가장 많이 알고있는 정보 전문가를 선택해야 한다. => Movie 이다.

    예매하라4

    가격을 계산하기 위해서는 다시 할인요금이 필요하다. 하지만 이는 Movie의 정보 전문 분야가 아니다. 따라서 Movie는 다시 요금을 계산하는데 필요한 요청을 외부에 전송해야 한다. “할인 요금을 계산하라” 라는 메시지가 필요하다.

    이처럼 객체지향 설계는 협력에 필요한 메시지를 찾고, 메시지에 적합한 객체를 선택하는 반복적인 과정이다. 물론 모든 책임 할당 과정이 이렇게 단순하지는 않지만, 기본적으로 책임을 수행할 정보 전문가를 찾는 것이다.

  • 책임 주도 설계

    지금까지의 내용의 핵심은 협력을 설계하기 위해서는 책임에 초점을 맞춰야 한다는 것이다. 이처럼 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 설계하는 방법을 RDD(책임 주도 설계)라 부른다.

  • 책임 할당시 고려사항 2가지

    1. 메시지가 객체를 결정한다.

      메세지를 먼저 식별하고 메시지를 처리할 객체를 나중에 선택했다.

      • 최소한의 인터페이스
      • 충분히 추상적인 인터페이스 : 인터페이스는 무엇을 하는지 표현해야 하지만, 어떻게 수행하는지는 노출해서는 안된다.
    2. 행동이 상태를 결정한다

      객체가 존재하는 이유는 협력에 참여하기 위해서 이다. 따라서 객체를 객체답게 만드는 것은 상태(데이터)가 아니라 다른 객체에게 제공하는 행동이다.

      캡슐화를 위반하지 않기 위해 구현을 뒤로 미루면서 객체의 행위를 고려하기 위해서는 항상 협력이라는 문맥 안에서 객체를 생각해야 한다. 개별 객체의 상태와 행동이 아닌, 시스템의 기능을 구현하기 위한 협력에 초점을 맞춰야만 응집도가 높고, 결합도가 낮은 객체를 만들수있다. 상태는 단지 정상적인 행동을 수행하기 위한 재료일 뿐이다.

3. 역할과 협력

객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할이라고 부른다.

위 영화 예메 협력과정을 생각해 보자.

  1. 영화를 예매할 수 있는 적절한 역할이 무엇인가를 찾는다.

  2. 역할을 수행할 객체로 Screening 인스턴스를 선택한다.

예매하라5

익명의 역할을 찾은 후, 그 역할을 수행할 수 있는 객체를 선택하는 방식이다.

  • 유연하고 재사용 가능한 협력

역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문이다.

만일 익명의 역할이 없다고 해보자. Movie 가 가격을 계산하기 위해 “할인 요금을 계산해라” 라는 메시지를 보내면, AmountDiscountPolicy 와 PercentDiscountPolicy 인스턴스 2가지가 메시지에 응답할수 있다.

그렇다면 두 종류의 객체가 참여하는 협력을 다음과 같이 개별적으로 만들어야 할까?

예매하라6

04 설계 품질과 트레이드오프

05 책임 할당하기

06 메시지와 인터페이스

07 객체 분해

08 의존성 관리하기

09 유연할 설계

10 상속과 코드 재사용

11 합성과 유연한 설계

12 다형성

13 서브클래싱과 서브타이밍

14 일관성 있는 협력

15 디자인 패턴과 프레임워크

마치며 나아가기

부록 A 계약에 의한 설계

부록 B 타입 계층의 구현

부록 C 동적인 협력, 정적인 코드

results matching ""

    No results matching ""

    99 other / uml

    04 react / JSX