본문 바로가기

Study

객체지향의 사실과 오해 스터디 5장 발표 자료

목차

  • 05 책임과 메시지
    • 자율적인 책임
      • 설계의 품질을 좌우하는 책임
      • 자신의 의지에 따라 증언할 수 있는 자유
      • 너무 추상적인 책임
      • ‘어떻게’가 아니라 ‘무엇’을
      • 책임을 자극하는 메시지
    • 메시지와 메서드
      • 메시지
      • 메서드
      • 다형성
      • 유연하고 확장 가능하고 재사용성이 높은 협력의 의미
      • 송신자와 수신자를 약하게 연결하는 메시지
    • 메시지를 따라라
      • 객체지향의 핵심, 메시지
      • 책임-주도 설계 다시 살펴보기
      • What/Who 사이클
      • 묻지 말고 시켜라
      • 메시지를 믿어라
    • 객체 인터페이스
      • 인터페이스
      • 메시지가 인터페이스를 결정한다
      • 공용 인터페이스
      • 책임, 메시지, 그리고 인터페이스
    • 인터페이스와 구현의 분리
      • 객체 관점에서 생각하는 방법
      • 구현
      • 인터페이스와 구현의 분리 원칙
      • 캡슐화
    • 책임의 자율성이 협력의 품질을 결정한다

05 책임과 메시지

서문

  • 자신만의 명확한 책임을 부여받지 않은 현실의 사람들은 ‘책임감 분산’으로 인해 주어진 책임을 무시하는 경향이 있다.
    • 객체도 사람과 마찬가지로 명확한 책임과 역할을 부여받을 때 좋은 협력자가 될 수 있다.

자율적인 책임

설계의 품질을 좌우하는 책임

  • 객체는 요청을 수신했을 때에만 행동을 수행한다.
    • 따라서 각 객체에게 적절한 책임을 할당해야 한다.
  • 애플리케이션의 품질을 높이려면 모든 객체가 스스로의 의지로 판단하고 행동하는 자율성을 가져야 한다.

자신의 의지에 따라 증언할 수 있는 자유

  • 모자 장수의 책임은 증언하는 것이다.
    • 왕의 요청 방식에 따라, 자율성을 가지고 증언하거나 증언 방식이 왕에게 통제될 수 있다.
      1. 모자 장수에게 자율성을 주는 방식
        • 증언하라
      2. 모자 장수에게 상세하게 요청하는 방식
        1. 목격했던 장면을 떠올려라
        2. 떠오르는 기억을 시간 순서대로 재구성하라
        3. 말로 간결하게 표현하라
  • 왕의 요청
public class King implements Judge {

    private List<Object> baseOfJudgement;

    @Override
    public List<String> progressTrial(Witness witness) {

        baseOfJudgement = new ArrayList<>();

        // 1. 자율 보장
        baseOfJudgement.add(witness.testify());

        // 2. 책임 지시
        ((HatSeller) witness).recall();
        ((HatSeller) witness).reorganize();
        baseOfJudgement.add(((HatSeller) witness).conciselyExpress());

        return decide(baseOfJudgement);

    }
    
    private List<String> decide(List<Object> baseOfJudgement) {

        System.out.println(baseOfJudgement);
        // 1. 자율 보장 판단 근거 : [[contentsOfWitness with explanation(has more contents than interpretation)]]
        // 2. 책임 지시 판단 근거 : [[[contentsOfWitness with interpretation(reduced)](with simple expressions)]]

        baseOfJudgement.add("resultOfJudgement");
        return baseOfJudgement.stream().map(Object::toString).toList();

    }

}
  • 모자 장수의 응답
public class HatSeller implements Witness {

    List<String> memory = new ArrayList<>();
    List<String> memo = new ArrayList<>();
    List<String> recalledMemory;
    List<String> reorganizedMemory;
    List<String> conciseMemory;

