본문 바로가기

유니티 개발 정보/개념

유니티 메모리 관리 - 3

원문이 약간 길어 3~4개로 나눠어서 올릴 예정입니다. 
해당 글 원문은 http://unitygems.com/memorymanagement/에서 보실수 있습니다.
번역중에 이상한 부분은 댓글 남겨주시면 확인 후에 수정하겠습니다.
(저도 공부하면서 올리는 거라 미흡한 점이 많이 있습니다. 미흡한 부분이 있더라도 조금만 양해부탁드립니다^^;)






Struct vs Class

그래서 어떤 상황에서 어느 것을 사용해야 하는가? 클래스를 사용하는 것은 추가적인 변수(참조변수)가 추가 된다는 것을 알았다. 만약 수천 개의 객체를 만든다면, 수천 개의 추가적인 변수를 얻을 것이다. 하지만 구조체는 이들을 생성하지 않는다.

밑의 예제를 보자:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DogC{
    public string name { get; set;}
    public int age { get; set;}
}
 
public struct DogS {
    public string name;
    public int age;
}
 
using UnityEngine;
using System.Collections;
 
public class Memory : MonoBehaviour {
    DogC dogC = new DogC();
    DogS dogS = new DogS();
 
    void Update () {
        if(Input.GetKeyDown(KeyCode.Alpha1))
            Print (dogS);
        if(Input.GetKeyDown(KeyCode.Alpha2))
            Print (dogC);
        if(Input.GetKeyDown(KeyCode.Alpha3))
            print ("Struct: The dog's name is "+dogS.name +" and is "+dogS.age);
        if(Input.GetKeyDown(KeyCode.Alpha4))
            print ("Class: The dog's name is "+dogC.name +" and is "+dogC.age);
     }
 
    void Print(DogS d){
        d.age = 10;
        d.name = "Rufus";
        print ("Struct:The dog's name is "+d.name +" and is "+d.age);
     }
 
    void Print(DogC d){
        d.age = 10;
        d.name = "Rufus";
        print ("Class:The dog's name is "+d.name +" and is "+d.age);
    }
}

우선, 세번째 입력은 우리가 예상했던 값을 출력하지 않을 것이다. 구조체의 name과 age는 값이 사라졌다. 
반면에 클래스는 값을 유지한다.
(역주: 유니티에서 테스트해보시면 쉽게 이해하실 수 있습니다. 업데이트문에 있는 순서대로 입력을 해보시기 바랍니다. 참고로 KeyCode.Alpha1은 숫자1키를 말합니다.)  

우리가 말했던 것을 기억해라. 클래스는 참조 타입이고 구조체는 값 타입이다. 구조체에 대해서 함수를 호출 할 때, 구조체의 복사본을 전달하지만, 그 복사본은 원래의 데이터가 아니다. 그 데이터를 수정할 때, 우리는 단지 스택에 있는 복사본의 데이터를 수정하는 것이다. 이들은 함수의 마지막 부분에서 사라진다.

클래스를 매개변수로 받는 함수는 원래의 데이터로 접근할 수 있는 참조값을 전달 받는다. 
그리고 함수안에서 수정도 가능하다. 함수가 끝나더라도 수정한 것은 그대로 유지된다.

우리가 생각해야 하는 것은 무엇인가? 만약 구조체가 10개의 변수를 포함할정도로 크다면, 우리가 함수에 구조체를 전달할 때 스택에는 10개의 공간이 생성될 것이다. 지금 내가 이 구조체 50개를 가지고 있는 구조체배열을 전달한다면 스택에는 500개보다 더 큰 공간이 발생할 것이다. 스택의 사이즈는 제한되어 있고, 만약 구조체와 배열의 크기가 더 크다면 스택 오버플로우가 발생할 수도 있다.



클래스를 전달 할 때는, 오직 하나의 변수만 전달한다. 50개의 클래스 인스턴스를 전달할 때는 스택에 50개의 변수에 대한 공간만 생성될 것이다.

