1. IL2CPP

유니티 엔진의 내부는 C++로 구성되어있다.

따라서, 유니티가 C#을 사용하여 느리다 라는 것은 어느 정도 잘못된 말이지만

개발자 스크립팅 부분은 C#을 사용하였기에 아쉬운 점이 있던 것은 사실이다.

 

하지만, IL2CPP는 이런 유니티의 단점을 커버해준다.

IL2CPP는 Unity 프로젝트의 성능, 보안 및 플랫폼 호환성을 개선하는 등의 용도로 사용된다.

IL2CPP는 결과적으로 스크립팅 부분도 C++로 돌아가게끔 해주기 때문이다.

 

2. Mono

IL2CPP에 대해서 자세하게 알아보기 전에 몇가지 더 알아둬야 할 것이 있다.

본래 C#은 MS에서 윈도우 전용으로나 사용되던 언어였다. (현재는 .NET Core를 사용한다.)

 IOS나 안드로이드 같은 다른 환경에서는 .NET Framework가 구동되지 않았기 때문이었다.

 

크로스 플랫폼이 기본이 되어가는 어플리케이션 환경 속에서

이런 C# .NET의 단점은 너무나도 치명적이었다

이 단점을 보완하기 위하여 시미안(Ximian)이라는 곳에서 Mono 프로젝트가 오픈 소스로 공개되었고

Mono 프로젝트는 여러 플랫폼에서 .NET을 사용할 수 있게 해주었다.

현재는 MS에서 인수하여 개발을 진행하고 있다.

 

Unity 또한 게임 어플리케이션을 주로 개발하는데 사용하기에 Mono로 빌드하여 사용하였으나,

Mono 프로젝트가 오래되었고 IOS 환경에서 구동할 수 없는 치명적인 단점.

JIT(Just In Time) 방식의 컴파일러이기 때문에 유연하지만 느리다는 단점이 있어서 (게임은 퍼포먼스가 중요하다.)

IL2CPP라는 스크립팅 백엔드를 개발하게 된다.

 

3. IL2CPP의 특징

IL2CPP는 AOT(Ahead Of Time) 방식으로 구동된다.

말 그대로 소스 코드를 '미리' 컴파일 하는 방식으로 CIL을 C++로 미리 바꿔서 사용한다.

Mono의 JIT 방식은 코드를 CIL로 가지고 있다가 플랫폼에 맞춰 변환한다.

 

IL2CPP가 미리 컴파일 된 방식을 사용하는 만큼 속도 면에서 더 유리하다.

하지만, JIT 방식이 가지고 있던 핫 리로드와 같은 장점은 사용할 수 없다.

 

또한, IL2CPP로 빌드된 파일은 디컴파일로 코드를 제대로 확인하기 힘들어

어셈블리로 해석해야 하는 상황에 놓이기 때문에 최소한의 보안 역할도 할 수 있다.

Mono(JIT)와 IL2CPP(AOT)
패스트 런타임과 빌드도 선택할 수 있다.

4. IL2CPP의 구동 방식

IL2CPP의 구동 방식

 

  1. Unity 스크립팅 API코드를 일반 .NET DLL로 컴파일
  2. 관리되는 바이트코드 스트리핑을 적용 -> 빌드된 게임의 크기를 크게 줄여줍니다.
  3. 모든 관리되는 어셈블리를 표준 C++ 코드로 전환
  4. 생성된 C++코드와 IL2CPP의 런타임 부분을 네이티브 플랫폼 컴파일러로 컴파일
  5. 대상 플랫폼에 따라 실행 가능한 파일이나 DLL에 코드를 연결

더 간단하게 보면 아래의 그림과 깉이 될 것이다.

 

간략한 그림 설명

 

 

https://www.youtube.com/watch?v=-9X965jXrn8

 

1. 오클루전 컬링이란?

 

오클루전 컬링이란 다른 오브젝트에 의해 가려져서 카메라에 보이지 않을 때 (Occlusion)

해당 오브젝트의 렌더링을 비활성화 하는 기능이다.

 

기본적으로 카메라는 절두체(프러스텀) 컬링을 수행하여 카메라의 뷰 절두체에 속하지 않는 모든 렌더러를 제외한다.

하지만 절두체 컬링은 렌더러가 다른 게임 오브젝트에 의해 가려지는지를 확인하지 않으므로,

Unity가 최종 프레임에 표시되지 않는 렌더러에 대한 렌더링 작업에 CPU 및 GPU 시간을 여전히 낭비할 수 있다.

오클루전 컬링은 Unity가 이러한 낭비되는 작업을 수행하지 못하도록 방지한다.

 

  • 오클루더 : 다른 오브젝트를 가리는 역할. 사전 연산을 걸쳐서 데이터를 미리 Bake 해놓는다. 따라서 Static 오브젝트이다.
  • 오클루디 : 오클루더에 의해 가려지는 오브젝트.

 

