01 협력, 객체, 클래스
객체지향은 객체를 지향하는 것이다.
1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다. 객체를 중심에 두는 접근 방법은 설계를 단순하고 깔끔하게 만든다.
2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
객체는 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재다.
02 도메인의 구조를 따르는 프로그램 구조
문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인(domain)이라고 부른다.
객체지향 패러다임이 강력한 이유는 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
03 클래스 구현하기
도메인 개념들의 구조를 반영하는 적절한 클래스 구조를 만들었다면, 프로그래밍 언어를 이용해 이 구조를 구현할 차례이다.
클래스는 내부와 외부로 구분되며, 훌륭한 클래스를 설계하기 위한 핵심은 어떤 부분을 외부에 공개하고 어떤 부분을 감출 지를 결정하는 것이다.
클래스의 내부와 외부를 구분해야 하는 이유는 무엇일까? 그 이유는 경계의 명확성이 객체의 자율성을 보장하기 때문이다.
그리고 더 중요한 이유로 프로그래머에게 구현의 자유를 제공하기 때문이다.
자율적인 객체
객체는
- 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재라는 것.
- 객체는 스스로 판단하고 행동하는 자율적인 존재
객체지향 이전 패러다임에서는 데이터와 기능이라는 독립적인 존재를 서로 엮어 프로그램을 구성
이와 달리 객체지향은 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현할 수 있게 했다.
이처럼 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화(encapsulation)라고 부른다.
대부분의 객체지향 프로그래밍 언어들은 상태와 행동을 캡슐화하는 것에서 한 걸음 더 나아가 외부에서 접근을 통제할 수 있는 접근 제어(access control) 매커니즘도 함께 제공한다. 많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private과 같은 접근 수정자(access modifier)를 제공한다.
객체 내부에 대한 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서다.
객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하는 것이다.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.
하나는 외부 접근 가능한 부분으로 퍼블릭 인터페이스(public interface)라고 부른다.
다른 하나는 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분으로 이를 구현(implementation)이라고 부른다.
일반적으로 객체는 상태는 숨기고 행동만 외부에 공개해야 한다.
프로그래머의 자유
프로그래머의 역할을 클래스 작성자(class creator)와 클라이언트 프로그래머(client programmer)로 구분하는 것이 유용하다.
클래스 작성자는 새로운 데이터 타입을 프로그램에 추가하고,
클라이언트 프로그래머는 클래스 작성자가 추가한 데이터 타입을 사용한다.
객체의 외부와 내부를 구분하면 클라이언트 프로그래머가 알아야 할 지식의 양이 줄어들고 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어진다.
설계가 필요한 이유는 변경을 관리하기 위함이다.
객체의 변경을 관리할 수 있는 기법 중에서 가장 대표적인 것이 바로 접근 제어다.
04 상속과 다형성
코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다.
클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.
유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것이다.
코드의 의존성과 실행 시점의 의존성이 다를수록 코드를 이해하기 어려워진다.
> 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문
코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더욱 유연해지고 확장 가능해진다.
이와 같은 의존성의 양면성은 설계가 트레이드오프 산물이라는 사실을 잘 보여준다.
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실,,
반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉽지만 재사용성과 확장 가능성은 낮아진다는 사실,,,
훌륭한 객체지향 설계자로 성장하기 위해선 유연성과 가독성 사이에서 고민해야 한다.
무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다. 이것이 객체지향 설계가 어려우면서도 매력적인 이유,,,
차이에 의한 프로그래밍
클래스를 하나 추가하고 싶은데, 그 클래스가 기존 어떤 클래스와 매우 흡사하다면?
그 클래스를 가져와 약간만 추가하거나 수정해서 새로운 클래스를 만들 수 있다면 좋을 것이다.
더 좋은 방법은 그 클래스의 코드를 전형 수정하지 않고도 재사용하는 것일 것이다.
이를 가능하게 해주는 것이 바로 상속이다.
부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)이라고 부른다.
상속과 인터페이스
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문
상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 되며 결과적으로 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcasting)이라고 한다.
업캐스팅이라고 부르는 이유는 일반적으로 다이어그램을 작성할 때 부모 클래스를 자식 클래스의 위에 위치시키기 때문이다.
다형성
동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성이라고 부른다.
- 메시지: 공개된 다른 객체의 인터페이스를 통해 요청하는 것
- 메서드: 수신된 메시지를 처리하기 위한 자신만의 방법
다형성은 컴파일 시간 의존성과 실행 시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
다형성을 구현하는 방법은 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
다시 말해 메시지와 메서드를 실행 시점에 바인딩하는 것이다.
이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라고 부른다.
구현 상속과 인터페이스 상속
상속을 구현 상속과 인터페이스 상속으로 분류할 수 있다.
순수하게 코드 재사용 목적으로 상속하는 것을 구현 상속이라고 한다.
다형적인 협력을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속하는 것을 인터페이스 상속이라고 부른다.
상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.
05 추상화와 유연성
추상화의 힘
추상화의 첫 번째 장점은 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다는 것이다.
두 번째 장점은 추상화를 이용하면 설계가 좀 더 유연해진다는 것이다.
추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
이런 특징은 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 한다.
또한 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다.
이 개념은 매우 중요한데, 재사용 가능한 설계의 기본을 이루는 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문이다.
유연한 설계
추상화를 중심으로 새로운 클래스를 추가하는 것 만으로 기능을 확장할 수 있다.
유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다.
결론은 유연성이 필요한 곳에 추상화를 사용하면 된다는 것이다.
상속
상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다. 하지만 두 가지 관점에서 설계에 안 좋은 영향을 미친다.
하나는 상속은 캡슐화를 위반한다는 것이고,
다른 하나는 설계를 유연하지 못하게 만든다는 것이다.
상속을 이용하기 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
결과적으로 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다. 결과적으로 과도한 상속은 변경에 취약해진다.
합성
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.
또한 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 조합해서 사용할 수밖에 없다.
'도서 > Object' 카테고리의 다른 글
[Object] Chapter 05 - 책임 할당하기 (0) | 2021.08.26 |
---|---|
[Object] Chapter 04 - 설계 품질과 트레이드오프 (0) | 2021.08.03 |
[Object] Chapter 03 - 역할, 책임, 협력 (0) | 2021.07.30 |
[Object] Chapter 01 - 객체, 설계 (0) | 2021.07.03 |