JS03 - 객체 (1)

자바스크립트의 객체란? / 프로퍼티 / 원시 값과 객체 / 값에 의한 전달과 참조에 의한 전달

2022-01-16에 씀

본 시리즈는 모던 자바스크립트 Deep Dive 책을 참고하여 작성하고 있습니다.

객체

객체는 왜 필요한가?

(그림 출처)

절차지향 프로그래밍은 프로그램이 순차적으로 처리되도록 하여 전체가 유기적으로 연결되도록 한다. 컴퓨터가 작업을 처리하는 방식과 유사하여 객체지향에 비해 빠르다. 그러나 프로그램이 복잡해지고 커질수록 코드의 유지보수가 어려우며, 실행 순서가 정해져 있으므로 실행 순서가 바뀌어야 할 경우 결과가 동일하단 것을 보장할 수 없다. 대표적으로 C언어가 절차지향을 따른다.

객체지향 프로그래밍은 실제 세계의 모든 사물을 객체로 모델링해 개발하는 방법이다. 위 그림의 "객체지향 분석" 부분에서는 고객과 자판기라는 객체를 모델링하고, 두 객체가 상호작용하는 방식으로 "절차지향 분석"과 같은 행동을 하도록 했다. 고객과 자판기라는 객체는 돈과 제품이라는 상태를 가지고, 돈이나 제품을 주고받는 등의 동작을 가지고 있으며 서로 상호작용한다. 대표적으로 Java가 객체지향을 따른다.

절차지향 프로그래밍은 프로그램의 순서와 흐름을 먼저 세우고 필요한 자료구조와 함수를 설계하는 방식이고, 객체지향 프로그래밍은 자료구조와 모듈을 먼저 설계한 다음에 이들의 실행 순서와 흐름을 짜는 방식이라고 할 수 있다. (설명 출처)

자바스크립트는 명령형, 함수형, 프로토타입 기반 객체지향 프로그래밍을 지원하는 멀티 패러다임 프로그래밍 언어이다. 그런데 자바스크립트는 객체지향을 지원하는 언어이지만 특이하게도 public, private, protected와 같은 접근 제어자 키워드가 없다. 대신에 프로토타입을 지원하여 클래스 기반 객체지향보다 더 효율적이고 강력한 객체지향을 제공한다.

ES6에서는 클래스가 도입되었고, ES2019에서는 class 안에서 private 필드를 선언할 수 있게 되었다.

객체지향 프로그래밍의 특징

  1. 추상화: 다양한 속성 중에서 필요한 속성만 간추려 내어 표현
  2. 캡슐화
  3. 데이터와 기능을 하나로 묶어서 관리
  4. 외부에서 내부의 데이터를 접근하는 것을 제한
  5. 상속: 상위 개념의 특징(데이터, 함수)을 하위 개념이 물려받음 → 불필요한 중복을 줄이고 코드를 재사용
  6. 다형성: 같은 호출이라도 호출된 객체에 따라 다른 동작을 하도록 함 (오버라이딩)

객체란?

객체의 정의는 아래와 같다.

자바스크립트를 구성하는 거의 모든 것은 객체이고, 이 객체는 프로퍼티와 메서드로 구성된 집합체이다. 원시 값(number, string, undefined, ...)을 제외한 모든 것이 객체이다. 객체에는 함수, 배열, 정규 표현식 등이 있다.

원시 값객체
나타내는 값단 하나의 값다양한 타입의 값을 하나의 단위로 구성한 복합적 자료구조
변경 가능한가?X (immutable)O (mutable)
예시3, “hello”, undefined, null, NaNvar person = {name: “yejin”, id: 3, getName() {return name}}
var people = [ person1, person2 ]

