여기서 나오는 객체는 자바스크립트 컬렉션 객체가 아니라 프로그래밍 개념 객체를 다룬다. 자스는 일단 나중에 이야기하자.
객체란 객체지향이란 말은 매우 친숙해서 마치 아는 것 같다. 하지만 이번 자바스크립트를 공부하면서 객체를 더 모르게되었다. 좀 더 객체를 자세히 알아보려고 이번 포스팅을 시작했다.
반복된 행동은 우아하지 않다. 반복되는 로직(또는 명령어)을 따로 묶어 우리는 함수라 한다.
(또는 함수(function), 서브루틴(subroutine), 루틴(routine), 메서드(method), 프로시저(procedure) 다 비슷비슷하다.)
우리의 머리는 데이터를 구조화시켜 생각하는 것이 편하다. 그래서 구조화된 데이터
= 레코드(record)
가 필요하다.
(비슷한 말로 객체(Object), 구조체(struct)가 있다.)
// 자료형 => 구조체
typedef struct Person {
int age;
int grade;
}Person
// 명령 => 함수
void sayAge(int age) { printf("%d",age);};
그럼 그 구조화된 데이터에서만 필요한 로직이 묶을 수는 없을까?
스몰토크(smalltalk)에서 객체(Object)
란 해답을 제공한다. 우리가 아는 java, python 등의 객체지향의 방법을 제시했다. 바로 class
를 만들고 그것을 new
로 instanace
를 생성하는 방법이다. (실질적 원조는 simula라고 한다. 그럼에도 순수 객체 지향의 방향성을 보여준 언어는 스몰토크라 한다.)
객체지향 프로그래밍에서 객체는 클래스의 인스턴스이다. 클래스는 자료와 자료를 다루는 명령의 조합을 포함한다. 객체가 메시지를 받아 자료를 처리하고 메시지를 보낸다.
자바의 메모리 구조를 보면 객체는 HEAP에 실체(Instance)하는 것을 볼 수 있다. 클래스에 정의된 메서드는 Methode Area에 의해 중복되지 않게 관리된다.
즉, 메모리적인 관점에서 보면 자바에서 객체란 (데이터 + 함수)라고 할 수 있다. 이는 다른 클래스를 사용하는 객체지향 언어인 파이썬, C# 등도 마찬가지라 할 수 있다.(내부구조가 어떻든 객체를 데이터와 행위(함수)의 조합으로 표현한다는 의미이다.)
사실 c로 불편할 뿐이지 객체지향에서 말하는 자료와 행위를 가진 자료구조도 만들 수 있다. 아까 위의 코드를 바꿔보자
// 자료형 => 구조체
typedef struct Person {
int age;
int grade;
void (*SayAge)(struct Person *this);
}Person
// 명령 => 함수
함수를 struct
안으로 넣고 자신을 받는 구조라고 볼 수 있다. 즉 우리가 사용하는 모든 메서드 Object.methods()는 자신을 첫번째 인자로 사용하는 방법이라 생각할 수 있다.
우리는 분류
라로 세상을 파악한다. 비슷한 것들은 요소들을 그룹을 만들고, 다른 특성을 가지고 있는 요스를 묶인 그룹을 만들어 분류한다. 즉 유사성으로 분류
한다.
우리는 유사성에 따라 어떤 그룹들을 같이 모을 수도 있습니다. 이렇게 계속 모을 수 있고 하나의 큰 그룹이 될때까지 반복된다..
여기서 요소는 객체이고, 그룹은 클래스다. 아래 그림은 자동차와 비행기로 관점에서 생성한 객체개념이다.
도출한 클래스(하위 클래스)들은 그들의 조상(상위 클래스)의 모든 속성과 동작을 상속받는다.
상속에 관한 혼란을 피하기 위해 만들 두 가지 개념이 있다. 그것들은 아마도 객체들간의 두 가지 관계다. 먼저 객체는 다른 객체로부터 도출한다. 이것을 is-a관계라고 부른다. 예를 들어, Car는 Vehicle이다 (Car is a Vehicle).
다음으로 객체는 다른 객체들로 이루어져 있다. 이러한 관계를 has-a관계라고 부른다.
예를들어, Vehicle은 Body를 가지고 있다 (Vehicle has a Body).상속 개념은 is-a 관계와 관련이 있으며, has-a 와는 관련이 없다.
typedef struct Person {
// this를 구현
struct Person *this;
// 멤버 변수, 필드
int age_;
int money_;
// 멤버 함수, 메서드
void (*SayAge)(const struct Person *this);
// 접근 함수, 액세스 메서드
int (*get_money)(const struct Person *this);
void (*set_money)(struct Person *this, int money);
} Person;
Person *newPerson(int age, int grade);
void DeletePerson(struct Person *person_ptr);
void SayAge_(const struct Person *this);
int get_money_(const struct Person *this);
void set_money_(struct Person *this, int grade);
// 생성자 역할을 한다.
Person *NewPerson(int age, int money) {
// (1) 메모리를 Heap에 할당하고 객체 초기화
Person *tmp = (Person *)malloc(sizeof(Person));
tmp->age_ = age;
tmp->money_ = money;
// (2) 함수 포인터 등록하고 외부함수를 메서드에 등록
tmp->this = tmp;
tmp->SayAge = SayAge_;
tmp->get_money = get_money_;
tmp->set_money = set_money_;
return tmp;
}
// (3) 메모리를 해체시킨다.
void DeletePerson(struct Person *person_ptr) { free(person_ptr); }
void SayAge_(const struct Person *this) { printf("%d\n", this->age_); }
int get_money_(const struct Person *this) { return this->money_; }
void set_money_(struct Person *this, int money) { this->money_ = money; }
int main() {
Person *person = NewPerson(15, 90);
// (1) 마치 메서드
person->SayAge(person);
person->set_money(person, 80);
int money = person->get_money(person);
printf("%d\n", money);
DeletePerson(person);
retrun 0;
C++와는 다르게, 포인터를 일일히 인자로 넣어줘야하는 불편함은 있다.
메모리를 살펴보면 외부함수로 한번 구현된 코드를 초기화 시 참조할 뿐이니깐 위에서 봤던 JVM과 비슷하다.
그럼 이제 Person 클래스를 상속받는 Student 클래스를 만들어보자.
typedef struct Student {
// 부모 클래스를 가리키 메서드를 사용하기 위해 선언
Person super;
struct Student *this;
// Student 필드, 메서드 추가
int grade_;
int (*get_grade)(const struct Student *this);
void (*set_grade)(struct Student *this, int grade);
} Student;
Student *NewStudent(int age, int money, int grade);
void DeleteStudent(Student *this);
int get_grade_(const Student *this);
void set_grade_(Student *this, int grade);
Student *NewStudent(int age, int money, int grade) {
Student *tmp = NULL;
tmp = (Student *)NewPerson(age, money);
tmp = (Student *)realloc(tmp, sizeof(Student));
tmp->this = tmp;
// tmp->person = (Person *)tmp;
tmp->get_grade = get_grade_;
tmp->set_grade = set_grade_;
return tmp;
}
void DeleteStudent(Student *this) { free(this); }
int get_grade_(const Student *this) { return this->grade_; }
void set_grade_(Student *this, int grade) { this->grade_ = grade; }
int main() {
Person *person = NewPerson(15, 90);
// (1) 마치 메서드
person->SayAge(person);
person->set_money(person, 80);
int money = person->get_money(person);
printf("%d\n", money);
DeletePerson(person);
Student *student = NewStudent(16, 80, 90);
student->person.SayAge((Person *)student->this);
return 0;
}
완벽한 oop는 아니라도 대충 흉내는 내었다. 여기서 더 java나 c++처럼 oop개념을 쓰기 위해선 필요한 코드가 점점 늘어날 것이다. 언어자체에서 OOP를 지원한다는 것은 이런 보일러플레이트를 많이 줄일 수 있게 된다.
처음에 부모 클래스(Person)로 Heap할당(malloc)하고 나중에 자식 클래스(Student) 사이즈로 재할당(realloc)한 것을 볼 수 있다. 위에 그림을 참고하면 된다. 결국 손자 클래스가 생겨도 메모리에선 계속 재할당만 일어날 뿐이다.(나중에 나올 Javascript의 상속과는 매우 다른 개념이다.)
자세한 건 아래글을 참조하자 : 아님 oop in c로 검색하면 많이 나옴
inheritance-and-Polymorphism-in-C
AN_OOP_in_C.pdf
object-oriented-programming-in-c
그렇다면 다른 해결방법은 어떨까요? 스몰토크의 방언 셀프(self)에선 다른 해결책을 제시했고, 이는 자바스크립트에 영향을 주었다.
전통적인 클래스 기반 OO 언어는 뿌리깊은 이중성을 기반으로 한다.
클래스
는 객체의 기본 품질과 동작을 정의합니다.객체인스턴스
는 클래스의 특정 표현입니다.예를 들어 Vehicle 클래스가 있다. 생각해보십시다. name
을 가지고 건착자재(materisals)
를 전달(drive)
하는 다양한 작업을 할 수 있다.
// 의사코드
class Vehicle {
name;
drive(materials);
}
Bob's_car = new Vehicle(name);
그럼 Bob's car
라는 객체(인스턴스)는 이론적으로는 건축 자재를 전달하라는 메시지를 보낼 수 있어야합니다.
하지만 Bob's_car
가 스포츠카라면 건축자재를 전달하는 행위(Method)는 적합하지 않다.
그럼 SpotsCar
나 Truck
클래스를 만들어 Truck
에 drive()
메커니즘을 제공해야 한다. 그리고 SpotsCar
는 더 빠르게 운전(drive)
할 수 있게해야 된다.
그러나 이렇게 깊은 모델디자인은 더 깊은 통찰력을 요구한다. 이 통찰력은 문제가 발생하기 전에는 알아차리기 힘들다.
이러한 문제들이 프로토타입 기반 객체지향의 등장 이유다.
먼 미래에 일련의 객체와 클래스가 어떤 품질을 가질 지 확실하게 예측할 수 없다면 클래스 계층 구조를 제대로 설계할 수 없기 때문이다.
상황과 요구는 계속 변하고 그럴때마다 객체를 분리하기 위해 다시 만들던가 리팩토링해야 합니다.
스몰토크같은 동적 언어에서 메서드는 변경가능했다. 그러나 상속받은 자식 클래스가 잘못된동작을 일어날 수 있어서 매우 신중하게 변경해야 했다.
c++와 같은 컴파일 되는 언어에서는 실제로 프리 컴파일 된 서브 클래스 메서드가 중단될 수 있다.
그래서 Self에선 이런 클래스와 객체 인스턴스 간의 이중성을 제거하려 했다.
어떤 클래스를 기반으로 하는 객체의 인스턴스를 가지지 않고 기존 객체의 복사본을 만들어 변경합니다. 즉 Vehicle
객체를 복사해 Bob's_car
를 생성한다. 그리고 drive_fast()
메서드를 추가하여 포르쉐 911이라 모델링한다.
이렇게 주로 사본을 만드는 데 사용되는 기본 객체를 프로토타입이라고 한다. 이 기술은 변경을 엄청 단순하게 한다. 기존 객체가 부적절한 모델이면 단순하게 수정된 객체를 사용하면 된다. 기존 객체를 사용하는 코드는 변경할 필요가 없다.
이론적으로 Self의 객체는 모든 독립된 entity
이다.(js도 마찬가지) 자체에는 클래스나 메타 클래스가 없다. 특정 객체에 대한 다른 객체에 영향을 미치지 않지만, 경우에 따라서 영향을 미쳐야 할 수 있다.
일반적으로 객체는 로컬 슬롯(js에서 객체 자신의 필드들)만 이해할 수 있자만 부모 객체를 나타내는 하나 이상의 슬롯(부모 포인터)을 가짐으로써 자신이 없는 행동(메서드)를 부모에게 위임할 수 있다.
만약 좀 더 자세한 원리가 궁금해지면 아래 주소를 참고하세요. 약간의 차이는 있습니다.
https://boycoding.tistory.com/108
이제 JS의 특별한 객체 시스템에서 어느정도 이해가 되었다. 이외에도 좋은 코딩을 위한 여러방법들이 많지만, JS의 객체를 이해하기는 너무 멀어지는 것 같아 이만으로 정리해야 할것 같다.
프로토타입 시스템에 대한 이해를 바탕으로 JS만의 OOP를 살펴보는 좋은 기회였다.