2021-07-30 데이터 타입

2021-07-30
  • JavaScript

데이터, 기억할게!



오늘은 데이터 타입에 대한 이야기를 해보자. 처음 프로그래밍 언어를 배우면, 흔히 “Hello, World”를 화면에 어떻게든 출력하는데 성공하고 나면, 으레 다음 과정으로 대체로 변수 선언과 할당을 맛보게 된다. 이 과정을 인간 일상에 빗대어 설명하다 보면 결국 ‘머릿속’에 ‘기억한다’ 정도의 수준을 벗어나지 못한다.

이제는 조금 더 멀리 가보자. 머릿속과 기억이라는 비유를 들추고 장막 안쪽을 들여다보면 그 안에는 CPU와 메모리가 있고, 다시 이 둘을 0과 1로 매만지는 메커니즘이 존재할 뿐이다. 개발자라면 기억 보다는 ‘메모리 할당’과 ‘해제’라고 부르는 게 익숙해야 하지 않을까.

우리는 여러 프로그래밍 언어 중 자바스크립트의 데이터 관리를 위한 몇 가지 원리와, 그 데이터를 다루는 형식인 타입에 대해 살펴보도록 하자.


데이터가 메모리에 집 지을 때 평수를 정하는 방법

어떠한 컴퓨터 프로그래밍 언어든 메모리라는 컴퓨터의 자원에 접근하고 활용하는 언어만의 특정한 방식을 가지고 있다.

일반적으로 C 같은 언어를 매니지드(Managed) 언어라고 부른다. 개발자가 프로그램이 점유하게 될 컴퓨터 메모리 상의 모든 공간을 관리해야 한다. 변수의 크기를 직접 선언, 할당하고, 해제해야 한다. 더 많은 권한에 따른 책임을 갖게 된다. 그래서 이러한 매니지드 언어를 활용한 개발의 경우 개발자의 역량에 따라 프로그램의 성능이 극단적으로 차이를 보이게 된다고 한다.

반면 자바스크립트는 이 자원에 접근하고, 점유하고, 다시 해제하여 되돌려주는 일련의 과정들을 개발자가 아닌 언어 자체에서 지원한다. 개발자가 메모리를 직접 관리하지 않는다는 뜻에서 언매니지드(Unmanaged) 언어라고 부른다. 더 이상 사용하지 않는 데이터 공간을 정리해주는 가비지 컬렉팅(GC, Garbage Collecting) 기능을 언매니지드 언어의 특징 중 하나로 볼 수 있다. 메모리를 수동으로 다루지 않기 때문에 굉장히 편리하지만, 편리한만큼 메모리를 세밀히 다룰 수 있는 수단 자체가 개발자에게 주어지지 않는다는 뜻이기도 하다.

메모리는 데이터를 저장 할 수 있는 메모리 셀의 집합체이고, 메모리 셀 하나의 크기는 1바이트다. 컴퓨터는 이를 단위 삼아 데이터를 읽고 쓴다. 이러한 메모리 위에 개발자는 필요한 공간을 할당하고 저장하고자 하는 값을 할당하게 된다.

C에서는 같은 숫자라도 메모리에서 차지하는 공간의 크기에 따라 서로 다른 자료형으로 구분(ex. char - 8비트, short - 16비트, int - 32비트, float - 32비트, double - 64비트 등)한다. 반면 자바스크립트는 모든 숫자가 하나의 number라는 자료형으로 분류된다. number 타입은 무조건 64비트 배정도 부동소수점(double precision float)으로 처리 되어 8바이트(8 * 8bit)를 차지하게 된다.

참고 : 64bit = 1bit(부호 - sign) + exponent(지수 - 11bit) + 가수(fraction - 52bit)

ex. 1.fraction * 2^n


float imgDouble-precision floating-point format (wikipedia)

이렇게 자바스크립트가 자료형에 대한 압박 없이 타입을 간단한 방식으로 사용할 수 있는 것은 과거에 비해 넉넉해진 메모리 크기와 연관이 있다. 타이트한 메모리 관리의 필요성이 적은 자원을 효율적으로 쓰는 방법(최적화)과 연관이 깊었다면, 거꾸로 그 관리의 필요성이 비교적 작게 느껴지는 환경 속에서는 메모리를 하나하나 관리하는 것이 개발 생산성을 저해하는 요소가 될 수도 있다는 뜻이다.


변수, 식별자

집에 이름을 붙이는 과정을 생각해보자. 우선 “서울특별시 강남구 테헤란로 427은 토니꺼” 라고 선언한다. 다른 용도로는 사용하지 않도록 ‘서울특별시 강남구 테헤란로 427’이라는 공간에 ‘토니네 집’이라는 이름을 붙여주고, 온 세상 사람들에게 이 사실을 알게 한다.

