객체지향을 다시 보기: 『객체지향의 사실과 오해』 1~3장 정리와 깨달음

개요

최근 객체지향의 사실과 오해를 읽으며 객체지향 프로그래밍(OOP; Object-Oriented Programming) 스터디를 시작했습니다. 이번 스터디는 단순한 내용 정리를 넘어, 자유로운 대화를 통해 OOP를 깊이 이해하고 실제 코드에 적용하는 것을 목표로 합니다.

기존 "내용 요약형" 스터디는 반복적이고 집중력이 흐트러지는 한계를 느꼈습니다. 이를 보완하기 위해, 개발 팟캐스트처럼 자연스러운 대화 형식을 도입했습니다. 정해진 분량을 읽고 온 뒤, 각자 내용을 소개하고 질문하거나 의견을 주고받으며 토론을 이어갑니다. 단순 정리에 그치지 않고, 각 장의 핵심 개념을 다양한 시각에서 바라보고 해석하는 것을 지향합니다.

글은 각 장별로 '내용 정리'와 '코드 적용'을 번갈아 소개하는 형식으로 구성했습니다. 이론과 실습을 함께 경험하며, 객체지향 설계 감각을 자연스럽게 키우는 것이 목표입니다.

오늘의 글을 통해 얻을 수 있는 것


1장. 협력하는 객체들의 공동체

"객체지향의 목표는 실세계를 모방하는 것이 아니다. 오히려 새로운 세계를 창조하는 것이다." - 『객체지향의 사실과 오해』, p.21

물론 실세계의 비유—예를 들면 손님, 점원, 바리스타 같은 관계—는 객체지향 개념을 이해하는 데 효과적입니다. 하지만 결국 객체지향은 '현실 세계를 재현'하는 것이 아니라, 역할(Role), 책임(Responsibility), 협력(Collaboration) 을 기반으로 독립적이고 자율적인 객체들이 상호작용하는 새로운 시스템을 만들어내는 것이 목표입니다.

다만 이 글에서는 객체지향의 본질을 보다 쉽게 이해하기 위해, '카페'라는 현실 세계 모델을 활용해 예제 코드를 함께 소개합니다. 손님(Customer), 캐시어(Cashier), 바리스타(Barista)라는 익숙한 역할을 바탕으로, 객체들이 어떻게 역할과 책임을 갖고 협력하는지를 구체적인 코드로 풀어낼 예정입니다. 이를 통해 현실 세계 비유를 발판 삼아, 객체지향 설계 감각을 더욱 자연스럽게 익히고자 함입니다.

cafe image

객체지향 설계의 핵심 키워드

객체지향 설계는 다음 세 가지 키워드를 중심으로 전개됩니다.

예를 들어, 카페를 방문한 손님은 커피를 주문하는 역할과 책임을 다하고, 점원과 함께 협력합니다.

class Customer {
	constructor(name) {
		this.name = name;
	}

	order(menu, cashier) {
		cashier.takeOrder(menu);
	}
}

이상적인 객체의 특성

이러한 객체는 몇 가지 중요한 덕목을 갖추어야 합니다. 첫째, 객체는 협력적이어야 합니다. 외부 객체와 메시지를 주고받으며 공동 작업에 참여함으로써, 단독이 아니라 다른 객체들과 함께 문제를 해결해 나갑니다. 둘째, 객체는 자율적이어야 합니다. 자신의 상태와 행동을 스스로 결정하며, 외부에는 '무엇을' 수행하는지만 공개하고 '어떻게' 수행하는지는 숨깁니다. 이는 객체가 외부로부터 불필요한 간섭을 받지 않고, 자신의 내부를 스스로 관리할 수 있게 하는 기본 원칙입니다. 이러한 특성은 캡슐화(Encapsulation) 라는 개념으로 구체화됩니다.

캡슐화란, 객체가 자신의 내부 구현을 감추고, 외부에는 필요한 인터페이스만을 제공하는 방식으로, 객체의 독립성과 안정성을 높이는 중요한 설계 원칙입니다.

예를 들어, 점원은 자신이 주문을 전달하고자 하는 바리스타를 외부에 노출시키지 않고, 오직 메시지(takeOrder)를 통해 행동을 요청합니다.

class Cashier {
	#barista;

	constructor(barista) {
		this.#barista = barista; // 내부 상태를 직접 노출하지 않음
	}

	takeOrder(menu) {
		this.#barista.makeDrink(menu);
	}
}

클래스는 수단일 뿐, 핵심은 객체

"클래스가 객체지향 프로그래밍 언어의 관점에서 매우 중요한 구성요소(construct)인 것은 분명하지만 객체지향의 핵심을 이루는 중심 개념이라고 말하기에는 무리가 있다. 자바스크립트같은 프로토타입(prototype) 기반의 객체지향 언어에서는 클래스가 존재하지 않으며, 오직 객체만이 존재한다." - 『객체지향의 사실과 오해』, p.37

