[C#] 세대별 가비지 컬렉션 알고리즘


얼마 전 포스팅에서 가비지 컬렉션의 동작원리에 대해 알아보았습니다.

그렇다면 오늘은 가비지 컬렉션의 성능을 높이기 위한

세대별 가비지 컬렉션 알고리즘에 대해 알아보도록 하겠습니다.


가비지 컬렉션에 대해 공부를 하는 이유는 유니티에서 게임오브젝트를

동적 생성/삭제 하는 과정에서 가비지 컬렉터의 활동 빈도를 높이게 된다고 합니다. 


가비지 컬렉터의 비용은 공짜가 아니기 때문에 이 또한 신중하게 관리되어야 할 메모리입니다.

때문에 메모리 풀을 이용하여 필요한 오브젝트를 미리 생성해놓고 사용하게 되는데

이 또한 가비지 컬렉터의 원리를 제대로 이해해야만 최대 효율로 이용할 수 있겠다는 생각이 들어서 입니다.


세대별 가비지 컬렉션 알고리즘

들어가기전에 이 알고리즘의 핵심만 간단하게 짚어보자면

오래 살아남을 것 같은 객체를 모아놓고 비교적 관심을 덜 주자는 것입니다.

그렇게 된다면 모든 객체에 관심을 주는 것보다 효율적인 관리방식이 될 수 있겠지요.


그렇다면 객체의 수명을 어떻게 예측해야 할까요?


01.   새로 할당된 객체들 모두 0세대에 포함됩니다.

0세대가 임계치에 도달하게되면 가비지 컬렉터는 0세대에 대해 가비지 컬렉션을 수행하게 됩니다.


02.   가비지 컬렉션 수행 후 살아남은 객체들을 1세대로 옮겨지게 됩니다.


03.   이후 또다른 객체가 할당되어 들어온다면 그 객체들은 당연히 0세대에 포함됩니다.

0세대가 또 다시 임계치에 도달하게 되면 가비지 컬렉터는 0세대에 대해 가비지 컬렉션을 수행합니다.


04.   이번에는 1세대가 임계치에 도달하게 되었습니다.

마찬가지로 가비지 컬렉션을 수행합니다.


05.   1세대의 가비지 컬렉션에서 살아남은 객체는 2세대로 옮겨지게 됩니다.

객체의 수명은 이런식으로 가비지 컬렉터에서 오래 살아남았다는 기준으로 정해지게 됩니다.


06.   이러한 방식으로 0세대, 1세대, 2세대를 구분하여 각각의 임계치에 도달하면 해당 세대에 대해 가비지 컬렉션을 수행하게 됩니다.


-->  0세대가 포화된다면 오로지 0세대만 가비지 컬렉션을 수행 한 후 1세대로 이동

1세대가 포화된다면 0세대와 1세대만 가비지 컬렉션을 수행 한 수 각각 1세대, 2세대로 이동


2세대 또한 똑같은 메커니즘으로 가비지 컬렉션을 수행할까요?


-->  2세대가 포화되게 된다면 더 이상 다른 곳으로 옮겨지지 않기 때문에, 전체 가비지 컬렉션을 수행하게 됩니다.


위의 세대별 가비지 컬렉션 알고리즘으로 인해 생성과 삭제가 빈번한 객체에 대해서만

가비지 컬렉터가 신경을 쓸 수있게 하는 메커니즘이 완성되었습니다.


하지만 2세대가 포화되게 된다면 전체 가비지 컬렉션이 수행된다는 항목에 주의를 기울여야 합니다.

전체 가비지 컬렉션이 수행되면 CLR은 앱의 실행을 잠시 멈추고 전체 가비지 컬렉션을 수행함으로써

여유 메모리를 확보하려 듭니다. 앱이 차지하고 있던 메모리가 크면 클수록 전체 가비지 컬렉션의

시간이 길어지므로 앱이 정지하는 시간도 그만큼 늘어나겠지요.

이것이 바로 우리가 가비지 컬렉션을 이해해야하는 중요한 이유입니다.




[모바일 퍼즐 게임] 피즈 펑크! Pz Punk! : The Candy Thief


기획/제작 : 김영조

제작 기간 : 3주

제작 툴 : 유니티



유튜브 영상 링크 : https://www.youtube.com/watch?v=YqW_wrVXG44

다운로드 링크 (안드로이드) : https://play.google.com/store/apps/details?id=com.YJ.PZPunk


----------------------------------------------------------------------------------------------------


게임을 만든 방법

1. DFS 알고리즘 (재귀 함수)

화면상의 과일들을 움직여 3개 이상이 되면 미완성 캔디로 만드는 과정이 필요합니다.
그렇다면 이동된 과일의 상하좌우를 탐색하여 3개 이상이면 캔디로, 아니라면 다시 원위치로 돌아가야 합니다.
이동된 과일의 인접한 부분뿐만 아니라 같은 색으로 이어진 모든 과일들의 주변을 탐색해야 하기 때문에
이를 구현하기 위해 두 가지의 선택지를 생각했습니다.

1. BFS 알고리즘    (큐, 넓이 우선 탐색)
2. DFS 알고리즘    (스택, 재귀 함수, 깊이 우선 탐색)

두 가지 중 DFS 알고리즘이 좀 더 직관적으로 구현하기 좋겠다 생각했고
과일의 체크를 담당하는 Checker라는 클래스를 만들어 재귀 함수를 구현했습니다.

아래는 Checker 클래스의 Connect_Check_Recurse() 재귀 함수의 간략한 설명입니다.

1. 매개 변수로 들어온 x와 y는 해당 과일의 배열 내 인덱스 값이기 때문에 배열의 범위를 벗어나면 안 됩니다. (배열이 곧 퍼즐 판의 크기)
2. 이미 이 메서드로 체크가 된 과일인 경우 체크 대상에서 제외합니다. (같은 타입의 과일이 인접해 있다면 무한 호출에 빠지기 때문입니다.)
3. 현재 떨어지고 있는 과일일 경우 바닥에 착지한 시점부터 체크가 진행되어야 합니다.
4. 같은 타입의 과일인지 체크하고, 같은 타입이라면 이 과일을 기준으로 또다시 상하좌우 체크를 떠나게 됩니다.


유저가 선택한 과일이 지정한 방향으로 이동이 완료되면
선택한 과일을 시작으로 Connent_Check_Recurse() 함수가 호출되게 됩니다.



S를 시작으로 재귀 함수의 호출 순서대로 숫자를 표기했습니다.


1. 유저가 선택한 과일 S를 시작으로 상하좌우 순서로 체크를 떠나기 때문에 1번 과일로 가게 됩니다.

2. 1번 과일이 같은 타입이기 때문에 1번 과일을 기준으로 다시 상하좌우 체크를 하게 됩니다.

3. 1번 과일의 상, 좌, 우측에 있는 과일은 같은 타입이 아니고, 밑은 이미 체크된 과일이기 때문에 재귀 함수의 탈출 조건에 걸리게 됩니다.

4. 1번 과일의 재귀 함수가 완전히 종료되고, 최근에 호출되었던 재귀 함수로 돌아가 나머지 작업을 수행하게 됩니다.

5. 14번째 진행과정은 배열의 범위를 초과한 범위이기 때문에 탈출 조건에 걸리게 됩니다.



2. 버킷 릴레이


위 그림의 번호는 배열 내의 x축 인덱스 값을 나타냅니다.

1번 과일과 2번 과일의 위치를 바꾸어 봅니다.

눈으로 보기에는 성공적으로 위치가 바뀌어 보이지만, 메모리상의 위치는 변함이 없습니다.


1) 화면상의 과일들은 유저의 조작에 따라 인접한 과일과 바뀌는 액션이 필요합니다.

