Objects Study - Chapter2. 객체지향 프로그래밍

객체지향을 설계하는 방법

  • 클래스가 아닌 실제 working하는 instance(객체)에 초점을 둔다.
  • 클래스는 객체들의 특징을 추상화 한 것
  • 클래스 -> 객체가 아닌 객체들을 정의하고 객체들의 특징을 기반으로 클래스를 정의한다.
  • 객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
  • 객체는 독립적인 존재가 아닌 기능을 구현하기 위해 협력하는 구성원의 일원으로 봐야한다.
    • 객체를 협력하는 공동체의 일원으로 바라보게 되면 설계를 유연하고 확장 가능하게 만든다.

따라서 객체를 나열 -> 타입으로 분류 -> 클래스 작성 하는 순서를 따르는 것이 좋다.

도메인 구조를 따르는 프로그램 구조

도메인이란? 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
도메인 개념을 구현하기 위해 클래스를 사용
클래스 이름은 도메인 개념과 동일하거나 최대한 유사하게 짓는다.

클래스를 설계할 때는 내부와 외부로 나눈다.

  • 내부는 private, 외부에 공개할 것은 public으로 만든다.
  • 경계의 명확성이 객체의 자율성을 보장한다.
  • 클래스를 사용하는 입장에서는 public으로 공개된 인터페이스만 알면 된다. (내부의 구현에 대해서는 알 필요가 없다.)
  • 클래스를 만들고 수정하는 입장에서는 private으로 된 부분은 클래스 외부에서 참조 하지 않으니 마음대로 구현을 변경해도 된다.
  • 이러한 내/외부의 경계를 잘 나눠야 하는 이유는 변경을 관리 하기 위해서이다.
    • 변경될 가능성이 있는 세부적인 구현 내용을 private 영역에 감춤으로써 변경으로 인한 혼란을 최소화 할 수 있다.

객체는 상호 협력하는 존재들이다.

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있다.
  • 요청 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.

객체의 협력

1
2
3
4
5
6
7
class A(
private val b: B
) {
fun call() {
b.method() // 의존성을 가지는 B 클래스의 method 라는 메세지를 전송할 수 있다.
}
}

1
2
3
4
5
class B {
fun method() {
doAction()
}
}

  • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메세지를 전송(send a message) 하는 것 뿐이다.
    (의역하자면 다른 객체가 가지고 있는 공개된 인터페이스의 method signature를 호출한다.)
  • 메세지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메세지를 처리할 방법을 결정한다.
    수신된 메세지를 처리하기 위한 자신만의 방법을 메서드(method)라고 부른다.
    (의역하자면 요청 받은 클래스의 인터페이스를 구현한 mehtod body 부분을 메서드라고 하는 것 같다.)
  • 메세지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발한다.
    (메세지 : method signature, 메서드 : 실제 method 구현부)

상속

  • 상속은 객체 지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
  • 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메세지를 수신할 수 있다.
    그렇기 때문에 자식 클래스는 부모 클래스와 동일한 Type으로 볼 수 있다.
  • 상속은 부모 클래스의 코드를 기반으로 전혀 수정하지 않거나 일부분을 추가/수정 하여 새로운 클래스를 만들 수 있다. (코드의 재사용)
    • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍 (programming by difference)라 한다.
  • 자식 클래스가 부모 클래스를 대신하는 것을 업 캐스팅(up casting)이라 한다.

상속의 단점

  • 상속은 캡슐화를 위반한다.
    • 상속을 이용하기 위해선 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
    • 부모 클래스의 구현이 자식 클래스에 노출 되기 때문에 캡슐화가 약화된다.+
    • 캡슐화는 자식 클래스와 부모 클래스의 강결합을 만들어 부모 클래스가 변경되야 하는 경우 자식 클래스도 함께 변경되게 된다.
    • 상속을 과도하게 사용하면 코드를 변경하기 어렵게 된다.
  • 설계를 유연하게 하지 못한다.
    • 부모 클래스와 자식 클래스의 관계를 컴파일 타임에 결정한다.

합성 (composition)

  • 인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법을 합성이라 한다.
  • 인터페이스에 정의된 메세지를 통해서만 재사용이 가능하기 때문에 구현을 캡슐화 하기 용이하다
  • 의존하는 인스턴스를 교체하는 것이 비교적 쉬워 설계를 유연하게 만든다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Movie {
    private DiscountPolicy discountPolicy;

    public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
    }
    }


    Movie avartar = new Movie(new AmountDiscount());
    avartar.changeDiscountPolicy(new PercentDiscountPolicy());

다형성

  • 동일한 메세지를 수신할 수 있지만, 어떤 메서드가 실행될 지는 수신하는 클래스에 따라 달라진다.
    (인터페이스의 메서드를 호출할 수 있지만 메서드의 실행은 인터페이스를 구현한 클래스에 따라 달라진다.)
  • 실행되는 메서드는 Runtime에 결정된다.
    • 이를 Lazy Binding 또는 Dynamic Binding이라 한다.

다형성의 Trade off

  • 다형성을 사용하게 되면 코드의 의존성과 Runtime의 의존성이 서로 다를 수 있다.
    • 어떤 클래스를 주입하느냐에 따라 의존성이 달라진다.
    • 하지만 코드 상으로는 인터페이스 / 추상클래스에 대한 의존성만 가지고 있다.
  • 확장 가능한 객체 지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다.
  • 코드의 의존성과 실행시점의 의존성이 다르면 코드를 이해하기 어렵고 디버깅을 어렵게 만든다.

추상화의 힘

  • 추상화 계층만 따로 떼어 놓고 보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
    (뒤에 자세한 구현부는 모르겠지만 전체적인 틀을 잡기 편하다)
  • 추상화를 이용하면 설계가 조금 더 유연해 진다.
  • Runtime에 주입되는 코드는 추상화를 이용해서 정의한 상위의 흐름을 따라가게 된다.
  • 디자인 패턴(design pattern)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 매커니즘을 활용하고 있다.
  • 추상화를 이용하여 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
  • 유연성이 필요한 곳에 추상화를 사용하라

참고

  • Objects(코드로 이해하는 객체지향 설계) - 객체지향 프로그래밍