로그인게임하기
Unity C# 강좌

[Unity C# 3] 값 타입과 참조 타입, 박싱과 Nullable

2026.10.14

Unity C# 강좌 · 3편

Vector3 를 함수에 넘겨 고쳤는데 원래 값이 그대로인 적이 있나요? C# 의 타입은 값 타입과 참조 타입으로 나뉘고, 넘기는 방식이 다릅니다. 이번 편에서는 그 차이에서 시작해, 값 타입이 object 로 바뀌는 박싱과, 값 타입에 null 을 담는 Nullable 까지 봅니다.

수업 정보

2024년 부산 콘텐츠 아카데미 퍼스널 트레이닝 수업에서 MyLesson Unity 실습 프로젝트의 Strategy_04_Nullable.cs 로 가르친 내용입니다. 수업은 다섯 단계로 나뉘고, 단계마다 코드가 조금씩 바뀝니다.

VALUE VS REFERENCE

고쳤는데 그대로인 Vector3

Strategy_04_Nullable.csAssets/Source/Strategy_04_Nullable.cs68–76줄
public void Func(Vector3 pos) // call by value
{
    pos.x = 1000;
}

public void Func(MyClass pos) // call by reference
{
    pos.x = 1000;
}
Strategy_04_Nullable.csAssets/Source/Strategy_04_Nullable.cs151–161줄
Vector3 ppp = new Vector3(1, 1, 1);
Func(ppp);
Program.WriteLine(ppp);
Program.WriteLine(ppp.GetType());
object objPPP = ppp;
objPPP.GetType();

MyClass mc = new MyClass(1, 1, 1);
Func(mc);
Program.WriteLine(mc);
Program.WriteLine(mc.GetType());
실행 결과게임 화면의 출력 창
(1.00, 1.00, 1.00)
UnityEngine.Vector3
(1000, 1, 1)
Strategy_04_Nullable+MyClass

두 Func 는 똑같이 pos.x = 1000 을 합니다. 그런데 Vector3 는 그대로이고, MyClass 는 바뀌었습니다.

값 타입 (struct)참조 타입 (class)
예int, float, bool, Vector3, DateTimestring, 배열, List, MyClass, GameObject
변수에 든 것값 그 자체객체가 있는 곳(참조)
함수에 넘기면값이 복사되어 넘어간다. 함수 안에서 고친 것은 복사본참조가 복사되어 넘어간다. 같은 객체를 가리키므로 고치면 원본이 바뀐다
기본값0 으로 채운 값null
call by reference 라는 말

코드 주석에는 class 쪽을 call by reference 라고 적었습니다. 엄밀히는 "참조를 값으로 넘긴다" 가 맞습니다. 함수 안에서 pos = new MyClass(…) 처럼 변수 자체를 바꾸면 원본 변수는 바뀌지 않습니다. 변수 자체를 넘기려면 ref 키워드를 씁니다.

STEP 1

박싱과 언박싱

값 타입을 object 자리에 넣으면, C# 은 값을 상자(box)에 담아 힙에 올리고 그 상자의 참조를 넘깁니다. 이것이 박싱입니다. 상자에서 값을 꺼내 다시 값 타입으로 되돌리는 것이 언박싱입니다.

Strategy_04_Nullable.cs수업 자료 1단계5–27줄
private void DoBoxingUnBoxing(params object[] args)
{
    Program.WriteLine("DoBoxingUnBoxing의 파라메터로 넘어올 때 박싱(값 타입 >>> 참조 타입)됩니다.");
    for (int i = 0; i < args.Length; i++)
    {
        if (args[i] is int)
        {
            int n = (int)args[i];
            Program.WriteLine("Typecast 코드로 인해 언박싱(참조 타입 >>> 값 타입)됩니다.");
            Program.WriteLine(n + "is int");
        }
        else if (args[i] is char)
        {
            char c = (char)args[i];
            Program.WriteLine("Typecast 코드로 인해 언박싱(참조 타입 >>> 값 타입)됩니다.");
            Program.WriteLine("'" + c + "' is char");
        }
    }
}

public void Run()
{
    DoBoxingUnBoxing(10, 'D');
  1. 10, 'D'값 타입
  2. params object[] args박싱: 상자에 담김
  3. args[i] is int상자 안의 타입 확인
  4. (int)args[i]언박싱: 값으로 꺼냄

params object[] 는 인자를 몇 개든 받아 object 배열로 묶습니다. 그래서 10 과 'D' 가 넘어오는 순간 박싱이 일어납니다. 박싱은 힙에 상자를 만드는 일이라 매 프레임 많이 일어나면 쓰레기(GC)가 쌓입니다. 2편의 제네릭이 반가운 이유이기도 합니다.

1편의 ArrayList

ArrayList 에 int 를 Add 하면 매번 박싱됩니다. List<int> 는 int 를 그대로 담아 박싱이 없습니다.

STEP 2 · 3

Nullable<int>, null 을 담는 값 타입

int 는 값 타입이라 null 이 없습니다. 하지만 "아직 정하지 않음" 을 표현하고 싶을 때가 있습니다. 데이터베이스의 빈 칸 같은 경우입니다. 그래서 값에 "있음 · 없음" 표시를 붙인 Nullable<int> 가 있습니다.

Strategy_04_Nullable.cs수업 자료 2단계5–16줄
public void Run()
{
    Nullable<int> noValue = new Nullable<int>();
    object noValueBoxed = noValue;
    Program.WriteLine("noValueBoxed == null: {0}", noValueBoxed == null);

    Nullable<int> someValue = new Nullable<int>(5);
    object someValueBoxed = someValue;
    Program.WriteLine("someValueBoxed == null: {0}", someValueBoxed == null);
    Program.WriteLine("type of someValueBoxed: {0}", someValueBoxed.GetType());
    Program.WriteLine("type of int: {0}", typeof(int));
}
실행 결과게임 화면의 출력 창
noValueBoxed == null: True
someValueBoxed == null: False
type of someValueBoxed: System.Int32
type of int: System.Int32

Nullable 은 박싱할 때 특별하게 행동합니다. 값이 없으면 상자를 만들지 않고 그냥 null 이 되고, 값이 있으면 안의 int 만 상자에 담습니다. 그래서 박싱된 someValue 의 타입을 물으면 Nullable 이 아니라 System.Int32 라고 답합니다.

Strategy_04_Nullable.cs수업 자료 3단계7–17줄
Nullable<int> noValue = new Nullable<int>();
//Program.WriteLine(noValue.GetType());
Program.WriteLine("noValue is int: <b>{0}</b>", noValue is int);
Program.WriteLine("noValue is Nullable<int>: <b>{0}</b>", noValue is Nullable<int>);
Program.WriteLine("noValue == null: <b>{0}</b>", noValue == null);

Program.WriteLine();

Nullable<int> someValue = new Nullable<int>(5);
Program.WriteLine(someValue.GetType());
Program.WriteLine(someValue);
실행 결과게임 화면의 출력 창
noValue is int: False
noValue is Nullable<int>: False
noValue == null: True

System.Int32
5

값이 없는 Nullable 은 is 검사에서 무엇과 비교해도 false 입니다. is 는 null 에 대해 늘 false 이기 때문입니다. 있는지 확인할 때는 == null 이나 다음 단계의 HasValue 를 씁니다.

STEP 4

int? 는 Nullable<int> 의 줄임말

Strategy_04_Nullable.cs수업 자료 4단계7–19줄
int? noValue = new int?();
Program.WriteLine("noValue is int: <b>{0}</b>", noValue is int);
Program.WriteLine("noValue is Nullable<int>: <b>{0}</b>", noValue is Nullable<int>);
Program.WriteLine("noValue.HasValue: <b>{0}</b>", noValue.HasValue);
Program.WriteLine("noValue == null: <b>{0}</b>", noValue == null);

Program.WriteLine();

int? someValue = new int?(5);
Program.WriteLine(someValue.GetType());
Program.WriteLine(someValue);
someValue++;
Program.WriteLine(someValue);
실행 결과게임 화면의 출력 창
noValue is int: False
noValue is Nullable<int>: False
noValue.HasValue: False
noValue == null: True

System.Int32
5
6

int? 는 Nullable<int> 와 같은 타입입니다. 실제 코드에서는 거의 이 줄임말을 씁니다. ++ 같은 연산도 그대로 됩니다. 값이 없을 때 연산하면 결과도 null 입니다.

속성

HasValue

값이 있으면 true

속성

Value

값을 꺼냄. 값이 없으면 예외

메서드

GetValueOrDefault()

값이 없으면 0(또는 지정한 값)

연산자

x ?? 10

x 가 null 이면 10

STEP 5

as 로 안전하게 꺼내기

Strategy_04_Nullable.cs수업 자료 5단계5–16줄
private void PringAsInt(object obj)
{
    int? castedInt = obj as int?;
    Program.WriteLine(castedInt != null ? castedInt.Value : "null");
}

public void Run()
{
    PringAsInt(77);
    PringAsInt("50");
    PringAsInt(555.55f);
    PringAsInt(90m);
실행 결과게임 화면의 출력 창
77
null
null
null

(int)obj 처럼 캐스팅하면 obj 가 int 가 아닐 때 예외가 납니다. obj as int? 는 꺼낼 수 있으면 값을, 아니면 null 을 줍니다. as 는 null 이 될 수 있는 타입에만 쓸 수 있어서 as int 는 안 되고 as int? 로 씁니다. "50" 은 문자열이고, 555.55f 는 float, 90m 은 decimal 이라 int 로 꺼낼 수 없어 null 이 나옵니다.

요즘 C# 의 패턴

if (obj is int n) { … } 처럼 쓰면 확인과 꺼내기를 한 줄에 할 수 있습니다. 1편의 instance is Strategy myClassInstance 가 이 모양입니다.

RETRO

돌아보며

이번 편에서 얻은 것

  • 값 타입은 값이, 참조 타입은 참조가 복사되어 넘어간다
  • 값 타입이 object 자리에 가면 박싱, 꺼내면 언박싱
  • int? 로 값 타입에 null 을 담고, as int? 로 안전하게 꺼낸다

다음에 풀 질문

  • 함수 자체를 변수에 담아 넘길 수 있을까
  • 버튼을 눌렀을 때 부를 함수를 어떻게 넘기나

정리

  1. 넘기는 것이 다릅니다. 값 타입은 값의 복사본이, 참조 타입은 같은 객체를 가리키는 참조가 넘어갑니다.
  2. 박싱은 공짜가 아닙니다. 값 타입을 object 로 다루면 상자가 힙에 생깁니다. 제네릭은 이 비용을 없앱니다.
  3. int? 는 값에 있음 표시를 붙인 것입니다. HasValue 로 확인하고, as int? 로 실패해도 예외 없이 꺼냅니다.

다음 편에서는 버튼을 눌렀을 때 실행할 함수를 넘기는 방법, 델리게이트와 람다를 봅니다.

← 개발 노트 목록