객체는 0개 이상의 프로퍼티와 메서드로 구성된다고 했다. 프로퍼티는 키(key)와 값(value)으로 구성된다. 프로퍼티의 값에는 자바스크립트에서 사용 가능한 모든 값이 들어갈 수 있다. 만약 프로퍼티의 값이 함수일 경우, 그 프로퍼티와 함수는 메서드(method)라고 부른다. 메서드는 자신이 속한 객체의 프로퍼티 값을 참조하고 조작할 수 있다. 즉, 객체는 그 상태를 나타내는 값인 프로퍼티와, 상태를 참조하고 조작하는 동작메서드를 통해 상태와 동작을 하나의 단위로 구조화할 수 있다.

객체 생성하기

  1. 객체 리터럴: 중괄호 내에 0개 이상의 프로퍼티를 정의
  2. Object 생성자 함수
  3. 생성자 함수
  4. Object.create 메서드
  5. 클래스 (ES6)

프로퍼티

프로퍼티의 키를 통해 프로퍼티 값에 접근할 수 있고, 값의 식별자 역할을 한다. 심벌 값을 사용할 수 있지만 문자열을 사용하는 것이 일반적이다.

1var key = "key_name_3";
2var obj = \{
3 keyName1: "식별자 네이밍 규칙을 따르는 경우",
4 "key-name-2": "식별자 네이밍 규칙을 따르지 않는 경우(특수문자 포함됨)",
5 [key]: "동적으로 프로퍼티 키 생성하기",
6 2: "숫자 값을 프로퍼티 키로 사용하기"
7};
8
9console.log(obj.keyName1); // 식별자 네이밍 규칙을 따르는 경우
10console.log(obj["key-name-2"]); // 식별자 네이밍 규칙을 따르지 않는 경우(특수문자 포함됨)
11console.log(obj.key_name_3); // 동적으로 프로퍼티 키 생성하기
12console.log(obj);

프로퍼티에 접근할 때는 위와 같이 두 가지의 방법으로 접근할 수 있다. 존재하지 않는 프로퍼티에 접근할 경우 에러가 아니라 undefined를 반환한다.

프로퍼티 사용하기

1var cat = \{ name: "chunsik", age: 2 }
2
3console.log(cat.name); // 프로퍼티 접근
4cat.age += 1; // 프로퍼티 값 갱신
5cat.hobby = "놀기"; // 프로퍼티 동적 생성
6delete cat.age; // 프로퍼티 삭제

ES6 - 객체 리터럴 확장 기능

1const name = "chunsik";
2const age = 2;
3
4const cat = \{
5 name,
6 age,
7 sayHi() \{ console.log("meow"); }
8};
9
10console.log(cat);
11// \{name: 'chunsik', age: 2, sayHi: *f* sayHi()}
12
13cat.sayHi() // meow

원시 값과 객체

원시 값

값의 데이터 타입은 크게 원시 타입과 객체 타입으로 나누어진다. 이 두 타입을 나누는 기준은 값의 변경 가능 여부이다. 원시 타입의 값은 변경이 불가능하고, 객체 타입의 값은 변경이 가능하다.

값은 표현식이 평가되어 생성된 결과로, 메모리 공간에 저장된다. 값이 변경 가능하다는 것은 메모리 공간에 들어가 있는 값 자체를 바꿀 수 있다는 것이다. 이 경우, 변수가 참조하는 메모리 공간의 주소는 바뀌지 않는다.

그러나 자바스크립트의 원시 타입 값은 불변성(immutability)을 가지는 변경 불가능한 값이다. 따라서 메모리 공간에 한 번 할당되면 그 공간에 있는 값을 수정할 수 없다. 변수에 재할당이 일어나면 변수는 새로운 메모리 공간을 확보하여 그 공간에 재할당된 값을 저장하고, 참조하는 메모리 공간을 변경한다.

이러한 특징 덕분에 데이터 신뢰성을 확보할 수 있다. 만약에 변경 가능한 값이었다면, 참조하고 있는 메모리 공간의 값이 의도치 않게 변경될 수 있는 위험성을 가지게 된다. 그러면 값의 변경, 즉 상태의 변경을 추적하기 어렵다.

문자열의 경우

