기어가더라도 제대로

2. 객체지향 프로그래밍 본문

책 리뷰/오브젝트

2. 객체지향 프로그래밍

Damagucci-juice 2022. 12. 31. 16:09

2장에서는 영화 예매 시스템을 만들어보면서 사용자가 영화 예매 시스템을 이용해 쉽고 빠르게 보고 싶은 영화를 예매할 수 있도록 해보자.

객체에 대한 접근

영화 예매 도메인에서 객체들을 설정해야한다. 그 객체들은 아직 클래스일지 인터페이스일지 모르지만, 어떠한 역할을 하는 주체정도로 설정하고 가는 것이 좋다. 영화 예매 도메인을 구성하는 객체는 단순하게 일반화하자면 다음과 같다. 

  • 영화(Movie): 영화가 가지고 있는 기본적인 정보를 들고 있는 객체
  • 상영(Screening): 실제로 관객들이 영화를 관람하는 사건을 표현
  • 할인 정책(Discount Policy): 할인 요금을 결정
  • 할인 조건(Discount Condition): 가격의 할인 여부를 결정

새로운 도메인을 설정할 때 일반적인 시각과 다른점이 재밌었다. 그 점은 "상영" 이라는 부분인데, 관객들이 실질적으로 구매하는 것은 영화가 아니라 영화가 상영될 때 볼 권리를 사는 것이기 때문이다. 엄연히 영화 자체가 가지고 있는 정보들과는 다르다. 도메인을 설정하면서 놓치기 쉬운 부분이 일반 세상의 용어들을 담으면서도, 순간 지나칠 수 있는 부분들을 세심하게 보는 것이 어렵다. 여기서는 "상영" 이라는 객체가 그 역할을 한다고 생각한다. 

할인 정책과 할인 조건의 관계도 흥미롭다. 할인 정책은 가격의 요금을 얼마나 할인 할 것이냐 하는 것이고, 할인 조건은 할인을 받을 수 있는 조건을 충족했는지를 판단하는 객체이다. 이 부분도 일반적인 접근으로 생각하면, 할인이 되는가 안되는가만 고려를 하지 두 부분으로 나눠서 접근하기가 쉽지 않다. 

객체 지향 프로그래밍에서 집중해야할 부분

보통은 클래스를 어떻게 만들까를 많이 고민한다고 하는데, 객체를 어떻게 만들지를 먼저 고민해야한다고 한다. 클래스에 어떤 상태와 동작을 넣는지는 그 다음이고.

  1. 어떤 객체들이 필요한지 고민하기
    • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것이므로, 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 상태와 행동을 가지는지를 먼저 결정해야한다.
  2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야한다. 
    • 사람도 그렇고 소프트웨어의 객체도 혼자서는 살 수 없다. 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다. 
    • 공동체의 일원으로 객체를 바라보는 설계는 유연하고 확장 가능하다. 

아직 2장이지만 조영호님의 전작인 "객체 지향의 사실과 오해" 를 읽어본 독자라면 "역할, 책임, 협력" 을 귀에 못이 박히도록 들었을 것이다. 이번작에서는 더욱 구체적인 예시와 코드로 그 점을 확인할 수 있다.

도메인 구조

  • 도메인: 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

p.41 - 오브젝트 - 도메인 모식도

영화 예매 시스템의 도메인이다. 객체들의 관계를 표현한다. 1 : * 은 1: n 이고 1: 1..* 은 하나 이상이 반드시 존재한다는 뜻이다. 
1: 0...1 은 존재하거나 없거나 두 가지 상태만을 지니는 것을 표현한다. 

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

도메인의 개념과 관계를 반영하도록 프로그램을 구조화 해야하기 때문에 클래스의 구조는 도메인의 구조와 유사한 형태를 띠어야 한다. 

도메인 구조를 따르는 클래스 구조

접근 제어와 자율성

Screening(상영)이라는 클래스를 만들 때 가시성이 주제로 떠오른다. 왜 내부의 상태를 클래스 외부에서 알면 안되는 것일까? 또한 막아 놓고 메서드로 공개하는 이유는 무엇일까? 