    @Override
    public void witness(String contentsOfWitness) {

        memory.add(contentsOfWitness + interpret(contentsOfWitness));
        memo.add(contentsOfWitness + explain(contentsOfWitness));

    }

    private String interpret(String contents) {
        return " with interpretation";
    }

    private String explain(String contents) {
        return " with explanation(has more contents than interpretation)";
    }

    @Override
    public List<String> testify() {

        if (memo.get(memo.size() - 1).length() > memory.get(memory.size() - 1).length()) {
            return memo;
        }

        return memory;

    }

    public void recall() {

        recalledMemory = new ArrayList<>();
        recalledMemory.add(memory.get(memory.size() - 1));

    }

    public void reorganize() {

        reorganizedMemory = recalledMemory;
        Collections.sort(reorganizedMemory);

    }

    public List<String> conciselyExpress() {

        for (int i = 0; i < reorganizedMemory.size(); i = i + 2) {

            conciseMemory = new ArrayList<>();
            conciseMemory.add(reorganizedMemory.get(i) + "(reduced)");

        }

        return selectExpressions(conciseMemory);

    }

    private List<String> selectExpressions(List<String> conciseMemory) {

        List<String> selectedExpressions = new ArrayList<>();
        selectedExpressions.add(conciseMemory + "(with simple expressions)");
        return selectedExpressions;

    }

}
  • 재판 결과
public class TrialApplication {

    public static void main(String[] args) {

        Witness witness = new HatSeller();
        witness.witness("contentsOfWitness");

        Judge judge = new King();

        System.out.println(judge.progressTrial(witness));
        // 1. 자율 보장 결과 : [[contentsOfWitness with explanation
                            (has more contents than interpretation)], resultOfJudgement]
        // 2. 책임 지시 결과 : [[[contentsOfWitness with interpretation(reduced)]
                            (with simple expressions)], resultOfJudgement]

    }

}
  • 자율을 보장하는 경우 왕은 판단 근거로 더 자세한 목격담을 들을 수 있으며, 이를 통해 상세한 판결을 내릴 수 있다.
    • 재판 애플리케이션의 사용자는 더 높은 품질의 실행 결과를 얻게 된다.
  • 책임을 지시하는 경우 왕은 생략된 내용의 목격담만을 들을 수 있으며, 판결 근거가 부실해진다.
    • 재판 애플리케이션의 사용자는 더 낮은 품질의 실행 결과를 얻게 된다.
  • 객체에게 명확한 책임을 부여하면서도 자율성을 보장하면 좋은 애플리케이션을 설계할 수 있다.

너무 추상적인 책임

  • 자율성을 보장하려고 너무 추상적인 책임을 부여하는 것도 문제가 될 수 있다.
    • 상황에 따라 적합한 책임의 수준을 설정할 수 있는 개발자의 안목이 필요하다.

‘어떻게’가 아니라 ‘무엇’을

  • 왕의 두 번째 방식의 요청처럼 ‘어떻게’ 해야 하는지 세세하게 언급하면 모자 장수의 자율성이 제한된다.
  • 요청 시 ‘무엇’에 집중하면 자율적인 객체 간 협력을 구성할 수 있다.

책임을 자극하는 메시지

  • 객체는 요청 메시지를 수신할 때만 행동하므로, 메시지는 객체 간 협력에 있어 핵심적인 역할을 한다.

메시지와 메서드