--> 이를 구현하기 위해서 선택한 방법은 버킷 릴레이 방식으로 과일들의 오프셋을 계산해 주는 방식입니다.


A) 과일들이 나열되어 있습니다. 이제 1번과 2번 과일의 위치가 바뀌는 과정을 설명하겠습니다.
B) 1번 과일과 2번 과일의 정보를 바꾸어 줍니다. (과일의 타입 또한 교환되기 때문에 타입이 바뀌게 됩니다.)
2) 두 과일의 위치를 체인지 합니다. 

위의 동작은 한 프레임안에 동작하는 것이기 때문에 플레이어의 눈에는 이동하지 않은 것으로 보이게 됩니다.


그 후 여러 프레임에 걸쳐 두 과일의 위치가 원래 자리로 돌아가게 됩니다.

ex)
1번 과일의 position.x - 2번 과일의 position.x = -1 이라면
위 두 과일의 x축 거리는 (절댓 값) 1만 큼 차이 난다는 것을 의미합니다.

따라서 계산된 값을 서서히 0으로 만드는 과정에서 둘의 위치값에 적절한 수를 연산해 준다면
원하는 위치까지 서서히 이동하는 모션을 구현할 수 있습니다.

이 방식을 사용하면 바로 옆의 과일만 스위치 할 수 있는 것이 아니라
멀리 있는 과일과도 위치가 바뀌는 모션을 사용할 수 있습니다.

