6. 메시지와 인터페이스
객체지향 프로그래밍의 가장 흔한 오해는 애플리케이션이 클래스의 집합으로 구성된다고 생각하는 것이다.
클래스는 개발자가 사용하는 도구일뿐, 이 도구에 집착하게 되면 경직되고 유연하지 못한 설계를 야기하게 된다.
따라서 우리는 객체가 수행하는 책임에 초점을 맞추고, 책임을 통해 객체가 수신할 수 있는 메시지를 디자인하도록 해야 한다.
이 디자인을 통해 나오는 결과물이, 객체가 가진 퍼블릭 인터페이스(public interface) 를 구성하며,
이를 위해 유연하고 재사용이 가능한 인터페이스를 만들 수 있도록 설계 원칙과 기법을 적용해야 한다.
6.1. 협력과 메시지
두 객체 사이의 협력 관계를 표현하는 가장 전형적 형태는 클라이언트-서버 모델일 것이다.
flowchart LR Client -- request --> Server Server -- response --> Client
여기에 데이터베이스를 추가해보자.
flowchart LR Client -- request --> Server Server -- request --> Database Database -- response --> Server Server -- response --> Client
복잡한 기능일수록 각 객체가 독립적으로 수행하는 것보다 다른 객체와 협력하여 처리할 수 있음을 알 수 있다.
위의 차트에서 request
, response
로 표현된 부분이 바로 메시지(message) 이다.
이 메지지는 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단이며,
다른 객체에게 도움을 요청하는 것을 메시지 전송(message sending) 혹은 메시지 패싱(message passing) 이라고 부른다.
이때 메시지를 전송하는 객체가 메시지 전송자(message sender) 가 되며, 메시지를 수신하는 객체가 메시지 수신자(message receiver) 가 된다.
메시지는 각각 오퍼레이션 이름(operation name) 과 인자(argument) 로 구성되며, 메시지 전송시엔 메시지 수신자가 추가된다.
메시지를 수신했을 때 실제로 어떤 코드가 실행되는지는 메시지 수신자인 객체가 어떤 타입인가에 따라 달라진다.
이처러 메시지를 수신했을 떄 실제로 실행되는 함수 또는 프로시저는 **메서드(method)**라고 부른다.
메시지 전송자는 메서드 내부의 구현체에 대해 신경쓰지 않고 전송할 메시지에 대해서만 인지하면 되므로, 메시지 전송자와 수신자가 느슨하게 결합될 수 있게 해준다.
낮은 결합도는 결과적으로 유연하고 확장 가능한 코드를 작성할 수 있게 된다.
객체는 캡슐화 원칙에 의해 접근할 수 있는 영역과 접근할 수 없는 영역이 구분된다.
다른 객체에 메시지를 전송하려면 접근할 수 있는 영역을 알아야하고 이 부분은 퍼블릭 인터페이스로 취급한다.
이 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation) 이라고 하며, 이는 수행 가능한 어떤 행위에 대한 추상화이다.
그리고 오퍼레이션의 이름과 필요로 하는 파라미터의 목록을 합쳐 시그니처(signature) 라고 부른다.
메서드는 이 시그니처에 구현을 더한 개념이다.
6.2. 인터페이스와 설계 품질
좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 충족해야한다.
최소한의 인터페이스란 꼭 필요한 오퍼레이션만을 인터페이스에 포함하는 것을 의미하며,
추상적인 인터페이스라 어떻게 수행하는 지가 아니라 어떤 행위를 하는지 표현하는 것을 의미한다.
이제 퍼블릭 인터페이스의 품질에 영향을 주는 기법들에 대해서 파악해보자.
6.2.1. 디미터 법칙
메시지를 먼저 결정하고 객체가 메시지를 따르게 하는 설계 방식은 객체가 외부에 제공하는 인터페이스가 독특한 스타일을 따르도록 강제하곤 한다.
이런 스타일을 디미터의 법칙(Law of Demeter) 라고 부른다.
디미터의 법칙은 다른 객체가 어떠한 자료를 갖고 있는지 알 필요가 없다는 것을 의미하며, 최소 지식 원칙으로도 알려져 있다.
다시말해 다른 객체의 정보를 알고 있다는 것 자체가 강한 결합을 유도하므로, 아예 모른다는 것을 전제로 두고 설계하는 것을 의미한다.
6.2.2. Tell, Don’t Ask Style
“묻지 말고 시켜라” 라는 원칙이다.
디미터의 법칙을 준수한 스타일을 장려하는 용어이다.
메시지 전송자는 메시지 수신자의 상태를 기반으로 결정을 내린 후, 메시지 수신자의 상태를 바꿔서는 안된다.
6.2.3. 의도를 드러내는 인터페이스
메서드가 어떤 행위를 수행하는지 드러내는 것은 매우 중요하다.
이 행위를 메서드의 이름으로 표현되며 이 메서드가 “무엇”을 하는지 드러내도록 해야한다.
6.3. 명령-쿼리 분리 원칙
“묻지말고 시켜라”에 이어 “가끔씩 필요로 물어야 한다”에 도움을 주는 원칙으로 명령-쿼리 분리 원칙(Command-Query Separation Principle) 이 있다.
이 원칙은 퍼블릭 인터페이스에 오퍼레이션을 정의할 때 참고할 수 있는 지침을 제공해준다.
원칙을 이해하기 전에 용어에 대해서 먼저 알아보자.
어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine) 이라고 하며,
루틴은 다시 프로시져(Procedure) 와 함수(Function) 으로 구분할 수 있다.
프로시져는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류이며,
함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류이다.
다르게 설명해보자면
프로시져는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
명령(Command) 과 쿼리(Query) 는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름이다.
객체의 상태를 수정하는 오퍼레이션을 명령으로, 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라고 볼 수 있다.
간단하게 아래와 같이 정리할 수 있다.
- 프로시저 == 명령
- 쿼리 == 함수
명령-쿼리 분리 원칙의 핵심은 오퍼레이션은 부수효과를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 한다는 것이다.
명령과 쿼리를 뒤섞으면 실행 결과를 예측하기가 어려워지거나 버그를 양산할 수 있다.
따라서 가장 깔끔한 해결책은 명령과 쿼리를 명확하게 분리하는 것이다.