이전까지는 클래스를 객체와 동일시하는 오해를 가지고 있었습니다. 특히 자바스크립트로만 개발을 해오면서, class 문법을 사용해 객체를 생성하는 방식이 자연스럽게 "클래스 = 객체" 라는 인식으로 굳어졌던 것 같습니다. 하지만 이번 학습을 통해, 자바스크립트는 본래 프로토타입 기반 언어라는 사실을 다시금 정확히 이해할 수 있었습니다.

ES6 이후 등장한 class 문법은 편의성을 위한 문법적 설탕(Syntactic Sugar) 에 불과하며, 실제로는 내부적으로 프로토타입을 기반으로 동작합니다. 즉, 본질적으로 객체끼리 직접 위임(delegation) 관계를 맺어 협력하는 구조를 가지고 있다는 것입니다.

이 점을 깨닫고 나니, 객체지향 설계에서 중요한 것은 "클래스"라는 틀에 맞춰 객체를 만드는 것이 아니라, 객체 그 자체가 가진 역할과 책임, 그리고 협력 관계를 어떻게 설계할 것인가에 있다는 점이 더욱 선명해졌습니다.

예를 들어, 클래스를 사용하지 않고 객체만으로도 다음처럼 협력을 구현할 수 있습니다.

const barista = {
	makeDrink(menu) {
		console.log(`${menu} 제조 완료`);
	},
};

const cashier = {
	barista,
	takeOrder(menu) {
		this.barista.makeDrink(menu);
	},
};

const customer = {
	order(menu, cashier) {
		cashier.takeOrder(menu);
	},
};

customer.order('카페라떼', cashier);

2장. 이상한 나라의 객체

"많은 사람들이 객체지향을 직관적이고 이해하기 쉬운 패러다임이라고 말하는 이유는 객체지향이 세상을 자율적이고 독립적인 객체들로 분해할 수 있는 인간의 기본적인 인지 능력에 기반을 두고 있기 때문이다." - 『객체지향의 사실과 오해』, p.41

객체의 정의: 식별자, 상태, 행동

1장에서 객체가 '무엇을 해야 하는지'를 다뤘다면, 2장부터는 '객체가 어떻게 이루어져 있는지'를 본격적으로 다룹니다. 객체를 구성하는 세 가지 핵심 요소는 다음과 같습니다.

상태와 행동의 관계

객체의 행동은 단순한 기능 호출이 아니라 자신의 상태를 기반으로 결정되며, 그 행동의 결과는 다시 객체의 상태를 변화시킵니다. 즉, 행동은 상태에 의존하고 상태는 행동에 의해 변화합니다.

예를 들어, 카페 모델에서 고객이 음료를 받는 행동을 생각해봅시다. 처음에 고객은 음료를 가지고 있지 않은 상태입니다. 하지만 음료를 받는 행동을 수행하면, 고객의 상태는 변합니다.

class Customer {
	#hasDrink = false;

	receive(drink) {
		this.#hasDrink = true;
		console.log(`'${drink}'를 받았습니다.`);
	}
}

이렇게 상태와 행동은 서로 긴밀하게 연결되어 있으며, 객체의 생명력을 유지하는 핵심 메커니즘입니다.

식별자의 중요성

객체는 외부적으로 구별될 수 있어야 합니다. 단순히 값만 같다고 해서 같은 객체로 취급할 수는 없습니다. 객체는 고유한 식별자(Identity) 를 통해 서로 구분되며, 이 식별자는 객체의 상태가 변하더라도 변하지 않는 동일성의 기준이 됩니다.

이번 학습을 통해, 저는 "값"과 "객체"를 혼동했던 과거의 실수를 명확히 인식하게 되었습니다. 특히 자바스크립트에서는 객체가 메모리 상에서 참조 타입(Reference Type)으로 존재하기 때문에, 동등성(equality)동일성(identity) 의 차이를 정확히 이해하는 것이 매우 중요합니다.

예를 들어 다음과 같은 코드를 보겠습니다.

const obj1 = { name: '후니훈' };
const obj2 = { name: '후니훈' };

console.log(obj1 === obj2); // false (서로 다른 식별자)

obj1obj2는 속성 값은 같지만, 서로 다른 메모리 공간에 존재하기 때문에 동일한 객체로 간주되지 않습니다. 즉, 상태가 같아도 식별자가 다르면 다른 객체입니다.

초기에는 저 역시 객체의 식별자 개념을 단순히 기술적으로 구현하는 것으로 이해했습니다. 그래서 각 객체에 crypto.randomUUID()를 부여하여 식별자를 생성하는 방법을 사용했습니다.