만약 계산 결과가 (절댓 값) 4만큼 차이가 난다면 1차이가 났을 때보다 더욱 긴 거리를 이동하게 될 것입니다.


2) 캔디로 만들어진 자리는 빈 공간이 되기 때문에 그 자리를 하늘에서 떨어지는 과일로 채우는 액션이 필요합니다.


이 경우에도 버킷 릴레이 방식을 활용하여 과일이 하늘에서 떨어지는 것처럼 구현할 수 있었습니다.


3. 클래스 재활용

DFS 알고리즘으로 연결된 과일이 몇개인지 확인하는 Checker라는 클래스를 만들었습니다.

1) 게임 시작과 동시에 과일의 초기 배치

--> 3개 이상이 되면 과일이 캔디가 되어버리기 때문에 초기 배치에는 3개 이상이 연결되어 있으면 안 됩니다.
--> 난이도를 생각해서 2개씩 뭉쳐있는 것이 좋다고 판단하여 초기 배치에는 같은 과일이 2개씩 붙어있게 만들었습니다.
--> 적절한 난수를 이용한다면 난이도를 유동적으로 바꿀 수 있습니다.


과일의 초기 배치를 담당하는 FruitMaker 클래스에서 Checker 클래스를 활용하여 초기 배치에 성공했습니다.

2) 과일을 인접 과일과 스위치

--> 선택된 과일이 유저가 선택한 방향으로 옮겨졌다면 이동 완료와 동시에 주위 과일들을 탐색하여
캔디로 바꿀 것인지 혹은 원 위치로 다시 돌아갈 것인지 결정해야 합니다.

과일의 이동을 담당하는 FruitCtrl 클래스에서 Checker 클래스를 활용하여 3개 이상이면 변환, 아니라면 다시 스위치 하도록 구현했습니다.

3) 빈 공간을 떨어지는 과일로 채우기

--> 캔디로 바뀐 빈 공간은 다시 다른 과일들로 채워져야 합니다.
--> 하늘에서 과일이 떨어져 빈 공간을 채우는 형식으로 모두 떨어진 뒤에는 인접한 곳에 자신과 같은 과일이 있는지 확인해야 합니다.

이 또한 과일의 이동을 담당하는 FruitCtrl 클래스에서 Checker 클래스를 활용하여 떨어진 과일의 인접 과일을 체크하도록 했습니다.

4) 미완성 캔디가 이어진 수에 따른 Crush

--> 미완성 캔디의 이어진 수가 많을수록 더욱 많은 보너스 점수를 받을 수 있습니다.
--> 때문에 미완성 캔디의 이어진 수를 확인하는 것도 주요 작업 중 하나입니다.


Checker 클래스의 상하좌우로 동일한 객체를 찾는 로직을 이용하여 미완성 캔디의 수를 카운트했습니다.


4. 오브젝트 풀

과일로 만들어진 캔디를 박스에 담는 것이 주요 점수원이기 때문에 캔디를 매번 생성해 주어야 합니다.


1) 게임이 끝날 때까지 많게는 수 천 번의 캔디 생성이 있을 수 있기 때문에 캔디를 게임 시작과 동시에 적정량을 미리 생성했습니다.
2) 생성된 캔디는 캔디 리스트에 저장되어 비활성 상태로 대기하게 됩니다.
3) 캔디가 필요한 위치에 활성 상태로 나타나게 되며, 화면에서 사라진다면 다시 캔디 리스트에 비활성 상태로 대기하게 됩니다.


5. 구글 플레이 게임 서비스


퍼즐 게임의 특성상 점수의 기록이 매우 중요하기 때문에 구글 플레이 게임 서비스와 연동하여 랭킹 시스템을 추가했습니다.
또한 18가지의 업적을 추가하여 게임을 하면서 숨겨진 미션들을 완료할 수 있게 콘텐츠를 추가했습니다.


