본문 바로가기

IT Book Summary/Object: 객체지향설계

Chapter 06 메시지와 인터페이스

객체지향 프로그래밍의 흔한 오해 - 어플리케이션이 클래스 집합으로 구성된다는 것.

 

클래스라는 구현도구에 집착하면 경직되고 유연하지 못한 설계에 이를 확률이 높아짐.

 

클래스가 아니라 객체를 지향해야한다.

 

애플리케이션은 클래스로 구성되지만 메시지를 통해 정의된다.

 

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

 

이번장의 주제는 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 원칙과 기법이다.

- 구현과 부수효과는 캡슐화 하고 높은 응집도와 낮은 결합도를 가진 인터페이스를 만들 수 있는 지침을 제공하는 원칙들

 


01. 협력과 메시지

 

 

클라이언트-서버 모델 (전통적인 메타포)

 

( 클라이언트 ) 객체는 메시지로 요청 -> 수신한 ( 서버 ) 객체는 요청을 처리한 후 응답

단방향 상호작용

 

ex) 무비는 클라이언트와 서버 역할을 동시에 수행한다.

 

협력의 관점에서 객체는 두가지 종류의 메시지 집합으로 구성

- 객체가 수신하는 메시지의 집합

- 외부의 객체에게 전송하는 메시지의 집합

 

 

메시지와 메시지 전송

 

먼저 협력에 관련된 다양한 용어의 의미와 차이점을 이해하자

 

메시지(message) - 객체들이 협력하기 위한 의사소통 수단

 

메시지 선동(message sending), 메시지 패싱(message passing) - 

 

메시지 전송자(message sender) - 메시지를 전송하는 객체

수신자(message receiver) - 메시지를 수신하는 객체

 

메시지는 오퍼레이션명 (operation name)과 인자(argument)로 구성된다

메시지 전송은 메시지에 수신자를 추가한것.

 

ex) isSatisfiedBy(screening) -> 메시지

     condition.isSatisfiedBy(screening) -> 메시지전송

 

 

메시지와 메서드

 

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

 

동일한 이름의 변수(condition)에게 동일한 메시지를 전송하더라도 객체 타입에 따라 실행되는 메서드가 달라질 수 있음.

 

객체는 메시지와 메서드라는 두 가지 서로다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행시점의 의미가 달라질 수 있음.

 

메시지를 전송한다고 해서 어떤 메서드가 실행되는지 알 수 없음.

 

메시지 수신 객체가 적절한 메서드를 선택해서 응답하는 것.

 

실행 시간에 수신자의 클래스에 기반해서 메시지를 메서드에 바인딩. 

 

전송자와 수신자가 느슨하게 결합하는것. 유연하고 확장 가능한 코드.

 

 

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

 

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

 

퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation)

- 수행가능한 어떤 행동에 대한 추상화

- 내부의 구현 코드는 제외하고 단순히 메시지와 관런된 시그니처를 가리키는 경우가 대부분(?)

 

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

- 실제 구현을 포함

- 오퍼레이션의 가능한 구현

 

UML 에서의 정의 

 

오퍼레이션 

  • 실행하기 위해 객체가 호출될 수 있는 변환이나 정의에 관한 명세
  • 인터페이스의 각 요소
  • 구현이 아닌 추상화

메서드

  • 오퍼레이션에 대한 구현
  • 오퍼레이션과 연관된 알고리즘 또는 절차를 명시

 

Client  -- 1.메시지 전송--> Operation (2.오퍼레이션 호출) -->Server  3.메서드실행

 

시그니처

 

오퍼레이션의 이름과 파라미터 목록을 합쳐 시그니처 (signiture) 라고 부른다

 

객체가 수신할 수 있는 메시지가 객체의 퍼블릭 인터페이스와 그 안에 포함될 오퍼레이션을 결정

 

객체의 퍼블릭 인터페이스객체의 품질을 결정

 

--> 메시지객체의 품질을 결정

 

 

 


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

 

좋은 인터페이스 ? 꼭 필요한 오퍼레이션만 (최소한), 어떻게 수행하는지가 아닌 무엇을 하는지를 표현 (추상적)

 

디미터 법칙 (Law of Demeter)

객체 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라

only talk to your immediate neighbors

 