this.id = crypto.randomUUID();

하지만 스터디 과정에서 다른 분들의 피드백을 들으며, 중요한 깨달음을 얻을 수 있었습니다. 식별자는 단순히 UUID 같은 기술적 값이 아니라, 객체를 구별하는 논리적 기준이라는 것입니다. 즉, UUID는 하나의 구현 방법일 뿐이며, 어떤 객체를 어떤 기준으로 "같다" 또는 "다르다"고 판단할지에 대한 모델링 상의 의도가 핵심입니다.

그렇다면, 객체는 무엇을 식별자로 사용할 수 있을까?

실제 객체에서는 다음과 같은 방법으로 식별자를 설정할 수 있습니다.

어떤 방식을 사용하든 핵심은 "객체를 구별할 수 있는 일관된 기준" 을 갖는 것입니다.

이러한 관점을 배우고 나니, 앞으로 객체를 설계할 때 단순히 기술적인 편의를 먼저 고려하는 것이 아니라, "이 객체를 어떤 기준으로 식별할 것인가" 부터 먼저 고민해야겠다는 생각이 들었습니다. 이는 코드의 품질뿐만 아니라, 시스템의 구조와 안정성에도 큰 영향을 미치는 중요한 설계 판단임을 느꼈습니다.

캡슐화와 자율성

객체는 자신의 상태를 외부로부터 직접적으로 드러내지 않습니다. 오직 정해진 메시지(행동) 를 통해서만 자신의 상태를 변경하거나 정보를 제공합니다. 이러한 특성 덕분에 객체는 외부의 의도치 않은 간섭으로부터 보호되고, 자율적으로 동작할 수 있습니다.

예를 들어, Customer 객체가 음료를 받는 상황을 생각해볼 수 있습니다. 외부에서는 단순히 receive(drink)라는 메시지를 보내는 것뿐입니다. 음료를 받았다는 사실을 외부에서 직접 상태를 수정하는 것이 아니라, 상태 변경은 오직 객체 자신만이 수행합니다.

export default class Customer {
	#hasDrink = false;

	constructor(name) {
		this.id = crypto.randomUUID();
		this.name = name;
	}

	receive(drink) {
		this.#hasDrink = true;
		console.log(`[고객] ${this.name}이(가) '${drink}'를 받았습니다.`);
	}

	hasReceivedDrink() {
		return this.#hasDrink;
	}
}

위 코드에서 #hasDrink는 private 필드로 선언되어 있어, 외부에서는 직접 접근하거나 변경할 수 없습니다. 음료를 받았는지 여부는 receive(drink) 메서드를 통해서만 내부적으로 업데이트되며, 음료를 받았는지 확인할 수 있는 방법은 hasReceivedDrink() 메서드를 호출하는 것입니다.

이러한 구조 덕분에 객체는 외부 세계의 불필요한 간섭 없이 자신의 상태를 스스로 책임지고 관리할 수 있으며, 더 견고하고 변화에 강한 시스템을 구성할 수 있습니다.

3장. 타입과 추상화

객체지향 프로그래밍에서 '추상화' 란 복잡한 세부사항을 모두 노출하는 대신, 본질적인 부분만을 남기고 불필요한 세부사항을 감추는 사고 방식을 말합니다. 추상화는 복잡한 현실을 단순화하고, 핵심에 집중할 수 있게 도와주는 매우 중요한 기법입니다.

이번 학습을 통해, 저는 객체지향에서 추상화란 단순히 코드를 줄이는 테크닉이 아니라, 복잡한 문제를 효과적으로 다루기 위한 사고의 도구라는 점을 깊이 이해하게 되었습니다.

추상화란 무엇인가

"지하철 노선도 디자인에서 가장 중요한 것은 얼마나 사실적으로 지형을 묘사했느냐가 아니라 역과 역 사이의 연결성을 얼마나 직관적으로 표현했느냐다." - 『객체지향의 사실과 오해』, p.73

추상화는 복잡한 구조나 정보를 효과적으로 다루기 위해, 공통점은 강조하고 차이점은 감추는 일반화를 수행하는 과정입니다.

예를 들어 런던 지하철 노선도는 실제 지리적 거리나 방향을 정확하게 반영하지 않습니다. 그 대신, 각각의 노선과 환승 정보라는 핵심 정보만을 강조하여 복잡한 지형을 훨씬 간단하게 표현합니다.

객체지향에서도 마찬가지로, 객체의 모든 세부 구현을 신경 쓰는 것이 아니라, "이 객체는 어떤 행동을 할 수 있는가" 를 중심으로 이해하고 다루게 됩니다.

타입과 객체