2. 오클루전 컬링을 사용해야 할 때

 

  • 오클루전 컬링은 작고 윤곽이 또렷한 영역이 견고한 게임 오브젝트에 의해                                                                  서로 명확하게 분리된 씬에서 가장 잘 작동한다. (EX. 복도로 연결된 방)
  • 오클루전 컬링을 사용하여 동적 게임 오브젝트를 가릴 수 있지만, 동적 게임 오브젝트는 다른 게임 오브젝트를 가릴 수 없다. 프로젝트가 런타임 시점에 씬 지오메트리를 생성하는 경우에는 Unity의 빌트인 오클루전 컬링이 프로젝트에 적합하지 않다. (런타임 시 생성되는 장애물은 오클루전 적용이 불가능)
  • 야외 씬 같이 오브젝트끼리 가려지는 경우가 많지 않은 경우에는 오브젝트를 가림으로써 얻는 이득보다 오클루전 컬링 연산에 드는 비용이 더 클 수 있다.

 

한 마디로, 오클루전 컬링은 좁은 맵에서 움직이지 않는 견고한 장애물이 많을 때 효과적이다.

 

 

https://tsyang.tistory.com/84
https://docs.unity3d.com/kr/current/Manual/OcclusionCulling.html

 

 

 

'C# > Unity3D' 카테고리의 다른 글

[Unity3D] Mono, IL2CPP, AOT, JIT  (0) 2022.09.26
[Unity3D] 싱글톤 패턴 (Singleton Pattern)  (0) 2022.02.07
[Unity3D] 디펜던시 인젝션 (DI)  (0) 2022.01.27
1. Garbage Collector란?

 

프로그램이 운영체제로부터 할당받는 대표적인 메모리 공간이 있는데,

Code (우리가 만든 코드), Data (전역, 스태틱 변수), Stack (지역, 매개 변수), Heap (동적 할당 변수) 으로 나누어져 있다.

 

이 중, Code와 Data는 당연히 프로그램의 시작과 종료 시점에 메모리 할당과 해제가 이루어지고,

Stack은 사용 영역이 종료되는 시점 즉, 스코프 ( { } ) 가 종료되는 시점에 LIFO 순서로 할당 해제된다.

 

문제는 Heap인데, 동적 할당된 변수는 메모리 해제 지점이 불명확하기에 직접 메모리 해제를 해야한다. (Memory Leak)

C, C++ 같은 Unmanaged 환경에서는 사용자가 직접 코드를 통해 해제 명령 (free, delete) 을 내려줘야 했지만,

(물론, RAII 스마트 포인터를 사용하는 방법도 있다.)

 

C#, JAVA로 대표되는 Managed 환경에서는 Garbage Collector라는 기능을 통해 Heap 영역을 자동으로 관리해준다.

 


2. C#의 GC 사용처

 

직역하면 '쓰레기 수집가' 라는 뜻이 되는 가비지 컬렉터는

.NET에서 제공하는 기능이며, 메모리에서 더 이상 사용되지 않는 객체들을 자동으로 정리해준다.

 

C#은 변수 타입이 두 가지로 나누어지는데,

값 ( int, char, float, enum, struct ) 와 참조 ( class, string, object ) 타입이다.

 

값 타입은 Stack에 저장되고 해제되기에 따로 관리할 필요는 없고,

참조 타입은 Stack에는 주소가 저장되고 Heap에 데이터가 저장되어 관리가 필요하다.

 

즉, 참조 타입의 변수를 메모리에서 제거할 때 가비지 컬렉터가 주로 사용된다.

 


3. C# GC의 작동 방식

 

가비지 컬렉터가 Heap을 알아서 정리해준다는 것은 알았다.

그렇다면, 가비지 컬렉터는 어떻게 Heap을 정리해주는 것일까?

 

C#은 .NET (Unity는 Mono 혹은 IL2CPP)의 CLR (공용 언어 런타임)이 제공하는

Managed Heap이라는 Heap을 사용한다.

 

말 그대로 '관리되는 힙' 이라고 불리는 이 영역은 포인터를 통해서

다음 객체가 할당될 메모리 지역을 메모리의 포인터로 가리킨다.

 

포인터가 다음 저장 장소를 가리킨다.

 

 

GC가 메모리를 제거할 때는 여러가지 방법을 사용한다.

 

  • Reference Counting

Reference counting은 참조 횟수 기반 GC의 구성 알고리즘이다.

 

참조 카운팅의 동작 방식은 간단하다.

동적으로 할당된 메모리 주소가 참조될 때마다 count를 1 증가시키고, 참조를 끊을 땐 1 감소시킴으로써

count를 체크하여 0이 될 경우 (더 이상 참조되지 않는 메모리) 즉시 메모리를 해제하는 방식이다.

 

C#에서는 잘 사용되지 않고, Python과 C++에서 가끔 사용된다.

C++의 스마트 포인터 std::shared_ptr가 참조 카운트 방식으로 순수 동작한다.

 

참조 횟수를 카운트하여 사용한다.

 

  • Mark & Sweep

Mark & Sweep은 추적 기반 GC의 구성 알고리즘이다.

 

가비지 컬렉터에는 GC Root라는 것이 있다. 

GC Root들은 힙 외부에서 접근할 수 있는 변수나 오브젝트를 뜻한다.

GC Root는 말그대로 가비지 컬렉션의 Root라는 뜻이다.

 