어쩌다 플스 게임이 하고 싶어서 토니 집을 가게 됐다. 그러면 아무 택시나 잡아타고 구체적인 주소 대신 ‘토니네 집’을 가달라고 하면 된다. 택시 기사는 어렵지 않게 그 이름을 알아 듣고 ‘서울특별시 강남구 테헤란로 427’로 승객을 데려다 주게 된다.

만약 토니 집이 서울특별시 강남구 테헤란로 427에서 428로 이사 갔다고 해서 변할 건 없다. 사람들이 이사 여부와 새로운 집 주소만 잘 알고 있다면, 427이든 428이든 토니 집을 찾아갈 수 있을 것이다.

부득이 비유를 들어 설명했다. 여기서 토니 집 주소인 ‘테헤란로 427’은 메모리 상의 주소(ex. 0x1234)이자 변할 수 있는 값인 변수(ex. ‘테헤란로 427’ -> ‘428’)에, ‘토니네 집’이라는 이름은 식별자에 해당한다. 변수명이라고 생각해도 좋다.

“변수(variable)는 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름을 말한다. (…) 값의 위치를 가리키는 상징적 이름인 변수는 프로그래밍 언어의 컴파일러 또는 인터프리터에 의해 값이 저장된 메모리 주소로 치환되어 실행된다. 따라서 개발자가 직접 메모리 주소를 통해 값을 저장하고 참조할 필요가 없고 변수를 통해 안전하게 값에 접근할 수 있다.”

이웅모, <모던 자바스크립트 Deep Dive>

“변수란 결국 변경 가능한 데이터가 담길 수 있는 공간 또는 그릇”

정재남, <코어 자바스크립트>

위 예시를 자바스크립트 코드로 옮겨보면 다음과 같다.

// 변수 명: 토니네_집, 변수 값: 427
let 토니네_집 = "서울특별시 강남구 테헤란로 427";

// 변수에 값을 재할당 (변수 값: 427 -> 428)
토니네_집 = "서울특별시 강남구 테헤란로 428";

새로운 식별자를 생성하고 자바스크립트 엔진에 식별자의 존재를 알리는 행위를 선언(declaration)이라고 한다. 변수에 값을 저장하는 것을 할당(assignment)(대입, 저장)이라고 하고, 이 변수에 저장된 값을 읽어 들이는 것은 참조(reference)라고 한다. "변수명은 그 값의 이름이므로, 그 이름을 통하면 값을 참조할 수 있다"고 간단히 정리해두자.

위 코드를 다시 살펴보면 우선 let 변수 선언문을 사용해 ‘토니네_집’ 이라는 변수를 선언했다. 메모리의 특정 공간에 “서울특별시 강남구 테헤란로 427”이라는 문자 리터럴을 저장해두고, 해당 메모리 주소를 ‘토니네_집’이라는 변수와 연결지었다.

이제 자바스크립트 인터프리터는 런타임에 ‘토니네_집’이라는 식별자를 만나게 되면 식별자와 연결된 메모리 주소를 받아들게 되고, 그 주소를 찾아가 “서울특별시 강남구 테헤란로 427”이라는 문자열을 최종적으로 만나는 과정을 거치게 된다.

여기서 변수명 “토니네_집”이 저장된 위치의 메모리 주소와 “서울특별시 강남구 테헤란로 427”은 각각 변수 영역데이터 영역에 각기 다르게 저장된다. 이유만 간단하게 말하자면, 전자는 필요한 메모리 크기가 숫자(64비트 = 8바이트)로 정해져 있고, 후자는 길이에 따라 가변적으로 달라지는 바이트 크기를 가진 문자열 형태이기 때문이다.


자바스크립트의 데이터 타입

이상 자바스크립트에서 변수가 생성되고 값이 부여되는 과정을 살펴보았다. 다음은 간단히 자바스크립트 내에 존재하는 값의 종류인 타입에 대해서 알아보자.

자바스크립트에는 크게 기본형(원시형, primitive)참조형(reference) 데이터가 있다.

기본형 데이터로는 숫자(number), 문자열(string), 불리언(boolean), undefined, null과 ES6에서 추가된 심볼(symbol) 등이 있다. 사실, 이 이외의 값은 객체라고 할 수 있다.

참조형 데이터에는 객체(object)가 있는데, 배열, 함수, 날짜, 정규표현식Map, WeakMap, Set, WeakSet 등 최신 문법에서 추가된 자료형도 포함된다.