메시지

  • 객체는 메시지를 통해서만 다른 객체에 접근할 수 있다.
  • 메시지는 수신자(대상 객체 참조 변수명)과 메시지 이름(메서드명), 메시지의 인자(매개변수, parameter)로 구성되어 있다.
    • 왕은 메시지를 보낼 대상이 증인이라는 사실을 인지해야 한다.
    • 왕은 ‘증언하라’라는 메시지 이름을 통해 증인(모자 장수)에게 접근한다.
    • 해당 과정에서 필요하다면 (어제, 왕국)이라는 인자를 함께 전달할 수 있고. 모자 장수는 추가 정보를 활용할 수 있게 된다.
    • 객체참조변수명.메서드명(매개변수1, 매개변수2) 형태로 전송한다.
      • 예) 증인.증언하라(어제, 왕국)
      witness.testify(LocalDate.*now*().minusDays(1), Location kingdom);
      
  • 수신자는 본인이 메시지를 처리할 수 있는지 확인 후, 처리할 수 있다면 반드시 수행해야 할 책임을 갖는다. 즉, 처리 가능한 요청에 대해 거절할 수 없다.
  • 객체는 스스로 책임을 처리하지 않는다. 메시지를 통해 요청을 받았을 때만 처리한다.
  • 객체는 자신의 책임을 자유롭게 처리할 수 있으며, 다른 객체는 해당 과정을 확인하거나 간섭할 수 없다.
    • 따라서 올바른 응답값만 반환할 수 있다면 책임 수행 방법을 자유롭게 변경해도 외부에서 인지하거나 외부에 영향을 미치지 않는다.
    • 이를 통해 내⋅외부의 철저한 분리가 이루어진다.
      • 객체 간 결합도가 감소한다.

메서드

  • 메서드는 메시지를 처리하기 위해 내부에서 선택하는 방법이다.
  • 메시지 수신의 처리 순서는 다음과 같다.
    1. 메시지를 처리할 수 있는지 확인한다.
    2. 메시지를 처리하기 위해 사용할 메서드를 선택한다.
    3. 메시지를 처리한다.
    4. 요청에 대한 처리 결과(응답값)를 반환한다.
  • 객체지향 언어의 핵심적 특징 중 하나는 컴파일 시간이 아니라 실행 시간에 수행 메서드를 선택할 수 있다는 것이다.
public class TrialApplication {

    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        String witnessName = sc.next();

        Witness witness = witnessName.equals("HatSeller") ? new HatSeller()
                : witnessName.equals("Cook") ? new Cook()
                : witnessName.equals("Alice") ? new Alice()
                : null;

        witness.witness("contentsOfWitness");

        Judge king = new King();

        System.out.println(king.progressTrial(witness));

    }

}
  • 재판 애플리케이션의 사용자는 실행 시간에 증언을 수행할 객체를 선택할 수 있으며, HatSeller, Cook, Alice는 각자의 메서드를 통해 수신된 요청을 처리한다.

다형성

  • 다형성은 동일한 메시지를 수신해도 객체마다 다르게 반응하는 것이다.
  • 메시지는 ‘무엇’이 실행될지는 결정하지만 ‘어떻게’ 실행할 것인지는 결정할 수 없다.
    • 따라서 각 객체들은 수신된 메시지를 자율적인 방식으로 처리할 수 있다.
      1. 모자 장수는 기억 상기와 메모 참조 중 하나의 방법을 선택해 진술한다.
      2. 요리사는 항상 메모를 이용해서 진술한다.
      public class Cook implements Witness {
      
          List<String> memory = new ArrayList<>();
          List<String> memo = new ArrayList<>();
      
          @Override
          public void witness(String contentsOfWitness) {
              memo.add(contentsOfWitness);
          }
      
          @Override
          public List<String> testify() {
              return memo;
          }
      
      }
      
      public class Alice implements Witness {
      
          List<String> memory = new ArrayList<>();
      
          @Override
          public void witness(String contentsOfWitness) {
      
              Witness witness = new HatSeller();
              witness.witness(contentsOfWitness);
              hear(witness);
      
          }
      
          @Override
          public List<String> testify() {
              return memory;
          }
      
          private void hear(Witness witness) {
              memory = witness.testify();
          }
      
      }
      
      3. 앨리스는 사건을 직접 목격하지 않고 모자 장수에게 목격하도록 요청한다. 이후 모자 장수에게 전해 들은 내용을 토대로 진술한다.
    • 왕은 해당 과정이나 수신자의 종류를 알 수 없으며, 원하는 결과만 얻으면 되기 때문에 신경 쓰지 않는다.
  • 다형성을 반대로 표현하면, 서로 다른 각 객체들이 동일한 책임을 가진다는 것을 의미한다.
  • 다형성은 수신자의 종류를 캡슐화하여 설계의 유연성과 재사용성을 증진한다.
  • 다형성을 통해 객체 타입 결합도를 메시지 결합도로 바꿔 결합을 약하게 할 수 있다.