클래스 내부의 메서드가 아래 조건을 만족하는 인스턴스에만 메시지 전송

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

디미터 코드 위반!! 기차충돌(train wreck)

screening.getMovie().getDiscountConditions(); 

- 메시지 전송자가 수신자 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지 전송하는 나쁜 코드

 

screening.calulateFee(audienceCount);

- 수신자 내부 구조에 관해 묻지않고 원하는것을 단순하게 요청

 

 

묻지말고 시켜라(Tell, Don't Ask)

 

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

 

외부에서 해당 객에의 상태를 기반드로 결정을 내리는 것은 캡슐화를 위반.

 

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

 

이 원칙에 따르면 자연스럽게 정보전문가에게 책임을 할당 더 높은 응집도를 가진 클래스가 됨.

 

내부의 상태를 이용해 어떤 결정을 내리는 로직이 객체 외부에 존재하는가?

 - 해당 객체가 책임져야 하는 어떤 행동이 객체 외부로 누수된것.

 

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

 

 

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

 

  • 메서드가 작업을 어떻게 수행하는지를 나타내라
  • 메서드가 '어떻게'가 아니라 '무엇'을 하는지를 드러내는 것.

이처럼 매서드 이름을 짓는 패턴 - 의도를 드러내는 선택자(Intention Revealing Selector)

 

외부의 객체가 메시지를 전송하는 목적을 먼저 생각하자

 

결과적으로 협력하는 클라이언트 의도에 부합하는 메서드 이름을 짓게 됨.

 

ex) 할인 여부를 판단하는 메소드 isSatisfiedBy(Screening Screening){ ... }

 

의도를 드러내는 인터페이스 (Intention Revealing Interface)

- 구현과 관련된 모든 정보를 캡슐화하고 객체의 퍼블릭 인터페이스에는 협력과 관련된 의도만을 표현해야 한다는 것.

 

 

객체에게 묻지말고 시키되

구현방법이 아닌

클라이언트의 의도를 드러내야 한다.

 

 

함께 모으기

 

예시) 디미터 법칙을 위반하는 1장의 티켓 판매 도메인

audience.getBag().minusAmount(ticket.getFee());

 

인터페이스와 구현(객체의 내부구조)의 분리원칙을 위반

 

노출되는 객체사이의 관계가 많아 질수록 결합도가 높아짐. 요구사항 변경에 취약.

 

 

묻지말고 시켜라

예시) Audience  스스로 상태를 제어하는 객체

 

인터페이스에 의도를 드러내자

 

클라이언트의 의도를 표현하는 이름

TickerSeller 메서드 setTicket()이 아닌 sellTo()

Audience 메서드 setTicket() 이 아닌 buy()

Bag 메서드 setTicket()이 아닌 hold()

 

오퍼레이션 이름은 협력이하는 문맥을 반영해야한다.

 


03 원칙의 함정

 

원칙이 현재 상황에 부적합하다고 판단되면 과감히 무시하라

 

원칙이 유용한지를 판단하는 능력을 키우자.

 

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

 

디미터 법칙은 '오직 하나의 도트만을 사용하라' 하는 말로 요약되기도 해서

자바8의 IntStream을 사용한 코드가 법칙을 위반한다고 오해하기도 함.

 

IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();

 

of, filter, distinct 메서드는 IntStream 이라는 동일한 클래스의 인스턴스를 반환. 따라서 법칙위반 X

 

디미터 법칙은 결합도와 관련된것. 객체의 내부구조가 외부로 노출되는 경우로 한정

 

IntStream 은  다른 IntStream 으로 변환할뿐 객체를 둘러싸는 캡슐은 유지된다.

 

결합도와 응집도의 충돌

 

객체의 상태를 물어봄 -> 상태 반환 -> 그것을 기반으로, 상태를 변경하는 모든 코드를 객체에게 위임.

 

예시) Theater 가 Bag 의 상태를 변화시키지말고, Audience 에 위임메서드 추가

 

디미터 법칙과 묻지말고 시켜라 법칙을 무작정 따를 필요는 없음

 

예시) Screening 이 자신이 판단해야하지 않을 다른 책임을 지게되는 경우

- 객체의 응집도가 낮아질수 있음. 