너무 기본적인 사항은 건너 뛰고, 재미있을 만한 포인트들만 짚어보자.

  • 자바스크립트는 양의 무한대와 숫자가 아닌 값을 표현하기 위해 전역 변수 Infinity, NaN을 읽기 전용 값으로 미리 정의한다.
  • NaN의 경우 그 자신 뿐만 아니라 다른 값과 같은지 비교할 수 없다. 그래서 유일하게 x != x가 성립하는 값이고, 이 성질을 이용하여 isNaN 함수를 구현할 수 있다.
  • 자바스크립트에서 0.1 + 0.2 === 0.3은 성립하지 않는다. 이는 자바스크립트의 결함이 아니라 이진 표현법(binary representation) 부동소수점 숫자를 사용하는 언어의 공통적 현상이다. 1/2, 1/8, 1/1024 등은 정확히 표현할 수 있으나, 10진수 분수는 근사치로 표현하게 된다. 이 과정에서 발생하는 반올림 오차로 인해 위 예시와 같은 부정확한 연산이 발생한다. 이 때 Number.EPSILON이라는 정적 속성을 사용해 연산의 동일성을 확인할 수 있다.
  • 문자열은 16비트(2바이트) 값들이 연속적으로 나열된 변경이 불가능한 값이며, 각 문자는 유니코드 문자로 표현된다. 즉, 문자열 길이 값은 문자열에 들어 있는 16비트 값의 개수다.

undefinednull은 특별히 흥미롭다. 이 둘은 원시값이긴 하지만 자기 자신만을 값으로 갖는 독립적인 타입이다.

  • null아무 값도 갖지 않음을 가리킬 때 사용된다. 의도적인 부재를 나타낼 때 쓰면 좋다.
  • null은 객체다. typeof 연산자로 평가하면 ‘object’를 반환한다.
  • undefinednull보다 심한 부재 상태를 나타낸다. 초기화 되지 않은 변수, 존재 하지 않는 값에 접근하려고 할 때 얻는 값, 반환 값이 없는 함수의 반환 값 등이다. 따라서 시스템 적으로 부재 상태를 나타내는 용도로 사용될 수 있도록 직접 undefined를 사용하는 일은 없도록 하자.

symbol또한 굉장히 독특하고도 유용한 특성을 가지고 있어 잘 알아둘 필요가 있다.

  • 리터럴이 아닌 함수를 호출하여 생성하는 유일한 원시값이다. (ex. Symbol("심볼"))
  • 다른 값과 절대 중복되지 않는 유일무이한 값이다. 함수에 인자로 넘겨준 값은 단순히 해당 심볼을 설명해줄 뿐이다.
  • 이러한 성질을 사용하여 충돌할 일 없는 객체의 키값 등으로 사용할 수 있다. 상수처럼 취급하면 타입스크립트 등에서 사용되는 Enum 등을 구현할 수도 있다.
  • 한번 선언된 심벌은 전역 심벌 레지스트리(global symbol registry)에 등록된다. Symbol.for() 메서드를 사용하면 이 전역 레지스트리에서 특정 심볼을 찾거나, 새롭게 할당이 가능하다.
  • Symbol.iterator 같은 빌트인 심벌 값이 있고 이를 well known symbol이라고도 부른다.

덧붙여 래퍼(wrapper) 객체라는 개념이 있다. 객체에 프로퍼티와 메서드가 있는 것은 자연스럽다. 하지만 곰곰 생각해보면 숫자와 문자열에도 프로퍼티와 메서드가 있는 것처럼 느껴진다. 이 부분에 대한 힌트는 생성자 함수로 생성한 원시값과 리터럴로 생성한 원시값을 비교하면서 엿볼 수 있다.

참고로 리터럴이란 “사람이 이해할 수 있는 문자 또는 약속된 기호를 사용해 값을 생성하는 표기법”이다. 예컨대 숫자 3은 단순한 3이 아니라 ‘숫자 리터럴’이다. 자바스크립트 엔진은 이렇듯 사람이 알아볼 수 있는 리터럴을 읽고 평가하여 실제 숫자 3을 생성하게 된다.

// String { '0' : 'j', ..., length: 10, __proto__: { ... } }
const str1 = new String("javascript");

// "javascript"
const str2 = "javascript";

“여러분이 문자열의 프로퍼티를 참조하려고 할 때, 자바스크립트는 new String()울 호출한 것처럼 문자열 값을 객체로 변환한다. 이 객체는 문자열 메서드를 상속하며, 프로퍼티 참조를 살펴보는 데 사용된다. 일단 프로퍼티 참조가 해제되면 새로 생성된 임시 객체는 메모리에서 회수된다. (…) 문자열, 숫자, 불리언의 프로퍼티에 접근하려고 할 때 생성되는 임시 객체는 래퍼(wrapper) 객체로 알려져 있다.”