문자열은 유사 배열 객체이다. 따라서 배열처럼 각 문자에 접근할 수 있다. 그러나 문자열은 원시 타입의 값이므로, 변경 불가능한 값이며 읽기 전용 값이다. 따라서 예기치 못한 변경에서 자유롭고, 데이터의 신뢰성을 보장한다.

1var str = "string";
2console.log(str[2]); // r
3
4str[2] = "str";
5console.log(str); // string

객체 타입

원시 값은 메모리를 상대적으로 적게 사용하는 반면, 객체의 경우 크기가 매우 클 수도 있다. 객체는 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가되거나 삭제될 수 있고, 프로퍼티 값에 제약이 없어 매우 큰 프로퍼티가 들어올 수도 있다. 따라서 원시 값처럼 확보해야 할 메모리 공간의 크기를 정해둘 수가 없고, 할당될 때마다 새로운 메모리 공간을 할당하는 것은 낭비이다. 메모리의 효율적인 소비가 어렵고, 성능이 나빠진다.

이처럼 객체를 생성하고 관리하는 것은 복잡하고 비용이 많이 든다. 따라서 메모리를 효율적으로 사용하고, 비용을 절약하며, 성능을 향상시키기 위해서 원시 값과 다른 방식으로 동작하도록 설계되어 변경 가능한 값으로 관리된다.

자바스크립트에서는 프로퍼티 키를 인덱스로 사용하는 해시 테이블과 같은 방식으로 객체를 구현한다.

변수는 값이 저장된 메모리 공간의 주소를 가진다. 객체가 할당된 변수가 가지고 있는 메모리 주소를 통해 메모리 공간에 접근하면, 참조 값에 접근할 수 있다. 참조 값은 실제 객체가 저장된 메모리 공간의 주소이다.

객체는 변경 가능한 값이다. 따라서 재할당 없이도 객체를 직접 변경할 수 있다. 위에서 살펴봤듯이, 재할당을 하지 않고도 객체에 프로퍼티를 추가하거나, 삭제하거나, 갱신할 수 있다. 이 때, 객체를 할당한 변수의 참조 값(0x0000239F)은 변경되지 않는다.

얕은 복사와 깊은 복사

자바스크립트의 모든 값은 객체의 프로퍼티 값이 될 수 있다. 즉, 객체의 프로퍼티 값으로 객체가 들어갈 수도 있는 것이다.

1const obj = \{
2 x: \{ y: 1 }
3};

이 객체를 복사해서 사용하고 싶을 수도 있다. 복사해서 만들어진 객체와 원본 객체는 참조 값이 다른 별개의 객체가 된다. 복사를 하는 방법에는 얕은 복사와 깊은 복사가 있다.

1// 얕은 복사
2const obj_shallow = \{ ...obj };
3obj === obj_shallow; // false
4obj.x === obj_shallow.x; // true
5
6// Node.js 환경에서 실행
7// lodash --> 자바스크립트 유틸 기능을 제공하는 라이브러리
8// npm install lodash
9const _ = require('lodash');
10const obj_deep = _.cloneDeep(obj);
11obj === obj_deep; // false
12obj.x === obj_deep.x; // false

값에 의한 전달과 참조에 의한 전달

일반적으로, 함수를 호출하며 인수를 전달할 때 그 방법에는 두 가지가 있다. (자바스크립트에서만 해당되는 내용이 아니다)

  1. 값에 의한 전달 (Call by Value): 인수로 전달되는 변수의 값을 함수 내의 매개변수에 복사한다. 매개변수의 조작은 인수로 전달된 변수에 아무런 영향을 미치지 않는다.
  2. 참조에 의한 전달 (Call by Reference): 인수로 변수의 주소값을 전달한다. 매개변수를 조작하여 인수로 전달된 변수의 값을 함수 안에서 변경할 수 있다.

자바스크립트에서 원시 값과 객체의 차이를 살펴보기 위해, 아래 질문에 집중하여 값에 의한 전달과 참조에 의한 전달을 살펴보도록 할 것이다.

