Struct in JS

August 07, 2015

1. 정의

  • C 와 C++(또는 C# 등) 에서 구조화 된 데이터를 처리할 때, Struct 를 사용하는데 이를 구조체라 부른다.

    • 최초의 구조체는 C 언어에서 다양한 자료구조를 하나의 집합으로 만들어 관리하기위해 만들어졌다.

      • 구조체는 하나 이상의 자료구조를 가진 또 하나의 데이터 타입을 정의한다.
    • 이 글은 구조체를 다루는 여러 언어 중 [C# 구조체] 를 기준으로 하고있다.

    • 코드화
    public struct Song
    {
        public int lengthInSeconds, yearRecorded;
    
        // 생성자
        public Song(int p1, int p2)
        {
            lengthInSeconds = p1;
            yearRecorded = p2;
        }
    }
    
    class TestSong
    {
        static void Main()
        {
            // Initialize:
            // 생성된 객체는 Stack 메모리에 객체가 할당된다.
            Song song1 = new Song(213, 1994);
            Song song2 = new Song(248, 1988);
    
            // Display results:
            Console.WriteLine("lengthInSeconds = {0}, yearRecorded = {1}", song1.lengthInSeconds, song1.yearRecorded);
    
            Console.WriteLine("lengthInSeconds = {0}, yearRecorded = {1}", song2.lengthInSeconds, song2.yearRecorded);
    
        }
    }
    
    /* Output:
        lengthInSeconds = 213, yearRecorded = 1994
        lengthInSeconds = 248, yearRecorded = 1988
    */
    • 구조체Class 와 같이 여러 특성(생성자, 상수, 필드, 메서드, 속성, 인덱서, 연산자, 이벤트 등)들을 그룹화 하는데 사용된다.

      • 구조체클래스는 매우 비슷한 구조를 가지고 있으며, 사용 방법적용 이유가 유사하다.

2. C# 구조체의 특징

  • 값 타입이다.

  • 생성된 객체는 Stack 메모리에 할당된다.

  • 구조체는 보통 작은 데이터를 다루는데 적합하다.

    • 하지만 이것클래스표현할 수 없는 것은 아니다.(그냥 작은 Class를 만들면 그만이다;;)

      • 그럼에도 불구하고 구조체를 써야하는 이유는 분명 존재할 것이다.
  • new 연산자를 사용하지 않고도 인스턴스화할 수 있다.

    • 하지만 생성자를 호출하지 않으므로, 선언된 맴버를 따로 초기화 해야하는 불편함이 존재한다.
    public struct Song
    {
    	public int lengthInSeconds, yearRecorded;
    
    	// 생성자
    	public Song(int p1, int p2)
    	{
    		lengthInSeconds = p1;
    		yearRecorded = p2;
    	}
    }
    
    class TestSongNoNew
    {
    	static void Main()
    	{
    		// new 연산자 없이 인스턴스를 생성한다.
    		// Stack 메모리에 객체가 할당된다.
    		// 생성자를 호출하지 않는다.
    		Song song1;
    
    		// 속성 초기화
    		song1.lengthInSeconds = 213;
    		song1.yearRecorded = 1994;
    
    		// Display results:
    		Console.WriteLine("lengthInSeconds = {0}, yearRecorded = {1}", song1.lengthInSeconds, song1.yearRecorded);
    
    	}
    }
    
    /* Output:
    	lengthInSeconds = 213, yearRecorded = 1994
    */
  • 구조체구조체 또는 클래스에서 상속될 수 없으며, 클래스의 기본 클래스가 될 수 없다.

    • 클래스와 달리, 상속 구조를 구현할 수 없다.
  • 클래스와 같이 인터페이스를 구현할 수 있다.

  • 구조체는 Stack 영역에 할당되는 **값 타입**이고, 클래스는 Heap 영역에 할당되는 **참조 타입**이다.

    • Stack 영역(정적 할당)이란?

    • Stack 영역은 컴파일 시점에서 크기가 결정되는 요소들이 저장되는 메모리 영역이다.

      • 함수 내부 지역 변수매개 변수Stack 영역에 할당되며, 함수 종료 시 소멸된다.
    • Stack 영역은 LIFO(Last In First out)라는 자료구조 가진다.

      • 즉 마지막에 넣은(Push) 데이터가 가장 먼저 나가는것을(Pop) 의미한다.

        • Stack Push 순서:

          • A > B > C > D > E
        • Stack Pop 순서:

          • E > D > C > B > A
      • 그럼 JS Stack 은?

        • JS 는 동적 언어 이므로 컴파일 시점이 아닌 런타임 시점에서 메모리가 할당(allocation)된다.

          • C, C++, C#(정적 언어)
        • JS 원시 타입(string, number, boolean, undefined, null)은 고정된 크기를 가지며, Stack 메모리 영역에 할당된다.

        • JS 의 모든 실행 컨텍스트ECStack(일종의 Call Stack) 내부로 할당된다.

          • 아래와 같은 방법으로 ECStack Count 를 셀 수 있다.

            function getStackCount(){
            
            	var i = 0;
            
            	try{
            		(function _stack(){
            
            			++i && arguments.callee();
            		})();
            	}
            	catch(e){
            
            		console.log(e.message);
            
            		return i;
            	}
            }
            
            // get call stack count
            console.log('call stack count: ' + getStackCount());
            
            /* output
            
             Maximum call stack size exceeded
            
             // 15,745 번 함수 호출 후 Stack overflow 가 발생했다.
             call stack count: 15745
            
             */
    • Heap 영역(동적 할당)이란?

      • Heap 영역은 런타임 시점에서 크기가 결정되는 요소들이 저장되는 메모리 영역이다.

        • Object 또는 Array Data Type 등을 가진다.
      • Heap 영역은 런타임 시점에서 메모리를 가변적으로 동적 할당반환(GC를 통해) 시키는 구조를 가진다.

      • GC할당Heap 메모리 영역의 반환(de-allocated(수거))을 담당한다.

        • GC(Garbage Collection)란?

          • 자동 메모리 관리의 한 형태이며, 프로세스 수행 중, 더 이상 필요가 없어진 객체가 점유하는 메모리에 대해 반환 작업을 수행한다.

          • 프로그래머가 직접 메모리를 반환할 수는 없다.(반드시 GC를 통해 반환된다)

          • 단 GC에게 메모리 반환 조건을 만들어 줄 수는 있다.

            
            var obj = {x: 1, y: 2};
            
            // GC에게 메모리 반환 조건을 만들어 준다.
            obj = null;
            
    • JS Heap 메모리 영역 테스트 해보기!!

      • V8 Handle scope Diagram

      • 아래 코드는 Heap 메모리 영역에 객체할당하는 예제이다.

      // global execution context
      
      // 전역 실행 컨텍스트에서 함수 객체를 하나 선언한다.
      
      // 선언된 함수 객체는 Heap 메모리 영역에 할당된다.
      function globalA(){
      	this.x = 1;
      	this.y = 2;
      }
      
      function init(){
      
      	// _global 전역 변수에 생성된 인스턴스를 할당한다.
      	// 이때 전역 변수(참조 변수)는 Heap 메모리 영역에 할당된 객체를 참조하게된다.
      
      	_global = new globalA();
      }
      
      init();
      • 전역 실행 컨텍스트에서 선언된 globalA 함수 객체의 Heap 메모리 영역.

      • _global 전역 변수가 참조하는 객체Heap 메모리 영역.

      • Chrome Inspector를 통해 할당된 Heap 메모리 영역을 살펴볼 수 있다.

      • 만약 함수 지역 변수객체를 할당했다면, 그 객체(Heap 영역)의 메모리 반환 시점은 언제일까?

        • 결론: 지역 변수에 할당된 객체 메모리 반환 시점은 함수 종료 시점이 된다.

          • 테스트 1: 전역 변수와 함수 지역 변수에 동일한 객체를 할당해본 후 Profile 을 통해 Heap 영역을 관찰해본다.

            // global execution context
            
            // 전역 실행 컨텍스트에서 함수 객체를 하나 선언한다.
            
            // 선언된 함수 객체는 Heap 메모리 영역에 할당된다.
            function globalA(){
            	this.x = 1;
            	this.y = 2;
            }
            
            function init(){
            
            	// _district 지역 변수에 생성된 인스턴스를 할당한다.
            	// 이때 지역 변수(참조 변수)는 Heap 메모리 영역에 할당된 객체를 참조하게된다.
            
            	// 단 지역 변수에 할당된 객체의 "메모리 반환 시점"은, 함수 종료 시점(함수 실행 컨텍스트 종료 시점)이 된다.
            
            	var _district = new globalA();
            }
            
            init();
          • 지역 변수에 할당된 객체Heap 메모리 영역에서 반환되어 보이지 않는다.

3. JS 로 Struct 구현해보기

  • 위에서 나열한 구조체특징클래스본질적으로 다른 부분인, 데이터 생성에 대한 부분을 JS 를 통해 구현해 보았다.

    • 구조체: 타입(Stack 메모리 영역에 할당되며, 값이 복사되어 할당된다)

    • 클래스: 참조 타입(Heap 메모리 영역에 할당되며, 참조 값이 할당된다)

  • 구조체의 특징 중 극히 일부를 구현한것이며, 타 언어에서 구현된 구조체라 할 수는 없다.


function Struct(val){

	if (
		typeof val === 'string' ||
		typeof val === 'number' ||
		typeof val === 'boolean' ||
		val === null ||
		val === undefined
	){
		console.error('primitive type value: ' + val);
		return val;
	}

	var _copy = function(val){

		var ret = null;
		var constructor = val.constructor;

		if (constructor === Function){

			// 함수(상황에 따라 생성자 함수가 될 수도 있다) 객체 내부 this 값을 apply 함수를 통해 초기화 시키고, 그 결과를 반환하는 함수를 생성한다.
			ret = function $F(){

				// this === global or $F of instance object
				return val.apply(this, arguments);
			};

			// 전달된 함수 객체의 prototype(객체 원형)을 할당한다.
			ret.prototype = val.prototype;
		}
		else if (constructor === String || constructor === Number || constructor === Boolean) {
			ret = new constructor(val.valueOf());
		}
		else if (constructor === Date){
			ret = new constructor(val.getTime());
		}
		else if (constructor === Object || constructor === Array){

			ret = new constructor();

			for (var n in val){

				if (val.hasOwnProperty(n)){
					ret[n] = val[n];
				}
			}
		}

		return ret;

	}(val) || val;


	return _copy;
}

	// 5 가지의 원시 타입 값 할당
Struct('test'); // test
Struct(1); // 1
Struct(true); // true
Struct(undefined); // undefined
Struct(null); // null

/*
	모든 경우(모든 타입)에 전달된 원본 값(객체)이 아닌 복사 된 값을 반환한다.
*/

/*
 함수 객체 선언
 */
var $$ = function() {

	var $$ = function(){
		return new _$$();
	};

	var _$$ = function(){
		this.x = 1;
	}
	// 나를통해 생성될 객체 원형에 새로운 객체를 할당한다.
	_$$.prototype = {
		getX: function() {
			return this.x;
		}
	};

	$$.staticMethod = function(){};

	return $$;
}();

/*
 다른 유형의 함수 객체 선언
 */
function Plus(x, y){

	x = x || 0;
	y = y || 0;

	return parseFloat(x + y);
}

var originalFn = $$;
var structFn = Struct($$);

console.dir(originalFn); // function
console.dir(structFn); // function

console.dir(originalFn()); // object
console.dir(structFn()); // object

var originalFn1 = Plus;
var structFn1 = Struct(Plus);

console.dir(originalFn1); // function
console.dir(structFn1); // function

console.dir(originalFn1(1, 3)); // 4
console.dir(structFn1(2, 3)); // 5

var structStr = Struct(new String('test'));
var structNum = Struct(new Number(1));

console.dir(structStr); // string object
console.dir(structNum); // number object

var structObject = Struct({x: 1, y: 2});
var structArray = Struct([1, 2, 3]);

console.dir(structObject); // object
console.dir(structArray); // array

var structBoolean = Struct(new Boolean(false));
console.dir(structBoolean); // boolean object


var structDate = Struct(new Date());
console.dir(structDate); // date object

4. 정리하며

  • 그럼 언제 사용하면될까?

    • 개인적인 생각으로는 이럴때가 아닌가 싶다?

      • 클래스로 만들기에는 너무 간단한 구조인 경우.

        • 사실 이 경우, 반드시 구조 만의 문제는 아닐 수 있다.

          • 만약 모든 경우에 클래스를 사용한다면, 위에서 언급한 봐와같이 객체Heap 영역에 할당될 것이며, 결국 GC 는 그 메모리 반환을 위해 쓸때 없는 리소스를 낭비하게 될 것이다.
      • 상속 구조를 만들 필요가 없는 경우.

    • 하지만, 이 두 가지 특징 모두 각 언어(C/C++/C# 등 구조체를 가진 모든 언어)가 가지는 **특성**에 따라 충분히 변할 수 있는 부분이므로 모든 언어에 적용된다고 말할 수는 없다.

      • 간단히 말해, 특정 언어구조체를 어떤 방식으로 구현하느냐에 따라, 사용 범위도 크게 달라질 수 있다는 말과 같다.

5. 참고 URL


Profile picture

Written by mohwa