유연하고 확장 가능하고 재사용성이 높은 협력의 의미

  • 객체 간 정보가 적으면 다음과 같은 장점이 있다.
    1. 송신자가 수신자를 구체적으로 선택할 필요가 없으므로 협력이 유연해진다.
    2. 수신자의 변경은 송신자에게 영향을 미치지 않으므로, 요청 수행 방식을 쉽게 변경할 수 있다.
    3. 같은 요청을 여러 객체들이 수행할 수 있기 때문에, 다양한 방식으로 협력의 수행 과정을 재사용할 수 있다.

송신자와 수신자를 약하게 연결하는 메시지

  • 메시지만을 통해 상호작용하는 객체의 관계는 객체 간 결합도를 낮춰 설계의 유연성, 확장성, 재사용성을 증진한다.
    • 따라서 좋은 메시지를 선택하는 것이 중요하다.

메시지를 따라라

객체지향의 핵심, 메시지

  • 객체지향 애플리케이션은 객체들 간 연쇄적인 메시지의 송∙수신을 통한 협력으로 구축된다.
  • 객체지향의 위력은 객체들의 특성과 행위를 텍스트로 표현하는 클래스가 아니라 객체들 간 협력을 위한 메시지에서 나온다.
  • 데이터-주도 설계는 데이터를 중심으로 객체를 설계하는 것으로서, 객체의 내부 구조를 드러냄으로써 자율성이 감소한다.
  • 좋은 책임 설계는 객체 간 협력에서 각자에게 무엇을 제공하고 무엇을 얻느냐를 고려하는 메시지에 달려 있다.
  • 메시지 중심 설계를 통해 객체가 메시지를 선택하는 것이 아니라, 메시지가 객체를 선택해야 한다.

책임-주도 설계 다시 살펴보기

  • 책임-주도 설계는 책임을 다하기 위해 협력하는 객체들을 중심으로 설계하는 기법이다.
    1. 애플리케이션이 수행하는 여러 기능을 시스템의 책임으로 인식한다.
    2. 이를 구현하기 위해 협력할 객체들을 찾는다.
    3. 책임을 수행하기 위해 다른 객체의 도움이 필요하면 요청에 필요한 메시지를 결정한다.
    4. 결정된 메시지를 수신하기 적합한 객체를 선택한다.
    5. 수신자도 다른 객체의 도움이 필요하면 같은 과정을 반복한다.

What/Who 사이클

  • 책임-주도 설계에서 What/Who 사이클은 필요한 행위(메시지 이름)를 먼저 결정하고 이후에 그 행위를 수행할 객체를 결정하는 과정을 뜻한다.
    • 객체의 속성은 행위를 결정하지 않는다. 필요한 행위가 있으면 그때 수행 가능한 객체를 찾는 것이다.
    • 이 과정을 통해 인터페이스를 설계한다.
      • 이러한 인터페이스 설계 방법은 테스트-주도 설계의 핵심 아이디어가 된다.