변수에 변수를 할당했을 때, 무엇이 어떻게 전달되는가?

값에 의한 전달

변수에 원시 값을 가지는 변수를 할당하면, 할당받는 변수에는 할당되는 변수의 원시 값이 복사되어 전달된다. 이를 값에 의한 전달이라고 한다.

값에 의한 전달은 자바스크립트에서만 사용되는 용어가 아니다. 엄밀히 말하자면 변수에는 메모리 주소가 전달된다. 식별자는 값을 기억하는 것이 아니고 메모리 주소를 기억하며, 값을 가져오기 위해서 메모리 주소를 참조하기 때문이다. 따라서 값에 의한 전달도 사실은 값을 전달하는 것이 아닌, 메모리 주소를 전달한다. 대신 그 메모리 주소를 통해 메모리 공간에 접근하면 값을 참조할 수 있다.

1var original = 50;
2var copy;
3
4copy = original;
5
6console.log(original); // 50
7console.log(copy); // 50
8
9original = 80;
10
11console.log(original); // 80
12console.log(copy); // ?

위와 같은 상황에서, copy 변수에 original 변수의 값을 할당하면, original 변수가 가진 값이 복사되고, 그 복사된 값이 copy 변수에 할당된다. 두 변수는 다른 메모리 공간에 저장된 별개의 값을 가지고 있으므로, original 변수를 재할당해도 copy 변수가 가진 값에는 영향을 주지 않는다.

자바스크립트 엔진에 따라 그림과 같이 동작하지 않을 수도 있다. 위의 그림에서는 변수에 원시 값을 가지는 변수가 할당되자마자 새로운 메모리 공간에 원시 값이 복사되었지만, 어떤 엔진에서는 값이 할당되었을 때에는 원시 값 메모리 공간을 같이 참조하다가 재할당이 이루어졌을 때 새로운 메모리 공간 할당이 이루어질 수도 있다.

참조에 의한 전달

원시 값과는 다르게, 객체는 여러 개의 식별자가 하나의 객체를 공유할 수 있다. 즉, 여러 변수가 하나의 객체를 참조하고 있을 수 있다.

1var obj = \{ name: "lee" }
2var copy = obj;

그림처럼, obj와 copy라는 두 변수가 가지고 있는 메모리 공간은 다르지만, 그 메모리 공간이 가지고 있는 참조값은 0x000000F2로 동일하다. 즉, 두 변수가 가진 참조값이 같은 메모리 공간을 가리키고 있고, 이것은 하나의 객체가 있는 메모리 공간을 여러 개의 변수가 참조하고 있다는 뜻이다. 따라서, 하나의 변수에서 객체를 변경하면 서로 영향을 주고받게 된다.

1obj.age = 55;
2copy.name = "Choi";
3
4console.log(obj);
5console.log(copy);
6
7// 둘 다 \{ name: "Choi", age: 55 }

정리하자면, 자바스크립트에서 값에 의한 전달참조에 의한 전달 모두 식별자가 기억하고 있는 메모리 공간에 저장된 참조값을 복사해서 전달한다. 차이는 값에 의한 전달은 원시 값을 참조하는 참조값을 전달하고, 참조에 의한 전달은 객체의 참조값이 저장된 메모리 공간을 참조하는 참조값을 전달한다.

  1. 변수는 메모리 공간의 주소를 기억하고 있다.
  2. 만약 변수가 원시 값을 가지고 있다면 → 원시 값이 저장된 메모리 공간의 주소를 기억하고
  3. 변수가 객체 값을 가지고 있다면 → 변수가 기억하고 있는 메모리 공간의 주소를 참조하면 → 그 메모리 공간에는 실제 객체가 있는 메모리 공간 주소를 기억하고 → 그 메모리 공간 주소를 참조하면 실제 객체를 얻을 수 있다
1var x = 80;
2var y = \{ name: "Chunsik", age: 3 };

프로필 사진

조예진

이전 포스트
객체지향 프로그래밍이란?
다음 포스트
Props와 State와 Ref