카테고리 없음

오브젝트[06 메시지와 인터페이스]

punch.pro 2023. 2. 12. 03:21

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr

책 목차:

1장 객체, 설계 : https://inhyeok-blog.tistory.com/32
2장 객체지향 프로그래밍 : https://inhyeok-blog.tistory.com/33
3장 역할, 책임, 협력 : https://inhyeok-blog.tistory.com/34
4장 설계 품질과 트레이드오프 : https://inhyeok-blog.tistory.com/36
5장 책임 할당하기 : https://inhyeok-blog.tistory.com/37
6장 메시지와 인터페이스 :
7장 객체 분해 :
8장 의존성 관리하기 :
9장 유연한 설계 :
10장 상속과 코드 재사용 :
11장 합성과 유연한 설계 :
12장 다형성 :
13장 서브클래싱과 서브타이핑 :
14장 일관성 있는 협력 :
15장 디자인 패턴과 프레임워크 :


훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 협력 안에서 객체가 수행하는 책임에 초첨을 맞춰야 한다. 이때 책임이 객체가 수신할 수 있는 메시지의 기반이 된다. 객체가 수신하는 메시지들은 객체의 퍼블릭 인터페이스를 구성한다. 그럼  책임주도 설계를 하면 훌륭한 인터페이스를 얻을 수 있을 것 처럼 보인다. 하지만 그렇지 않다. 좋은 인터페이스는 몇가지 설계 원칙과 기법을 익히고 적용해야 한다. 이런 원칙과 기법들을 살펴보는 것이 이번 장의 주제이다. 

01. 협력과 메시지

클라이언트-서버 모델

두 객체 사이의 협력을 설명하는 전통적인 메타포는 클라이언트-서버 모델이다. 클라이언트 객체는 메시지를 전송하고, 서버 객체는 메시지를 수신한다. 객체는 협력에 참여하는동안 클라이언트와 서버 모두를 수행하는 것이 일반적이다. 

위의 그림에서 Movie는 가격을 계산하라는 메시지를 받는 서버인 동시에, 할일 요금을 계산하라는 메시지를 보내는 클라이언트인 것이다.

객체가 활용하는 메시지는 크게 수신하는 메시지와 송신하는 메시지로 나뉜다. 보통 객체를 설계할 때 수신하는 메시지를 신경쓰지만, 송신하는 메시지 역시 중요하다.

여기서 요점은 객체가 협력하기 위해서 메시지를 활용한다는 것이다.

메시지와 메시지 전송

"메시지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단"

메시지 전송 or 메시지 패싱 : 객체가 다른 객체에게 도움을 요청하는 것

메시지 전송자 or 클라이언트 : 메시지를 전송하는 객체

메시지 수신자 or 서버 : 메시지를 수신하는 객체

 

메시지 : 오퍼레이션명 + 인자

메시지 전송 :  메시지 수신자 + 오퍼레이션명 + 인자

 

언어별 메시지 전송 표기법

메시지와 메서드

메서드 : 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저

 

객체가 다른 객체에게 메시지를 보내면 메시지 수신자의 실제 타입에 따라 다른 메서드가 실행될 수 있다. 메시지 전송과 메서드의 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 하므로서, 두 객체 사이의 결합도를 낮추고 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.

퍼블릭 인터페이스와 오퍼레이션

퍼블릭 인터페이스 : 객체가 의사소통을 위해 외부에 공재하는 메시지의 집합

오퍼레이션 : 퍼블릭 인터페이스에 포함된 메시지(수행 가능한 어떤 행동에 대한 추상화로, 메시지와 관련된 시그니처)

메서드 : 실제로 수행되는 코드

 

객체가 다른 객체에게 메시지를 전송하면 런타임 시스템은 메시지 전송을 오퍼레이션 호출로 해석하고 메시지 수신자의 실제 타입을 기반으로 적절한 메서드를 찾아 실행한다.

메시지, 오퍼레이션, 메서드 사이의 관계

시그니처

시그니쳐 : 오퍼레이션이나 메서드의 명세를 의미한다. 오퍼레이션명+파라미터목록(+반환값 : 몇몇 언어는 반환값을 포함)

 

객체가 수신할 수 있는 메시지는 퍼블릭 인터페이스와 그 안에 포함될 오퍼레이션이 결정한다. 퍼블릭 인터페이스는 객체의 품질을 결정하기 때문에 메시지가 객체의 품질을 결정한다.

02. 인터페이스와 설계 품질

3장에서 언급한바와 같이 좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건이 있다. 이는 꼭 필요한 오퍼레이션만 인터페이스에 포함하고, How보단 What을 표현하는 시그니쳐를 만들라는 것이다. 그런데 앞서 살펴본 책임 주도 설계는 이 두가지 조건을 만족시켜준다. 

비록 책임주도 설계가 훌륭한 인터페이스를 얻게 도와주지만, 퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법에 대해 알아보는 것은 중요하다.

  • 디미터 법칙
  • 묻지 말고 시켜라
  • 의도를 드러내는 인터페이스
  • 명령-쿼리 분리