데이비드 플래너건, <자바스크립트 완벽 가이드>


불변성 (immutability)

불변성이란 말 그대로 ‘변경 불가능한 성질’이며 읽기 전용(read-only)을 뜻한다.

정확히 말하자면 이 불변성은 변수가 아니라 원시 값에 대한 설명이다. 자바스크립트에서는 실제로 문자열의 각 문자를 변경할 수 없다(immutable). 문자열을 수정하는 모든 문자열 메서드는 새로운 문자열을 반환한다. 이러한 불변값의 성질은 데이터의 신뢰성을 보장하고, 값의 변경을 추적하기 쉽도록 해준다.

만약 “myAlphabet”이라는 식별자에 할당된 값이 a에서 ab로 바뀌었다면, 이는 a 자체를 수정한 것이 아니다. ab라는 원시값이 새로운 주소에 생성된 뒤, 식별자에 할당되었을 뿐이다.

그러면 원시형 자료가 아닌 참조형 자료의 불변성은 어떨까?

객체는 자신의 값을 변경할 수 있다(mutable). 객체는 값으로 비교되지 않는다. 두 객체가 같은 프로퍼티와 값을 가지고 있어도 두 객체는 같지 않다. 배열 또한 같은 순서로 같은 원소를 갖고 있다고 하더라도 같지 않다. 객체는 참조로 비교되기 때문이다.

플레이리스트를 예시로 들어보자. 음원 사이트에 개별 곡들이 존재하고, 사용자는 각각의 곡을 임의로 묶어 플레이리스트로 관리할 수 있다. 만약 플레이리스트가 한번 등록되면 수정할 수 없는 컨셉이라고 하더라도, 플레이리스트 내부 값들의 변경을 막을 수는 없다. 플레이리스트가 참조하는 개별 곡이 저작권 혹은 계약 문제로 언제든 내려갈 수 있기 때문이다.

객체를 변수에 할당하는 것은 단순히 참조를 할당하는 것이다. 이는 객체의 새로운 복사본을 생성하지 않는다. 원시형 자료와 다른 점이다.

따라서, 참조형 객체의 불변성을 고려한다면 얕은 복사와 깊은 복사를 잘 구분하여 사용해야 한다. 혹은 immer 같은 라이브러리, Object.freeze() 같은 객체 동결 메서드를 사용할 수도 있다.

리액트 같은 라이브러리를 다루다보면 불변 객체를 사용할 일이 많다. 아래 포스트는 React에서 참조형 데이터의 불변성이 어떻게 활용되는지에 대해 정리한 예시(링크)이다. 방어적 복사(depensive copy)동시성(concurrency) 등의 용어와 함께 생각해보면 불변객체의 필요성을 일부 납득할 수 있을 것 같다.

싱글 스레드인 자바스크립트에는 별로 해당되지 않는 이슈인가 싶긴 하지만, 멀티 스레드와 프로세스를 지원하는 언어에서는 동시성(concurrency) 문제와 연관하여 불변객체는 중요한 개념이다.

하나의 객체를 두고 여러 스레드가 동시에 접근해 작업을 한다면 어떨까? 현재 자신이 작업하고 있는 내용이 다른 스레드가 건드리지 않은, 순수한 객체라는 사실을 보장할 수 있을까? 그래서 방어적 복사(depensive copy)를 활용해 복사본을 만든 뒤, 외부로부터 접근 받을 일이 없음을 보장하도록 만든다.


참고로, 함수형 프로그래밍 패러다임의 영향을 받은 라이브러리나 패턴 같은 경우 이러한 불변 객체를 적극적으로 사용하는 편이다.

함수형 프로그래밍에서는 각각의 함수가 가급적 함수 내부의 요소가 함수 외부 세계에 영향을 미치는 부수효과(Side Effect) 없이 input과 output으로만 이루어지도록 한다. 이렇듯 외부의 값을 받은 뒤 완전히 새로운 값으로 가공한 뒤 리턴하도록 하는 함수를 순수 함수(Pure Function)라고 부른다.


예상 면접 질문

  • 자바스크립트에서 0.1 + 0.2의 연산결과가 0.3이 아닌 이유를 설명하고, 이에 대한 해결 방안을 제시해주세요.

  • nullundefined에 대해 설명해주세요.

  • 불변성에 대해 설명하고, 불변성을 개발에 활용하였던 예시를 제시해주세요.


참고 자료

  • 정재남, <코어 자바스크립트>

  • 이웅모, <모던 자바스크립트 Deep Dive>

  • 데이비드 플래너건, <자바스크립트 완벽 가이드>

Profile picture

saengmotmi

'내가 원하는 건 문학이 아닌 기쁨이다.'