루비로 배우는 객체지향 디자인 - 4장

Created
Jul 6, 2020 12:47 PM
Tags
루비로 배우는 객체지향 디자인 - 3장
💡
이번 편은 코드는 없고 UML 만 예제 도표로 있다보니 가능하면 내용 정리 뿐 아니라 직접 본문을 읽어보는 것을 추천

유연한 인터페이스 만들기

  • 애플리케이션은 클래스로 구성되어있지만 메세지를 통해 정의된다.
    • 소스 코드 저장소에 무엇이 들어갈지를 결정하는 것은 클래스지만, 애플리케이션의 움직임을 반영하는 것은 메세지이며 이 메세지가 애플리케이션을 살아 움직이게 한다.
    • 따라서 디자인을 할 때 객체 사이를 가로지르는 메세지에 관심을 두어야 한다.
    • 객체 사이의 소통은 인터페이스를 통해 이루저인다.
notion imagenotion image
  • 그림 4.1
    • 왼쪽 애플리케이션의 객체는 재사용하기 어렵다. 자기 자신을 너무 많이 드러내고 주변 객체에 대해 너무 많이 알고 있다.
    • 오른쪽은 조립 가능하게 구성되어 있다. 각 객체는 자기 자신을 최소한으로 드러내며 다른 객체에 대해 가능한 한 조금 알고 있다.
    • 핵심 문제는 각 클래스가 무엇을 하는지에 있는 것이 아니라 무엇을 드러내는지(퍼블릭 인터페이스)에 있다.
  • 레스토랑에서 손님이 메뉴를 보고 음식을 주문할 때 겉으로는 "음식을 시킨다 음식이 나온다" 로 정의할 수 있지만, 그 안에 음식을 조리하는 부엌에서는 엄청나게 많은 일들이 일어난다. 하지만 손님에게는 보이지 않는다.
    • 식당에는 손님이 사용할 수 있는 퍼블릭 인터페이스, 즉 메뉴판이 있다.
    • 부엌 안에서는 수 많은 메세지가 오가고 있지만 이 메세지는 private 하기 때문에 손님에게 보이지 않는다. 음식 시켰다고 손님이 직접 부엌에 들어가진 않으니까.
  • 퍼블릭 인터페이스
    • 클래스의 핵심 책임을 드러낸다.
    • 다른 객체에 의해 호출될 수 있다.
    • 쉽게 변경되지 않는다.
    • 다른 객체가 안정적으로 의존할 수 있다.
    • 테스트를 통해 꼼꼼하게 문서화되어 있다.
    • 즉, 클래스의 책임을 명시해주는 계약서이다.
  • 프라이빗 인터페이스
    • 세부적인 구현을 담당한다.
    • 다른 객체에 의해 호출되지 않는다.
    • 필요에 따라 언제든 변경할 수 있다.
    • 다른 객체가 의존하기에는 위험하다.
    • 테스트에서 다루지 않을 수도 있다.
  • 새로운 예시: 자전거 여행 회사
    • 자전거 여행을 주선하며 로드바이크 여행과 마운틴 바이크 여행을 다룬다. 각 여행길은 여행객이 방문할 수 있는 횟수가 제한되어 있고, 정해진 수의 가이드 겸 정비공이 필요하다. 등등
    • 명세를 정리하면
      • 여행객은
      • 여행길을 선택하기 위해서
      • 정해진 날짜에
      • 자신에게 맞는 난이도의
      • 자전거를 빌릴 수 있는
      • 여행길 목록을 보고싶어 한다.
  • 의도를 구성하기
    • 테스트를 먼저 작성한다? 이게 가능한 사람은 디자인 경험이 풍부하기 떄문이다. 이런 수준의 고수들이라면 객체들이 무엇을 할 수 있는지, 객체들이 전체 애플리케이션 속에서 어떻게 소통해야 하는지에 대한 이미지를 이미 떠올리고 있다. 어떤 "의도" 를 가지고 애플리케이션을 만들지 구상해놓고 있기 때문에 첫 번째 테스트를 짤 수 있는 것이다.
    • Customer, Trip, Route, Bike, Mechanic 등의 클래스명이 떠올랐다. 이 클래스를 떠올릴 수 있던 이유는 애플리케이션 속의 명사들, 즉 정보(data)와 행동(behavior)을 가지고 있는 명사를 표현하기 떄문이다. 이를 도메인 객체라 부르자.
    • 도메인 객체들은 매우 명시적이다. 지속적이고 큼지막하고 눈에 보이는 현실 세계의 실재들을 지칭하고, 결국 데이터베이스에서도 표현되기 떄문이다.
    • 디자인 전문가들은 도메인 객체에 집중하지 않으면서 이 객체들을 인지한다. 이들은 도메인 객체가 아니라 도메인 객체들이 주고받는 메세지에 주목한다.
  • 시퀀스 다이어그램 사용
    • 객체들의 배치와 메세지 전송 전략에 대해 검토해 볼 수 있는 간단한 방법 제공
    • 규칙에 얽매이지 말고, 편한대로 다이어그램을 화이트보드에 그리고, 필요한 만큼 수정하고, 원하는 것을 얻었다면 지워버리자.
    • (이후는 시퀀스 다이어그램을 끼고 설명하는 것들이 많아서 많이 생략하고 핵심적인 내용만 발췌)
  • 시퀀스 다이어그램을 그리면서 메세지를 중심으로 디자인을 이야기할 수 있게 되었다. 클래스를 결정하고 그 클래스의 책임을 찾아 나서는 대신 메세지를 결정하고 이 메세지를 누구에게 전송할 지 찾아보게 되었다.
  • 송신자가 원하는 것을 요구하는 메세지와 수신자가 어떻게 행동해야 하는지 알려주는 메세지의 차이를 구분하는 것은 매우 중요하다. 둘 사이의 차이를 이해하는 것이 잘 정의된 퍼블릭 인터페이스를 가지고 재사용이 가능한 클래스를 만들기 위한 핵심 포인트이다.
