협력, 객체, 클래스
객체지향은 말 그대로 객체를 지향하는 것이다.
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
따라서, 객체지향 프로그래밍을 하기 위해선 2가지 사항에 집중해야 한다.
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라
- 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것(객체를 구현하는 방법)
- 클래스 이전에 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 생각해라
- 객체를 협력하는 공동체의 일원으로 생각하는 것은 유연하고 확장 가능한 설계를 가능하게 만듬
- 상태와 행동을 정의 후 공통된 특성과 상태를 가진 객체들을 타입으로 분류하여 클래스를 구현하라
도메인의 구조를 따르는 프로그램 구조
도메인이란
문제를 해결하기 위해 (소프트웨어 혹은 시스템 구현) 사용자가 프로그램을 사용하는 분야
객체지향 패러다임의 강력함 - 추상화
객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
즉, 사용자의 요구사항들이 객체로 봐라볼 수 있기 때문에 도메인을 구성하는 개념들이 자연스럽게 프로그램의 객체와 클래스로 연결될 수 있다.
일반적으로 클래스의 이름은 대응되는 도메인 개념의 이름과 동일하거나 적어도 유사하게 지어야 한다.
<예시> 영화 예매 도메인
- 영화 - Movie
- 상영 - Screening
- 할인 정책 - DiscountPolicy
- 할인 조건 - DiscountCondition
- 예매 - Reservation
클래스 구현하기
클래스를 구현하거나 다른 개발자에 의해 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것
클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정하는 것
클래스의 내부와 외부를 구분하는 이유
- 경계의 명확성이 객체의 자율성을 보장하기 때문
- 프로그래머에게 구현의 자유를 제공하기 때문
자율적인 객체
- 객체는 상태와 행동을 함께 가지는 복합적인 존재
- 객체는 스스로 판단하고 행동하는 자율적인 존재
캡슐화
객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현
이와 같이 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 부른다.
접근 제어
외부에서의 접근을 통제할 수 있는 매커니즘 제공
이를 구현하기 위해 프로그래밍 언어들은 접근 수정자를 제공한다. (public, protected, private)
객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서이다.
객체는 스스로 상태를 관리하고, 판단하고, 행동하는 하나의 단위이며,
객체지향은 이러한 자율적인 객체들의 공동체를 구성하는 것이다.
퍼블릭 인터페이스와 구현
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.
외부에서 접근 가능한 부분으로 이를 퍼블릭 인터페이스라고 부르며, 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분으로 이를 구현이라고 부른다.
일반적으로 객체의 상태는 숨기고 행동(기능)만 외부에 공개해야 한다.
클래스의 속성은 private 으로 선언해서 감추고 외부에 제공해야 하는 일부 메서드만 public 으로 선언해야 한다.
이때 퍼블릭 인터페이스에는 public 으로 지정된 메서드만 포함된다.
그 밖의 private 메서드나, protected 메서드, 속성은 구현에 포함된다.
프로그래머의 자유
클래스 작성자와 클라이언트 프로그래머
클래스 작성자는 새로운 데이터 타입을 프로그램에 추가, 클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용
접근 제어 매커니즘과 구현 은닉
객체의 외부와 내부를 구분하여 클라이언트 프로그래머가 알아야 할 지식의 양을 줄이며, 클랙스 작성자가 자유롭게 구현을 변경할 수 있는 가능성을 넓힌다.
이러한 설계가 필요한 이유는 변경을 관리하기 위해서 이다.
객체 사이의 의존성을 적절히 관리하여 변경에 대한 파급효과를 제어할 수 있는 다양한 방법을 제공한다.
협력하는 객체들의 공동체
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청할 수 있다.
요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답한다.
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송하는 것 뿐이다.
다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신했다고 이야기 한다.
그리고 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
(이처럼 수신된 메시지를 처리하기 위한 객체 자신만의 방법을 메서드라고 부른다.)
메시지와 메서드를 구분하는 것은 객체지향 패러다임이 유연하고, 확장 가능하며, 재사용 가능한 설계를 만들 수 있도록 한다.
이는 곳 다형성과도 연결지을 수 있다.
상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
코드 상으론 Movie 는 DiscountPolicy 만 의존한다.
그리고 실제 필요한 DiscountPolicy 는 Movie 가 생성되는 시점에 주입시키므로써 실행 시점에 의존할 수 있게 된다.
코드의 의존성과 실행 시점의 의존성 사이엔 트레이드오프가 존재
- 코드의 의존성과 실행 시점의 의존성이 다르면 다를 수록 코드 이해하기 어려워짐
- 코드의 의존성과 실행 시점의 의존싱이 다르면 다를 수록 코드는 더 유연해지고 확장 가능해진다.
각각의 트레이드오프가 있기에, 설계는 적절한 의존성을 찾아내는 과정이다.
차이에 의한 프로그래밍
상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
상속을 이용하면 부모 클래스의 구현(속성과 행동)은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
위와같이 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라 부른다.
상속과 인터페이스
상속을 통해 코드의 재사용성도 높일 수 있지만, 자식 클래스는 자신의 퍼블릭 인터페이스에 부모 클래스의 인터페이스를 포함할 수 있게 된다.
인터페이스의 경우 객체가 이해할 수 있는 메시지의 목록을 정의한다.
즉, 해당 인터페이스를 구현하는 객체가 어떤 메시지를 외부로부터 수신 받을 수 있는지를 정의하는 것이다.
결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 외부 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다. (리스코프 치환 법칙, 업캐스팅)
다형성
다형성 이란, 동일한 메시지(메서드 호출)를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수진하는 객체의 클래스가 무엇이냐에 따라 달라지는 것을 의미한다.
다시 앞서 보여주었던 예제를 살펴보면,
Movie 는 DiscountPolicy 만 의존하며 해당 객체에 calculateDiscountAmount 메시지를 전송한다.
하지만 이를 실행하는 것은 실행 시점에 Movie 와 협력하는 객체의 실제 클래스가 무엇인지에 따라 달라진다.
다형성은 객체지향 프로그램의 컴파일 시간 의존성과 실행시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
Movie 의 컴파일 시간의 의존성은 DiscountPolicy 를 향하지만,
실행 시점엔 AmountDiscountPolicy 혹은 PercentDiscountPolicy 과 상호작용 한다.
이처럼 다형성 은 컴파일 시간 의존성과 실행 시간 의존성을 다르게 만들 수 있는 객체지향의 특성을 이용하여,
실제 실행 시점에 서로 다른 메서드를 실행시킬 수 있도록 한다.
다형성을 통해 실행 시점에 실행될 메서드를 결정짓는 것을
지연 바인딩(Lazy Binding) 또는 동적 바인딩(Dynamic Binding) 이라고 부른다.
이와 반대로, 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을
초기 바인딩(Early Binding) 또는 정적 바인딩(Static Binding) 이라고 부른다.
상속만이 다형성을 구현할 수 있는 유일한 방법은 아니다.
다형성이란 추상적인 개념이며 이를 구현할 수 있는 방법은 많다.
인터페이스와 다형성
구현 상속과 인터페이스 상속
상속은 크게 구현 상속과 인터페이스 상속으로 분류할 수 있다.
구현 상속을 서브클래싱이라고 부르고, 인터페이스 상속을 서브타이핑이라고 부른다.
구현 상속은 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 의미한다.
반면에, 다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스(메시지 수신)를 공유할 수 있도록 상속을 사용하는 것을 인터페이스 상속이라 부른다.
상속 사용의 주된 이유를 코드 재사용에 있다 생각할 수 있지만, 가장 큰 의미는 인터페이스(역할, 책임)의 상속을 하기 위한 인터페이스 상속에 있다. 따라서 인터페이스 재사용이 아닌 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 수 있다.
※ 참고 - 인터페이스 상속 역시 업캐스팅 및 리스코프 치환 법칙이 성립한다.
추상화와 유연성
추상화의 힘
추상화 는 같은 계층에 속하는 클래스들이 공통으로 가질 수 있는 인터페이스를 정의하며,
구현의 일부(추상 클래스) 혹은 전체(자바 인터페이스) 를 자식 클래스가 결정할 수 있도록 결정권을 위임한다.
추상화의 장점
- 요구사항의 정책을 보다 높은 수준에서 서술할 수 있다.
- 설계의 유연성을 더 할 수 있다.
다시말해, 추상화를 이용해 상위 정책을 높은 수준에서 표현하면,
기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장 가능하게 만든다.
이는 재사용 가능한 설계의 기본을 이루는 디자인 패턴 이나 프레임워크 모두 추상화를 이용해 상위 정책을 정희하는 객체지향의 메커니즘을 사용하는 이유이다.
코드 재사용
상속은 코드를 재사용하기 위해 널리 사용되는 방법이다.
그렇지만, 상속 이외에 합성 역시 이를 위해 사용되는 방식이다.
합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 의미한다.
상속 보다 합성이 더 좋은 이유
- 캡슐화 위반
- 유연하지 않는 설계
상속의 경우, 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
이는 자식과 부모가 강하게 결합되도록 만들기에 부모를 변경할 때 자식 클래스도 함께 변경될 확률이 높아진다.
결과적으로 과도한 상속의 사용은 코드를 변경하기 어렵게 만든다.
상속의 경우 부모와 자식의 관계를 컴파일 시점에 결정한다.
따라서 다형성의 핵심인 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
반면, 합성의 경우 부모와 자식이 컴파일 시점에 하나의 단위로 강하게 결합하는 데 비해 인터페이스를 통해 약하게 결합된다는 특징이 있다.
이처럼 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성 이라고 부른다.
참고
- 해당 게시글은 조영호 님의 "오브젝트" 를 정리하여 작성하였습니다.
- 보다 자세한 내용은 책을 통해 확인하시길 적극 권장합니다.
'객체지향' 카테고리의 다른 글
오브젝트 - 역할, 책임, 협력 (0) | 2021.04.24 |
---|---|
오브젝트 - 객체, 설계 (0) | 2021.04.14 |
객체 지향 설계의 5가지 원칙 (0) | 2020.11.04 |
댓글