5.1 절차형 사고방식
- C와 같은 절차형 언어는 단 하나의 작업만 담당하는 프로시저라 부르는 작은 단위로 코드를 구성한다.
- 프로시저는 프로그래머가 코드를 읽고 관리하기 쉽도록 추상화한 것이다. 프로시저는 프로그램이 하는 일을 중심으로 만든 개념이다.
- 절차형 접근 방식은 일정한 단계에 따라 진행하는 프로그램에 적합하다.
- 절차지향 프로그래밍(Procedural Programming)은 프로그램을 순차적인 절차(절차, 함수, 루틴)로 구성하여 실행하는 프로그래밍 패러다임이다. 프로그램의 흐름이 위에서 아래로 자연스럽게 진행되며, 데이터를 함수(절차)들이 처리하는 방식으로 동작한다.
5.2 객체지향 철학
- 절차형 접근 방식은 프로그램이 하는 일을 중심으로 접근하는데 반해 객체지향 접근 방식은 모델링하려는 현실 세계의 대상이라는 관점에서 접근한다.
- 객체지향 프로그래밍(Object-Oriented Programming)에서는 프로그램을 작업(task) 단위가 아니라 실제 대상에 대한 모델 단위로 구성한다.
- 물리적인 대상을 클래스, 컴포넌트, 속성, 동작의 관점에서 분석하자.
5.2.1 클래스
- 클래스란 개념을 적용하면 구체적인 대상과 그 대상에 대한 정의를 구분할 수 있다.
- 클래스는 어떤 대상의 유형을 정의하는 속성을 정리한 것이다.
- 객체란 속성(데이터)과 동작(메서드)을 함께 가지는 대상을 의미한다. 프로그래밍에서 객체는 단순히 어떤 대상을 가리키는 게 아니라, 특정한 구조와 기능을 포함한 실체를 뜻한다.
- 모든 객체는 특정 클래스에 속한다.
- 객체는 어떤 클래스에 속하는 구체적인 예(인스턴스)라고도 볼 수 있다.
- C 언어 관점에서 볼 때 타입과 변수를 클래스와 객체에 비유할 수 있다.
5.2.2 컴포넌트
- 비행기처럼 현실에 존재하는 복잡한 대상도 작은 부품(컴포넌트)으로 구성되어 있다.
- OOP에서는 객체를 작은 컴포넌트 단위로 나누는 사고방식이 굉장히 중요하다.
- 컴포넌트는 본질적으로 클래스와 같다. 클래스보다 작고 구체적이라는 점만 다르다.
5.2.3 속성
- 객체는 속성(프로퍼티)으로 구분한다.
- 속성을 클래스 관점에서 볼 수도 있다. 클래스 속성의 값은 그 클래스에 속한 모든 객체에서 똑같지만 객체 속성의 값은 그 클래스에 속한 객체마다 다를 수 잇다.
- 속성은 객체의 특성을 표현한다. 다시 말해 속성으로 객체끼리 구분할 수 있다.
5.2.4 동작
- 동작(행위)은 객체가 직접 하거나 그 객체로 할 수 있는 일을 표현한다.
- 속성과 마찬가지로 동작도 클래스 관점과 객체 관점으로 구분된다.
- OOP에서는 어떤 기능을 수행하는 코드를 프로시저가 아닌 클래스 단위로 묶는다. 클래스가 여러 가지 동작을 수행하고, 서로 상호 작용하는 방식을 정의함으로써 데이터를 조작하는 코드를 훨씬 다양하게 제공할 수 있다. 이러한 클래스의 동작은 메서드로 구현한다.
5.2.5 중간 정리
5.3 클래스 세상에 살기
- 소프트웨어를 클래스 관점으로 개발하는 데 적용할 수 있는 접근 방식은 크게 두 가지가 있다.
- 클래스를 단순히 데이터와 기능을 잘 묶어주는 수단으로만 여기고 프로그램 전반에 걸쳐 사용해서 가독성과 유지 보수성을 높이는 것이다.
- 다른 방식은 OOP 패러다임을 완벽히 적용해서 처음부터 모든 것을 클래스로 표현하는 것이다.
- 가장 바람직한 방법은 이러한 두 가지 점근 방식을 적절히 조합하는 것이다.
5.3.1 과도한 클래스화
- 객체지향 시스템을 창의적으로 설계하는 것과 사소한 것까지 클래스로 만들어 팀원들을 불편하게 만드는 것은 분명 다르다. 프로이트의 표현을 응용하면 변수는 변수일 뿐이다.
- 클래스는 어디까지나 프로그래머가 코드를 관리하는 데 도움을 주기 위한 것임을 명심한다. 구체적인 이유 없이 단지 좀 더 객체지향적으로 보이기 위해 클래스를 사용하는 것은 바람직하지 않다.
5.3.2 지나치게 일반화한 클래스
- 클래스로 정의할 필요가 없는 것까지 클래스로 만드는 것보다 더 나쁜 것은 클래스를 지나치게 일반화하하는 것이다.
- 클래스를 과도하게 일반화하면 구체적인 대상을 표현하기 힘들다. 클래스를 유연하고 재활용하기 좋게 만들고 싶은 의도로 작성한 것이 오히려 혼란만 가중시킬 뿐이다.
5.4 클래스 관계
- 프로그래밍을 하다 보면 서로 다른 클래스가 공통적인 속성을 가지거나, 최소한 두 개가 서로 관련된 경우가 있다.
- 클래스 관계에는 has-a 관계와 is-a 관계 두 가지가 있다.
5.4.1 has-a 관계
- A는 B를 가진다 혹은 A에 B가 있다고 표현되는 클래스 관계를 has-a 관계 (집계 관계, 포함 관계, 소유 관계)라 한다.
- 한 클래스가 다른 클래스의 일부라고 상상하면 이해하기 쉽다. 컴포넌트는 일반적으로 has-a 관계로 나타낸다.
- has-a 관계에는 두 가지 유형이 있다.
- 집계(aggregation) : 집계된 객체(컴포넌트)는 집계를 수행한 대상이 제거되더라도 남아 있다. 예를 들어 동물원 객체는 여러 동물 객체로 구성되어 있는데, 동물원 객체가 사라지더라도 동물 객체는 사라지지 않고, 다른 동물원으로 옮겨질 것이다.
- 합성(composition) : 여러 객체로 구성된 객체가 제거되면 포함된 객체도 함께 사라진다. 예를 들어 여러 버튼이 담긴 창 객체가 제거되면 그 안에 담긴 버튼 객체도 함께 제거된다.
5.4.2 is-a 관계(상속)
- is-a 관계는 OOP의 핵심 개념이다. 그러므로 파생, 서브클래싱, 확장, 상속 등으로 다양하게 표현한다.
- 클래스는 현실 세계가 여러 가지 속성과 동작을 가진 객체로 구성된다는 점에 근거를 두고 모델링하는 것이고, 상속은 이러한 객체가 주로 계층 구조를 형성한다는 관점에서 모델링하는 것이다. 이러한 계층 구조가 바로 is-a 관계(상속 관계)다.
- 기본적으로 상속은 A는 일종의 B다 또는 A는 B를 상당히 많이 닮았다로 표현한다.
- 클래스를 is-a 관계로 엮으려면 공통 기능을 베이스 클래스(상위 클래스)로 묶어서 다른 클래스가 확장할 수 있게 만들어야 한다. 그래야 공통적인 부분에서 변경사항이 발생할 때 상위 클래스만 고쳐도 다른 하위 클래스에 수정사항을 똑같이 반영할 수 있다.
+ 상속 기법
- 다른 클래스를 상속한 파생(하위) 클래스를 부모 클래스(상위 클래스, 베이스 클래스, 슈퍼 클래스)와 구분하는 몇 가지 방법이 있다. 파생 클래스는 이러한 기법들 중 한개 혹은 여러 개를 조합해서 만든다. 이러한 관계를 A는 일종의 B로서...라는 특성이 있다고 표현한다.
- 기능 추가
- 파생 클래스는 기능을 더 추가해서 부모 클래스를 확장할 수 있다.
- 기능 변경
- 파생 클래스는 부모 클래스가 가진 메서드를 변경하거나 무시(오버라이드)할 수 있다.
- 베이스 클래스를 추상 클래스로 정의하면 이를 상속하는 모든 파생 클래스는 베이스 클래스에 구현되지 않은 메서드를 모두 구현해야 한다. 추상 베이스 클래스에 대한 인스턴스는 생성할 수 없다.
- 속성 추가
- 베이스 클래스를 상속한 파생 클래스는 새로운 속성을 추가할 수도 있다.
- 속성 변경
- C++는 메서드를 오버라이드하는 것처럼 속성도 오버라이드할 수 있다. 하지만 이렇게 하는 것이 바람직하지 않을 때가 많다. 베이스 클래스의 속성을 가리기 때문이다. 다시 말해 베이스 클래스는 특정한 이름의 속성에 어떤 값을 가지고 있는 상태에서 파생 클래스가 그 속성과 같은 이름을 정의해서 다른 값을 표현할 수 있다.
- 여기서 속성을 변경하는 것과 파생 클래스의 속성값이 다른 것은 완전히 다른 개념이다.
+ 다형성
- 다형성(폴리모피즘)이란 일정한 속성과 메서드를 표준으로 정해두면 그 형식에 맞는 객체라면 어느 것이든 서로 바꿔서 적용할 수 있다는 개념이다. 클래스 정의는 객체와 그 객체를 다루는 코드가 서로 맺는 계약과 같다.
5.4.3 has-a 관계와 is-a 관계 구분하기
- 두 관계를 구분하는 기준이 확실하지 않을 때가 많다는 데 있다. 따라서 구현할 클래스의 용도를 분석하고, 기존 클래스에 있는 기능을 단순히 이용하기만 하는지 아니면 기존 기능을 변경하거나 새 기능을 추가하는지 확실히 파악해야 한다.
- 동작을 바꾸지 않고도 베이스 클래스 대신 파생 클래스를 사용할 수 있어야 한다는 LSP(리스코프 치환 원칙)를 적용하면 is-a관계와 has-a 관계를 쉽게 구분할 수 있다.
+ has-a vs is-a 쉽게 구분하는 법
개념 의미 예제 코드 구현
has-a 관계포함 관계 (A는 B를 가진다) "자동차는 엔진을 가진다." Car has an Engine (멤버 변수 포함)
is-a 관계상속 관계 (A는 B이다) "개는 동물이다." Dog is an Animal (클래스 상속)
+ 어떤 관계를 써야 할까?
"A는 B이다(is-a)?" → YES 상속 (is-a 관계)
"A는 B를 포함하는가(has-a)?" → YES 포함 (has-a 관계)
불필요한 상속보다는 포함을 우선 고려하는 것이 좋음 (상속 남용 주의)
+ 정리
has-a 관계: 클래스 내부에 다른 객체를 포함하는 관계 (멤버 변수로 가짐)
is-a 관계: 클래스가 다른 클래스를 상속받는 관계 (부모-자식 클래스)
5.4.4 not-a 관계
- not-a 관계란? A is not a B → "A는 B가 아니다." 일반적으로 잘못된 상속을 방지하기 위해 고려해야 하는 개념이다.
- 두 클래스 관계 중 어느 것이 적합한지 따지기 전에 먼저 그런 관계가 성립할 수 있는지부터 살펴봐야 한다.
- 객체지향 방식으로 계층을 구성하려면 억지로 관계를 형성하지말고 기능 관점에서 관계를 표현해야 한다.
- 특별히 속성이나 메서드를 갖지 않는 클래스가 있거나 추상 베이스 클래스가 아닌 클래스의 속성과 메서드를 다른 파생 클래스에서 모두 오버라이드한다면 설계를 바꾸는 것이 좋다.
5.4.5 클래스 계층
- 객체지향 계층은 클래스 관계를 이렇게 여러 계층으로 표현한다. 구체적인 요구사항에 따라 객체 계층을 설계한다.
- 제대로 구성된 객체지향 계층은 다음과 같은 특성을 갖는다.
- 기능적으로 의미 잇는 관계에 따라 클래스를 구성한다.
- 공통 기능을 베이스 클래스로 뽑아냈기 때문에 코드를 재활용하기 쉽다.
- 부모가 추상 클래스가 아닌 이상 부모의 기능을 과도하게 오버라이딩하는 파생 클래스가 없다.
5.4.6 다중 상속
- 다중 상속을 통해 베이스 클래스를 여러 개 둘 수 있다.
- 다중 상속을 반대하는 이유
- 다중 상속 관계는 시각적으로 표현하기 복잡하다.
- 다중 상속 때문에 구조의 명확성이 깨질 수 있다.
- 다중 상속은 구현하기 힘들다.
5.4.7 믹스인 클래스
- 믹스인 클래스(첨가 클래스)는 지금까지 소개한 것과 다른 종류의 클래스 관계를 표현한다.
- C++에서 믹스인의 문법은 다중 상속과 같지만 의미는 전혀 다르다. 믹스인 클래스는 '이 클래스가 할 수 있는 일이 또 뭐가 있나?'라는 질문에 '~도 할 수 있다'라는 답을 제시한다. 믹스인 클래스는 is-a 관계를 완전히 구현하지 않고도 기능을 추가할 때 사용한다. 일종의 공유 관계라고 볼 수 있다.
- 믹스인 클래스는 사용자 인터페이스 코드에서 흔히 볼 수 있다.
- 믹스인 클래스와 베이스 클래스의 차이점은 코드보다는 생각하는 방식에 있다.
- 일반적으로 믹스인 클래스는 상당히 제한된 용도로만 사용하기 때문에 다중 상속보다 이해하기 쉽다.
- 믹스인 클래스는 거대한 계층 구조를 형성할 일이 드물어서 기능이 서로 충돌할 가능성도 낮다.
- 믹스인(Mixin) 클래스란?
- 믹스인(Mixin) 클래스는 다른 클래스에게 특정 기능을 추가하는 데 사용되는 보조적인 클래스이다. 즉, 독립적으로 인스턴스를 만들지 않고, 다른 클래스에 기능을 "섞어 넣기"(mixin) 위해 사용하는 것이다.
- 보통 다중 상속을 활용하여 사용된다.
- "is-a" 관계가 아닌, "can-do" 관계를 표현할 때 적합하다.
5.5 정리
5.6 연습 문제
'Programming II > C++' 카테고리의 다른 글
[전문가를 위한 C++] CHAPTER7 메모리 관리 (0) | 2025.04.05 |
---|---|
[전문가를 위한 C++] CHAPTER6 재사용을 고려한 설계 (0) | 2025.04.05 |
[전문가를 위한 C++] CHAPTER4 전문가답게 C++프로그램 설계하기 (0) | 2025.04.02 |
[전문가를 위한 C++] CHAPTER3 코딩 스타일 (0) | 2025.03.30 |
[전문가를 위한 C++] CHAPTER2 스트링과 스트링 뷰 다루기 (0) | 2025.03.30 |