본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

06 CHAPTER 메시지와 인터페이스

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

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

wikibook.co.kr

 

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

 

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

훌륭한 객체지향 코드를 얻기 위해서는 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다.

중요한 것은 책임이 객체가 수신할 수 있는 메시지의 기반이 된다는 것이다.

 

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

훌륭한 퍼블릭 인터페이스를 얻기 위해서는 책임 주도 설계 방법을 따르는 것만으로는 부족하다.

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





◈ 협력과 메시지

클라이언트-서버 모델

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

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

 

대부분의 사람은 객체가 수신하는 메시지의 집합에만 초점을 맞추지만 협력에 적합한 객체를 설계하기 위해서는 외부에 전송하는 메시지의 집합도 함께 고려하는 것이 바람직하다.

 

더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 하고 두 객체 사이의 협력을 가능하게 해주는 매개체가 바로 메시지이다.



메시지와 메시지 전송

한 객체가 다른 객체에게 도움을 요청하는 것을 메시지 전송 또는 메시지 패싱이라고 부른다.



메시지와 메서드

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

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

 

메시지 전송을 코드 상에 표기하는 시점에는 어떤 코드가 실행될 것인지를 정확하게 알 수 없다.

실행 시점에 실제로 실행되는 코드는 메시지를 수신하는 객체의 타입에 따라 달라지기 때문이다.

 

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

 

실행 시점에 메시지와 메서드를 바인딩하는 메커니즘은 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.



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

퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션이라고 부른다.

 

메시지를 수신했을 때 실제로 실행되는 코드는 메서드라고 부른다.

 

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

1. 메시지 전송.

2. 오퍼레이션 호출.

3. 메서드 실행.

따라서 퍼블릭 인터페이스와 메시지의 관점에서 보면 ‘메서드 호출’보다는 ‘오퍼레이션 호출’이라는 용어를 사용하는 것이 더 적절하다.



시그니처

이름과 파라미터 목록을 합쳐 시그니처라고 부른다.

 

오퍼레이션은 실행 코드 없이 시그니처만 정의한 것이다.

 

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

 

다형성의 축복을 받기 위해서는 하나의 오퍼레이션에 대해 다양한 메서드를 구현해야만 한다.

 

객체의 퍼블릭 인터페이스가 객체의 품질을 결정하기 때문에 결국 메시지가 객체의 품질을 결정한다고 할 수 있다.




 

 인터페이스와 설계 품질

책임 주도 설계 방법은 메시지를 먼저 선택함으로써 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.

메시지가 객체를 선택하게 함으로써 클라이언트의 의도를 메시지에 표현할 수 있게 한다.

 

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

1. 디미터 법칙.

2. 묻지 말고 시켜라.

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

4. 명령-쿼리 분리.



디미터 법칙

협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 바로 디미터 법칙이다.

 

“오직 하나의 도트만 사용하라.”

 

협력 경로를 제한하면 결합도를 효과적으로 낮출 수 있다.

 

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

 

디미터 법칙과 캡슐화.

디미터 법칙이 가치 있는 이유는 클래스를 캡슐화하기 위해 따라야하는 구체적인 지침을 제공하기 때문이다.
캡슐화 원칙이 클래스 내부의 구현을 감춰야 한다는 사실을 강조한다면
디미터 법칙은 협력하는 클래스의 캡슐화를 지키기 위해 접근해야 하는 요소를 제한한다.
디미터 법칙은 협력과 구현이라는 사뭇달라 보이는 두 가지 문맥을 하나의 유기적인 개념으로 통합한다.
클래스의 내부 구현을 채워가는 동시에 현재 협력하고있는 클래스에 관해서도 고민하도록 주의를 환기시키기 때문이다.

 

 

디미터의 법칙을 위반하는 코드의 전형적인 모습인 기차 충돌.

기차 충돌은 클래스의 내부 구현이 외부로 노출됐을 때 나타나는 전형적인 형태.

메시지 수신자의 캡슐화는 무너지고,

메시지 전송자가 메시지 수신자의 내부 구현에 강하게 결합된다.

 

디미터 법칙을 따르도록 코드를 개선하면 메시지 전송자는 더 이상 메시지 수신자의 내부 구조에 관해 묻지 않게 된다.

 

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

 

디미터의 법칙은 객체의 내부 구조를 묻는 메시지가 아니라 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭인다.