주로 인스턴스의 변수의 가시성은 Private이고 메서드의 가시성은 Public이다. 클래스의 내-외부를 구분짓기 위한 장치다. 핵심은 어떤 부분을 공개하고 감출지 결정하는 것이다. 그 이유는 자율성과 관련이 있다. 경계가 명확하면 객체의 자율성을 보장하기 때문이다. 

자율적인 객체

이런 말이 어색한 부분도 있을 텐데, 이는 객체의 두가지 이유때문에 그러하다. 

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

객체지향 이전에는 데이터와 기능을 분리해서 설계를 했다면, 객체 지향에서는 관련된 데이터와 기능을 한데 묶어 "객체"라는 주인공을 등장 시켰다. 이렇듯 데이터와 기능을 내부로 함께 묶는 것을 캡슐화라고 한다. 캡슐화와 더불어서 접근 제어 메커니즘을 통해 이를 더욱 공고히 한다. 외부에서의 접근을 막는 이유스스로 판단하고 자율적으로 상태를 관리하고 행동하는 객체를 만들기 위함이다. 외부 간섭 최소화의 과정이라고 봐도 좋다. 이렇듯 캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.

인터페이스와 구현

  • 인터페이스(Interface): 얼굴과 얼굴을 맞대는 부분이다. 객체가 캡슐화되어있지만 의사소통을 하기 위해 열려있는 부분도 필요한데, 이런 부분이 인터페이스이다. 
  • 구현(Implementation): 외부에서는 접근이 불가능 하고 오직 내부에서만 접근 가능한 부분이다. 

컴퓨터 공학을 보면 사람사는 세상과 비슷한데, 여기서도 그것을 느낀다. 사람들도 학교, 직장, 사교모임에서 정보를 얻기도 하고, 얼굴을 맞대고 지내기도 하지만, 정작 중요한 결정은 얻은 정보를 토대로 스스로 해야한다. 남이 하자는 대로 줏대 없이 휘둘리는 사람을 우유부단하다고 하는데 객체 지향에서의 객체도 그러한 태도를 지양한다. 프로그래머가 객체를 설계할 때 항상 염두에 두어야하는 것이 인터페이스와 구현의 분리(Separation of interface and implementation)이다.

Money 클래스가 필요한가요?

1장에서는 돈을 처리하는 타입을 Long 타입으로 결정했다. 이번 장에서는 Money 라는 클래스를 만들어서 관할하게 했다. 이는 2가지 이점이 있다.

  • 저장하는 값이 금액과 관련이 있다는 의미를 전할 수 있다. 
  • 금액과 관련된 로직이 서로 다른 곳에 중복되어 구현되는 것을 막는다. 

이를 통해 도메인의 의미를 풍부하게 할 수 있다는 객체지향의 장점을 끌어낼 수 있다. 객체를 사용해 의미를 더 명시적이고 분명하게 표현한다면 Money 와 같은 클래스의 도입을 고려하면 좋겠다.

협력을 통해 할 수 있는 일

객체간 협력을 통해 영화 예매권을 발급할 수 있고, 예매의 할인 금액도 계산할 수 있다. 

짧게 객체에 협력에 대해서 짚어보자. 

  • 요청: 한 객체가 다른 객체에게 인터페이스에 공개된 행동을 수행하도록 요청
  • 응답: 요청 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답
  • 메시지 전송: 다른 객체와 상호작용하는 방법
  • 메시지 수신: 요청이 도착할 때 메시지를 수신
  • 메서드: 수신된 메시지를 처리하기 위한 자신만의 방법

메시지와 메서드는 다르다.

철자뿐만 아니라 하는 의미도 조금씩 다르다. 뒤에서 할인 정책에 따라 계산하는 금액이나 비율이 달라지는 현상을 볼 수 있다.
이를 다형성(Polymorphism) 이라 이야기한다. 그래서 어떤한 할인 정책이 메시지를 전달 받느냐에 따라서 수행하는 메서드가 달라질 수 있다는 것이다. 똑같은 메시지를 받아도 저마다 나온 결과가 다를 수 있는 것 처럼..

