타입과 추상화
- 추상화와 복잡성
- 개념의 세 가지 관점
- 타입은 개념이다
- 데이터 타입
- 일반화와 특수화
- 동적 모델과 정적 모델
**타입(type)**과 **추상화(abstraction)**에 대해 이야기해보자. 2장까지 개별 객체가 뭔지를 이해했으면, 이제 그 객체들을 어떻게 분류하고 정리할 것인가를 다뤄야 해. 결국 인간이 세상을 이해하는 방식 — 추상화 — 을 소프트웨어 설계에 적용하는 거지.
세상은 복잡하잖아. 그 복잡성을 다루기 위해 인간이 사용하는 가장 강력한 도구가 추상화야. 추상화는 불필요한 세부사항을 버리고 핵심만 남기는 거거든.
저자가 드는 예시: 지하철 노선도. 실제 지하철 노선의 지리적 위치, 곡선, 거리는 전부 무시하고 역과 역 사이의 연결 관계만 남기지. 정확하지 않지만, 목적(어디서 어디로 갈 수 있는가)에 대해서는 충분해. 이게 추상화의 힘이야 — 목적에 맞게 불필요한 것을 버리는 거지.
추상화에는 두 가지 차원이 있어:
- 일반화/특수화 — 공통된 특성을 뽑아내서 상위 개념을 만들고, 세부적인 차이로 하위 개념을 나누는 것. "음료"는 "아메리카노"와 "라떼"의 일반화야
- 불필요한 세부사항 제거 — 현재 맥락에서 중요하지 않은 속성이나 행동을 무시하는 것
객체지향에서 타입 시스템이 바로 이 추상화를 프로그래밍 레벨에서 구현한 거지.
저자는 **개념(concept)**을 세 가지 관점에서 분석해. 이게 타입을 이해하는 기반이 되거든.
- 심볼(symbol) — 개념을 가리키는 이름. "자동차"라는 단어
- 내연(intension) — 개념의 정의. "엔진으로 구동되는 바퀴 달린 이동수단" 같은 설명. 어떤 것이 이 개념에 속하려면 만족해야 하는 조건
- 외연(extension) — 개념에 속하는 실제 사례들의 집합. 내 차, 옆집 차, 택시 등 모든 자동차
이 세 관점이 중요한 이유: 프로그래밍에서 타입을 정의할 때도 이 세 가지를 생각해야 하기 때문이야. 타입의 이름(심볼)만 있고 내연이 불명확하면, 이 타입에 뭐가 들어가야 하고 뭐가 안 들어가야 하는지 모호해지거든.
여기가 핵심 주장이야. 저자는 타입 = 개념이라고 말해.
프로그래밍에서 타입은 보통 "int, string, boolean" 같은 데이터 타입으로 먼저 접하게 되는데, 저자는 그보다 넓은 의미에서 타입을 바라보지. 타입은 객체를 분류하는 기준이야. 같은 타입에 속하는 객체들은 동일한 행동을 수행할 수 있어.
여기서 중요한 포인트: 객체의 타입을 결정하는 건 상태가 아니라 행동이야.
두 객체가 같은 데이터를 가지고 있어도, 다른 행동을 한다면 다른 타입이지. 반대로 다른 데이터를 가지고 있어도, 같은 행동을 할 수 있다면 같은 타입으로 볼 수 있어. 이건 2장에서 "행동이 상태를 결정한다"고 했던 것과 일맥상통하지.
예를 들어, 어떤 것이 "새"인지 아닌지를 판단할 때 "깃털이 있는가"(상태)보다 "날 수 있는가"(행동)가 더 본질적인 기준이 된다는 거야. 물론 펭귄처럼 반례가 있긴 하지만, 핵심은 행동 중심으로 타입을 사고하라는 거지.
그렇다면 프로그래밍 언어에서의 데이터 타입은 뭘까? 저자는 데이터 타입도 결국 같은 원리라고 설명해.
int 타입은 "덧셈, 뺄셈, 곱셈 같은 산술 연산을 적용할 수 있는 값들의 집합"이야. 여기서도 타입을 결정하는 건 **적용 가능한 연산(행동)**이지, 내부적으로 비트가 어떻게 배치되어 있는지(상태)가 아니거든.
string 타입에 산술 연산을 적용하면 에러가 나잖아. 타입 시스템은 결국 이 객체에 어떤 행동을 요청할 수 있는가를 제약하는 거야. 이게 타입 안전성(type safety)의 본질이지.
**일반화(generalization)**와 **특수화(specialization)**는 타입 간의 관계를 정의하는 방법이야.
- 일반화: 여러 타입의 공통적인 행동을 뽑아내서 상위 타입을 만드는 것. "프로그래머"와 "디자이너"에서 "직원"이라는 상위 타입을 뽑아내는 거지
- 특수화: 상위 타입에 구체적인 행동을 추가해서 하위 타입을 만드는 것. "직원"에서 "프로그래머"라는 하위 타입을 만드는 거야
이 관계에서 핵심 원칙이 있어 — 슈퍼타입의 행동은 서브타입에서도 유효해야 해. "직원"이 할 수 있는 모든 행동은 "프로그래머"도 할 수 있어야 하지. "프로그래머"는 거기에 더해서 "코딩하다" 같은 추가 행동을 가질 뿐이야.
이게 **리스코프 치환 원칙(LSP)**의 근간이거든. 상위 타입이 올 자리에 하위 타입을 넣어도 프로그램이 정상 동작해야 해. 타입의 일반화/특수화를 행동 기준으로 제대로 설계하면, 이 원칙은 자연스럽게 지켜지지.
여기서도 저자의 일관된 메시지가 반복돼: 일반화/특수화를 상태(데이터)가 아닌 행동 기준으로 해야 해. "프로그래머는 직원의 데이터에 프로그래밍 언어 필드가 추가된 것"이 아니라, "프로그래머는 직원의 행동에 코딩이라는 행동이 추가된 것"으로 생각해야 하거든.
마지막으로 저자는 객체를 바라보는 두 가지 모델을 구분해.
- 동적 모델(dynamic model) — 객체가 실행 시간에 어떻게 변하는지를 포착한 거야. 특정 시점의 상태 값, 상태 변화의 흐름, 객체 간의 상호작용 과정. **스냅샷(snapshot)**이라고도 부르지
- 정적 모델(static model) — 객체의 타입, 관계, 속성을 시간과 무관하게 정의한 거야. 클래스 다이어그램이 여기에 해당해. 타입 모델이라고도 부르고
둘 다 필요해. 동적 모델은 시스템이 실제로 어떻게 동작하는지를 보여주고, 정적 모델은 시스템의 구조를 보여주지.
문제는 많은 개발자가 정적 모델에만 집중한다는 거야. 클래스 다이어그램을 먼저 그리고, 상속 계층을 먼저 설계하고, 데이터 구조를 먼저 잡잖아. 하지만 실제 소프트웨어는 동적이지. 객체들이 실행 시간에 메시지를 주고받고, 상태가 변하고, 협력하는 과정이 본질이거든.
저자의 조언: 동적 모델을 먼저 생각하고, 정적 모델은 나중에 정리해라. 객체들이 런타임에 어떻게 상호작용하는지를 먼저 구상하고, 그걸 바탕으로 타입과 클래스를 정의하는 게 올바른 순서야.
정리
3장 읽고 기억할 거 세 가지:
- 타입은 개념이고, 개념은 분류의 기준이야. 타입을 int/string 같은 데이터 타입으로만 생각하지 말고, 객체를 분류하는 행동 기준의 범주로 이해해
- 타입을 결정하는 건 상태가 아니라 행동이야. 같은 데이터를 가져도 행동이 다르면 다른 타입이지. 일반화/특수화도 행동 기준으로 해야 해
- 동적 모델을 먼저, 정적 모델은 나중에. 클래스 다이어그램부터 그리지 말고, 런타임에 객체들이 어떻게 상호작용하는지를 먼저 생각해