묻지 말고 시켜라.

디미터 법칙은 훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다는 사실을 강조한다.

 

묻지 말고 시켜라 원칙을 따르면 밀접하게 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.

 

내부 상태를 묻는 오퍼레이션을 인터페이스에 포함시키고 있다면 더 나은 방법은 없는지 고민해 보라.

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

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

 

상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로 써 인터페이스를 향상시켜라.

 

훌륭한 인터페이스를 수확하기 위해서는 객체가 어떻게 작업을 수행하는지를 노출해서는 안 된다.

인터페이스는 객체가 어떻게 하는지가 아니라 무엇을 하는지를 서술해야한다.



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

메서드가 작업을 어떻게 수행하는지를 나타내도록 지으면 좋지 않은 이유.

 

1. 동일한 작업을 수행하는 메서드 이름을 다르게 하면 두 메서드 내부 구현을 정확하게 이해하지 못한다면 동일한 작업을 수행한다는 사실을 알아채기 어렵다.

 

2. 메서드들이 클라이언트로 하여금 협력하는 객체의 종류를 알도록 강요한다면 메서드 수준에서 캡슐화를 위반하게 된다.

코드 변경이 필요할 경우 단순히 참조하는 객체를 변경하는 것뿐만 아니라 호출하는 메서드를 변경해야한다.

메서드의 동작 방법이 변경된다면 메서드의 이름 역시 변경해야 한다.

메서드 이름을 변경한다는 것은 메시지를 전송하는 클라이언트의 코드도 함께 변경해야한다는 것을 의미한다.

따라서 책임을 수행하는 방법을 드러내는 메서드를 사용한 설계는 변경에 취약할 수밖에 없다.




메서드가 ‘무엇’을 하는지 드러내는 것이 좋은 명명 방법이다.

 

어떻게 수행하는 지를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름이다.

결과적으로 협력을 설계하기 시작하는 이른 시기부터 클래스의 내부 구현에 관해 고민할 수밖에 없다.

반면 무엇을 하는지를 드러나도록 메서드의 이름을 짓기 위해서는 객체가 협력 안에 수행해야하는 책임에 관해 고민해야 한다.



무엇을 하느냐에 따라 메서드의 이름을 짓는 패턴을 의도를 드러내는 선택자(Intention Revealing Selector)라고 부른다.

 

객체에게 묻지 말고 시키돼 구현 방법이 아닌 클라이언트 의도를 드러내야 한다.



함께 모으기.

근본적으로 디미터 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반한다.

 

디미터 법칙과 묻지 말고 시켜라 스타일을 따르면 자연스럽게 자율적인 객체로 구성된 유연한 협력을 얻게 된다.

 

일단 디미터의 법칙과 묻지 말고 시켜라 스타일을 따르는 인터페이스를 얻었다면 인터페이스가 클라이언트 의도를 올바르게 반영했는지를 확인해야 한다.

 

미묘하게 다른 의미를 가진 세 메서드가 같은 이름을 가지고 있다는 사실은 클라이언트 개발자를 혼란스럽게 만들 확률이 높다.

 

오퍼레이션은 클라이언트가 객체에게 무엇을 원하는지를 표현해야 한다.

다시 말해 객체 자신이 아닌 클라이언트의 의도를 표현하는 이름을 가져야 한다.



 

 원칙의 함정

원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다.



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

동일한 클래스의 인스턴스를 반환하는 메서드가 기차 충돌처럼 이어져 있더라도 디미터 법칙을 위반하지 않는다.

 

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

 

기차 충돌처럼 보이는 코드라도 객체의 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 그것은 디미터 법칙을 준수한 것이다.

 

기차 충돌처럼 보이는 코드가 있는데 디미터 법칙을 위반하는지 헷가리는 상황이라면 다음과 같은 질문을 던져라.

“여러 개의 도트를 사용한 코드가 객체의 내부 구조를 노출하고 있는가?”



결합도와 응집도의 충돌.

묻지 말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과만 귀결되는 것은 아니다.

모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다.

결과적으로 객체는 상관 없는 책임들을 한꺼번에 떠안게 되기 때문에 결과적으로 응집도가 낮아진다.

 

클래스가 원칙을 준수하기 위해 다른 책임을 떠안게 된다면 그 클래스의 본질적인 책임에 해당되는지 판단해보아야 한다.

 