GC Root에서 시작해 이 Root가 참조하는 모든 오브젝트, 또 그 오브젝트들이 참조하는 

다른 오브젝트들을 탐색해 내려가며 마크(Mark)한다. 이게 바로 가비지 컬렉션의 첫번째 단계인 Mark단계이다.

 

Mark가 끝나면 가비지 컬렉터는 힙 내부를 전체를 돌면서 Mark되지 않은 메모리들을 해제한다.

이 과정을 Sweep이라고 부른다.

 

C#에서는 이 방식과 아래의 세대별 크기별 관리를 통해 GC를 구성하였다.

 

GC Root와 연결된 객체를 Mark하고 안된 객체는 Sweep

 

  • 세대별 관리법

이 외에도 CLR은 객체를 세대별로 나누어 관리하여 메모리를 좀 더 효율적으로 관리하는데,

 

0세대는 GC가 적용된 적이 없는 객체

2세대는 GC가 2번 이상 적용되었지만 아직 남아있는 객체 (즉, GC Root가 존재해 Sweep 되지 않은 객체)

1세대는 0세대와 2세대의 중간이다.

 

세대가 낮을수록 해제될 확률이 높기 때문에 낮은 세대부터 관리하게 된다.

2세대 객체로 인해 힙이 가득 차는 현상을 Full GC라고 하는데 이 때 프로그램을 일시 중단하고 다시 GC를 실행한다.

 

이런 방법들로 GC가 메모리에서 객체를 제거하고 나면

제거된 공간들을 하나로 모아 재활용 할 수 있게 해주는 메모리 컴팩션을 진행한다.

이로 인해, 단편화(fragmentation, 빈 공간 또는 자료가 여러 개의 조각으로 나뉘는 현상) 를 줄일 수 있다.

 

  • 객체 크기별 관리법

C# 에서는 85KB 를 기준으로 객체를 SOH( Small Object Heap ), LOH( Large Object Heap )으로 나누는데,

LOH 는 SOH와 달리 할당과 동시에 2세대로 시작한다.

LOH는 메모리 재배치 시 오버헤드가 크기 때문에 재배치를 하지 않기에 사용 시 주의해야 한다.

 

C#에서는 .NET이 Mark & Sweep + 세대별 관리법 + 객체 크기별 관리법 + α 를

사용하여 구성된 추적 기반 GC를 주로 사용한다.

 

 


 

[C++] Reference Counting

개요 Reference Counting은 객체의 소유권 관리( = 라이프 사이클 )의 방법 중 하나로 객체를 참조(포인팅) 하고 있는 횟수를 추적하여 그 횟수가 0이 되면 메모리에서 해제(소멸)한다. 대부분의 Managed L

nobilitycat.tistory.com

 

https://imasoftwareengineer.tistory.com/103

 

https://luv-n-interest.tistory.com/922

'C#' 카테고리의 다른 글