종합해보면, 클래스는 메모리를 절약하지만 구조체는 대신 더 빠르다. 핵심은 2개의 균형을 알아내는 것이다. 
5개~8개의 멤버변수를 가질 때 구조체를 사용하는 것을 고려해라. 그 보다 위에는 클래스를 사용해라.


You must also bear in mind that every time you access a variable which is a structure (for example by doing transform.position) you are creating a copy of that structure on the stack - that copy takes time to create.
구조체 변수에 접근할 때마다(예를 들어 transform.position를 사용할 때) 스택에 구조체의 복사본을 만들고, 복사하는데도 시간이 든다는 것을 명심해야 한다.
(역주: 구조체 변수에 접근할 때마다 스택에 구조체의 복사본을 만든다는게 잘 이해가 안되서
원문과 같이 나뒀습니다. 혹시 이글을 보시고 부연설명이 가능하신분은 댓글로 좀 부탁드리겠습니다.)

클래스를 만들때 마다, 힙에 메모리를 할당할 것이다. 이는 빠른속도로, 버려진 클래스들을 위한 가비지 컬렉션 사이클로 이어진다. 구조체는 항상 스택에 할당되며 이런 이유로 가비지 컬렉션을 절대 유발시키지 않는다.

유니티에서 구조체는 종종 3~4개의 변수를 가진다. 
(예를 들어, position, color, quaternion, rotation, scale...이들 모두다 구조체이다)


오래가지 못하는, 자주 할당되는 객체는 구조체의 좋은 후보가 된다. 왜냐하면 이들은 가비지 컬렉션을 유발시키지 않기 때문이다. 오래가고 큰 객체들은 클래스의 좋은 후보들이다. 왜냐하면 그들은 접근될 때 마다 복사되지 않으며, 그들의 지속된 존재는 가비지 컬렉션을 유발시키지 않는다. 
Vector3, RayCastHit같이 많은 작은 객체들은 바로 이 이유때문에 유니티에서 구조체인 것 이다.
다음을 넘어가기전에 마지막 핵심 하나가 있는데,
함수에서 구조체를 수정하고 값을 계속 유지하기 위해서, 구조체를 참조로써 전달하는 것이 가능하다.


구조체 변수를 정의한 함수가 종료되었을 때 구조체에 대한 참조를 유지할 수 없다.이 같은 문제를 해결하기 위해 클로저의 사용(람다 함수 또는 인라인 함수에 의해서 생성된)처럼 많은 방법이 있지만 그것은 꽤 고급 기술에 속한다.
1
2
3
4
5
void PrintRef(ref DogS d){
    d.age = 10;
    d.name = "Rufus";
    print ("Structure:The dog's name is "+d.name +" and is "+d.age);
}

우리는 위에 것을 다음처럼 호출할 것이다:

1
2
3
4
void Update(){
    PrintRef(ref dogS);
    print ("Structure:The dog's name is "+dogS.name +" and is "+dogS.age);
}

당신은 키워드 ref에 대해서 알았다. 이것은 컴파일러에게 스택에 구조체를 복사하지 말고 구조체에 대한 참조를 만들라고 말하는 것이다. 함수 밖에서 구조체는 함수 안에서 받은 값들이 유지된다.
이 키워드는 어떤 값타입 형식과 사용될 수 있다.


Creating a Reference
참조 만들기


우리는 방금 전에 값타입 변수에 참조를 만드는 것이 가능하다는 것을 봤다. 우리는 함수 안에 정수형 변수를 만들고, 그것이 후에 사용되기 위해서 계속해서 유지되는 것을 원한다고 생각해보자.
간단하게, 함수내부에 변수를 선언하는 것은 함수의 끝에 사라지는 automatic 변수를 만드는 것이다.


우리는 동적으로 할당되는 변수를 만들 수 있다. 이는 참조와 함께 힙에 저장될 것이다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;
using System.Collections;
 
public class Test:MonoBehaviour{
    int number;
 
    void Update () {
        if(Input.GetKeyDown (KeyCode.C))
            CreateVariable();
        if(Input.GetKeyDown (KeyCode.Space))
            PrintVariable(number);
    }
 
    void CreateVariable(){
         //int number = new int();
         number = new int();
         number = 10;
        print ("In function "+number);
    }
 