[C#] Raycast2D


어제 저녁부터 마우스 포지션에 있는 오브젝트가 레이에 맞지 않아서

유니티 레퍼런스, 구글링 등을 통해 Raycast2D에 대해 알아보았다.


정말 틀린 곳을 도무지 모르겠는데 자꾸만 오브젝트가 레이에 맞지 않더라..



일단 첫 줄의 Vector2 pos = Camera.main.ScreenToWorldPoint (Input.mousePosition);

화면상의 마우스 포인트를 월드 좌표로 바꾸어서 pos란 변수에 저장.


두 번째.

Ray2D 구조체를 생성해준다.

Ray2D의 첫 번째 변수는 레이가 만들어질 위치. 쉽게 말해 총구라 할 수 있다.

두 번째 변수는 레이가 어떤 방향으로 날아갈 것인가. 총구의 방향이라 할 수 있다.

여기까지 ok


세 번째.

RaycastHit2D가 달라는대로 변수를 넘겨준다.

1. 레이가 만들어질 위치 2. 레이가 날아갈 방향 3. 어디까지 날아갈 것인가(거리) 4. 레이어 마스크

앞서만든 Ray2D구조체의 첫 번째 변수 --> origin

두 번째, 방향을 가리키는 변수 --> direction

세 번째, 네 번째는 생략 가능

오키 여기까지도 문제없음


그런데.. 왜 오브젝트가 검출이 안되냐 이 말이지..

콜라이더도 다 붙여 놓았는데..


정말 이거 때문에 이 더운날 반나절 동안 혼자 씩씩대면서 열폭하던 중...

2D게임 만들면서 평면으로 보지 않고 있었네..?



1. Func 델리게이트


결과를 반환하는 메소드를 참조하기 위해 만들어졌습니다.

.NET 프레임워크에는 모두 17가지 버전의 Func 델리게이트가 있습니다.


ex)

public delegate TResult Func <out TResult>()

public delegate TResult Func <in T, out TResult> (T arg)

public delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2)

....

public delegate TResult Func <in T1, in T2, ... in T16, out TResult> (T1 arg1, T2 arg2, .... , T16 arg16)


Func 델리게이트는 입력 매개 변수가 하나도 없는 것부터 16개까지 다양한 버전이 존재합니다.

매개 변수가 16개 이상인 경우, 혹은 ref, out한정자로 수식된 매개 변수를 사용해야 하는 경우가

아니라면 .NET에서 제공하는 기본 Func를 사용하면 되겠습니다.


ex)

using System;


namespace FuncTest{


class MainApp{


static void Main(string[] args){


Func<int> func1 = () => 10;

Console.WriteLne("func1 = {0}", func1());


Func<int,int> func2 = (x) => x*2;

Console.WriteLne("func2= {0}", func2(2));


Func<double, double, double> func3 = (x,y) => x*y;

Console.WriteLne("func3 = {0}", func3(2.0 , 3.0));


}

}

}


실행 결과


func1 = 10

func2 = 4;

func3 = 6.00000000000000



2. Action 델리게이트


Action 델리게이트는 Func 델리게이트과 거의 같으나, Action 델리게이트는 변환 형식이 없습니다.

Action 델리게이트도 Func 델리게이트와 마찬가지고 .NET 프레임워크에 17가지 버전이 선언되어 있습니다.


ex)

public delegate void Action <>()

public delegate void Action <in T> (T arg)

public delegate void Action <in T1, in T2> (T1 arg1, T2 arg2)

....

public delegate void Action <in T1, in T2, ... in T16> (T1 arg1, T2 arg2, .... , T16 arg16)


Func와는 달리 결과를 반환하지 않고 일련의 작업을 수행하는 것이 목적입니다.


ex)


using System;


namespace ActionTest{


class MainApp{


static void Main(){


Action ac1 = () => Console.WriteLine("Action()");

act1();


int result = 0;

Action<int> ac2 = (x) => result = x * x;


ac2(3);

Console.WriteLine("result = {0}", result);


Action<double, double> ac3 = (x,y) = > {


double pi = x/y;

Console.WriteLine("Action<T1, T2>({0}, {1}) = {2}", x,y,pi);

};


ac3(22.0, 7.0);

}

}

}


실행 결과


Action()

result = 9

Action<T1, T2>(22, 7) = 3.14285714285714





오늘은 유니티의 mesh에 대해서 알아보도록 하겠습니다.


mesh란 쉽게 말해 점, 폴리곤(삼각형), UV를 관리하는 구조체입니다.

컴퓨터 그래픽, 특히 게임 그래픽은 삼각형이 기본이 되는데

이 삼각형(폴리곤) 2개를 가지고 사각형을 만든 다음 텍스쳐를 입히는 방식입니다.


폴리곤에 대해서 좀 더 얘기해보자면 mesh의 한 단위를 가리키는 것으로

이것이 많이 사용될수록 3D모델의 디테일이 좋다진다 할 수 있습니다.


이렇듯 mesh는 게임 제작에서 사용되는 모든 모델의 기초구조라고 생각하시면 됩니다.


오늘은 mesh의 verices, triangles, uv를 가지고 mesh가 만들어지는 과정에 대해 얘기해볼까 합니다.


앞서 말씀드렸듯이 mesh란 것은 삼각형들이 모여 만들어진 것입니다.

위의 상자에 보이는 v0,v1,v2.. 가 vertex(꼭지점)가 되는 것이고, 그것들로 이어진 삼각형이

