반응형
06 오브젝트 - 메시지와 인터페이스
- 훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다.
- 더 정확히는, 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다.
- 객체지향 애플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지다.
6.1 협력과 메시지
6.1.1 클라이언트-서버 모델
- 협력은 어떤 객체가 다른 객체에게 무언가 요청할 때 시작된다.
- 객체가 다른 객체에게 접근할 수 있는 유일한 방법은 메시지를 전송하는 것뿐이다.
- 두 객체 사이의 협력 관계를 설명하기 위해 전통적으로 클라이언트-서버 모델이라는 메타포를 사용한다.
- 협력안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다.
- 객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다.
- 협력에 적합한 객체를 설계하기 위해서는 수신하는 메시지와 전송하는 메시지의 집합을 함께 고려해야 한다.
6.1.2 메시지와 메시지 전송
- 메시지 : 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단. 오퍼레이션명과 인자로 구성된다.
- 메시지 전송 or 메시지 패싱 : 한 객체게 다른 객체에게 도움을 요청하는 것
- 메시지 전송자 : 메시지를 전송하는 객체
- 메시지 수신자 : 메시지를 수신하는 객체
6.1.3 메시지와 메서드
- 메서드 : 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저
- 코드상에서 동일한 이름의 변수에게 동일한 메시지를 전송하더라도, 객체의 타입에 따라 실행되는 메서드가 달라진다.
- 메시지와 메서드 구분은 메시지 전송자와 메시지 수신자가 느슨하게 결합될 수 있게 한다.
- 전송자는 수신자가 어떤 인스턴스인지, 어떤식으로 요청을 처리하는지 알 필요가 없다.
- 수신자는 누가 메시지를 전송했는지 알 필요가 없고 메시지를 처리하기 위해 필요한 메서드를 스스로 결정할 수 있다.
6.2 인터페이스와 설계 품질
- 좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다.
- 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지 표현한다.
- 책임 주도 설계 방법은 메시지를 먼저 선택함으로써 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는것을 방지한다.
1. 디미터 법칙
- 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라.
- 오직 하나의 도트만 사용하라.
- 클래스가 특정한 조건을 만족하는 대상에게만 메시지를 전송하도록 프로그래밍 하라.
- this 객체
- 메서드의 매개변수
- this의 속성
- this의 속성인 컬렉션의 요소
- 메서드 내에서 생성된 지역 객체
# 디미터의 법칙을 지키는 코드
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Money fee = screening.calculateFee(audienceCount);
return new Reservation(customer, screening, fee, audienceCount);
}
}
# 디미터의 법칙을 위반하는 코드
screening.getMovie().getDiscountConditions();
- 메시지 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송하는 코드를 기차 충돌이라고 부른다.
묻지 말고 시켜라
- 메시지 전송자는 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안된다.
- 훌륭한 메시지는 객체의 상태에 관해 묻지 말고 원하는것을 시켜야 한다.
의도를 드러내는 인터페이스
- 메서드의 이름을 짓는 방법은 어떻게가 아니라 무엇을 하는지 드러내는 것이다.
- 어떻게 수행하는지를 드러내는 이름이란 메서드의 내부 구현을 설명하는 이름이다.
- 결과적으로 협력을 설계하기 시작하는 이른 시기부터 클래스의 내부 구현에 관해 고민하게 된다.
- 무엇을 하는지 드러내도록 메서드의 이름을 짓기 위해서는 객체가 협력 안에서 수행해야 하는 책임에 관해 고민해야 한다.
- 오퍼레이션은 클라이언트의 의도를 표현하는 이름을 가져야한다.
public class TicketSeller {
public void sellTo(Audience audience) { ... }
}
public class Audience {
public Long buy(Ticket ticket) { ... }
}
public class Bag {
public Long hold(Ticket ticket) { ... }
}
6.3 원칙의 함정
- 소프트웨어 설계에 법칙이란 존재하지 않는다. 법칙에는 예외가 없지만 원칙에는 예외가 넘쳐난다.
- 설계는 트레이드오프의 산물이다.
- 원칙이 현재 상황에 부적합하다고 판단된다면 과감하게 원칙을 무시하라.
- 원칙을 아는 것보다 더 중요한 것은 언제 원칙이 유용하고 언제 유용하지 않은지를 판단할 수 있는 능력을 기르는 것이다.
6.3.1 디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.
- 디미터법칙은 "오직 하나의 도트만을 시용하라" 라는 말로 요약되기도 한다.
- 따라서 아래의 코드가 기차 충돌을 초래하기 때문에 디미터 법칙을 위반한다고 생각할 수도 있다.
IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
- 위 코드에서 of, filter, distinct 메서드는 모두 IntStream이라는 동일한 클래스의 인스턴스를 반환한다.
- 따라서 이 코드는 디미터 법칙을 위반하지 않는다. 디미터 법칙은 결합도와 관련된 것이며, 결합도가 문제가 되는 것은 객체의 내부 구조가 외부로 노출되는 경우로 한정된다.
- 기차 충돌처럼 보이는 코드라도 객체의 내부 구현을 외부로 노출하지 않는다면 그것은 디미터 법칙을 위반하지 않는다.
- 위 코드는 객체의 내부에 대한 어떤 내용도 묻지 않고, 객체를 다른 객체로 변환하는 작업을 수행하라고 시킬 뿐이다.
- 따라서 묻지 말고 시켜라 원칙을 위반하지 않는다.
6.3.2 결합도와 응집도의 충돌
- 묻지 말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과로만 귀결되는 것은 아니다.
- 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다.
- 결과적으로 객체는 상관 없는 책임들을 한꺼번에 떠안게 되기 때문에 결과적으로 응집도가 낮아진다.
- 클래스는 하나의 변경 원인만을 가져야 한다.
for(Movie each : movies) {
total += each.getFee();
}
- 가끔씩은 묻는 것 외에는 다른 방법이 존재하지 않는 경우도 존재한다.
- 컬렉션에 포함된 객체들을 처리 하는 유일한 방법은 객체에게 물어보는 것이다.
- 위의 코드에서 Movie에게 묻지 않고도 movies 컬렉션에 포함된 전체 영화의 가격을 계산할 수 있는 방법이 있을까?
- 디미터 법칙의 위반 여부는 묻는 대상이 객체인지, 자료 구조인지에 달려있다고 설명한다.
- 객체는 내부 구조를 숨겨야하므로 디미터 법칙을 따르는것이 좋지만 자료구조라면 당연히 내부를 노출해야하므로 디미터 법칙을 적용할 필요가 없다.
6.4 명령 - 쿼리 분리 원칙
- 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈을 루틴(routine)이라고 부른다.
- 루틴은 다시 프로시저(procedure)와 함수(function)로 구분할 수 있다.
- 프로시저와 함수는 부수효과와 반환값의 유무라는 측면에서 명확하게 구분 된다.
- 프로시저는 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류다.
- 이에 반해 함수는 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류다.
- 프로시저와 함수를 명확하게 구분하기 위해 루틴을 작성할 때 다음과 같은 제약을 따라야 한다.
- 프로시저는 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
- 함수는 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
- 명령(Command)과 쿼리(Query)는 객체의 인터페이스 측면에서 프로시저와 함수를 부르는 또 다른 이름이다.
- 객체의 상태를 수정하는 오퍼레이션을 명령이라고 부르고 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리라고 부른다.
- 명령-쿼리 분리 원칙은 오퍼레이션은 부수효과를 발생시키는 명령이거나, 발생시키지 않는 쿼리 중 하나여야 한다는 것이다.
- 어떤 오퍼레이션도 명령인 동시에 쿼리여서는 안 된다.
- 따라서 명령과 쿼리를 분리하기 위해서는 다음의 두 가지 규칙을 준수해야 한다.
- 객체의 상태를 변경하는 명령은 반환값을 가질 수 없다.
- 객체의 정보를 반환하는 쿼리는 상태를 변경할 수 없다.
- 명령-쿼리 분리 원칙을 한 문장으로 표현하면 "질문이 답변을 수정해서는 안 된다"는 것이다.
6.4.1 반복 일정의 명령과쿼리 분리하기
- 명령과 쿼리를 뒤섞으면 실행 결과를 예측하기가 어려워질 수 있다.
- 겉으로 보기에는 쿼리처럼 보이지만 내부적으로 부수효과를 가지는 메서드는 이해하기 어렵고, 잘못 사용하기 쉬우며, 버그를 양산하는 경향이 있다.
- 가장 깔끔한 해결책은 명령과 쿼리를 명확하게 분리하는 것 01다.
- isSatisfied 메서드는 반환 값을 돌려주고 reschedule 메서드는 반환 값을 돌려주지 않는다.
- 인터페이스를 훑어보는 것만으로도 isSatisfied 메서드가 쿼리이고, reschedule 메서드가 명령이라는 사실을 한눈에 알 수 있다.
public class Event {
public boolean isSatisfied(RecurringSchedule schedule) { .. . }
public void reschedule(RecurringSchedule schedule) { ... }
}
- 퍼블릭 인터페이스를 설계할 때 부수효과를 가지며 값을 반환하지 않는 명령과, 부수효과 없이 값을 반환하는 쿼리를 분리해라.
- 코드는 예측 가능하고 이해하기 쉬우며 디버깅이 용이한 동시에 유지보수가 수월해진다.
6.4.2 명령 - 쿼리 분리와 참조 투명성
- 쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라도 상관이 없다.
- 명령이 개입하지 않는 한 쿼리의 값은 변경되지 않기 때문에 쿼리의 결과를 예측하기 쉬워진다.
- 또한 쿼리들의 순서를 지유롭게 변경할 수도 있다.
- 명령과 쿼리를 분리함으로써 명령형 언어 안에서 참조 투명성 (referential transparency)의 장점을 제한적이나마 누릴 수 있다.
- 참조 투명성이란 “어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성” 이다.
f(1) + f(1) = 6
f(1) * 2 = 6
f(1) - 1 = 2
# 이제 f(1)을 함수의 결과값인 3으로 바꿔보자.
3 + 3 = 6
3 * 2 = 6
3 - 1 = 2
- 이처럼 어떤 값이 변하지 않는 성질을 불변성(immutability)이라고 부른다.
- 어떤 값이 불변한다는 말은 부수효과가 발생하지 않는다는 말과 동일하다.
- 참조 투명성의 또 다른 장점은 식의 순서를 변경하더라도 결과가 달라지지 않는다는 것이다.
- 객체지향 패러다임이 객체의 상태 변경이라는 부수효과를 기반으로 하기 때문에 참조 투명성은 예외에 가깝다
- 하지만 명령-쿼리 분리 원칙을 사용하면 이 균열을 조금이나마 줄일 수 있다.
- 명령-쿼리 분리 원칙은 부수효과를 가지는 명령으로부터 부수효과를 가지지 않는 쿼리를 명백하게 분리함으로써 제한적이나마 참조 투명성의 혜택을 누릴 수 있게 된다.
728x90
반응형
'스터디 > 오브젝트' 카테고리의 다른 글
오브젝트 09 - 유연한 설계 (0) | 2024.03.25 |
---|---|
오브젝트 07 - 객체 분해 (1) | 2024.03.05 |
오브젝트 05 - 책임 할당하기 (0) | 2024.02.20 |
오브젝트 04 - 설계 품질과 트레이드오프 (0) | 2024.02.19 |
오브젝트 03 - 역할, 책임, 협력 (0) | 2024.02.13 |
댓글