"데이터 타입은 메모리 안에 저장된 데이터의 종류를 분류하는 데 사용하는 메모리 집합에 관한 메타데이터다. 데이터에 대한 분류는 암시적으로 어떤 종류의 연산이 해당 데이터에 대해 수행될 수 있는지를 결정한다." - 『객체지향의 사실과 오해』, p.91

객체를 추상화할 때 중요한 도구가 바로 타입입니다. 타입은 공통된 책임과 행동을 가진 객체들을 하나의 개념으로 묶어줍니다. 타입에 속하는 객체들은 동일한 메시지를 받을 수 있으며, 비슷한 책임을 수행합니다.

이것은 객체 내부 구현이 다를지라도, 외부에서는 일관된 방식으로 객체와 상호작용할 수 있게 해준다는 점에서 매우 중요합니다.

이번 스터디에서는 CafePerson이라는 추상 타입을 만들어, Customer, Cashier, Barista 세 객체를 이 타입 아래에 묶었습니다.

export default class CafePerson {
	constructor(name) {
		if (new.target === CafePerson) {
			throw new Error('CafePerson은 직접 인스턴스화 할 수 없습니다.');
		}
		this.name = name;
		this.id = crypto.randomUUID();
	}

	getRole() {
		throw new Error('getRole()은 서브클래스에서 구현되어야 합니다.');
	}

	introduce() {
		return `[${this.getRole()}] ${this.name}`;
	}
}

이 추상 타입을 상속하는 각 객체들은, 자신만의 역할(Role)에 따라 getRole()을 오버라이드하고, 필요한 고유 행동(메서드)을 추가합니다.

export default class Customer extends CafePerson {
	#hasDrink = false;

	getRole() {
		return '고객';
	}

	receive(drink) {
		this.#hasDrink = true;
		console.log(`[고객] ${this.name}이(가) '${drink}'를 받았습니다.`);
	}
}

모든 CafePerson 타입의 객체는 introduce()를 통해 자기소개를 할 수 있지만, 구체적으로 어떤 역할을 수행하는지는 각 서브타입이 결정합니다. 이러한 타입 기반 구조는 코드의 일관성과 확장성이 크게 향상시킬 수 있습니다.

다형성과 캡슐화

타입 추상화가 제공하는 또 다른 강력한 기능은 다형성(polymorphism) 입니다. 다형성이란, 동일한 메시지를 보내더라도 객체마다 다른 방식으로 처리할 수 있는 특성을 의미합니다.

예를 들어, CafePerson 타입을 통해 모든 구성원에게 introduce() 메시지를 보낼 수 있지만, 실제로는 Customer, Cashier, Barista 각각 고유한 방식으로 자신을 소개합니다.

console.log(customer.introduce()); // [고객] 레니
console.log(cashier.introduce()); // [캐시어] 후니훈
console.log(barista.introduce()); // [바리스타] 가배

이처럼 외부에서는 메시지를 통해 다른 객체와 협력하고, 내부에서는 자율적으로 객체가 행동을 수행하게 되는 것. 이것이 바로 다형성과 캡슐화의 강력한 결합입니다.

타입 계층의 일반화/특수화

추상 타입은 일반적인 행동과 구조를 정의하고, 구체 타입은 이를 특수화하여 구체적인 역할을 수행합니다.

일반화/특수화 관계를 잘 설계하면, 새로운 역할이 추가될 때에도 시스템이 유연하고 일관성 있게 확장될 수 있습니다.

나오면서: 객체지향을 다시 바라보기

『객체지향의 사실과 오해』 1~3장을 공부하면서, 저는 그동안 막연하게 알고 있던 객체지향 프로그래밍(OOP)에 대해 훨씬 깊고 구체적인 이해를 얻게 되었습니다.

처음 자바스크립트를 배우면서 객체를 접했을 때, 저는 객체를 단순히 "데이터와 함수를 묶어놓은 구조" 정도로만 인식했습니다. 또한 class 문법을 당연하게 사용하면서, 클래스와 객체를 별다른 구분 없이 동일시해왔던 것도 사실입니다. 하지만 이번 학습을 통해 객체지향이 말하는 '객체'는 단순한 데이터 구조가 아니라, 식별자(Identity), 상태(State), 행동(Behavior) 을 갖춘 자율적이고 협력적인 존재임을 분명히 이해하게 되었습니다.

특히 기억에 남는 것은,

객체는 단순히 '어떤 데이터를 가지고 있느냐'가 아니라, '어떤 책임을 맡고 있으며, 어떤 협력에 참여하는가' 로 설계의 중심이 이동해야 한다는 관점을 얻을 수 있었습니다. 이 관점은 앞으로 제가 개발자로서 객체를 만들고 시스템을 설계할 때, 훨씬 더 본질적이고 견고한 사고를 가능하게 해줄 것이라고 믿습니다.