본문 바로가기

카테고리 없음

6장 메세지와 인터페이스

반응형

훌륭한 객체지향 코드를 얻기 위해서는 구체적인 클래스(코드)가 아니라 객체를 지향해야 한다.

협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다.

책임이 객체가 수신할 수 있는 메세지의 기반이 된다.

 

객체가 수신하는 메세지들이 객체의 퍼블릭 인터페이스를 구성한다.

유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는데 도움이 되는 설계 원칙과 기법을 익히고 적용하자.

협력과 메세지

클라이언트-서버 모델

클라이언트-서버 모델은 두 객체 사이 협력 관계를 설명하기 위해 사용되는 전형적인 메타포다.

협력 안에서 메세지를 전송하는 객체를 클라이언트

메세지를 수신하는 객체를 서버라고 부른다.

협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.

메세지는 객체 사이의협력을 가능하게 하는 매개체다
서버는 다른 협력에서 클라이언트가 될 수 있다
협력에 적합한 객체설계는 외부에 전송하는 메세지의 집합도 함께 고려한다.

메세지와 메세지 전송

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

도움 요청은 메세지 전송, 메세지 패싱이라 부르고 메세지를 전송하는 객체를 메세지 전송자라고 부른다.

메세지를 수신하는 객체를 메시지 수신자라고 부른다.

 

메세지는 오퍼레이션명과 인자로 구성된다.

메세지 전송은 여기에 메세지 수신자를 추가한다.

따라서 메세지 전송은 메시지수신자.오퍼레이션명(인자) 형태다.

언어별 메세지 전송 표기법

메시지와 메서드

메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자의 실제 타입에 따라 달라진다.

(인터페이스와 실체화 클래스의 차이)

메세지를 수신했을 때 실제로 실행되는 함수 또는 프로시저를 메서드라고 부른다.

동일변수(condition)에 동일 메세지를 전달하더라도 객체 타입에 따라 실행되는 메서드가 달라질 수 있다.

즉, 컴파일 시점과 실행 시점의 의미가 달라질 수 있다. 메세지와 메서드가 실행 시점에 연결된다.

켄트백의 말

메서드와 메세지의 구분은 메세지 전송자와 메세지 수신자가 느슨하게 결합할 수 있게 한다.

전송자는 어떤 메세지를 전송해야 하는지만 알면 된다, 수신자가 어떤 클래스의 인스턴스인지와 처리 방법을 몰라도 되기 때문이다.

메세지 수신자 또한 누가 보냈는지 알 필요가 없다. 자신의 방법(메소드)으로 필요한 메소드를 스스로 결정해 자율적으로 처리한다.

즉, 서로는 인터페이스만 알면 되고 서로가 누구인지 알 필요는 없다.

 

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

  • 퍼블릭 인터페이스 : 객체가 의사소통을 위해 외부로 공개하는 메세지의 집합
    • 오퍼레이션 : 퍼블릭 인터페이스에 포함된 메세지
      • 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화
      • 메시지와 관련된 시그니처를 가리킴 (함수명과 인자)
  • 메서드 : 메시지 수신 시 실제로 실행되는 코드(실제 구현)

오퍼레이션이란 실행하기 위해 객체가 호출될수 있는 변환이나 정의에 관한 명세다.

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

퍼블릭 인터페이스와 메세지 관점에서는 "오퍼레이션 호출"이 메서드 호출보다 적절하다.

시그니처

시그니처(signiture)는 오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합친 명칭이다.

오퍼레이션은 코드가 아닌 시그니처만을 정의한 것이다.

메서드는 시그니처에 구현을 더한 것이다.

메세지를 수신하면 오퍼레이션의 시그니처와 동일한 메서드가 실행된다.

오퍼레이션 관점에서 다형성이란 동일한 오퍼레이션 호출에 대해 서로 다른 메서드들이 실행되는 것이라 정의할 수 있다.

 

메시지가 퍼블릭 인터페이스(오퍼레이션 집합)을 결정하므로 메세지가 객체의 품질을 결정한다.

인터페이스와 설계 품질

좋은 인터페이스는 최소한의 인터페이스이며, 추상적인 인터페이스다.

꼭 필요한 오퍼레이션만을 인터페이스에 포함한다.

추상적인 인터페이스는 무엇을 하는지를 표현한다.