    void PrintVariable(int n){
        print (n);
        n+=10;
    }
}

첫 번째로, 스크립트에 전역으로 정수형 변수 number를 선언한다. 우리는 이 변수를 CreateVariable함수에서 사용하고, new int()를 사용하여 number에 정수형 변수 참조를 대입한다.



이 상황을 명확히 하기 위해서, new 키워드는 힙영역에 만들어진 변수에 대한 참조를 반환한다.
이 참조는 number 변수안에 저장되고, 이제 number 변수는 새롭게 생성된 변수의 주소를 가지고 있다. number를 사용하는 것은 간접적으로 이 변수를 사용하는 것이다. 우리가 새롭게 생성한 변수는 함수 밖에서도 여전히 존재한다.










주석으로 된 부분을 고려해보면, (//int number = new int();)
만약 이 같은 방법으로 사용했다면, 우리는 함수의 끝에서 참조를 잃었을 것이다.
다음 청소 라운드에서, 가비지 컬렉터는 참조가 없는 데이터(함수 안에 선언된 number)를 찾을 것이고 그것을 메모리로부터 제거할 것이다.
이 같이 우리가 함수 내부에서 했던 것처럼 지역 변수를 선언하는 것을 생각해 보면, int number은 전역 int 
number 변수를 숨긴다는 것을 알 수 있다.


참조안에서 int같은 원시 변수를 유지할 때,  컴파일러는 boxing(박싱)이라고 불리는 함수를 수행한다. 이는 기본적으로 원시 변수를 클래스 안으로 싸는 것이다.
이런 식으로 선언된 변수는 힙에 할당되고, 이들이 해제될 때 가비지 컬렉션이 발생하도록 한다. 이들은 기본적인 원시 변수보다 더 많은 메모리를 차지한다.
(역주: 박싱에 대한 그림 참조)

기본적인 박싱 상황을 생각해보자. Debug.Log(object)함수를 사용할 때, 매개변수는 object이다. 
이는 어떤 변수의 객체도 함수로 전달할 수 있다는 의미이다. 그러나 정수형을 전달할 때, 컴파일러는 정수형을 오브젝트 안으로 넣어 버린다. 이렇게 처리되는 것을 예상함으로인해 박싱을 최대한 적절하게 사용하는 것이 가능하다.



1
2
3
4
5
void PrintingManyTimes(int n){
    object obj = n;
    for(int i = 0;i<500;i++)
        Debug.Log(obj);
}
이 상황에서 컴파일러는 매번 Debug.Log를 호출할 때마다 박싱을 수행하는 것이 필요가 없다. 박싱은 한번 일어나고 500번 호출될 뿐이다.






Static
정적

우리는 이제 튜토리얼의 마지막 부분으로 가고 있다. 한번 더 말하지만, 당신이 C에 대한 배경지식을 가지고 있다면, 당신이 static(정적)에 대해서 아는 것은 잊어 버려라. 
여기서의 static의 사용은 다르다.


.NET의 정적 변수는 High Frequency Heap이라고 불리는 Heap의 특별한 공간에 저장된다.

(역주: High Frequency Heap에 대한 추가 설명)
정적 변수가 참조타입이든 값타입이든 상관없이 모든 정적 변수는 힙에 저장된다.
얼마나 많이 인스턴스가 만들어졌는지 상관없이 통틀어서 오직 하나의 슬롯(공간)만이 있다.
(그렇지만 그 공간을 채우기 위해 인스턴스를 만들필요는 없다.)
이 힙은 "High frequency heap"으로 알려져 있으며, 이 힙은 보통의 가비지 컬렉션되는 힙으로 부터 분리되어 있다. 이는 어플리케이션 도메인당 하나씩 존재한다.


앞에서 언급한바와 같이, 우리는 튜토리얼에서 종종 정적변수를 만나며, 초심자는 단지 쉽게 사용할 수 있기를 기도한다. 점잖은 교사는 종종 정적변수의 이익과 위험에 대해서 충분히 설명하는데 시간을 쓰지 않는다. 우리는 이제 정적 객체가 무엇인지, 어떻게 사용하는지, 어디에 그것을 사용하고, 사용하지 않는지에 대해서 명확히 하려고 한다.

여기 정적 변수의 개념을 소개한 빠른 비디오 영상이 있다.(영어)


Static classes
정적 클래스


정적 클래스는 객체 인스턴스를 가지지 않는 클래스이다. 당신은 정적 클래스의 객체 인스턴스를 만들 수 없을 것이다.

그 결과로, 오직 하나의 인스턴스를 가진다. 방금 내가 "그것의 어떤 인스턴스도 만들 수 없다"고 말한 것때문에 혼란스러울수도 있다.
프로그램은 Heap에 이 정적객체를위해 메모리를 할당할 것이다. 그리고 유저의 정적객체에 대한 어떤 인스턴스화도 허용하지 않을 것이다.

정적 객체는 그들의 첫번째 호출에서 생성되며, 프로그램이 끝날 때 파괴될 것이다.
(혹은 크래쉬가 일어나거나..)


1
2
3
4
public static class GameManager{
    public static int score; 
    public static float amountOfTime;
}

당신은 GameManager타입의 객체를 선언할 수 없을 것이다.
GameManager GM = new GameManager();
이는 에러를 리턴할 것이다.


우리는 간단하게 다음의 명령어를 통해서 정적 클래스의 멤버에 접근할 수 있을 것이다.
GameManager.score = 10;
이는 게임안에서 언제 어디서나 가능하다. 정적 클래스 또는 멤버는 모든 파일안에서 접근가능하고 수명또한 전체 프로그램의 수명과 같다.


An example for using static class
정적 클래스 사용 예시

새로운 씬을 불러올 때, 이전 씬의 모든 변수는 파괴된다. 만약 예를들어 score같은 변수를 보존하기를 원한다면 다음과 같은 해야한다.
DontDestroyOnLoad(score);
정적 클래스의 유용한 장점은 그들의 수명에 있다. 정적 클래스는 죽지 않기때문에, 만약 씬을 불러올 때 score변수가 살아남기를 원한다면, 우리는 간단하게 정적 클래스를 사용할 수 있다. 레벨의 끝부분에 저장되기 원하는 값을 정적 멤버에 저장하고, 새로운 씬을 시작할 때 그 값을 복원한다.

if(newLevel){
    GameManager.score = tempScore;
    Application.LoadLevel(nextLevel);
}

그리고 새로운 레벨에 대한 스크립트에서:

1
2
3
void Start(){
    scoreTemp = gameManager.score;
}

여기서 작은 트릭은, 끝날때까지 GamaManager.score를 사용하지 않는 것이다.

한 레벨당 최대 200포인트를 얻을수 있는 레벨이 10개가 있는 게임을 생각해보자.

한 유저는 한번에 게임을 끝냈다. 그리고 2000포인트를 얻었다.

다른 유저는 여러번 플레이해서 게임을 끝냈고, 5000포인트를 얻었다.

두번째 플레이어는 좋지 않을 뿐만 아니라 더 많은 포인트를 얻었다. 왜냐하면 그는 각 레벨을 여러번 시도했고 그 포인트가 축적되었다. 만약 다음 레벨을 불러올 때 정적변수에 포인트를 전달한다면, 레벨을 재시도할때는 반드시 축적된 포인트를 취소해야 한다.

게임을 나가는 동안, 정적 변수의 데이터는 저장공간으로 보내질 수 있다.


정적 클래스와 정적 변수는 프로그램 실행중에 그들이 처음으로 맞닥뜨릴때 할당된다.
당신이 실제로 A 정적 클래스의 변수나 함수에 한번도 접근하지 않는다면 A 정적 클래스는 절대 만들어 지지 않는다.


'유니티 개발 정보 > 개념' 카테고리의 다른 글

코루틴(Coroutine)++  (3) 2013.10.08
유니티 메모리 관리 - 4(마지막)  (0) 2013.10.03
유니티 메모리 관리 - 2  (0) 2013.08.09
유니티 메모리 관리 - 1  (0) 2013.08.07
유니티 코딩 규칙  (4) 2013.07.22