Post

객체지향 프로그래밍

2. 객체지향 프로그래밍

2.1 영화 예매 시스템

할인 조건

할인 정책

2.2 객체지향 프로그래밍을 향해

협력, 객체, 클래스

진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.

이를 위해서는 프로그래밍하는 동안 다음의 2가지에 집중

  • 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라

    어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 함

  • 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.

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

도메인(domain): 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.

도메인의 개념과 관계를 반영하도록 프로그램을 구조화해야 하기 때문에 클래스의 구조는 도메인의 구조와 유사

클래스 구현하기

클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분짓는 것

클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것

클래스의 외부와 내부를 구분하는 이유?? -> 경계의 명확성이 객체의 자율성을 보장하기 때문 -> 프로그래머에게 구현의 자유 제공

자율적인 객체

객체는 상태(state)행동(behavior)을 함께 가지는 복합적인 존재

객체는 스스로 판단하고 행동하는 자율적인 존재

객체지향에서는 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음

이처럼 데이터와 기능을 객체 내부로 함께 묶는 것 -> 캡슐화

객체지향 프로그래밍에서 접근 제어(access control) 메커니즘 과 접근 수정자(access modifier) 제공

객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해

객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하는 것

객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 함

캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.

  • 외부에서 접근 가능한 부분: 퍼블릭 인터페이스(public interface)
  • 외부에서 접근 불가능한 부분: 구현(implementation)

인터페이스와 구현의 분리(separation of interface and implementation) 원칙은 훌륭한 객체지향 프로그래밍을 만들기 위해 따라야 하는 핵심 원칙

일반적으로 객체의 상태는 숨기고 행동만 외부에 공개해야 한다.

클래스의 속성은 private으로 선언해서 감추고 외부에 제공해야 하는 일부 메서드만 public으로 선언

프로그래머의 자유

프로그래머의 역할을 클래스 작성자(class creator)클라이언트 프로그래머(client programmer)로 구분

클래스 작성자는 새로운 데이터 타입을 프로그램에 추가

클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용

클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다. -> 구현 은닉(implementation hiding)

구현 은닉은 클래스 작성자와 클라이언트 프로그래머 모두에게 유용한 개념

객체의 외부와 내부를 구분하면 클라이언트 프로그래머가 알아야 할 지식의 양이 줄어들고 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어진다.

-> 클래스를 개발할 때 마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력

설계가 필요한 이유는 변경을 관리하기 위해서

객체의 변경을 관리할 수 있는 기법 중에서 가장 대표적인 것이 바로 접근 제어

변경될 가능성이 있는 세부적인 구현 내용을 private 영역 안에 감춤으로써 변경으로 인한 혼란 최소화

협력하는 객체들의 공동체

시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용 -> 협력(collaboration)

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

협력에 관한 짧은 이야기

객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)할 수 있다.

요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response)한다.

객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐

다른 객체에게 요청이 도착할 때 해당 겍체가 메시지를 수신

수신된 메시지를 처리하기 위한 자신만의 방법: 메서드(method)

메시지와 메서드를 구분하는 것은 매우 중요!

메시지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발

2.3 할인 요금 구하기

할인 정책과 할인 조건

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();

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

    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);
}

추상 클래스 + 추상 메서드 활용

TEMPLATE METHOD 패턴: 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴

할인 정책 구성하기

2.4 상속과 다형성

컴파일 시간 의존성과 실행 시간 의존성

어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스의 객체의 메서드를 호출할 경우 두 클래스 사이의 의존성이 존재

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다

유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것

하지만, 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다.

코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야하기 때문

코드의 의존성과 실햄 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다.

-> 트레이드오프의 산물

훌륭한 객체지향 설계자로 성장하기 위해서는 항상 유연성과 가독성 사이에서 고민해야 한다.

차이에 의한 프로그래밍

상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법

상속을 이용하면 클래스 사이에 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함 가능

부모 클래스와 다른 부분만 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법 -> 차이에 의한 프로그래밍(programming by difference)

상속과 인터페이스

상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문

일반적인 인식(상속의 목적이 메서드나 인스턴스 변수의 재사용)과 다름

상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함

-> 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스와 부모 클래스를 동일한 타입으로 간주

자식 클래스가 부모 클래스를 대신하는 것: 업캐스팅(upcasting)

다형성

동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라짐: 다형성

다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 함

다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력

다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다. -> 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)

구현 상속과 인터페이스 상속

구현 상속(implementation inheritance) = 서브클래싱(subclassing): 순수하게 코드 재사용

인터페이스 상속(interface inheritance) = 서브타이핑(subtyping): 다형적인 협력을 위해 인터페이스 공유

인터페이스와 다형성

자바의 인터페이스는 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 정의

2.5 추상화와 유연성

추상화의 장점

  • 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술 가능

    세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현 가능

    표현의 수준 조정

    재사용 가능한 설계의 기본을 이루는 디자인 패턴(design pattern)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘으로 활용

  • 설계가 더 유연해짐

    추상화를 이용해 상위 정책을 표현하면 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장 가능

유연한 설계

추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.

추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문

컨텍스트 독립성(context independency)을 통해 유연한 설계가 필수적인 분야에서 진가 발휘

추상 클래스와 인터페이스 트레이드오프

구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다.

고민하고 트레이드오프 해라!!

코드 재사용

코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법

합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법

상속

상속은 캡슐화를 위반하고, 설계를 유연하지 못하게 한다.

상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.

결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화됨

캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 변경될 확률을 높인다.

상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정 -> 실행 시점에 변경 불가능

상속보다 인스턴스 변수로 관계를 연결한 설계가 더 유연함

합성

합성: 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법

합성은 상속이 가지는 두 가지 문제점을 모두 해결

인터페이스에 정의된 메시지를 통해서만 재사용 가능하기 때문에 구현을 효과적으로 캡슐화가능

의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.

대부분의 설계에서는 합성과 상속을 함께 사용해야 함

코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용해야 한다.

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.