묻지 말고 시켜라

  • 데메테르 법칙은 메시지를 먼저 결정하고, 해당 메시지를 객체가 따르게 하는 방식으로 인터페이스를 설계하는 기법이다.
  • 메시지를 결정하는 시점에 어떤 객체가 메시지를 수신할지 미리 알지 못하면, 수신자의 내부 구조를 고려한 메시지를 작성할 수 없게 된다.
    • 객체의 자율성을 높이고 수신자의 캡슐화를 증진하며, 송신자와 수신자의 결합도가 낮아진다.
  • 메시지가 ‘어떻게’ 해야 하는지가 아닌 ‘무엇’을 해야 하는 지를 담으면 인터페이스가 간소화된다.
    • 외부에서의 해당 객체 의존도가 낮아진다.
      • 객체 간 결합도가 낮아지고 설계가 유연해진다.

메시지를 믿어라

  • 객체지향의 전체 시스템은 메시지를 통한 객체 연결이 모여 완성된다.
    • 이를 통해 유연(협력 대상 자유롭게 교체)하고 재사용성(동일한 협력 과정에 다양한 타입 객체 참여)이 높은 설계를 할 수 있다.

객체 인터페이스

인터페이스

  • 인터페이스는 두 사물이 만나는 경계에서 서로 상호작용할 수 있도록 연결해 주는 방법 또는 장치를 의미한다.
    • 말, 글, 몸짓, (스마트폰을 사용하는) 손가락, 리모컨, 엘리베이터 버튼, GUI, API는 모두 인터페이스이다.
  • 인터페이스의 특징은 다음과 같다.
    1. 내부 구조 또는 동작 방식을 알지 못해도 인터페이스의 사용 방법만 알면 해당 객체를 사용할 수 있다(추상화).
    2. 내부 구조의 변경은 인터페이스에게 전혀 영향을 끼치지 않는다(캡슐화).
    3. 수신 대상 객체가 바뀌어도 인터페이스가 동일하면 문제없이 상호작용할 수 있다(다형성).
    • 예) 인터페이스를 가지는 자동차의 특징은 다음과 같다.
      1. 내부 구조 또는 동작 방식을 알지 못해도 인터페이스를 통해 자동차를 운전할 수 있다(추상화).
      2. 내부 구조가 변해도 동일한 방식으로 운전할 수 있다(캡슐화).
      3. 모든 종류의 자동차는 운전자에게 동일한 인터페이스를 제공한다(다형성).

메시지가 인터페이스를 결정한다

  • 객체가 수신할 수 있는 메시지가 인터페이스의 형태를 결정한다.

공용 인터페이스

  • 외부에 공개된 인터페이스를 공용 인터페이스라고 한다.
  • 객체지향의 모든 상호작용은 메시지를 통해 이루어지므로, 사적인 인터페이스도 스스로에게 메시지를 전송해야만 접근할 수 있다.
  • 왕이 모자 장수와 협력할 수 있는 유일한 수단은 ‘증언하라’라는 메시지를 보내는 것이다.
    • 모자 장수의 공용 인터페이스는 ‘증언하라’라는 메시지를 수신할 수 있어야 한다.
    • 메시지는 공용 인터페이스의 형태를 결정한다.
public interface Witness {

    void witness(String contentsOfWitness);

    List<String> testify();

}

책임, 메시지, 그리고 인터페이스

  • 객체지향의 힘은 외부와 내부를 분리하는 것에 있다.

인터페이스와 구현의 분리

객체 관점에서 생각하는 방법

  • 객체지향에서 인터페이스의 세 가지 원칙은 다음과 같다.
    1. 추상적인 인터페이스
      • 예) ‘증언하라(testify)’
    2. 최소 인터페이스
      • 예) ‘재판하라(progressTrial)’
      public interface Judge {
      
          List<String> progressTrial(Witness witness);
      
      }
      
    3. 인터페이스와 구현의 차이 인식

구현

  • 객체에서 공용 인터페이스에 포함되지 않는 모든 부분은 구현에 속한다.
    • 구현은 상태(field), 행동(method)으로 구성되어 있다.
  • 구현과 공용 인터페이스를 통해 객체의 내⋅외부를 구분한다.
    • 설계를 단순하고 유연하게 해 준다.

