포스트

2장. 객체지향 프로그래밍

2장. 객체지향 프로그래밍


❐ 1. 영화 예매 시스템


🌀 1-1. 요구사항 살펴보기

용어 정리

  • 영화 : 영화에 대한 기본정보를 표현
  • 상영 : 실제로 관객들이 영화를 관람하는 사건을 표현


요금 할인 관련

  • 할인 조건 : 할인 여부 결정
    • 순서 조건
    • 기간 조건
  • 할인 정책 : 할인 요금 결정 (1개 이하만 가능)
    • 금액 할인 정책
    • 비율 할인 정책


❐ 2. 객체지향 프로그래밍을 향해


🌀 2-1. 협력, 객체, 클래스

진정한 객체지향 페러다임으로의 전환 : 클래스가 아닌 객체에 초점을 맞춰라.

  • 어떤 객체가 필요한지 고민하라.
  • 객체를 독립적인 존재가 아닌, 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐라.


🌀 2-2. 도메인의 구조를 따르는 프로그램 구조

도메인

  • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야


🌀 2-3. 클래스 구현하기

클래스의 경계를 구분지어라.

  • 왜?
    • 경계의 명확성이 객체의 자율성을 보존하기 때문.
    • (더 중요한 포인트) 프로그래머에게 구현의 자유를 제공하기 때문.


자율적인 객체

  • 객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해함이다.
  • 자율적인 객체를 위해 우리는 캡슐화, 접근 제어 메커니즘을 사용한다.
  • 인터페이스와 구현의 분리 원칙은 훌륭한 객체지향 프로그래밍을 만들기 위해 따라야하는 핵심 원칙
    • 인터페이스 : 외부에서 접근 가능한 부분
    • 구현 : 외부에서 접근할 수 없는 내부 동작
  • 이 원칙은 인터페이스(외부 계약)는 그대로 두고 구현(내부 로직)만 바꾸어도 외부 코드(클라)는 아무 영향을 받지 않음.


프로그래머의 자유

  • 두 종류의 프로그래머
    • 클래스 작성자 (class creator)
      • 새로운 클래스(데이터 타입)를 만드는 사람
      • 내부 구현을 어떻게 짤지 결정함
    • 클라이언트 프로그래머 (client programmer)
      • 만들어진 클래스를 사용하는 사람
      • 인터페이스만 보고 사용함
  • 접근 제어(private 등)는 ‘구현 숨기기’를 위한 언어 지원장치


구현 은닉

  • 클래스 내부 구현은 숨기고, 외부에는 인터페이스만 공개한다.


인터페이스와 구현을 분리하면 무엇이 좋아지는가?

  • 비공개 영역(private)은 마음대로 바꿀 수 있다.
  • 의존성을 줄여 유지보수성과 확장성이 크게 올라간다.
  • 궁극적으로 “설계가 쉬워지고 변경이 쉬워진다”
    • (기억하기) 객체지향 설계의 목적 = “변경에 유연한 시스템 만들기”


🌀 2-4. 협력하는 객체들의 공동체

협력 (Collaboration)

  • 객체지향 프로그램을 작성할 때는
    1. 협력의 관점에서 어떤 객체가 필요한지를 결정
    2. 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성


🌀 2-5. 협력에 관한 짧은 이야기

객체간 상호작용

  • 객체는 요청/응답을 할 수 있다.
  • 서로 다른 객체가 상호작용을 할 수 있는 유일한 방법은? 메시지 전송
  • 수신된 메시지를 처리하는 자신만의 방법을 메소드(method)라고 한다.


메시지 ≠ 메서드

  • 메시지(message)
    • 한 객체가 다른 객체에게 “이 행동을 해달라”는 요청을 보내는 의사소통 방식.
    • 즉, 어떤 객체에게 어떤 동작을 요구하는 “신호”나 “명령”인 셈.
    • 예) screening.reserve(...)처럼 “예약해 줘”라는 메시지를 Screening 객체에게 전달하는 것.
  • 메서드(method)
    • 메시지를 받은 객체가 자신만의 방식으로 그 메시지를 처리하는 구체적인 알고리즘/함수.


❐ 3. 할인 요금 구하기


