안녕하세요 개발자 문문입니다.
오늘은 메시지와 인터페이스를 공부하겠습니다.
- 서론
- 훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아닌 객체를 지향해야 한다.
- 좀 더 정확하게 말하면 협력안에서 객체가 수행하는 책임에 초점을 맞춰야한다.
- 책임이 객체가 수신할 수 있는 메시지의 기반이 된다.
- 객체를 수신하는 메시지들이 객체의 퍼블릭 인터페이스를 구성한다.
- 이번 장에서는 퍼블릭 인터페이스를 만드는데 도움이 되는 설계 원칙과 기법을 살펴보자.
- 협력과 메시지
- 클라이언트 - 서버 모델
- 객체는 자신의 희망을 메시지라는 형태로 전송하고 메시지를 수신한 객체는 요청을 적절히 처리한 후 응답한다.
- 협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다.
- 객체는 협력에 참여하는 동안 클라이언트와 서버의 역할을 동시에 수행하는 것이 일반적이다.
- 협력의 관점에서 객체는 객체가 수신하는 메시지의 집합과 외부 객체에 전송하는 메시지의 집합 두 가지 종류로 구성된다.
- 메시지와 메시지 전송
- 메시지 전송(message sending), 메시지 패싱(message passing) : 한 객체가 다른 객체에게 도움을 요청하는 것
- 메시지 전송자(message sender) : 메시지를 전송하는 객체 - 클라이언트
- 메시지 수신자(message receiver) : 메시지를 수신하는 객체 - 서버
- 메시지는 오퍼레이션명과 인자로 구성되며 메시지 전송은 여기에 메시지 수신자를 추가한 것이다.
- ex) condition.isSatisfiedBy(screening); - condition : 메시지 수신자, isSatisfiedBy : 오퍼레이션명, screening : 인자
- 메시지와 메서드
- 메서드 : 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저
- 동일한 이름의 변수(condition)에게 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다.(다형성 - DiscountCondtion의 자식 클래스: PeriodCondition, SequenceCondition 중 누구의 인스턴스냐에 따라 달라짐)
public interface DiscountCondtion{
public void isSatisfiedBy(Screening screening);
}
public class PeroidCondition implements DiscountCondition()
{
...
public void isSatisfiedBy(Screening screening) {
//메서드
...
}
...
}
public class Main{
public static void main(Stirng[] args)
{
DiscountCondtion condtion = new PeriodCondition();
Screening screening = new Screening();
//메시지
//PreiodContion의 isSatisfiedBy 메서드 호출
condition.isSatisfiedBy(screening);
}
}
- 이렇게 메시지와 메서드를 분리시키면 메시지 전송자와 수신자 간의 결합이 느슨해진다.
- 전송자는 수신자가 어떤 클래스의 인스턴스인지, 어떤 방식으로 요청을 처리하는지 몰라된 된다.
- 수신자도 누가 메시지를 전송하는지 알 필요가 없다.
- 퍼블릭 인터페이스와 오퍼레이션
- 퍼블릭 인터페이스 : 의사소통을 위해 외부에 공개하는 메시지의 집합
- 오퍼레이션 : 퍼블릭 인터페이스에 포함된 메시지
- 위의 코드에서 DiscountCondition 인터페이스에 정의된 isSatisfiedBy가 오퍼레이션(구현이 아닌 추상화)
- PeriodCondtion에 정의된 isSatisfiedBy는 실제 구현을 포함하기 때문에 메서드이다.(오퍼레이션 구현)
- 위의 주석에는 메서드 호출이라 적었지만 퍼블릭 인터페이스와 메시지의 관점에서 보면 오퍼레이션 호출이라는 말이 더 적절하다.
- 시그니처
- 시그니처 : 오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합친 것
- 인터페이스와 설계 품질
- 최소한의 인터페이스 : 꼭 필요한 오퍼레이션만을 인터페이스에 포함
- 추상적인 인터페이스 : 어떻게 수행하는지가 아닌, 무엇을 하는지 표현
- 최소주의를 따르면서 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것
- 디미터 법칙
- 협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙
- 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것
- "오직 하나의 도트만 사용하라" - screening.getMovie().getDiscountCondition() 사용 X
- 아래의 조건을 만족하는 인스턴스에만 메서지를 전송하도록 프로그래밍 한다.
- this 객체 / 메서드의 매개변수 / this의 속성 / this의 속성인 컬렉션의 요소 / 메서드 내에서 생성된 지역 객체
- 디미터 법칙을 준수하면 부끄럼타는 코드(shy code)를 작성할 수 있다.
- 부끄럼 타는 코드 : 불필요한 어떤 것도 다른 객체에게 보여주지 않으며, 다른 객체의 구현에 의존하지 않는 코드
- 디미터 법칙을 따르는 코드는 메시지 수신자의 내부 구조가 전송자에게 노출되지 않으며, 메시지 전송자는 수신자의 내부 구현에 결합되지 않는다.
- screening.getMovie().getDiscountCondition() 이런식으로 사용하는 것을 기차 충돌(train week)라고 부른다.
- 클래스의 내부 구현이 외부로 노출되었을 때 나타나는 형태
- 메시지 수신자의 캡슐화는 무너지고, 메시지 전송자와 메시지 수신자의 내부 정보를 자세히 알게 된다.
- 디미터 법칙은 수신자에게 무언가를 시키는 메시지가 더 좋은 메시지라고 속삭이는 것
- 묻지 말고 시켜라
- 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다는 사실을 강조(디미터 법칙)하는 스타일의 메시지 작성을 장려하는 원칙
- 이 원칙을 따르면 연관된 정보와 행동을 함께 가지는 객체를 만들 수 있다.
- 이 원칙을 따르도록 메시지를 결정하다 보면 정보 전문가에게 책임을 할당하게 되고 높은 응집도를 가진 클래스를 얻을 확률이 높아진다.
- 상태를 묻는 오퍼레이션을 행동을 요청하는 오퍼레이션으로 대체함으로써 인터페이스를 향상시켜라.
- 훌륭한 인터페이스를 수확하기 위해서는 객체가 어떻게 작업하는지 노출시키면 안되고, 무엇을 하는지를 서술해야 한다.
- 의도를 드러내는 인터페이스
- 인터페이스의 의도를 드러내기 위해서 메서드의 이름을 잘 짓는것이 중요하다.
- 메서드의 이름을 '어떻게'가 아니라 '무엇'을 하는지 드러내는 것이다.
- '어떻게' : 내부 구현을 설명하는 이름 - isSatisfiedByPeriod
- '무엇' : 객체가 협력안에서 수행하는 책임 - isSatisfiedBy
- 이렇게 이름을 짓는 패턴을 의도를 드러내는 선택자(Intention Revealing Selector) 라고 부른다.
정리하면, 객체의 내부 구현을 드러내면 안되고, 객체의 메서드명을 무엇때문에 사용하는지 드러나게 해야한다.
객체의 정보 또한 내부 구현이기 때문에 정보를 표현하는 것 또한 내부에 구현을 해야한다.
즉, 객체의 메서드를 사용할 때는 의도만 알면된다.
이렇게 정리할 수 있을 것 같습니다.
- 원칙의 함정
- 디미터 법칙과 묻지 말고 시켜라 스타일은 절대적인 법칙은 아니다.
- 원칙이 현재 상황에 부적합하다고 판단되면 과감하게 원칙을 무시하라.
- 디미터 법칙은 하나의 도트(.)를 강제하는 규칙이 아니다.
IntStream.of(1, 15, 20, 3, 9).filter(x -> x > 10).distinct().count();
- 이 코드는 디미터 법칙을 위반하지 않는다.
- of,filter,distinct 모두 동일한 클래스의 인스턴스를 반환하기 때문
- 디미터 법칙은 결합도와 관련된 것으로 객체의 내부가 노출되었을 경우를 위반했다고 한다.
- 결합도와 응집도의 충돌
- 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션이 공존하게 된다.
- 객체는 상관없는 책임들을 한꺼번에 떠안게 된다.
- 클래스는 하나의 변경 원인만을 가져야 한다.
- 서로 상관없는 책임들이 함께 뭉쳐있는 클래스는 응집도가 낮으며 작은 변경으로 쉽게 무너질 수 있다.
- 내부 구조를 숨기기 위해 과도하게 책임을 위임하는 것은 위험할 수 있다.
- 명령-쿼리 분리 원칙
- 루틴(routine) : 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈
- 루틴은 프로시저와 함수로 구분
- 프로시저 : 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류 - 부수효과는 발생시킬 수 있지만 반환할 수 없다.
- 함수 : 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류 - 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
- 객체의 상태를 수정하는 오퍼레이션을 명령(Command)라 부르고, 객체와 관련된 정보를 반환하는 오퍼레이션을 쿼리(Query)라고 부른다.
- 명령은 반환값을 가질 수 없다.
- 쿼리는 상태를 변경할 수 없다.
- 명령과 쿼리는 명확하게 분리되어야 한다.
- 명령-쿼리 분리와 참조 투명성
- 명령과 쿼리를 엄격하게 분류하면 객체의 부수효과를 제어하기가 수월해진다.
- 명령과 쿼리를 분리함으로써 명령형 언어의 틀 안에서 참조 투명성(referential transparency)의 장점을 제한적이나마 누릴 수 있게 된다.
제가 이해한 것은 명령은 Setter , 쿼리는 Getter이고 예시의 코드에서는 Getter의 역할을 하는 메서드 안에 객체의 상태를 변경시키는 Setter 역할을 하는 메서드가 호출되었습니다.
이 Setter 때문에 코드의 이해가 어렵고 변경에 취약해지기 때문에 분리해서 사용하라는 의미인 것 같습니다.
'Object' 카테고리의 다른 글
| [Object] Chapter07. 객체 분해 (0) | 2025.11.03 |
|---|---|
| [Object] Chapter05. 책임 할당하기 (6) | 2025.08.12 |
| [Object] Chapter04. 설계 품질과 트레이드오프 (0) | 2025.07.26 |
| [Object] Chapter03. 역할, 책임, 협력 (0) | 2025.07.22 |
| [Object] Chapter02. 객체지향 프로그래밍 (0) | 2025.07.15 |