최소주의를 따르면서도 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것이다.

퍼블릭 인터페이스의 품질에 영향을 미치는 원칙

디미터 원칙

  • 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라.
  • 오직 인접한 이웃하고만 말하라(오직 하나의 도트만 사용하라)

 

디미터 법칙을 따르기 위해서는 클래스가 특정한 조건을 만족하는 대상에게만 메세지를 전송하도록 한다.

모든 클래스 C와 C에 구현된 모든 메소드 M에 대해, M이 메세지를 전송할 수 있는 객체는 다음에 서술된 클래스의 인스턴스여야 한다.

M에 의해 생성된 객체나, M이 호출하는 메서드에 의해 생성된 객체, 전역변수는 모두 M의 인자로 간주한다.

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

다시 정리하면

  • this
  • 메서드 매개변수
  • this속성
  • this속성 컬렉션 내부 요소
  • 메서드 내에서 생성된 지역 객체

디미터 법칙과 캡슐화

디미터 법칙은 객체가 자기 자신을 책임지는 자율적인 존재여야 한다는 사실을 강조한다.

정보를 처리하는데 필요한 책임을 정보를 알고 있는 객체에게 할당하기 때문에 응집도가 높은 객체가 만들어진다.

 

묻지 말고 시켜라

객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다.

이러한 스타일의 메세지를 작성하라.

어떻게 작업하는지를 노출해서는 안된다.

객체가 무엇을 하는지를 서술해야 한다.

상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체하라.

매 순간 디미터와 묻지 말고 시켜라를 생각하자. 밀접하게 연관된 정보와 행동을 함께 가지는 응집도 높은 객체를 만들 수 있다.

일단 시키고 결정하자
객체는 자신의 내부 보유 정보나 메세지 전송 결과로 얻는 정보만 사용해서 의사결정을 내리게 된다.

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

how를 기준으로 메세지를 명명하면, 내부 구현이 드러난다.

어떻게가 아니라 무엇을 하는지를 드러내라.

isSatisfiedByPeriod => isSatisfiedBy

클라이언트가 동일 작업 수행 메서드를 하나의 타입 계층으로 묶을 수 있는 가능성이 커져 유연한 협력을 얻을 수 있다.

즉, 의도를 드러내는 선택자(Intention Revealing Selector(Interfase)를 얻을 수 있다.

에릭 에반스 : 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현해야 한다.
묻지 말고 시켜라, how가 아닌 클라이언트의 의도를 드러내라

리팩터링

자신의 데이터를 자신이 책임지도록 한다.

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);
        }
    }
}

디미터 법칙에 따르면 Theater가 인자인 audience, 내부인자 ticketSeller에게 메세지 전송하는 것은 문제없음

 TicketSeller 티켓을 가져와 audience내부 bag에 setTicket을 하고 있음.

1. 판매를 TicketSeller의 책임으로 옮기자. (ticketseller.sellTo) 지역변수를 없앨 수 있다.

2. 구매를 Audience의 책임으로 옮기자. getBag할 필요가 없어진다.

3. ticket을 받아 처리하는 책임을 가방에게 넘긴다. hold

오퍼레이션의 이름은 협력이라느 문맥을 반영해야 한다.

 

 

자신의 상태를 스스로 관리하고 결정하는 자율적인 존재로 만든다!

 

원칙의 함정

원칙은 예외가 넘쳐난다. 법칙에는 예외가 없다.

설계는 트레이드오프의 산물이다.

원칙이 현 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시한다.

 

원칙에 대한 오해

디미터 법칙은 하나의 도트를 강제하는 법칙이 아니라, 오직 인접한 이웃이랑만 말하라는 것이다.

필드 객체의 내부 구조가 외부로 노출되는 경우가 아니면 위반이 아니다.

해당 코드에서 메서드들은 동일한 클래스의 인스턴스를 반환하므로, 디미터 원칙을 위반한게 아니다. 또한, 컬랙션 내부 요소들과의 소통도 위반이 아니다.

아래 코드는 Screening의 내부 상태를 가져와 사용하고 있다.

이를 막으려면 할인 여부를 screening에서 판단하고, PeriodCondition에서는 메세지를 전송해야 한다.

하지만, 할인 조건 판단이 Screening의 책임인가?

묻지 않고 계산이 가능한가?