디미터 법칙

디미터의 법칙은 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다. 쉽게말해서 "오직 하나의 도트만 사용하라"는 원칙이다. 예시를 들어보자.

클래스 C C의 내부에 있는 메서드 M이 있을 때 M에서 객체에게 보낼 수 있는 메시지는 다음과 같은 클래스의 인스턴스여야 한다.

(M에 의해 생성된 객체나, M이 호출하는 메서드에 의해 생성된 객체, 전역변수로 선언된 객체는 모두 M의 인자로 취급)

  • M의 인자로 전달된 클래스(C자체를 포함)
  • C의 인스턴스 변수의 클래스

디미터의 법칙을 따르면 부끄럼타는 코드(shy code)를 작성할 수 있다. 불필요한 어떤 것도 다를 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드를 작성할 수 있다는 것이다.

메시지를 통해 반환받은 객체에 연쇄적으로 메시지를 전송하는 형태의 코드를 기차 충돌이라고 부르는데, 이는 메시지 전송자가 메시지 수신자의 내부 정보를 자세히 알게 된다. 따라서 메시지 수신자의 캡슐화는 무너지고, 메시지 전송자는 수신자의 내부 구현에 강하게 결합된다.

묻지 말고 시켜라

객체의 상태를 물어보는 행위는 알게된 다른 객체의 상태를 기반으로 의사결정을 한다는 것이다. 당연하게도 객체의 외부에서 해당 객체의 상태를 기반으로 결정을 내리는 것은 객체의 캡슐화를 위반한다. 객체에게 상태를 물어보지 말고, 객체가 스스로 상태를 기반으로 의사결정을 하도록 시켜라.

 

"절차적인 코드는 정보를 얻을 후에 결정한다. 객체지향 코드는 객체에게 그것을 하도록 시킨다."

 

묻지 말고 시켜라 원칙에 따르면 자연스럽게 정보전문가에게 책임을 할당하게 되고, 높은 응집도를 얻게 된다. 개발을 하면서 내부의 상태를 묻는 오퍼레이션을 인터페이스에 포함하고 있거나, 내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 있다면 객체의 책임이 누수된 것이다. 

의도를 드러내는 인터페이스

메소드의 이름을 짓는 방법은 두가지가 있다. 내부 구현을 드러내는 방법과 협력 관계를 드러내는 구현이다. 캔트 백은 어떻게 하느냐가 아니라 무엇을 하느냐에 따라 메서드 이름을 짓는 패턴을 의도를 드러내는 선택자라고 부르고, 이 방식을 권장한다. 내부 구현을 드러내면 2가지의 대표적인 단점이 존재한다.

1. 메서드의 내부 구현을 정확히 이해하지 못하면 두 메서드가 같은 역할을 수행한다는 사실을 알지 못한다. 즉, 메소드에 대해 제대로 커뮤니케이션 하지 못한다.

2. 메서드 수준에서 캡슐화를 위반한다. 구현을 드러낸 메서드를 변현하려면 단순히 참조하는 객체를 변경하는 것뿐만 아니라 호출하는 메서드를 변경해야 하는 것이다.

사실 가장 불편한 점은 같은 역할을 가진 두 메서드가 대체되지 못한다는 것이다. (다형성을 성공적으로 구현할 수 없음)

에릭 에반스는 켄트 백의 의도를 드러내는 선택자를 인터페이스 레벨로 확장한 의도를 드러내는 인터페이스를 제시한다. 의도를 드러내는 인터페이스란 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에서 협력과 관련된 의도만을 표현해야 한다는 것이다.

03. 원칙의 함정

앞서 말한 원칙들은 절대적인 법칙이 아니다. 결국 설계는 트레이드오프이기 때문에 상황에 따라서 유연하게 원칙을 적용해야 한다. 어떤 상황에서 어떤 원칙을 고수할지 잘 판단하는 것이 바로 전문가의 역량인 것이다.

디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다

JAVA 8에서 사용하는 intstream은 디미터 법칙을 위반하고 있는가? 아니다. intstream은 메서드가 자기자신과 동일한 클래스의 인스턴스를 반환한다. 디미터의 법칙은 결합도를 낮추기 위한 목적이다. 따라서 내부 구현을 드러내지 않는 기차충돌코드(그렇게 보이지만 실제로는 그렇지 않다.) 는 여기에 존재하지 않는다. 맹목적으로 원칙만을 강조하지 말자.

결합도와 응집도의 충돌