[C#] 대리자 (Delegate, Event)  (0) 2022.04.26
[C#] 참조형 (Reference)  (0) 2022.04.25
[C#] 매개 변수 키워드 (params, ref, out, in)  (0) 2022.04.23
[C#] 가상, 추상, 인터페이스 비교  (0) 2022.01.18

1. Delegate

Delegate는 C++의 함수 포인터와 비슷한 개념으로 함수의 레퍼런스를 리스트에 저장하여 사용한다.

(정확히는 객체의 인스턴스 참조와 메서드의 참조를 동시에 갖고 있다.)

함수 포인터는 CallBack (함수가 함수를 부름)을 하기 위해 주로 사용되었는데,

Delegate도 메서드를 다른 메서드로 전달할 수 있도록 하기 위해 만들어졌다.

Delegate는 메서드의 파라미터로 호출되어 다른 메서드에 접근이 가능하고, 그냥 호출되어 모든 함수를 부를 수도 있다.

또 자료형, 매개변수가 같은 비슷한 역할의 메서드를 서로 연결해서 한 번에 활용할 수도 있다.

Delegate Chain이라고 부르고 연결된 메서드 리스트는 .Net MultiCastDelegate 클래스에서 관리한다.

 

    // 델리게이트 선언. 파라미터로 사용하기 위해서 public 처리해 주었다.
    public delegate int DelEx(params int[] a);

    // 델리게이트 예시 클래스. 싱글톤을 이용해 만들어보았다.
    public class DelegateEx
    {
        private static DelegateEx _instance;

        public static DelegateEx instance
        {
            get {
                if (_instance == null) _instance = new DelegateEx();
                return _instance;
            }
        }

        // 델리게이트에 넣어줄 함수 1.
        public int Plus(params int[] a) {
            int sum = 0;
            foreach (var i in a) { sum += i; };
            Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
            return sum;
        }

        // 델리게이트에 넣어줄 함수 2.
        public int Minus(params int[] a) {
            int sum = 0;
            foreach (var i in a) { sum -= i; };
            Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
            return sum;
        }

        // 델리게이트 사용 함수.
        public void DelChain()
        {
            // 최초 인스턴스 생성 시 반드시 할당 해주어야 한다.
            DelEx del = new DelEx(Plus);
            // new를 생략하여 사용도 가능.
            del = Minus;

            // 연산자를 이용하여 델리게이트 내부에 더하거나 빼줄수 있음.
            del += Plus;
            
            // 무명 메서드를 활용.
            del += delegate { Console.WriteLine("무명 메서드를 활용 : {0}", 777); return 15; };

            Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", del(5, 5, 5));
            DelExample(del);
        }

        // 메서드의 파라미터로도 사용할 수 있다.
        public void DelExample(DelEx ex)
        {
            // WriteLine은 가장 끝의 인덱스에 있는 리턴 정보를 가져온다. 즉, Minus의 반환값이 아닌 무명 메서드의 반환값만 가져온다.
            Console.WriteLine("가장 마지막 함수의 반환값입니다 : {0}", ex(5, 5, 5));
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            DelegateEx.instance.DelChain();
        }
    }

 

Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 인덱스의 반환값입니다 : 15
Minus에 들렀습니다 : -15
Plus에 들렀습니다 : 15
무명 메서드를 활용 : 777
가장 마지막 메서드의 반환값입니다 : 15

 

이 코드를 작성하면서 알게 된 점인데, 디버깅 시 자주 쓰는 Console.WriteLine 함수에

복수의 레퍼런스를 가진 델리게이트가 들어가면 가장 마지막에 들어간 레퍼런스의 메서드 리턴값만 호출된다.

 

2. Event

델리게이트는 보안 수준이 낮으면 클래스 내부에 있어도 다른 멤버처럼 외부에서 접근할 수 있다.

또 할당을(=) 통해 리스트의 멤버를 초기화 할 수도 있는데,

이러한 특징에서 나오는 단점을 보완하기 위하여 사용되는 것이 Event이다.

Event는 클래스 내부에서는 델리게이트와 비슷하게 동작하나

클래스 외부(정확히는 다른필드)에서는 호출과 할당을 할 수 없으며, 오직 연산자를 이용한 멤버 증감만 가능하다.

 

Event는 이름 그대로 어떤 메서드의 이벤트가 된다.

Event에 참여한 메서드는 Event가 발생 시(호출) 그 쪽을 주목(Notify)하고 행동한다.

디자인 패턴 중 옵저버 패턴이 이와 같은 특징을 가지고 Event를 사용하여 자주 구현된다.

 

    public class EventEx
    {
        public delegate int ExDel(params int[] a);
        // event는 delegate와 보안 수준을 맞춰주어야 한다.
        public event ExDel exEvent;


        // 이벤트에 넣어줄 함수 1.
        public int Plus(params int[] a)
        {
            int sum = 0;
            foreach (var i in a) { sum += i; };
            Console.WriteLine("Plus에 들렀습니다 : {0}", sum);
            return sum;
        }

        // 이벤트에 넣어줄 함수 2.
        public int Minus(params int[] a)
        {
            int sum = 0;
            foreach (var i in a) { sum -= i; };
            Console.WriteLine("Minus에 들렀습니다 : {0}", sum);
            return sum;
        }

        public void UseEvent() {
            // 클래스 내부에서는 호출 및 할당 둘 다 가능.
            ExCallBack(exEvent);
            exEvent = Plus;
        }

        void ExCallBack(ExDel exDel) {
            exDel(5, 5, 5, 5, 5);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EventEx eventEx = new EventEx();

            // 증감 연산자를 이용한 접근만 가능.
            eventEx.exEvent += eventEx.Plus;
            eventEx.exEvent += eventEx.Minus;

            // 클래스 외부에서 호출 및 할당 불가.
            // eventEx.exEvent = eventEx.Plus;
            // eventEx.exEvent();

            eventEx.UseEvent();
        }
    }
Plus에 들렀습니다 : 25
Minus에 들렀습니다 : -25

 

델리게이트는 메서드 간의 연결을 담당할 수 있다는 점에서 굉장히 매력적이지만 메서드와 객체의 레퍼런스를 가지고 있다는 점에서 다루기 예민한 부분도 있다. (물론, 레퍼런스가 아닌 주소를 직접 가진 함수 포인터보다는 안전하다.)

예민한 부분을 다루는 델리게이트를 개발 중 실수를 방지하는 것이 Event의 역할이라고 볼 수 있다.

'C#' 카테고리의 다른 글

[C#] Garbage Collector  (0) 2022.06.21
[C#] 참조형 (Reference)  (0) 2022.04.25
[C#] 매개 변수 키워드 (params, ref, out, in)  (0) 2022.04.23
[C#] 가상, 추상, 인터페이스 비교  (0) 2022.01.18

1. 참조 (Reference)란?

앞서 params, ref, out, in 같은 매개 변수를 호출 메서드로 전달하는 키워드에 대하여 알아보았다.

그 중 ref, out, in은 참조로 전달되어 호출 메서드가 전달 받은 매개 변수의 실제 값에 접근할 수 있다고 하였다.

참조란 무엇이길래 매개 변수의 실제값에 접근이 가능한걸까?

 

C++에는 주소값을 가지는 변수인 포인터가 존재한다.

우리는 포인터를 통해서 해당 주소를 가지는 변수에 직접 접근이 가능했고 참조자(&)를 통해서 안전한 접근도 가능했다.

간단하게 다른 변수의 장소를 가리키는 것을 '참조(Reference)' 라고 한다. (참조는 객체의 '별칭' 이라고도 한다.)

 

ref, out, in 키워드도 해당 변수의 주소에 접근함으로 실제값에 접근 가능한 것이다.

 

2. C#의 참조형

C#은 참조가 포인터를 완벽히 대체하며, 그로 인해 포인터 자료형을 일반적으로는 사용하지 않는다.

 

object, string, dynamic, array는 C#의 기본 참조 형식이다.

참조 형식답게 포인터를 통해 다른 객체의 장소를 가리키는 방식이다.

이들은 모두 스택(Stack) 영역에 주소가 저장되고, 이를 통해 힙(Heap)에 저장된 값에 접근한다.

 

string은 문자열을 저장하는 기능을 한다.

string는 new 키워드 없이 따옴표(" ")만을 이용하여 생성할 수 있고 +연산자를 이용해 서로 더할 수 있다.

 

object는 모든 클래스 중에 기본 클래스로 모든 데이터 타입의 루트형이다.

object는 모든 데이터형을 참조할 수 있지만, 해당 데이터 형의 특성을 잃어 해당 데이터형의 특성을 살리기 위해선

캐스팅을 필요로한다. 바꿔말하면 어떤 형으로든 캐스팅 가능하다.

 

dynamic 또한 object와 비슷하게 어떤 형으로도 캐스팅이 가능하다.

object와 다른 점은 데이터 형의 특성을 그대로 유지해 해당 데이터형의 특성을 그래도 활용할 수 있다.

하지만, 런타임 시 타입이 결정되기 때문에 다른 형식보다 느리다.

 

위 세가지 타입은 값을 할당하기만 해도 새로운 인스턴스를 생성하기에

다른 메서드에 실수로 접근하는 일이 줄어든다.

하지만, 배열은 배열 내 요소에 접근 시 참조 중인 메서드에 영향을 주게 되니

반드시 new를 통해 인스턴스를 생성해주어야 한다.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace params__ref__in__out
{
    class Example1
    {
        // array
        public int[] a = { 1, 2, 3, 4, 5 };
        public int[] b;
    }

    class Example2
    {
        // string
        public string a = "스트링입니다.";
        public string b;
    }

    class Example3
    {
        // object
        public object a = "다이나믹도 새로 생성됩니다.";
        public object b;
    }

    class Example4
    {
        // class
        public int a = 10;
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 인스턴스 생성
            Example1 ex1 = new Example1();

            // 객체 참조
            ex1.b = ex1.a;
            Console.WriteLine("{0}, {1}", ex1.a[1], ex1.b[1]);

            // b의 값 변경
            ex1.b[1] = 10;
            Console.WriteLine("{0}, {1}", ex1.a[1], ex1.b[1]);

            // 인스턴스를 새로 생성하면 연결이 끊어진다.
            ex1.b = new int[]{ 6, 7, 8, 9, 10 };

            ex1.b[1] = 30;
            Console.WriteLine("{0}, {1}", ex1.a[1], ex1.b[1]);


            // 인스턴스 생성
            Example2 ex2 = new Example2();

            Console.WriteLine("{0}", ex2.a);

            // 객체 참조
            ex2.b = ex2.a;
            // 문자열은 new를 사용하지 않아도 새 객체를 생성하는 것으로 취급한다.
            ex2.b = "저는 새로 생성됩니다.";

            Console.WriteLine("{0}, {1}", ex2.a, ex2.b);


            // 인스턴스 생성
            Example3 ex3 = new Example3();

            Console.WriteLine("{0}", ex3.a);

            // 객체 참조
            ex3.b = ex3.a;
            Console.WriteLine("{0}, {1}", ex3.a, ex3.b);
            // b의 값 변경. dynamic은 타입 캐스팅도 가능하다.
            ex3.b = 777;

            Console.WriteLine("{0}, {1}", ex3.a, ex3.b);


            // 인스턴스 생성
            Example4 ex4_1 = new Example4();

            // 객체 참조
            Example4 ex4_2 = ex4_1;
            Console.WriteLine("{0}, {1}", ex4_1.a, ex4_2.a);

            // ex4_2의 값 변경
            ex4_2.a = 20;

            Console.WriteLine("{0}, {1}", ex4_1.a, ex4_2.a);
        }
    }
}
2, 2
10, 10
10, 30
스트링입니다.
스트링입니다., 저는 새로 생성됩니다.
다이나믹도 새로 생성됩니다.
다이나믹도 새로 생성됩니다., 다이나믹도 새로 생성됩니다.
다이나믹도 새로 생성됩니다., 777
10, 10
20, 20

참조형의 메모리 할당 방식. https://introprogramming.info/라는 장소를 '참조'했다!

 

C# Class 또한 선언 시 참조형으로 선언된다. 그 후 new를 이용해 인스턴스를 할당하는 방식이다.

하지만, 구조체는 값형이다. 그래서 선언만 해주면 그 즉시 인스턴스가 생긴다.

 

// 클래스는 참조형이니 new를 반드시 사용.
ExClass exc = new ExClass();

// 구조체는 사용할 필요 X
ExStruct exs;

 

클래스는 참조 형식이니 대입을 할 경우 스택에 있는 인스턴스의 참조값(주소) 만 복사하여 대입을 한다.

즉, 4byte의 값만 복사가 일어나 굉장히 가볍다.

 

구조체는 값형이기 때문에 대입할 경우 구조체 용량 전체에 대한 복사가 이루어지므로 클래스와 비교해서 무겁다.

따라서 가급적 구조체는 크지않은 데이터에 대해서 사용하는 편이 좋다.

 

다만, 값형은 속도가 빠른 스택 영역에 저장되고 참조형은 상대적으로 느린 힙 영역에 저장되므로

객제의 크기가 작다면 구조체가 유리하고 객체의 크기가 크다면 클래스가 유리하다.

 

http://daplus.net/c-c-%EB%AC%B8%EC%9E%90%EC%97%B4-%EC%B0%B8%EC%A1%B0-%EC%9C%A0%ED%98%95/

'C#' 카테고리의 다른 글

[C#] Garbage Collector  (0) 2022.06.21
[C#] 대리자 (Delegate, Event)  (0) 2022.04.26
[C#] 매개 변수 키워드 (params, ref, out, in)  (0) 2022.04.23
[C#] 가상, 추상, 인터페이스 비교  (0) 2022.01.18

C#에서는 매개 변수 선언시 상황에 맞게 키워드를 사용하여 편리하게 개발할 수 있다.

그 키워드 중 자주 쓰이는 몇 가지를 소개하려고 한다.

in, out 키워드는 내부적으로 ref 키워드를 붙였을 때의 동작 방식과 같으며 특정 제약 조건이 추가된 것이다.

1. params (가변 매개 변수)

params 키워드는 매개 변수의 영문형 parameter의 복수형을 키워드로 표현한 것이다.

가변 길이의 매개 변수를 지정할 수 있고 1차원 배열만 매개 변수로 사용할 수 있다.

params을 사용하면 인수 배열로만 사용이 가능해지고 인수가 없어도 해당 함수를 사용할 수 있다

 

using System;
using System.Collections;

namespace Intro_Ex1
{
    class Example
    {
        public void UseParams(params int[] list) {
            foreach (int param in list) {
                Console.WriteLine(param);
            }
            Console.WriteLine();
        }
    }
    
    class Program
    {

        static void Main(string[] args)
        {
            Example example = new Example();
            int[] array1 = { 1, 3, 5, 7, 9 };

            // 쉼표로 구분된 인수 목록 사용
            example.UseParams(array1);

            // 지정된 형식의 인수 배열을 사용
            example.UseParams(2, 4, 6, 8, 10);

            // 가변 길이를 사용 가능하기에 인수가 없어도 사용 가능
            example.UseParams();
        }
    }
}
1
3
5
7
9

2
4
6
8
10

또한, param 키워드는 매개 변수 목록의 끝에 반드시 위치 해야한다.

예를 들면,

 

void UseParams(int a, int b, params int[] list) 처럼 위치가 맨 뒤면 가능하고

 

void UseParams(params int[] list, int a, int b) 처럼 위치가 맨 뒤가 아니면 불가능하다.

 

 

2. ref

ref 키워드는 값이 참조(reference)로 전달됨을 나타낸다. (Call by Reference)

참조로 값을 보낼 시 해당 실제 값에 직접 접근할 수 있으므로

전달받은 매개 변수의 실제 값을 함수 내에서 변경하는 것이 가능해진다.

즉, C#에 존재하지 않는 포인터의 기능을 ref가 대신한다고 볼 수 있다.

 

ref를 사용하면 데이터를 양방향으로 교환이 가능해진다.

 

또한, ref는 몇 가지 특징을 가진다.

  • 매개 변수는 메서드로 전달되기 전 반드시 초기화 해야한다.
  • 호출 메서드로 돌아가기 전에 초기화 할 필요는 없다.
  • out 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)

 

using System;
using System.Collections;

namespace Intro_Ex1
{
    class Example
    {
    	public void UseParams(params int[] list) {
            foreach (int param in list) {
                Console.WriteLine(param);
            }
            Console.WriteLine();
        }
        
        public void UseRefs(ref int[] list) {
            list = new int[] { 3, 6, 9, 12, 15 };
        }

        public void NonUseRefs(int[] list) {
            list = new int[] { 3, 6, 9, 12, 15 };
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            Example example = new Example();
            // 초기화 되어있는 데이터를 사용하여야 한다.
            int[] array1 = { 1, 3, 5, 7, 9 };

            // 키워드 미사용 시
            example.NonUseRefs(array1);
            example.UseParams(array1);

            // 키워드 사용 시 (꼭 인수 앞에 키워드를 붙여주어야 한다.)
            example.UseRefs(ref array1);
            example.UseParams(array1);
        }
    }
}
1
3
5
7
9

3
6
9
12
15

 

또한, ref는 참조 변수로도 활용이 가능한데 C++의 참조자(&)와도 어느정도 비슷한 규칙이 있다.

// 주소값이 없는 rvalue를 할당하고 있으므로 불가능. 키워드를 붙여도 불가능하다.
ref int a = 2;
ref int b = null;

// 선언 직후에 바로 할당하지 않았으므로 사용 불가능하다.
ref int c;
c = ref a;

// 주소값이 있는 lvalue를 할당하고 있으니 가능. 꼭 ref 키워드를 붙여주어야 한다.  
ref int d = ref a;

 

3. out

out 키워드도 ref와 유사한 기능을 한다.

ref와는 몇 가지 차이점을 가지는데,

  • 매개 변수는 메서드로 전달되기 전에 초기화 할 필요는 없다.
  • 호출 메서드로 돌아가기 전에는 반드시 초기화해야 한다.
  • ref 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)
using System;
using System.Collections;

namespace Intro_Ex1
{
    class Example
    {
        // 함수 내에서 매개 변수를 할당하지 않았으니 에러
        public void UseOut1(out int[] list)
        {
            
        }

        // 반드시 함수 내에서 매개 변수를 할당해주어야 한다.
        public void UseOut2(out int[] list)
        {
            list = new int[] { 4, 8, 12, 16, 20 };
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            Example example = new Example();

            // 반드시 초기화 할 필요는 없다.
            int[] array2;

            example.UseOut1(out array2);
            example.UseOut2(out array2);
        }
    }
}

 

4. in

in 키워드는 out과 반대이다.

차이점은 호출과 선언 모두 키워드를 붙여주어야 했던 ref, out과는 다르게

호출에는 안 쓰고 선언에만 쓰는 경우가 가능하다.

ref와 같이 반드시 초기화 된 변수만 사용할 수 있으며 읽기 전용으로만 사용이 가능하나 필수는 아니다.

즉, 외부의 값을 변경할 수 없다.

  • 매개 변수는 메서드로 전달되기 전에 초기화 할 필요는 없다.
  • 호출 메서드에서 초기화 할 수 없으며 읽기만 가능하다. (readonly)
  • ref 키워드를 사용한 메서드와 오버로딩 불가능하다. (컴파일이 동일하게 처리되기 때문)
using System;
using System.Collections;

namespace Intro_Ex1
{
    class Example
    {
        public void UseIn(in int[] list)
        {
            // 읽기 전용이므로 할당 및 초기화 불가능
            list = new int[] { 4, 8, 12, 16, 20 };

            // 읽기만 가능하나 반드시 해야할 필요는 없다.
            int[] s = list;
        }   
    }

    class Program
    {

        static void Main(string[] args)
        {
            Example example = new Example();

            // 반드시 초기화 되어야 한다.
            int[] array2 = { 1, 2, 3, 4, 5 };
			
            // ref, out하고는 다르게 호출 시 키워드 선택 가능
            example.UseIn(in array2);
            example.UseIn(array2);
        }
    }
}

'C#' 카테고리의 다른 글

[C#] Garbage Collector  (0) 2022.06.21
[C#] 대리자 (Delegate, Event)  (0) 2022.04.26
[C#] 참조형 (Reference)  (0) 2022.04.25
[C#] 가상, 추상, 인터페이스 비교  (0) 2022.01.18

1. Singleton 이란?

 

다른 클래스에서 공통적으로 많이 접근하는 클래스가 있을 경우 (ex GameManager) 

해당 클래스의 인스턴스(실체)를 하나로 유지하여 프로젝트에서 클래스의 데이터를 유지하고 객체를 단일화한다.

 

2. Unity에서 Singleton을 사용하는 이유

 

Manager류의 게임을 관리하는 오브젝트를 생성할 때 

인스턴스가 하나이기 때문에 데이터를 쉽게 유지할 수 있고,

정적 선언을 하기 때문에 그 유지된 데이터를 다른 클래스에서 쉽게 접근하여 사용이 가능하다.

 

3. 예제

 

public class GameManager : MonoBehaviour
{
	// static으로 정적 선언하여 프로그램이 끝날 때까지 유지하고 private하여 다른 클래스에서 수정할 수 없게 한다
    static GameManager _instance = null;

	// public 선언하여 다른 클래스에서 접근할 수 있게 하고 get 속성을 통해 접근만 가능하게 한다
    public static GameManager instance {
        get {
            if (_instance == null) {
                _instance = new GameManager();
            }
            return _instance;
        }
    }

	// MonoBehaviour를 상속받는 클래스가 인스턴스화 되는 과정을 막을 수 없으므로 Awake에서 null여부를 검사하여 예외처리를 한다
    void Awake() {
    	// 인스턴스가 비어있을 경우(막 생성되었을 때) 인스턴스를 this로 지정한다
        if (_instance == null) {
            _instance = this;
        }
        // 인스턴스가 비어있지 않을 경우(씬 이동 같은 경우로 인해 MonoBehaviour가 호출되어 인스턴스가 재생성 될 때) 재생성된 오브젝트를 파괴해준다
        else if (_instance != null) {
            Destroy(this.gameObject);
        }
        
        // 씬 로드시 해당 오브젝트를 파괴하지 않아야 다음 씬에도 유지가 된다
        DontDestroyOnLoad(this.gameObject);
    }
}

 

4. 이외의 방법

 

이외에도 MonoBehaviour를 상속받지 않고 사용하는 방법도 있다.

이 경우 하이어라키에 있는 인스턴스를 신경쓰지 않아도 되는 장점이 생긴다.

즉, 씬 이동시 인스턴스가 재생성 되는 과정을 신경쓰지 않아도 되니 Awake에서 다시 검사해주는 과정이 필요가 없다.

하지만, 말 그대로 메모리에만 존재하고 하이어라키에는 보이지 않아 인스펙터로 시각적으로 정보를 확인할 수 없다.

개발하는 입장에서 헷갈릴 수 있어 자주 사용하는 방법은 아니다. 

'C# > Unity3D' 카테고리의 다른 글

[Unity3D] Mono, IL2CPP, AOT, JIT  (0) 2022.09.26
[Unity3D] Occlusion Culling (오클루전 컬링)  (0) 2022.06.28
[Unity3D] 디펜던시 인젝션 (DI)  (0) 2022.01.27

1. Dependency Injection (의존성 주입)이란?

 

클래스 A가 어떤 작업을 수행하기 위해 클래스 B의 인스턴스에 의존해야 하는 경우,

B는 A의 디펜던시(의존성)이라고 한다.

 

Dependency Injection Pattern 줄여서, DI 패턴은 디펜던시를 따로 참조하지 않아도 주입해주는 기능을 한다.

 

// Case 1 (의존성이 있지만 스크립트 내에서 참조)
GameObject obj1;

void Awake() {
	obj = GameObject.Find("Player");
}

// Case 2 (유니티 인스펙터에서 접근하여 의존성을 주입)
public GameObject obj1;

// Case 3 (유니티 인스펙터에서 접근하여 의존성을 주입)
[Serializefield]
GameObject obj1;

 

 

2. 장점

 

Reduced Dependencies
- 종속성이 감소한다.
- components의 종속성이 감소하면 변경에 민감하지 않다.
More Reusable Code
- 재사용성이 증가한다.
- 일부 인터페이스의 다른 구현이 필요한 경우, 코드를 변경할 필요없이 해당 구현을 사용하도록 components를 구성할 수 있다.
More Testable Code
- 더 많은 테스트 코드를 만들 수 있다.
- Mock 객체는 실제 구현의 테스트로 사용되는 객체
종속성을 components에 주입할 수 있는 경우 이러한 종속성의 Mock 구현을 주입할 수 있다.
예를 들어, Mock 객체가 올바른 객체를 반환할 때, null을 반환할 때, 예외가 발생할 때 모두 처리한다.
More Readable Code
- 코드를 읽기 쉬워진다.
- components의 종속성을 보다 쉽게 파악할 수 있으므로 코드를 쉽게 읽을 수 있다.

참조 :

 

[Design Pattern] DI란 (Dependency Injection) - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

 

3. Zenject

 

Extenject Dependency Injection IOC | 유틸리티 도구 | Unity Asset Store

Use the Extenject Dependency Injection IOC from Mathijs Bakker on your next project. Find this utility tool & more on the Unity Asset Store.

assetstore.unity.com

Zenject를 통해 유니티에서 DI를 쉽고 기능적으로 사용할 수 있다.

'C# > Unity3D' 카테고리의 다른 글

[Unity3D] Mono, IL2CPP, AOT, JIT  (0) 2022.09.26
[Unity3D] Occlusion Culling (오클루전 컬링)  (0) 2022.06.28
[Unity3D] 싱글톤 패턴 (Singleton Pattern)  (0) 2022.02.07

가상 클래스 (virtural)

1. 가상 (virtural)

- virtual 키워드를 붙여 사용

- 파생 클래스에서 필요에 따라 오버라이드(재정의) 할 수 있지만 필수는 아님

- 자체적으로 완벽한 기능 제공 가능

 

추상 클래스 (abstract)

2. 추상 (abstract)

- abstract 키워드를 붙여 사용

- 추상 클래스는 사용 시 abstract 키워드를 반드시 붙여줘야 함

- 파생 클래스에서 필수적으로 오버라이드 필요

- 여러개의 파생 클래스에서 공통적인 정의를 제공하는 것이 목표

- CTRL + . 사용 시 함수 자동 구현

 

인터페이스 (interface)

3. 인터페이스 (interface)

- abstract와 비슷하지만 변수는 사용 불가. 단, 프로퍼티 (속성)은 사용 가능

= 부모 함수가 기능적으로 복잡해지는 것을 막기 위해 기능적으로 추가하는 것을 도와줌

- 보통 여러 클래스에 공통적인 기능을 추가하기 위해 사용

- CTRL + . 사용 시 함수 자동 구현

'C#' 카테고리의 다른 글

[C#] Garbage Collector  (0) 2022.06.21
[C#] 대리자 (Delegate, Event)  (0) 2022.04.26
[C#] 참조형 (Reference)  (0) 2022.04.25
[C#] 매개 변수 키워드 (params, ref, out, in)  (0) 2022.04.23

+ Recent posts