triangle polygon이 되는 것입니다. 그렇다면 꼭지점을 만들고 이어붙여 폴리곤을 만든다음 mesh로 만들면 되겠네요.



이차원 사각형을 나타내는 코드입니다. 먼저 Vector3로 꼭지점의 좌표를 배열로 만들어 줍니다.

그 다음 삼각형을 만들어 주어야 하는데, int형 배열을 사용해서 3개씩 이을 꼭지점(vertices)의 인덱스를 입력해주면 됩니다.

삼각형이기 때문에 꼭지점을 3개씩 맞춰줘야 합니다. 이번에는 텍스처를 한번 넣어보도록 하겠습니다.



여기서 UV라는 놈이 나오는데, 텍스처를 넣으려먼 이 UV좌표를 설정해주어야 합니다. UV의 좌표는 vertex와 동일 해야 합니다.

uv의 수치는 0~1까지이며 다음과 같습니다.


즉, vertex가 

2        1

3        4            이 순서로 찍혔기 때문에


uv 좌표 또한

2        1

3        4            이 순서로 찍어주어야 합니다.


그리고 나서 mesh.uv에 만든 uv 배열을 넣어주고 SetTexture라는 함수를 만들어 원하는 텍스처를 입혀주시면 됩니다~


참고 : http://www.devkorea.co.kr/reference/Documentation/ScriptReference/Mesh.html







[유니티 기초] 기분 좋은 터치 감각 (physic material , gravity)


오늘은 유니티로 간단한 게임을 만들어 보면서

액션 게임의 기본이 되는 기분 좋은 터치 감각을

어떻게 줄 수 있을까에 대해 알아보도록 하겠다.


매우 심오한 내용은 아니지만

하나의 '아이디어 출발점'은 될 수 있을 것이다.

대작 게임도 이러한 작은 아이디어에서 출발한다.




오늘 만들어볼 게임은 날아오는 공을

받아쳐서 '기분 좋은 터치감'으로 넘겨 버리는 게임이다.

왼쪽에서 초록색 공이 포물선을 그리면서 날아오고

빨간색 플레이어가 그 공을 받아쳐서 넘기면 된다.

간단한다.


01. 공이 포물선을 그리며 날아온다




02. 플레이어와 접촉




03. 기분 좋게 날려 버리기~




소스 코드는

player, ball, launcher 이렇게 세가지로 나뉜다.


01. Ball

생성과 동시에 Rigidbody의 velocity 메소드에 의해

포물선을 그리면서 날아가게 된다.


새로 배운 내용

void OnBecameInvisible()

오브젝트가 화면 밖으로 사라졌을 때 호출되는 함수이다.


02. Launcher

A키를 눌렀을 때 ballPrifab이 생성된다.


음.. 하지만 이 코드는 A키를 누를 때마다

Ball을 동적 생성하고 있으며, Ball스크립트를 보면

화면 밖으로 Ball이 사라졌을 때 해제를 해주고 있다.




동적 생성/해제는 추가적인 메모리를 필요로 하기 때문에

되도록이면 동적 생성/해제가 빈번하게 일어나는 오브젝트는

오브젝트 풀로 관리를 하는 것이 좋다. (후에 더 자세히 살펴보자.)



03. Player

is_landing --> 이단, 삼단 ... 점프를 방지하기 위한 플래그

A키를 누르면 플레이어가 점프를 하게 되는데

공중에 떠있어도 계속 점프를 하게되는 상황이 발생한다.


Floor에 접지되었을 때만 점프가 가능하게 해야한다.

플레이어의 점프 또한 Rigidbody의 velocity 메소드 사용.




새로 배운 내용

1. Debug.Break() -> 일시정지 시킨다.

간단한 디버깅을 할 때 Debug.Log를 자주 사용했다.

이와 비슷하게 Debug.Break()도 디버깅에 유용하게 쓰일 듯 하다.


2. Rigidbody --> mass

Ball이 플레이어와 접촉 했을 때,

두 오브젝트 모두 축축 쳐지는 느낌이 있었다.

이 미니게임의 목표는 '기분 좋은 터치감'이다.

경쾌하게 타격하여 공을 넘기는 것이 목표이다.


mass는 오브젝트의 무게를 설정하는 항목이다.

공의 무게가 너무 무거워서 접촉과 동시에

플레이어가 급하강 하는 모습을 볼 수 있다.

때문에 공의 mass를 0.01로 설정, 즉 공의 질량을

작게 하였으므로 플레이어는 공과 충돌해도 그대로 상승한다~


3. physic material

mass를 통해 질량을 달리하였다해도

'기분 좋게' 튕겨나가는 모습은 보이지 않는다.