로버트 마틴은 클린 코드에서 객체가 자료 구조 역할이면 디미터 법칙을 적용할 필요가 없다고 했다.

항상 시킬수는 없다. 원칙을 맹신하지 마라.

명령 쿼리 분리 원칙

가끔은 필요에 따라 시키지 못하고 물어야 할 때가 있다.

  • 루틴 : 절차를 묶어 호출 가능하도록 이름을 부여한 것
    • 프로시저 : 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
    • 함수 : 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.

명령은 프로시저다.

쿼리는 함수다.

명령 -쿼리 분리 원칙의 요지는 오퍼레이션은 명령(프로시저)나 쿼리(함수)여야 한다는 것이다. (배타적)

명령 : 객체의 상태를 변경한다. 반환값을 가질 수 없다.

함수 : 객체의 정보를 반환한다. 상태를 변경할 수 없다.

질문이 답변을 수정하면 안된다.

명령 쿼리 분리 원칙이 좋은 이유

메서드의 부수효과 여부(상태 변경)을 쉽게 찾을 수 있다.

즉, 유지보수 및 디버깅이 수월해진다.

반환값의 유무로 부수효과 유무를 추측할 수 있다

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

컴퓨터 세계와 수학의 세계의 차이는 부수효과다

대입문(프로시저)과 함수

수학은 x초기화 후 값 변경이 불가능. 멱등성

참조 투명성 : 어떤 표현식 e가 있을 때, e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성

수학은 동일 입력 동일 결과이므로 참조 투명성을 만족시키는 이상적인 예다.

어떤 값이 변하지 않는 성질을 불변성(immutability)이라 부른다.

즉 불변은 부수효과가 발생하지 않는다는 말과 동일하다.

 

함수는 값을 변경하지 않아 부수효과가 없다.

부수효과가 없는 세상에서는 모든 로직이 참조투명성을 만족시킨다.

식의 순서를 바꿔도 결과가 유지된다.

참조 투명성의 장점

모든 함수를 이미 알고 있는 하나의 결과값으로 대체할 수 있기 때문에 식을 쉽게 계산할 수 있다.

모든 곳에서 함수의 결과값이 동일하기 때문에 식의 순서를 변경하더라도 각 식의 결과는 달라지지 않는다.

 

객체지향 패러다임은 객체의 상태 변경이라는 부수효과를 기반으로 한다.

참조 투명성은 예외에 가깝다.

하지만 명령 쿼리 원칙을 사용하면 이 균열을 조금이나마 줄일 수 있다.

명령을 호출하기 이전 쿼리의 결과는 항상 똑같다.

책임에 초점을 맞춰라

의도를 드러내는 좋은 인터페이스를 설계하는 가장 쉬운 방법은 책임 주도 설계에 따르는 것이다.

도메인 개념을 설계하고, 메세지를 먼저 선택하고, 정보 전문가 객체를 찾아 책임을 할당한다.

적합한 객체가 아니라 협력에 적합한 메세지가 중요하다.

긍정적인 영향 보기

디미터 법칙 : 수신 객체를 모르는 상태에서 메세지가 객체를 선택하게 하여 객체 내부 구조에 대한 고민이 필요없어진다. 즉 객체 사이 구조적 결합도를 낮출 수 있다.

묻지 말고 시켜라 : 메세지를 먼저 선택하면 필요한 정보가 아닌 원하는 행동을 시키는 메세지를 전송할 수 있다.

의도를 드러내는 인터페이스 : 메세지 전송은 클라이언트 관점이다. 클라이언트의 의도가 명확히 드러난다.

명령-쿼리 분리 원칙 : 메세지는 객체가 어떤 일을 해야 하는지를 명시하고, 협력 속에서 객체의 상태를 예측하고 이해하기 쉽게 만들어준다.

 

계약에 의한 설계 by 버드런트 마이어

오퍼레이션의 시그니처는 오퍼레이션의 이름과 인자와 반환값의 타입만 명시할 수 있다.

사전조건, 사후조건, 불변식은 Design By Contract을 통해 제약을 인터페이스에 명시할 수 있다.

계약에 의한 설계는 협력을 위해 클라이언트와 서버가 준수해야 하는 제약을 코드 상에 명시적으로 표현하고 강제할 수 있는 방법이다.

 

반응형