요금 할인을 위한 협력

Movie 인스턴스는 할인요금을 계산하기 위해서 내부에 로직을 구현하지 않고 DiscountPolicy 객체에 계산하라는 메시지를 던진다. Movie는 비율할인 정책일지, 요금할인 정책일지 모르지만, 일단 던지는 것이다. 여기서 상속(Inheritance)과 다형성의 개념이 등장한다. 그리고 이 둘의 기반이 되는 추상화(abstraction)의 원리까지 나온다.

할인 정책은 비율 할인 정책과 금액 할인 정책으로 나뉜다. 두 클래스는 대부분의 코드가 유사하고 할인 요금을 계산하는 방식만 조금 다르다. 두 클래스 사이에 중복 코드를 제거하기 위해 공통으로 코드를 보관할 장소가 필요하다. DiscountPolicy 의 인스턴스를 생성할 필요가 없기 때문에 추상 클래스(abstract class) 로 구현한다. (Swift의 Protocol과 비슷하다. 다만 조금 다른 점은 프로토콜은 JAVA의 Interface 타입 같은 느낌이 나는데, swift의 프로토콜과 다른점은 추상 클래스는 메서드에 "구현"도 가능하다. 이를 Swift적인 관점에서 보자면 Protocol 과 extension Protocol 의 융합이라고 생각이 된다.) 

이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴이라고 부른다.

P.54, 템플릿 메서드 패턴

상속과 다형성

이를 이해하기 위해선 필연적으로 의존성이라는 것을 알아야한다. 

  • 의존성: 어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이에 의존성이 존재한다고 함

Movie가 DiscountPoilcy 에게 갖는 것이 의존성

Movie는 DiscountPolicy를 인스턴스 내부에 변수로 가지고 있다. 

코드 수준의 의존성: 컴파일 타임 의존성

Movie 클래스가 연결은 DiscountPolicy 에 연결이 되어있지만 실행할 때 실제로 의존하는 것은 AmountDiscountPolicy나 PercentDiscountPolicy 의 인스턴스이다. 하지만 코드 수준에서 Movie 클래스는 두 클래스에 의존하는 것이 아닌 DiscountPolicy에 의존한다. 결론적으로 존재를 모르다가 실행하면 존재를 알게된다는 것이다. 이게 어떻게 가능할까?

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 이는 쉽게 코드를 재사용 할 수 있으며, 확장 가능한 객체지향 설계가 되도록 돕는다. 

상속

클래스를 하나 추가하고 싶은데 그 클래스가 기존의 어떤 클래스와 매우 흡사하다고 가정하면, 새로운 클래스를 추가하는 쉬운 방법 중에 하나는 상속이다. 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법이다. 상속이 유용한 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. 

Movie가 DiscountPolicy에게 메시지를 전달할 수 있는 이유는 DiscountPolicy를 상속하는 AmountDiscountPolicy와 PercentDiscountPolicy에 동일한 인터페이스가 있음을 보장하기 때문이다. 다시 말해 Movie는 협력 객체가 금액 계산 메시지를 이해할 수만 있다면 그 객체가 어떤 클래스의 인스턴스인지는 상관하지 않는다는 것이다. 

이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 부른다. 

다형성

아까 메시지와 메서드가 다르다고 했었다. 여기서 그 이유를 말해보자. Movie가 calculateDiscountAmount 라는 메시지를 전송하면, 실행되는 메서드는 무엇인가? DiscountPolicy 자리에 AmountDiscountPolicy가 오는지, PercentDiscountPolicy 가 오는지에 따라 다르다. 다시 말해 Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이느냐에 따라 달라진다. 이것이 다형성이다.

