자바스크립트의 객체(메모리 할당, 가비지 컬렉션)

Kyung yup Lee·2021년 5월 18일
0

자바스크립트

목록 보기
7/12

자바스크립트의 객체

객체란.

객체란 일반적으로 객체 지향 프로그래밍에서 클래스를 통해 만들어지는 데이터를 말한다. 하지만 자바스크립트에서 말하는 객체는 종류가 다르다. 자바스크립트는 원시타입과 객체로만 데이터가 구성되어 있다. 때문에 객체는 데이터 타입의 일부라 볼 수 있다. 하지만 원시타입을 제외한, 함수, 배열, 정규 표현식등이 모두 객체로 만들어져 있어, 객체가 매우 중요하다고 할 수 있다.

객체를 생성하는 방법

자바와 C++ 같은 언어에서는 객체를 생성하려면 클래스를 사용해야 한다. 자바스크립트도 ES6 에서부터는 클래스를 지원한다. 하지만 그 방법 외에도 자바스크립트에는 객체를 생성하는 방법이 있다. 오히려 클래스를 사용해서 생성하는게 더 비효율적이다.

  • 객체 리터블
  • object 생성자 함수
  • 생성자 함수
  • Object.create 메서드
  • 클래스

가장 흔하게 볼 수 있는 방법이 객체 리터블이다.

	var a = {}

위와 같이 쓰면 객체가 값으로 만들어져 a 변수에 할당된다. 어떤 번거로운 절차 없이 객체를 생성해서 할당할 수 있다.

다양한 방법을 통해 객체를 생성할 수 있지만, 해당 내용은 다음에 더 공부를 하고 추가하도록 하겠다.

프로퍼티(property)

프로퍼티는 객체의 속성값을 뜻한다. 프로퍼티는 키와 값으로 구성된다. 프로퍼티의 경우 키에 특성이 있다.

프로퍼티의 키는 식별자로 작동한다. 하지만 반드시 식별자 네이밍 규칙을 따르지 않아도 된다. 이것은 프로퍼티의 키를 자바스크립트의 엔진이 해석할 때, 따옴표를 붙이는 것을 통해 문법 오류가 발생하는 것을 막을 수 있기 때문이다. 일반적인 식별자는 따옴표를 붙일 수 없다. 하지만 프로퍼티 키의 경우 식별자 네이밍 규칙을 따르지 않을 때 따옴표를 붙일 수 있다.

var a = {
  name : "hi",
  "last-name" : "there"
    }

위의 예제처럼 프로퍼티 키는 식별자의 규칙을 기본적으로 따라야 하지만 따옴표를 붙여서 이 규칙을 피할 수 있다. 하지만, 특별한 이유가 존재하지 않는 한 식별자 규칙을 따르는 것이 문제를 피할 수 있다고 생각한다.

프로퍼티 접근 방법

프로퍼티에 접근하는 방법은 두 가지가 있다.

  • 프로퍼티 접근 연산자(.) 으로 접근
  • [] 대괄호 접근자로 접근

이 둘은 접근 자체는 비슷하게 하지만, 대괄호 접근자는 반드시 내부에 지정하는 프로퍼티 키를 따옴표로 묶어 주어야 한다. 왜냐하면 이 대괄호 내에 따옴표를 써주지 않으면 이 식별자를 이미 선언된 변수로 판단하고, 이 변수를 찾아나서기 때문이다. 이런 걸 막기 위해서 해당 접근자 내에 있는 값이 문자열임을 명시해주어야 한다.

이 문제에 파생되어 만약 객체 내의 프로퍼티 키가 식별자 규칙을 지키지 않는다면 반드시 대괄호 접근자로만 프로퍼티에 접근해야 한다. 왜냐하면 접근 연산자(.) 에는 따옴표를 사용하지 못하기 때문이다.

var a ={
  hi:"lee",
  "last-name":"kim"
}

a."last-name"; //?
a.last-name; // syntax error
a["last-name"]; // 가능

위와 같은 객체의 last-name 프로퍼티에 접근한다고 했을 때, (.) 접근 연산자로는 접근할 수 없다는 것을 느낄 것이다.

메서드

자바스크립트에서의 모든 "값" 은 프로퍼티 값으로 사용할 수 있다. 이 말은 자바스크립트에서 함수도 값이므로 프로퍼티 값으로 사용될 수 있다는 뜻이다. 이렇게 함수가 프로퍼티 값으로 사용되게 될 경우 이를 메서드라고 한다.