- 캡슐화 향상시키는것보다 결합도를 낮추는것이 전체적으로 더 나을수 있음.

 

 

디미터 법칙의 위반여부는 객체인지 자료구조인지에 달려있음.

자료구조일 경우 당연히 내부를 노출해야하므로 법칙에서 제외

 

원칙이 필요하고 적절한 상황인지 판단해야함.

 


04 명령 - 쿼리 분리 원칙 (Command-Query-Separation)

 

어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈 루틴(routine)

 

프로시저(procedure)

- 정해진 절차에 따라 내부의 상태를 변경하는 루틴

- 부수효과를 발생(상태를 변경)시킬 수 있지만 값을 반환할 수 없다.

 

함수(function)

- 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴

- 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.

 

객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름

 

명령(Command)

- 객체의 상태를 수정하는 오퍼레이션

- 프로시저와 동일

 

쿼리(Query)

- 객체와 관련된 정보를 반환하는 오퍼레이션

- 함수와 동일

 

오퍼레이션은 부수효과를 발생시키는 명령 이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 함.

어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안됨

 

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

 

도메인의 중요한 두가지 용어 이벤트(event) 와 반복일정(recurring schedule)

 

이벤트 - 특정일자에 실제로 발생하는 사건

반복일정을 만족하는 특정일자와 시간에 발생하는 사건

 

반복일정 - 특정 시간 간격에 발생하는 사건전체를 포괄적으로 지칭하는 용어

 

예시) isSatisfied 메서드가 명령과 쿼리의 두가지 역할을 동시에 수행하기때문에 나오는 오류

 

명령과 쿼리를 뒤섞으면 실행결과를 예측하기가 어려워질 수 있음.

 

해결책

public class Event {
    public boolean isSatisfied(RecurringSchedule schedule){
        if(from.getDayOfWeek() != schedule.getDayOfWeek() ||
        !from.toLocalTime().equals(schedule.getFrom()) ||
        !duration.equals(schedule.getDuration())) {
            return false;
        }
        
        return true;
    }
    
    public void reschedule (RecurringSchedule schedule) {
        from = LocalDateTime.of(from.toLocalDate().plusDays(daysDistance(schedule)),
        schedule.getFrom());
        
        duration = schedule.getDuration();
    }
}

 

명령과 쿼리를 명확하게 분리하자.

 

 

public class Event {
    public boolean isSatisfied(RecurrringSchedule schedule) {...} // 쿼리
    public void reschedule(RecurrringSchedule schedule) {...}  // 명령
}

 

이제 클라이언트가 외부에서 직접 실행 가능. 

 

명령과 쿼리를 분리하면서 코드는 예측 가능하고 이해하기 쉬우며 디버깅, 유지보수가 용이

 

 

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

 

참조투명성(referential transparency) 

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

 

어떤값이 불변한다는 말은 부수효과가 발생하지 않는다는 말과 동일하다.

 

부수효과(side effect) 를 발생시키는 두가지 대표적인 문법 - 대입문과 함수(원래는 프로시저가 옳다)

 

참조투명성을 만족하는 식이 제공하는 두가지 장점

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

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

명령-쿼리 분리원칙을 사용하면 참조투명성을 좀더 만족할수 있다.

 

명령형 프로그래밍 함수형 프로그래밍

- 부수효과를 기반으로 함

- 상태를 변경시키는 연산들을 순서대로 나열

- 대부분의 객체지향 프로그래밍이 속함

(메시지에 의한 객체의 상태변경에 집중하기 때문)

 

- 부수효과가 존재하지 않는 수학적인 함수 기반

- 참조 투명성 장점 극대화

- 명령형에 비해 실행결과는 이해하고 예측하기 쉬움

- 하드웨어 발달로 병렬처리가 중요해진 요즘 함수형 패러다임을 접목하고있는 추세임

 

 

책임에 초점을 맞춰라

 

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

 

디미터 법칙, 묻지 말고 시켜라, 의도를 드러내는 인터페이스, 명령-쿼리 분리 원칙 을 자연스럽게 만족시키게 됨.

 

협력에 적합한 객체가 아니라 협력에 적합한 메시지를 먼저 고려하자.

 

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

실행시점의 제약과 조건은 나타내지 않음. - 이와 관련해 부록 A의 계약에 의한 설계 참고