본문 바로가기
Study/오브젝트

[오브젝트] 6장. 메시지와 인터페이스

by Nahwasa 2022. 12. 8.

스터디 메인 페이지

목차

    - ☆ 표시가 붙은 부분은 스터디 중 나온 얘기 혹은 제 개인적인 생각이나 제가 이해한 방식을 적어놓은 것으로, 책에 나오지 않는 내용입니다. 따라서 책에서 말하고자 하는 바와 다를 수 있습니다.

    - 모든 이미지의 출처는 오브젝트(조용호 저) 책 입니다.

     

     


    CHAPTER 06. 메시지와 인터페이스

    ⚈ 훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다. -> 즉, 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다.

    • 책임이 객체가 수신할 수 있는 메시지의 기반이 된다.
    • 애플리케이션은 클래스로 구성되지만 메시지를 통해 정의된다.

     

    이번 장은 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 데 도움이 되는 설계 원칙과 기법을 살펴보는 것이 주제이다.

     


    01 협력과 메시지

    ⚈ 클라이언트-서버(Client-Server) 모델

    • 협력은 어떤 객체가 다른 객체에게 무언가를 요청할 때 시작된다. -> 객체가 다른 객체에게 접근할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐이다.
    • 두 객체 사이의 협력 관계를 설명하기 위해 사용하는 전통적인 메타포가 클라이언트-서버 모델
    • 클라이언트 : 메시지를 전송하는 객체
    • 서버 : 메시지를 수신하는 객체
    • 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.

     

    객체는 협력에 참여하는 동안 클라이언트와 서버 역할을 동시에 수행하는 것이 일반적이다.

     

    객체가 독립적으로 수행할 수 있는 것보다 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다 -> 두 객체 사이의 협력을 가능하게 해주는 매개체가 바로 메시지이다.

     

    용어 정리

    • 메시지 : 객체들이 협력하기 위해 사용할 수 있는 유일한 의사소통 수단
      • 객체가 다른 객체와 협력하기 위해 사용하는 의사소통 매커니즘. 
      • 메시지는 오퍼레이션명(operation name)과 인자(argument)로 구성되며, 메시지 전송은 여기에 메시지 수신자를 추가한 것.
    • 메시지 전송(메시지 패싱) : 한 객체가 다른 객체에게 도움을 요청하는 것. 메시지 수신자, 오퍼레이션명, 인자의 조합.
      • condition.isSatisfiedBy(screening); // condition: 수신자, isSatisfiedBy: 오퍼레이션명, screening: 인자
    • 메시지 전송자(sender), 클라이언트 : 메시지를 전송하는 객체. 
    • 메시지 수신자(receiver), 서버 : 메시지를 수신하는 객체
    • 메서드 : 메시지를 수신했을 때 실제로 실행되는 함수 또는 프로시저
    • 퍼블릭 인터페이스 : 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합
    • 오퍼레이션 : 퍼블릭 인터페이스에 포함된 메시지
    • 시그니처 : 오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 부르는 것. 

     


    02 인터페이스와 설계 품질

    ⚈ 좋은 인터페이스

    • 최소한의 인터페이스 : 꼭 필요한 오퍼레이션만을 인터페이스에 포함
    • 추상적인 인터페이스 : 어떻게 수행하는지가 아니라 무엇을 하는지를 표현
    • 좋은 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것.

     

    퍼블릭 인터페이스의 품질에 영향을 미치는 원칙과 기법

    • 디미터 법칙
    • 묻지 말고 시켜라
    • 의도를 드러내는 인터페이스
    • 명령-쿼리 분리

     

    디미터 법칙(Law of Demeter)

    • 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라.
    • 디미터 법칙을 위반하는 설계는 인터페이스와 구현의 분리 원칙을 위반한다. 또한 위반할 경우 다른 객체의 내부 구조까지 속속들이 알고 있어야 하므로 사용하기도 어렵다.
    • "오직 하나의 도트만 사용하라" -> 단, 디미터 법칙은 객체의 내부 구조가 외부로 노출되는 경우의 결합도에 관한 이야기 이므로, 무조건 하나의 도트만 쓰라는게 아니다. 예를들어 이하 코드와 같이 IntStream이라는 동일한 인스턴스를 반환하는 경우에는 해당되지 않는다.(198page 내용) 
    IntStream.of(1, 15, 20, 3, 9).filter(x->x>10).distinct().count();
    • 이하는 디미터 법칙을 위반하는 코드의 전형적인 모습이다. 전송자가 수신자의 내부 구조에 대해 물어보고 반환받은 요소에 대해 연쇄적으로 메시지를 전송한다. -> 기차 충돌(train wreck) -> 클래스의 내부 구현이 외부로 노출된 경우이다.
    screening.getMovie().getDiscountConditions();

     

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

    • 객체의 상태에 관해 묻지 말고 원하는 것을 시켜야 한다.
    • 즉, 메시지 전송자가 수신자의 상태를 기반으로 결정을 내린 후 메시지 수신자의 상태를 바꿔서는 안 된다.
    • 예시
    // 묻지 말고 시켜라 원칙 위배
    public class Audience {
    	public Long setTicket(Ticket ticket) {
        	if (bag.hasInvitation()) {
            	...
            } else {
            	...
            }
        }
    }
    
    VS
    
    // 묻지 말고 시켜라 원칙에 맞게 수정한 코드
    public class Audience {
    	public Long setTicket(Ticket ticket) {
        	return bag.setTicket(ticket);
        }
    }

     

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

    • 메서드가 어떻게 수행하느냐가 아니라 무엇을 하느냐에 초점을 맞추면 클라이언트의 관점에서 동일한 작업을 수행하는 메서드들을 하나의 타입 계층으로 묶을 수 있는 가능성이 커진다.
    • 객체의 퍼블릭 인터페이스에 어떤 이름이 드러나야 하는지에 대한 지침을 제공함으로써 코드의 목적을 명확하게 커뮤니케이션할 수 있게 해준다.
    // 메서드가 작업을 어떻게 수행하는지
    public class PeriodCondition {
    	public boolean isSatisfiedByPeriod(Screening screening) {...}
    }
    public class SequenceCondition {
    	public boolean isSatisfiedBySequence(Screening screening) {...}
    }
    
    VS
    
    // 메서드가 무엇을 수행하는지
    public interface DiscountCondition {
    	boolean isSatisfiedBy(Screening screening);
    }
    public class PeriodCondition implements DiscountCondition {
    	public boolean isSatisfiedBy(Screening screening) {...}
    }
    public class SequenceCondition implements DiscountCondition {
    	public boolean isSatisfiedBy(Screening screening) {...}
    }

     


    03 원칙의 함정

    ⚈ 설계는 트레이드오프의 산물이다. 설계를 적절하게 트레이드오프 할 수 있는 능력이 숙련자와 초보자를 구분하는 가장 중요한 기준이다.

     

    묻지 말고 시켜라와 디미터 법칙을 준수하는 것이 항상 긍정적인 결과로만 귀결되는 것은 아니다. 모든 상황에서 맹목적으로 위임 메서드를 추가하면 같은 퍼블릭 인터페이스 안에 어울리지 않는 오퍼레이션들이 공존하게 된다. -> 응집도가 낮아진다.

     

    객체에게 시키는 것이 항상 가능한 것은 아니다. 가끔씩은 물어야 한다. 원칙을 맹신하지 마라.

     


    04 명령-쿼리 분리 원칙

    ⚈ 용어 정의

    • 루틴(routine) : 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능 모듈. 프로시저와 함수로 구분할 수 있다.
    • 프로시저(procedure) : 정해진 절차에 따라 내부의 상태를 변경하는 루틴의 한 종류이다. 부수효과를 발생시킬 수 있지만 값을 반환할 수 없다.
    • 함수(function) : 어떤 절차에 따라 필요한 값을 계산해서 반환하는 루틴의 한 종류이다. 값을 반환할 수 있지만 부수효과를 발생시킬 수 없다.
    • 명령(Command) : 객체의 인터페이스 측면에서 프로시저를 부르는 또 다른 이름. 객체의 상태를 수정하는 오퍼레이션.
    • 쿼리(Query) : 객체의 인터페이스 측면에서 함수를 부르는 또 다른 이름. 객체와 관련된 정보를 반환하는 오퍼레이션.

     

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

    • 오퍼레이션은 부수효과(side effect)를 발생시키는 명령이거나 부수효과를 발생시키지 않는 쿼리 중 하나여야 한다
    • "질문이 답변을 수정해서는 안 된다"
    • 쿼리는 객체의 상태를 변경하지 않기 때문에 몇 번이고 반복적으로 호출하더라도 상관이 없다. -> 명령이 개입하지 않는 한 쿼리의 값은 변경되지 않는다. 또한 쿼리들의 순서를 자유롭게 변경할 수도 있다.
    • 부수효과를 가지는 명령으로부터 부수효과를 가지지 않는 쿼리를 명백하게 분리함으로써 제한적이나마 참조 투명성의 혜택을 누릴 수 있게 된다.
      • 참조 투명성(referential transparency) : "어떤 표현식 e가 있을 때 e의 값으로 e가 나타나는 모든 위치를 교체하더라도 결과가 달라지지 않는 특성"

    댓글