메서드는 es6 부터 축약형으로도 사용될 수 있다.

메모리 동작 방식

객체와 원시타입은 차이가 많다. 기본적으로 프로그래밍 언어는 원시타입으로만 구성되는 것이 가장 이상적이다. 하지만 필연적으로 원시타입 이상의 데이터 타입을 지원해야 하는 경우가 반드시 생긴다. 자바스크립트는 이런 데이터 타입을 객체로 만들었다.

데이터는 기본적으로 immutable(변경 불가)로 작동하는 게 가장 좋다. 데이터의 불변성을 보장해야, 내가 예상하지 못한 변동이 발생하지 않을 것이라 장담할 수 있는 것이다. 원시 타입은 이런 작동이 가능하다. 왜냐하면 해당 데이터 타입을 통해 메모리의 크기를 할당하면서 확정지을 수 있기 때문이다. 예를 들어, 자바스크립트의 Number 타입은 배정밀도 64비트 부동소수점의 크기를 갖는다고 명시되어있다. 이는 8바이트라고 할 수 있는데, 이처럼 데이터 타입의 크기를 명시할 수 있기 때문에, 재할당이 일어난다 했을 때, 얼마 만큼의 데이터가 재할당이 일어날지 예측할 수 있다. 그렇기 때문에, 원시 데이터 타입은 immutable하게 새로운 메모리 공간을 할당하여 변수에 배치해도 예측 가능한 수준이라는 것이다.

반면, 객체는 mutable하게 만들어져 있는데, 이는 객체의 데이터 크기가 가변적이기 때문이다. 객체를 immutable 하게 만든다면 해당 객체를 복사하거나 매개변수로 넘겨줄 때마다 새로 재할당하게 되는데, 이 객체의 크기를 예상할 수가 없는 것이다. 이 객체가 1기가가 될지 10byte일지 알 수가 없기 때문에 이 언어의 설계를 할 때 객체를 immutable하게 만들 수가 없는 것이다. 이 때문에 객체는 mutable하게 만들어져 있다.

pass by value, pass by reference

이 immutable, mutable 한 값에서 파생되는 개념이 pass by value, pass by reference이다. 함수의 매개변수에 적용되면 call by value, call by reference 라는 개념으로 사용되기도 한다. 사실 위의 개념은 정말 파고들어가면 자바스크립트에서는 해당되지 않는 개념이다. 왜냐하면 자바스크립트는 매니지드 언어이기 때문에, 모든 변수는 메모리 주소와 매핑되어있고, 결국 전달되는 것은 메모리 주소이기 때문이다. C의 경우 메모리 주소를 직접 다루기 때문에 해당 메모리 주소를 전달하는지, 값을 전달하는지가 명확하다.

자바스크립트는 원시 타입의 경우 값을 메모리 공간에 할당한다고 했다. 그리고 객체의 경우에는 객체의 값은 힙 메모리 공간에 할당되고, 콜스택에는 이 메모리 공간의 주소가 저장된다. 때문에, 함수에서 매개변수를 호출하거나, 변수에 이 데이터를 재할당 하게 되면, 전해지는 데이터가 다르다.

function func1(a, b){
  
}

위의 a 매개 변수에는 원시타입의 변수를 인수로 넘겨주고, b에는 객체를 넘겼다고 하자. 이러면 a와 b 모두 기존에 있던 변수의 값을 복사하여 넘겨주게 된다. 하지만 원시타입은 저장되어있던 값이 일반 데이터 타입의 값이고, 객체는 객체의 메모리 변수가 저장되어 있었다. 때문에 이를 함수에 인수로 복사하게 되면 객체의 경우 메모리 주소가 복사되게 된다. 이렇게 다른 함수에서 이 주소 값을 사용해 데이터 조작을 하게되면 함수 외부의 원본 객체에 변형이 일어나게 된다. 이를 얕은 복사라 부른다. 이것이 call by reference이고, 이를 통한 데이터 조작은 조심해야 한다.

가비지 컬렉션

객체를 설명하면서 빠질 수 없는 개념이 가비지 컬렉션이다. 자바스크립트는 매니지드 언어라 메모리에 직접 접근할 수 없다고 했다. 그러면 이제 더 이상 사용하지 않는 메모리 부분을 어떻게 해제할 수 있는지를 알아야 한다.