🌀 3-1. 할인 요금 구하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
    private final List<DiscountCondition> conditions;

    public DefaultDiscountPolicy(DiscountCondition... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    @Override
    public Money calculateDiscountAmount(Screening screening) {
        for(DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening Screening);
}

  • 하나의 할인 정책은 여러 개의 할인 조건을 포함할 수 있음.


❐ 4. 상속과 다형성


🌀 4-1. Compile-time 의존성 vs Run-time 의존성

Movie는 어떤 구현체를 선택할까?

  • 현재 Movie와 DiscountPolicy는 의존성을 가지고 있음. (Movie -> DiscountPolicy)
  • 즉, 현재 구성도만으로는 Movie가 어떤 ‘DiscountPolicy 구현체’를 사용할지 알 수 없음.


Compile-time 의존성 ≠ Run-time 의존성

  • 이는 다형성과 의존성 역전(DIP) 원칙 덕분에 가능하다.
  • 컴파일 타임에는 추상화(인터페이스, 상위 타입)에만 의존하지만, 런타임 시점에는 실제 구현체가 결정된다.
  • SpringBoot의 의존성 주입(DI)이 대표적인 예다.


🌀 4-2. 차이에 의한 프로그래밍

차이에 의한 프로그래밍

  • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법
  • 상속 : 객체지향 코드를 재사용하기 위해 가장 널리 사용되는 방법


개인적인 생각 : 상속 별로임

  • 부모에 너무 종속적임
  • 그리고 변화에 굉장히 취약함.
  • 상속 보다는 구성(Composition)을 사용해서 개발하는게…


🌀 4-3. 상속과 인터페이스

상속 & 인터페이스

  • 인터페이스는 객체가 이해할 수 있는 메시지의 목록을 정의한다는 것을 기억해라.
  • 상속을 통해 자식클래스는 자신의 인터페이스 + 부모의 인터페이스를 포함하게 된다.
    • 즉, 자식은 부모가 수신할 수 있는 모든 메시지를 수신할 수 있음.
    • 고로 자식과 부모는 같은 타입으로 간주함.


🌀 4-4. 다형성

다형성에 대해서…

  • 다형성은
    • 객체지향 프로그램의 compile-time 의존성과 run-time 의존성이 다를 수 있음을 기반으로 한다.
    • 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
  • 다형성을 구현하는 방법은 매우 다양하지만
    • 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.


지연 바인딩(lazy binding) or 동적 바인딩(dynamic binding)

  • 객체지향이 compile-time 의존성과 run-time 의존성을 분리하고,
    하나의 메시지를 선택적으로 서로 다른 메서드에 연결할 수 있는 이유
  • 다시 말해 메시지와 메서드를 실행 시점에 바인딩한다는 것이다.


다형성이란 추상적인 개념일 뿐!

  • 클래스를 상속받는 것 만이 다형성을 구현할 수 있는 유일한 방법이 아님.
  • 다른 방법들에 뭐가 있을까?
    • 인터페이스(Interface)
    • 추상 클래스(Abstract Class)
    • 구성(Composition)과 위임(Delegation)
    • 함수형 프로그래밍의 함수 객체, 람다(Function Object, Lambda)


❐ 5. 추상화와 유연성


🌀 5-1. 추상화의 힘

추상화를 사용할 때의 장점

  1. 추상화 계층만 따로 떼어놓고 보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
    • 이런 특징은 세부사항이 없음에도, 상위 개념만으로 도메인의 중요한 개념을 설명할 수 있게 한다.
    • 상위 정책을 기술한다는 것은? 애플리케이션 협력 흐름을 기술한다는 것.
  2. 설계가 유연해진다.
    • 추상화를 이용한 설계는 필요에 따라 표현의 수준을 조정하는 것을 가능하게 해준다.
      • 추상화 수준을 올리면 ➔ 여러 구체적인 것들을 하나의 타입으로 다루고, 변화에 유연해짐
      • 추상화 수준을 내리면 ➔ 개별적인 특성을 더 세밀하게(상세하게) 표현


🌀 5-2. 유연한 설계

일관성 있던 협력 방식이 깨지는 케이스

1
2
3
4
5
6
7
8
9
10
11
12
public class Movie {
    private Money fee;
    private DiscountPolicy discountPolicy;
    
    public Money calculateDiscountAmount(Screening screening) {
        if (discountPolicy == null) {
            return fee;
        }
        
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}
  • discountPolicy == null 일 때는 discountPolicy 와의 협력이 깨지게 됨.
  • discountPolicy가 결정하지 않고, Movie 객체에서 그냥 fee를 반환함.


다시 일관성 있는 협력을 하게 하려면

1
2
3
4
5
6
public class NoneDiscountPolicy implements DiscountPolicy {
    @Override
    public Money calculateDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}
  • DiscountPolicy가 결정을 하도록, 새로운 구현체를 추가하면 됨.
  • 여기서 중요한 포인트
    • Movie, DiscountPolicy는 수정하지 않았다.
    • 단지 새로운 DiscountPolicy 구현체(NoneDiscountPolicy)를 추가하여 애플리케이션의 기능을 확장했다.


결론

  • 추상화는 구체적인 상황에 결합되는 것을 방지하기 때문에 유연한 설계를 가능하게 한다.
  • 유연성이 필요한 곳에 추상화를 사용하라.


🌀 5-3. 추상 클래스와 인터페이스 트레이드 오프

개념이 다르다면 인터페이스로 역할을 분리하라.

  • AmountDiscountPolicy, PercentDiscountPolicy는 “조건을 만족하면 할인 계산을 한다”라는 공통된 개념을 가진다.
    • NoneDiscountPolicy는 개념적으로 다른 정책이다.
    • “무조건 0원을 반환한다”라는 완전히 다른 개념
  • 기존 설계에서는
    • NoneDiscountPolicy가 DiscountPolicy 추상 클래스의 구현 방식에 의존하고 있었다
    • 즉, 개념적 결합 + 잘못된 상속
  • 따라서 개념이 다른 두 역할은 상속으로 묶이지 말아야 한다.
  • 해결책은 전략 패턴
    • 공통된 역할 = 인터페이스(DiscountPolicy)
    • 공통 구현 = DefaultDiscountPolicy
    • 변화되는 구현 = 각 정책 클래스로 구조를 재정립하는 것.


🌀 5-4. 코드 재사용

상속의 문제점 (두 가지 관점에서 설계에 안좋은 영향을 미친다.)

  1. 캡슐화 위반
    • 부모의 구현이 자식에게 노출
  2. 설계를 유연하지 못하게 만듬.
    • 상속은 부모와 자식의 관계를 compile-time에 결정한다.
    • 따라서, 실행 시점에 객체의 종류를 변경하는 것은 불가능


합성

  • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법
  • 합성은 위에서 언급한 상속의 두 가지 문제를 모두 해결한다.
  • 상속은 클래스를 통해 강결합되어 있는데 비해, 합성은 메시지를 통해 느슨한 결합을 한다.
  • 따라서 코드 재사용을 위해서는 상속 보다는 합성을 선호하는 것이 좋다.


그렇다 상속을 쓰지 말라는 건 아님.

  • 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 조합해서 쓸 수 밖에 없음.
  • 중복되는 로직이 모든 구현체에 필요한 경우에는 상속을 쓰는 방향
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.