이를 구현하기 위해서 실행될 메서드를 결정하는 시점이 중요한데, 컴파일 시점에 결정하는지 아니면 실행 시점에 결정하는지에 따라서 나뉜다. 

  • 지연 바인딩, 동적 바인딩(Lazy Binding, Dynamic Binding)
    • 메시지와 메서드를 실행 시점에 바인딩
  • 초기 바인딩, 정적 바인딩(Early Binding, Static Binding)
    • 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것

다형성은 지연 바인딩을 사용하기에 가능하다. 

구현 상속과 인터페이스 상속

JAVA 입장에서 서술된 책으로부터 Swift 입장으로 해석하자면, 

  • 인터페이스 상속: Protocol 를 상속받음
  • 구현 상속: Protocol + Extension 을 상속받음

추상화와 유연성

일반적인 개념에서 인터페이스에 초점을 맞춘다면 추상화라고 할 수 있다. 서브 타입들이 수신할 수 있는 메시지만을 정의하고 구현은 모두 자식 클래스가 결정할 수 있도록 결정권을 위임한다. 이는 두가지 장점으로 이어진다. 

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

P.65, 추상화는 일반적인 개념에서 표현한다.

추상화를 이용해 상위 정책을 기술한 다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다. 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문이다. 

유연한 설계의 예시 - 할인 정책이 없는 영화는 어쩌지?

할인 정책이 없다면, Movie 인스턴스에서 fee를 그대로 적용해왔다. 이는 할인 정책에 관한 결정을 DiscountPolicy에게 일임한다는 원칙에 위배된다. 할인 금액이 0원이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie 쪽에 있기 때문이다. 0원 할인 정책의 책임을 DiscountPolicy에게 일임 시켜보자.

방법은 간단하다. DiscountPolicy를 상속받는 NoneDiscountPolicy 클래스를 추가하는 것이다. 

p.68, 책임 전가

상속과 합성 -  무엇이 코드 재사용에 더 좋은 방식인가요?

상속은 위에서 살펴보았으니 협상에 대해 간단히 이야기하자면,
협상은 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법이다. 이것을 내가 이해하는 방법은 클래스 내부에 변수로서 다른 인스턴스를 보유하고, 변수에 정의된 인터페이스만을 통해서 코드를 재사용하는 방법이다. 

협상은 상속이 가지는 문제점인 캡슐화 위배와 의존성 약화를 모두 해결한다. 코드의 재사용을 위해서는 상속보다는 합성을 선호하는 것이 더 좋은 방법이다. 다만 협상만을 사용하고, 상속은 쓰지 말라는 것은 아니다. Movie는 DiscountPolicy는 합성관계로 연결되어 있지만, DiscountPolicy와 AmountDiscountPolicy, PercentDiscountPolicy는 상속 관계로 연결돼 있다. 결론적으로 합성을 선호하되 다형성을 위해 인터페이스를 재사용하는 경우에는 상속도 사용야하는 경우가 있다. 

결론

객체지향을 이해하기 위해서 객체들이 어떻게 동작하고, 어떤 관계를 맺는가에 대한 용어를 살펴보았다. 

  1. 인터페이스와 구현의 분리는 접근 제어와 캡슐화로 이룰 수 있다. 이는 객체의 자주성을 높이는 결과를 낳는다.
  2. 코드의 재사용하는 방법을 알아보기 위해 상속, 인터페이스, 다형성, 추상화, 합성등을 알아보았다.

책에 나온 내용을 많이 간추리고, 내가 생각하는 결론만을 말하다 보니 많이 날리기는 했는데, 책이 전하는 즐거움을 충분히 전하지 못한 듯하여 아쉬움이 남는다. 더 풍부한 감정을 느끼기 위해 책을 직접보시길 추천한다.
정말 읽으면서 감탄을 많이 한 책이다. 특히 프로그래밍 학습 극초반에 무슨 말인지 하나도 모르면서 읽다가 포기했는데 지금 읽으니까 더 재밌다. 

'책 리뷰 > 오브젝트' 카테고리의 다른 글

3. 역할, 책임, 협력  (0) 2023.01.04
1. 객체, 설계  (1) 2022.11.28
Comments