이 부분을 엔진에서 해결해준다. 자바스크립트 엔진은 가비지 컬렉션을 실행시켜 더 이상 참조되지 않아, 메모리에서 사용되지 않는 부분을 해제해 준다.
자바스크립트 엔진의 가비지 컬렉터는 기본적으로 mark-and-sweep 기법을 통해 가비지 컬렉션을 실행한다. mark-and-sweep 은 참조되지 않는 메모리를 모두 찾아내 해제시켜주는 기법을 말한다.

자바스크립트 엔진의 가비지 컬렉션(이하 gc)은 minor gc 와 major gc 로 나누어진다. minor gc 와 major gc의 기준은 메모리 공간이 얼마나 오랜 시간동안 참조되었는지이다. minor gc는 참조된지 얼마 안된 메모리 공간이기 때문에 상대적으로 빠르게 해제될 것이라 전제하고 있다.

minor 가비지 컬렉션

minor gc는 두 가지 메모리 공간을 나눠서 실행된다. 새로 만들어진 객체는 new라는 영역에 할당 된다. new 영역은 from 과 to 영역으로 나누어져 있어서, from 영역에 새로운 객체가 할당된다. from 영역은 가득 차면 gc 를 시작한다. 스택 포인터의 변수들을 시작점으로 from 에서 참조되는 객체들을 모두 to 영역으로 옮긴다. 이 과정을 거친 후 from 에 남아있는 메모리들은 참조되지 않는 뜻이니 모두 해제된다. minor gc 는 stop-the-world gc이나 데이터 양이 작아 신경쓰지 않아도 될 정도이다. 이 과정이 반복되고 나서도 계속 살아남은 데이터들은 old generaion 으로 옮겨진다.
https://speakerdeck.com/deepu105/v8-minor-gc

major 가비지 컬렉션

major gc는 오래 살아남은 메모리 들이 저장되는 공간(old generation)에서 일어나는 가비지 컬렉션이다. 기본적으로 old generation 은 메모리 단위가 크다. 때문에 gc가 일어나면 stop-the-world 현상이 오래 지속될 수 있고, 이를 해결하기 위한 방법이 존재한다.

major gc 는 mark-sweep-compact 과정으로 실행된다.

  • mark : 스택 포인터(root) 에서 시작 해서 참조되는 객체들을 연쇄적으로 찾는다. 이 과정에서는 dfs 가 이용된다.
  • sweep : 마킹되지 않은 모든 메모리를 해제한다.
  • compact : 메모리를 해제하고 나면 메모리가 조각화된다. 이를 해결하기 위해 압축을 진행한다.

위에서 gc에는 stop-the-world 현상이 발생할 수 있다 했고, 이를 해결해야 한다 했다. 우선 gc는 실행되면 mark를 하면서 그래프를 만든다. 애플리케이션이 멈춰야 하는 이유는 마킹과 앱 실행이 동시에 발생하면 marking하면서 만들어지는 그래프와 실제 메모리 상태가 달라질 수 있기 때문이다. marking이 되지 않았다고 판단했는데, gc가 진행되는 도중에 연결이 되면 참조된 메모리가 해제되어 버릴 것이다. 이는 문제로 이어진다. 이를 위해 gc가 실행되는 도중에는 앱이 멈춰야 하는 것이다. 하지만 old generation gc의 경우 메모리 크기가 크기 때문에, 이 stop현상이 사용자 경험에 까지 영향을 미칠 수 있다.

이를 해결하기 위해서 자바스크립트 엔진은 다양한 방법을 도입했다. 기본적으로 멀티스레딩을 통해 이 문제를 해결하려 한다.

  1. 인크리멘탈 GC : 점진적 GC라는 의미를 가진 인크리메탈 gc는 gc를 여러 단계로 분리해서 메인스레드 사이 사이에 넣어, 메인스레드가 gc를 위해 오랜 시간 멈춰있는 것을 막는다.
  2. 동시 마킹: 마킹을 진행하는 경우 멀티 스레딩을 통해 동시에 마킹을 진행한다. 이 시간 동안 앱 실행이 멈추지 않고, 스레드 간 동기화를 통해 참조 그래프가 변형되는 것을 막는다.
  3. 동시 스위핑/압축 : 마지막 마킹이 실행된 후, stop-the-world 를 일으키고, 스위핑과 압축을 멀티스레딩으로 진행한다.
  4. 레이지 스위핑 : gc의 스위핑은 꼭 필요할 때 까지 미뤄진다.
profile
성장하는 개발자

0개의 댓글