notion imagenotion image
notion imagenotion image
  • 그림 4.5 에서 그림 4.6 으로 가면서 Mechanic 의 세세한 동작을 다 알고 있던 Trip 이 그 책임을 Mechanic 에게 넘겨주었다.
    • 덕분에 Mechanic 에 포함된 퍼블릭 인터페이스의 양이 눈에 띄게 줄었다. 변화에 영향을 받는 객체들도 줄어들었다는 이야기가 된다.
  • 객체에게 주어진 맥락에서 독립시키기
    • 객체에게 필요한 맥락은 객체의 재사용성에 바로 영향을 미친다. 단순한 맥락일 수록 재사용하기 쉽고 테스트하기도 쉽다.
    • 복잡한 맥락 속에 위치한 객체는 사용하기도 어렵고 테스트하기도 어렵다. 이 객체들이 무엇을 하려고 하던지 복잡한 설정을 먼저 처리해야 한다.
    • 가능한 최고의 상황은 객체가 자신의 맥락로부터 완전히 독립되어 있는 것이다. 다른 객체의 존재를 전혀 모른채로 협업할 수 있는 객체는 기대하지 않았던 방식으로 참신하게 재사용될 수 있다.
  • 더 나은 인터페이스를 만들기 위해서는 일단 인터페이스가 있어야 한다. 잘 정의된 인터페이스가 있따는 것이 중요한 것이지, 인터페이스가 완벽해야 한다는 것이 중요한 것이 아니다.
  • 인터페이스에 대해 고민해 보자. 명확한 의도를 가지고 구현해 보자. 우리의 애플리케이션의 특징을 드러내고 그 미래를 결정하는 것은 바로 인터페이스이다. 테스트보다, 다른 그 어떤 코드보다도 인터페이스가 중요하다.
  • 퍼블릭 인터페이스의 메서드는 다음과 같아야 한다.
    • 일반적인 의미, 엄밀하고 명시적으로 규정되어 있어야 한다.
    • '어떻게' 보다는 '어떤' 것에 대해 말해야 한다.
    • 예측할 수 있는 한도에서나마 바뀌지 않을 이름을 지어야 한다.
    • 추가적인 인자는 순서와 관계없이 받을 수 있도록 하라. (루비라면 해시, JS라면 객체 리터럴)
  • 프라이빗 메서드 테스트는 작성하지 않거나, 꼭 만들어야 한담녀 퍼블릭 메서드 테스트와는 분리해 놓는 편이 좋다. 다른 프로그래머가 테스트를 보고 본의 아니게 언제 바뀔 지 모르는 프라이빗 메서드에 의존하지 않도록 해야 한다.
  • public, protected, private 등의 키워드를 명시적으로 사용하는게 만능은 아니지만
    • 오늘의 내가 미래의 프로그래머보다 더 나은 정보를 알고 있다고 믿고
    • 지금 불안정한 메서드라고 생각하는 것을 미래의 프로그래머가 실수로 사용하지 않도록 만들어야 한다고 생각하여
    • 메서드의 안정성에 대한 정보를 전달해 주었다면 미래에 대한 최소의 의무는 다 한 것이다.
  • 다른 클래스와 협업할 때는 그 클래스의 퍼블릭 인터페이스만 사용하도록 노력해야 한다.
  • 우리의 코드가 프라이빗 인터페이스에 의존하고 있다면 변화에 영향을 받을 위험이 증가한다. 어쩔 수 없이 사용하더라도 이 의존성을 고립시켜 위험을 최소화해야 한다.
  • 데메테르의 원칙
    • 메서드가 메세지를 전송하지 말 것
    • 메세지를 전달받은 객체가 바로 이어 다른 타입의 객체에게 메세지를 전달하지 말 것
    • "바로 옆의 이웃하고만 소통하라" or "점을 하나만 사용하라" (메서드 체이닝 함부로 하지 마라) 같은 표현