인터페이스와 구현의 분리 원칙

  • 좋은 객체는 서로의 구현을 모른 상태에서 인터페이스를 통해 상호작용하는 객체다.
  • 소프트웨어는 항상 변경되기 때문에, 인터페이스와 구현의 분리를 통해 변경의 파급효과를 최소화해야 한다.
    • 변경 시 외부에 영향을 미치는 위험 지대를 공용 인터페이스로 한정함으로써 안전 지대를 늘린다.
      • 변경에 의한 파급효과가 감소한다.
      • 외부에 영향을 미치지 않고 내부 구조를 변경할 수 있으므로, 객체의 자율성이 향상된다.
      • 이를 캡슐화라고 한다.

캡슐화

  • 캡슐화는 객체의 상태와 행위를 캡슐화함으로써 내부의 정보를 은닉하는 것이다.

상태와 행위의 캡슐화

  • 데이터 캡슐화라고 한다.
  • 객체는 상태와 행동이 합쳐진 자율적인 단위다.
    • 주로 상태는 data, 행동은 process로 구현된다.
      • 전통적인 개발 방식과 달리, data와 process를 엄격하게 구분하지 않고 통합해서 관리한다.
  • 객체의 상태와 행동 중 외부의 요청 수신에 필요한 일부 행동만을 공용 인터페이스를 통해 외부에 노출한다.
  • 상태는 객체 스스로 관리한다. 따라서 외부에 노출하지 않으며, 이는 객체를 자율적으로 만들어 준다.

사적인 비밀의 캡슐화

  • 외부 객체로부터 내부 정보를 은닉함으로써 달성한다.
    • 객체 내부의 변경사항을 외부에서 알지 못하도록 한다.
      • 외부의 공격과 간섭으로부터 내부를 보호함으로써 객체의 자율성을 보장할 수 있다.
    • 내부 정보를 은닉하기 위해 따로 외부와 소통할 수 있는 공용 인터페이스를 사용한다.
      • 외부 객체는 요청 시 대상의 인터페이스에만 의존하고, 세부 구현 사항에 기반한 메시지를 전송할 수 없다.

책임의 자율성이 협력의 품질을 결정한다

  • 책임의 자율성을 보장하면 이해가능성과 유연성이 증진된다.
    • 이는 다음과 같은 협력 설계 품질의 향상으로 귀결된다.
      1. 협력과정을 단순하게 한다.
        • 왕은 단순히 ‘증언하라’라고만 요청함으로써 세부 책임을 명시하는 경우에 비해 쉽게 모자 장수와 협력할 수 있다.
        • 세부적인 사항들을 배제하고 단순한 공통사항만을 드러내는 추상화를 달성할 수 있다.
      2. 내∙외부를 명확하게 분리한다.
        • 모자 장수는 자신의 증언 방법을 자율적으로 선택할 수 있다.
        • 외부 객체로부터 내부 구현을 감추는 캡슐화를 달성할 수 있다.
      3. 내부 구현을 변경하더라도 외부에 영향을 주지 않는다.
        • 자율성을 보장함으로써, 모자 장수가 어떤 방식으로 증언하든 왕은 동일하게 원하는 정보를 얻을 수 있다.
        • 객체 간 결합도를 낮춤으로써, 내부의 구현사항으로 인해 외부의 메시지를 변경할 필요가 없어진다.
      4. 다양한 협력 대상을 선택할 수 있다.
        • 증언 방식의 자율성이 보장되므로, 모자 장수뿐만 아니라 요리사나 앨리스도 증언이 가능하다.
        • 동일한 책임을 수행 가능한 객체가 다양해지므로, 유연성과 재사용성이 증가한다.
      5. 객체 역할에 대한 이해가 쉬워진다.
        • ‘증언하라’라는 책임은 모자 장수의 역할을 명확하게 이해할 수 있게 해 준다.
        • 객체 내부의 책임이 강하게 연결되므로, 객체의 응집도가 높아진다.