클래스의 캡슐화를 향상시키는 것보다 응집도를 높이고 결합도를 낮추는 것이 전체적인 관점에서 더 좋은 상황일 수도 있다.



디미터 법칙의 위반 여부는 묻는 대상이 객체인지, 자료 구조인지에 달려있다고 설명한다.

 

객체는 내부 구조를 숨겨야 하므로 디미터 법칙을 따르는 것이 좋지만

자료구조라면 당연히 내부를 노출해야 하므로 디미터 법칙을 적용할 필요가 없다.

 

객체에게 시키는 것이 항상 가능한 것은 아니다. 가끔씩은 물어야 한다.

여기서 강조하고 싶은 것은 소프트웨어 설계에 법칙이란 존재하지 않는다는 것이다.

 

 

 

 명령-쿼리 분리 법칙

어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴이라고 부른다.

 

루틴은 다시 프로시저함수로 구분할 수 있다.

 

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

 

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

 

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

 

명령프로시저랑 동일하고 쿼리함수와 동일하다.

 

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

 

명령과 쿼리를 분리하기 위해서는 다음의 두 가지 규칙을 준수해야 한다.

1) 객체의 상태를 변경하는 ‘명령’반환값을 가질 수 없다.

2) 객체의 정보를 반환하는 ‘쿼리’상태를 변경할 수 없다.

 

명령-쿼리 분리 원칙을 한 문장으로 표현하면 “질문이 답변을 수정해서는 안 된다”는 것이다.



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

명령과 쿼리의 두 가지 역할을 동시에 수행하고 있으면 버그를 찾기 어려워진다.

 

기능을 추가하는 과정에서 명령과 쿼리의 두 가지 역할을 동시에 수행하는 메서드를 만들게되는 실수를 하기쉽다.

 

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

가장 깔끔한 해결책은 명령과 쿼리를 명확하게 분리하는 것이다.

 

반환값을 돌려주는 메서드는 쿼리이므로 부수 효과에 대한 부담이 없다.

따라서 몇 번을 호출하더라도 다른 부분에 영향을 미치지 않는다.

반면 반환 값을 가지지 않는 메서드는 모두 명령이므로 해당 메서드를 호출할 때는 부수효과에 주의해야 한다.

어떤 메서드가 부수효과를 가지는지를 확인하기 위해 코드를 일일이 다 분석하는 것보다는 메서드가 반환 값을 가지는지 여부만 확인하는 것이 훨씬 간단하다.

 

명령과 쿼리를 분리한 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해진다.



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

쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라도 상관이 없다.

명령이 개입하지 않는 한 쿼리의 값은 변경되지 않기 때문에 쿼리의 결과를 예측하기 쉬워진다.

또한 쿼리들의 순서를 자유롭게 변경할 수도 있다.

메서드처럼 부수효과를 가진 명령이 호출되지 않는 한 순서와 횟수에 상관없이 호출될 수 있다.



책임에 초점을 맞춰라

디미터 법칙을 준수하고 묻지 말고 시켜라 스타일을 따르면서도 의도를 드러내는 인터페이스를 설계하는 아주 쉬운 방법이 있다.

메시지를 먼저 선택하고 그 후에 메시지를 처리할 객체를 선택하는 것이다.

 

메시지를 먼저 선택하는 방식이 디미터 법칙, 묻지 말고 시켜라 스타일, 의도를 드러내는 인터페이스, 명령-쿼리 분리 원칙에 미치는 긍정적인 영향을 살펴보면 다음과 같다.

 

1) 디미터 법칙.

수신할 객체를 알지 못한 상태에서 메시지를 먼저 선택하기 때문에 객체 내부 구조에 대해 고민할 필요가 없어진다.

 

2) 묻지 말고 시켜라.

클라이언트의 관점에서 메시지를 선택하기 때문에 필요한 정보를 물을 필요없이 원하는 것을 표현한 메시지를 전송하면 된다.

 

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

메시지를 전송하는 클라이언트의 관점에서 메시지의 이름을 정하기에 클라이언트가 무없을 원하는지, 그 의도가 분명하게 드러날 수 밖에 없다.

 

4) 명령-쿼리 분리 원칙.

협력이라는 문맥안에서 객체의 인터페이스에 관해 고민하게 되어 예측 가능한 협력을 만들기 위해 명령과 쿼리를 분리하게 될 것이다.