cutomer.bicycle.wheel.tire // (X) customer.bicycle.wheel.rotate() // (X) Object.keys(someObj).sort().join(',') // (?)
  • 예시에서 위의 두 개는 먼 곳에 있는 속성(tire) 혹은 행동(rotate)을 가져온다는 호출하고 있다. 이런 접근은 접근한 속성의 값을 바꾸지 않는다는 전제 아래에서 허용될 수 있지만, 만약 변경하려는 의도를 가지고 접근한다면 위험하다.
  • 세 번째 예제는 큰 문제가 없다. 점이 이어져 있지만 엄밀히 말하면 데메테르의 원칙을 위반하고 있는 것은 아니기 때문이다. Array(Iterable) -> Array(Iterable) -> String
  • 데메테르의 원칙 위반을 피하려면? 위임을 하는 방법도 있다. (자세한 설명은 없음)
    • 이 원칙의 본질은 "위임을 많이 사용하자" 가 아니다. 한 객체가 다른 객체를 어떻게 접근해야 할지 이미 알고 있는 상황에 대한 경계를 하라는 것이다.
    • 위의 자전거를 다루는 예시는 customer.ride 로 고칠 수도 있는 것이다.
    • 메세지 기반의 관점을 취하면서 새로운 메세지를 발견하고, 퍼블릭 인터페이스를 확립한다. 그리고 이 퍼블릭 인터페이스를 정의할 새로운 객체를 발견할 수 있다.
    • 반면 도메인 객체의 족쇄를 벗어버리지 못하면 이 객체들의 퍼블릭 인터페이스를 이용하여 기차놀이(메서드 체이닝)을 하게 될 것이다. 결과적으로 유연한 퍼블릭 인터페이스를 구성할 기회를 잃는다.
  • 다시 정리하자면
    • 메세지에 집중하여 예전에는 미처 파악하지 못했던 객체를 찾을 수 있다.
    • 메세지가 '수신자가 어떻게 행동해야 하는지' 를 알려주기보다 수신자를 믿고 전송자가 원하는 바를 말해주면서 자연스럽게 객체의 퍼블릭 인터페이스를 발전시킬 수 있다.
루비로 배우는 객체지향 디자인 - 5장