기차코드(도트가 계속 반복되는 코드)는 일반적으로 결합도를 높힌다. 하지만 결합도를 높히는 행위가 응집도를 낮추는 행위가 될 수도 있다. 예를 들어서 영화관에서 특정 영화의 수익률을 계산하는 로직이 있다고 하자. 그럼 이 로직은 분명 '영화'라는 객체에서 많은 정보를 가져올 것이다. 예컨데 가격, 상영시간, 비용 등을 고려할 수 있을 것이다. 그럼 우린 앞서 배운 디미터 법칙에 따라서 이 행위를 '영화'라는 객체에 위임해야 하는 것일까? 생각해보면 영화의 수익률을 계산하는 로직은 영화관에게 주어진 책임이다. 영화관의 장비 감가상각비, 임대료, 프로모션 비용 등이 수익률을 결정하기 때문이다. 따라서 수익률을 계산하는 로직이 영화로 옮겨가면 결합도는 낮아질 수 있지만, 응집도도 함께 낮아지는 것이다.

응집도가 낮아야 하는 이유는 객체의 변경 포인트가 하나여야 하기 때문이다. 이런 목적을 달성하기 위해서는 결합도가 조금 높아지는 희생을 감수해야 하기도 한다.

클린 코드에서 디미터 법칙의 위반 여부는 묻는 대상이 객체인지, 자료 구조인지에 달려있다고 설명한다. 객체는 내부 구조를 숨겨야 하므로 디미터의 법칙을 따르는 것이 좋지만 자료 구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없는  것이다.

 

다시한번 강조한다. 소프트웨어 설계에 법칙이란 조네재하지 않으니, 원칙을 맹신하지 마라. 

04. 명령-쿼리 분리 원칙

우선 명령과 쿼리가 무엇인지 명확하게 구분할 필요가 있다.

  • 명령(프로시저) : 부수효과를 발생시킬 수 있지만, 값을 반환할 수 없다.
  • 쿼리(함수) : 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.

이 두가지 요소를 분리해서 만든 인터페이스를 명령-쿼리 인터페이스라고 부른다. 그럼 명령과 쿼리를 분리해서 얻는 장점은 무엇일까?

반복 일정의 명령과 쿼리 분리하기

이 챕터에선 명령과 쿼리를 분리하지 않아서 버그를 만난 사례를 소개한다. 캘린더를 만들드는데 어떤 이벤트가 반복되는 일정(생일 등)인지를 판단하는 메서드가 있었다. 이 메서드는 Meeting.isSatisfied(Schedule schedule) 이었는데 이 메서드가 이벤트가 반복되는 일정에 포함되는지 확인하고, 포함되지 않는다면 이벤트 날자를 일정 날자로 변경하는 것이었다. 이는 쿼리인줄 알았던 메서드가 부수효과를 발생시키는 것이었고, 코드의 실행결과를 예측할 수 없게 만들었다. 부수효과를 발생시키는 쿼리는 버그를 양산한다. 따라서 이를 분리해서 isSatisfied(Schedule schdeule), reschedule(Schedule schdule) 인터페이스로 나눴고 결과적으로 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해 졌다.

명령-쿼리 분리와 참조 투명성

여기선 3가지 개념을 설명해야 한다. 바로 참조투명성과, 부수효과, 불변성이다.

  • 부수효과 : 상태의 변경이 일어나는 것
  • 참조 투명성 : 어떤 표현식(수학에서 f(x), 프로그래밍에서 isSatisfied())이 있을 때 표현식의 결과 값(프로그래밍에서 리턴값)으로 표현식이 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성
  • 불변성 : 표현식에 같은 인자를 넣으면 항상 같은 결과값이 나온다.

참조 투명성을 만족하는 식은 우리에게 두가지 장점을 제공한다.

  • 모든 함수를 이미 알고 있는 하나의 결괏값으로 대체할 수 있기 때문에 식을 쉽게 계산할 수 있다.
  • 모든 곳에서 함수의 결괏값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.

책임에 초점을 맞춰라

앞서말한 모든 방식을 효과적으로 지키는 아주 쉬운방법이 있다.

메시지를 먼저 선택하고 그 후에 메시지를 처리할 객체를 선택하라.

  • 디미터 법칙: 협력이라는 컨텍스트 안에서 객체보다 메시지를 먼저 결정하면 객체의 내부 구조에 대해 알지 못한 상태로 메시지를 선택하기 때문에 디미터 법칙을 위반할 위험을 최소화할 수 있다.
  • 묻지 말고 시켜라: 메시지를 먼저 선택하면 클라이언트 관점에서 메시지를 선택하기 때문에 필요한 정보를 물을 필요 없이 원하는 것을 표현한 메시지를 전송하면 된다.
  • 의도를 드러내는 인터페이스 : 메시지를 먼저 선택한다는 것은 클라이언트가 무엇을 원하는지, 그 의도가 분명하게 드러날 수 밖에 없다.
  • 명령-쿼리 분리 원칙: 메시지를 먼저 선택하는 것은 협력이라는 문맥 안에서 객체의 인터페이스에 관해 고민한다는 것을 의미한다. 객체가 단순히 어떤 일을 해야 하는지뿐만 아니라 협력 속에서 객체의 상태를 예측하고 이해하기 쉽게 만들기 위한 방법에 관해 고민하게 된다. 따라서 예측 가능한 협력을 만들기 위해 명령과 쿼리를 분리하게 될 것이다.

즉, 책임 주도 설계 원칙을 따르면 된다!