physic materal을 공에 추가해 보도록 하자.

이렇게 생긴 놈이다.





Friction은 마찰에 대한 항목

Bounciness는 수치가 클수록 오브젝트가

쉽게 튀어오를 수 있도록 한다.




이로써 포물선을 그리며 날아오는 공을

플레이어가 경쾌하게 받아 쳐 넘길 수 있는

'기분 좋은 터치감'을 가진 게임을 만들어보았다.


간단한 구현이라도 큰 게임의 핵심 요소가 될 수도 있기 때문에

차근차근 하나씩 쌓아나가야 할 것이다.




다음에는 Rigidbody의 Velociy와 AddForce의

차이점을 알아보고 활용 예에 대해서 알아보겠다.





[C#] 가비지 컬렉터 (Garbage Collector) 의 원리, 동작 메커니즘


CLR은 어떻게 메모리에 객체를 할당하는가?

C#으로 작성한 소스코드를 컴파일하고 이 파일을 실행하면,

CLR은 이 프로그램을 위한 일정 크기의 메모리를 확보한다.

C-런타임처럼 메모리를 쪼개는 일은 하지 않는다.


넓은 메모리 공간을 통째로 확보해서 하나의 관리되는 힙 (Managed Heap)을

마련한다. 그리고 CLR은 이렇게 확보한 관리되는 힙의 첫 번째 주소에

"다음 객체를 할당할 메모리의 포인터"를 위치 시킨다.




여기에 첫 번째 객체를 할당해보자.

object A = new object();


CLR이 코드를 실행하면 "다음 객체를 할당할 메모리의 포인터"가

가리키고 있는 주소에 A객체를 할당하고 포인터를 A 객체가

차지하고 있는 공간 바로 뒤로 이동시킨다.



또 다른 객체를 하나 만들어보자.

object B = new object();




보시다시피 CLR은 객체가 위치할 메모리를 할당하기

위해 메모리 공간을 쪼개 만든 연결 리스트를 탐색하는 시간과

재조정하는 작업도 필요하지 않다. 그저 메모리만 할당할 뿐


그렇다면 언제, 어떻게 메모리에서 해제될까?

일단 쓰레기인지 아닌지를 판별해야 하지 않을까?


if (true){

object a = new object();

}

객체의 내용물은 힙에, A가 위치한 힙 메모리 주소는 a에 있다.



if 의 블록이 끝나면 a는 없어지게 된다.

그렇다면 A는..?




a를 잃은 채 힙에 남아 있는 객체 A는 이제 코드의 어디에서도

접근할 수 없기 때문에 더 이상 사용할 수 없게 되었다.

즉 쓰레기가 되고, 가비지 컬렉터가 집어가게 된다.



가비지 컬렉터의 원리, 동작 메커니즘

사라져버린 a처럼 할당된 메모리의 위치를 참조하는 객채를 일컬어

루트(Root)라고 부른다. 루트는 a의 경우처럼 스택에 생성될 수도 있고

정적 필드처럼 힙에 생성될 수도 있다. .NET 에플리케이션이 실행되면

jit 컴파일러가 이 루트들을 목록으로 만들고, CLR은 이 루트 목록을

관리하며 상태를 갱신한다. 이 루트가 중요한 이유는 가비지 컬렉터가

CLR이 관리하고 있던 루트 목록을 참조해서 쓰레기를 수집하기 때문.



1. 작업을 시작하기 전에, 가비지 컬렉터는 모든 객체가 쓰레기라고 가정.

루트 목록 내의 어떤 루트도 메모리를 가리키지 않는다고 가정.


2. 루트 목록을 순회하면서 각 루트가 참조하고 있는 힙 객체와의

관계 여부를 조사. 루트가 참조하고 있는 힙의 객체가 또 다른 힙 객체를

참조하고 있다면 이 역시도 해당 루트와 관계가 있는 것으로 판단.

어떤 루트와도 관계가 없다면 쓰레기로 간주.


3. 쓰레기 객체가 차지하고 있던 메모리는 이제 '비어있는 공간'


4. 루트 목록에 대한 조사가 끝나면, 가비지 컬렉터는 이제 힙을

순회하면서 쓰레기가 차지하고 있던 '비어있는 공간'에 쓰레기의

인접 객체들을 이동시켜 차곡차곡 채워 넣는다.




가비지 컬렉터의 원리, 동작 메커니즘에 대해 살펴보았다.

다음 포스팅에서는 가비지 컬렉션의 성능을 높이기 위한

'세대별 가비지 컬렉션' 알고리즘에 대해 알아보겠다.

[C#] 가비지 컬렉터


C++에서 클래스를 만드는 과정에 있어

가장 신경썻던 부분이 new/delete를 통한

메모리 할당/해제가 아니었나 생각이 든다.


할당은 그렇다 쳐도 해제하는 것을 까먹는다거나

해제한 줄도 모르고 그 포인터에 접근하는 경우가 발생한다.

때문에 스마트 포인터를 사용하여 이 문제를 극복하곤 하는데

c#에서는 가비지 컬렉터라는 멋진 놈이 이러한 문제를 해결해준다.




C/C++의 메모리 할당

C/C++는 힙에 객체를 할당하기 위해 C-런타임은 객체를

담기 위한 메모리를 여러 개의 블록으로 나눈 뒤, 이 블록을

연결 리스트로 묶어서 관리하게 된다.


어떤 객체를 힙에 할당하는 코드가 실행되면, C-런타임은

메모리 연결 리스트를 순차적으로 탐색하면서 해당 객체를

담을 수 있을 만한 여유가 있는 메모리 블럭을 찾는다.




적절한 크기의 메모리 블록을 만나면, 프로그램은 이 메모리

블록을 쪼개서 객체를 할당하고 메모리 블록의 연결리스트를

재조정한다. 따라서 메모리 공간에 데이터를 집어넣는다는 것은

1. 탐색 2. 분할 3. 재조정의 오버헤드가 필요하다는 것이다.




C#의 메모리 할당

C#은 CLR이 자동 메모리 관리 기능을 제공한다.

이 기능의 중심에는 가비지 컬렉션 (Garbage Collection) 이 있다.

가비지 컬렉션은 프로그래머로 하여금 컴퓨터가 무한한 메모리를

가지고 있는 것처럼 간주하고 코드를 작성할 수 있게 한다.

(여기서 가비지란 더 이상 사용하지 않는 객체)




CLR 안에는 이러한 가비지 컬렉션을 담당하는

가비지 컬렉터 (Garbage Collector)라는 놈이 있다.

가비지 컬렉터는 객체 중에 쓰레기인 것과 쓰레기가

아닌 것을 완벽하게 분리해서 쓰레기들만 수거한다.




하지만 가비지 컬렉터 역시 CPU와 메모리 자원을 소모한다.

때문에 가비지 컬렉터가 최소환으로 이 자원을 사용할 수 있게

만들 수 있다면 성능을 아낀 자원의 양만큼 끌어올릴 수 있게 된다.




그렇다면 가비지 컬렉터가 최소환으로 자원을 사용하게 만들기 위해

우선 가비지 컬렉터가 어떻게 동작하는지에 대한 메커니즘을 이해해야 한다.

포스팅이 길어질테니 여기서 끊고, 다음 포스팅에서 가비지 컬렉터의

동작 메커니즘에 대해 알아보도록 하겠다.





[C#] string 그리고 가비지 컬렉션


유니티를 통해 3D 게임을 만들 때

최적화 문제로 굉장히 고생했던 적이 있다.

그 때 봤던 최적화 팁에서 string + string 은

가비지를 생성한다해서 StringBuilder의 Append를

이용하여 문자열을 병합하라고 했다. 그런데 도대체 왜?




해답은 바로 문자열은 변경할 수 없는 객체라는 것에 있다. (immutable)

그렇다면 변경할 수 없는 거랑 가비지 컬렉션이랑 무슨 관계가 있는 것일까?


앞서 말했듯 문자열은 한번 생성되면 변경되지 않는다.

하지만 사실 문자열을 사용하면서 변경에 대해 어려움을

느낀 경우는 잘 없을 것이다. 이러한 문자열의 표면적인

변경은 쉽지만 내부적으로는 항상 새로운 문자열이 만들어지게 된다.




string str = "하하하";

str += "쏭";


예상대로 "하하하쏭"이라는 결과가 나온다.




그러나 메모리 측면에서는 처음 생성한 str과

이후 추가 문자가 덧 붙여진 str은 완전 다른 객체이다.


즉, "쏭"이라는 글이 추가될 때 새로운 객체가 메모리에 할당되고

이 것을 str이 다시 참조하도록 되는 것이다. 처음에 만들었던

"하하하"라는 객체는 사라지게 되는 것이다. (가비지)




성능을 위해서라면 String 대신 StringBuilder를 사용하자.


후에 좀 더 자세히 다뤄볼 StringBuilder에 대해 알아보자.

앞에 말했듯이 문자열은 변경될 수 없는 객체이기 때문에

문자열에 대한 어떠한 변경도 항상 새로운 객체를 생성하게 된다.


문자열 변경 작업이 빈번하다던지, 대량으로 문자열을 연결할

경우에는 성능을 반드시 고려해볼 필요가 있다.

왜냐하면 앞서 말했듯 문자열의 변경은 가비지를 유발하기 때문.




이 대안으로 StringBuilder 클래스를 생각해 볼 수 있다.

자 그렇다면 string + string을 어떻게 하면 가비지없이

수행해낼 수 있는지 알아보도록 하자.


System.Text.StringBuilder sb = new System.Text.StringBuilder();

sb.Append("하");

sb.Append("하하");

sb.Append("쏭");


StringBuilder는 String과는 달리 변경 가능한 액체이기 때문에

(mustable) string + string의 가비지 문제를 해결할 수 있다.




추가

MSDN의 성능 고려사항을 짚고 넘어가 보자.


MSDN에 따르면 Concat 및 AppendFormat 메서드는 새 데이터를

기존 String 또는 StringBuilder 개체에 연결한다. String개체 연결

작업에서는 항상 기존 문자열과 새 데이터로 새 개체를 만든다. 


StringBuilder 개체는 연결된 새 데이터를 수용할 버퍼를 유지한다.

새 데이터는 공간이 있을 경우 버퍼 끝에 추가되고, 그렇지 않으면

더 큰 새로운 버퍼가 할당되고, 원래 버퍼의 데이터가 새 버퍼에

복사된 다음, 새 데이터가 새 버퍼에 추가된다.




String 또는 StringBuilder 개체에 대한 연결 작업의 성능은 얼마나

자주 메모리를 할당하는지에 따라 달라진다. StringBuilder 연결

작업에서는 StringBuilder 개체 버퍼가 너무 작아 새 데이터를 넣을 수 없는

경우에만 메모리가 할당되는 반면, String 연결 작업에서는 항상 메모리가 할당된다. 


따라서 고정된 수의 String 개체를 연결하는 연결 작업에는

String 클래스가 더 적합하다. 이 경우 개별 연결 작업이 컴파일러를

통해 단일 작업으로 결합될 수 있다. 임의의 개수의 문자열을 연결하는

(예 : 루푸에서 읨의의 개수의 사용자 입력 문자열을 연결할 경우)

연결 작업에는 StringBuilder 개체가 더 적합하다.





[C#] 이벤트와 델리게이트




어떠한 일이 생겼을 때 이를 알려주는 객체가

필요한 경우가 있다. 예를 들어 사용자가 버튼을

클릭했을 때 이를 알려주는 객체가 그 것이다.

이런 객체를 만들 때 사용하는 것이 바로 이벤트이다.

이벤트의 동작 원리는 델리게이트와 거의 비슷하다.


이벤트는 델리게이트를 event 한정자로 수식해서 만든다.




자 그럼 이벤트는 어떻게 만드는지 차근차근 보도록 하자.

1. 델리게이트 선언. 클래스 밖에 선언해도 되고 안에 해도 된다.

2. 선언한 델리게이트의 인스턴스를 event 한정자로 수식해서 선언

(여기서는 클래스 내부에 있어야 한다.)

3. 이벤트 핸들러를 작성한다. 선언한 델리게이트와 일치하는 메소드여야 한다.

4. 클래스의 인스턴스를 생성하고 이 객체의

이벤트에 3에 작성한 이벤트 핸들러를 등록한다.

5. 이벤트가 발생하면 이벤트 핸들러가 호출된다.






앞서 보았듯이, 이벤트는 델리게이트에 event 키워드로

수식해서 선언한 것에 불과하다. 과연 무엇이 다를까?


이벤트와 델리게이트의 가장 큰 차이점은 바로

외부에서 접근을 할 수 있느냐 없느냐




이벤트는 public 한정자로 선언되어 있어도

자신이 선언되어 있는 클래스 외부에서는

호출이 불가능하다. 반면 델리게이트는

public이나 internal로 수식되어 있으면

클래스 외부에서라도 얼마든지 호출 가능하다.





이벤트의 이러한 특성은 견고한 이벤트 기반

프로그래밍을 가능하게 한다.

때문에 이벤트는 객체의 상태 변화나

사건의 발생을 알리는 용도로 사용이 되고

델리게이트는 콜백용도로 사용이 된다.



'C#, 유니티' 카테고리의 다른 글

[C#] C/C++/C# 메모리 할당과 가비지 컬렉터  (0) 2017.04.03
[C#] string 그리고 가비지 컬렉션  (0) 2017.03.29
[C#] 익명 메소드  (0) 2017.03.05
[C#] 일반화 델리게이트  (0) 2017.03.05
[C#] 델리게이트 (delegate)  (0) 2017.03.05

+ Recent posts