프로그래밍 언어/C#

박싱 언박싱

뽀또치즈맛 2025. 5. 17. 04:01

박싱과 언박싱을 이해하려면 참조 타입과 값 타입에 대한 정의가 필요하다.

 

값 타입(Value Type)

 

값 타입의 정의

값 타입은 변수 자체에 데이터 값을 직접 저장하는 타입이다.

이러한 변수는 스택(stack)메모리에 할당되며, 복사 시 실제 데이터 값이 복사된다.

 

값 형식의 변수는 해당 형식의 인스턴스를 포함한다.

이는 해당 형식의 인스턴스에 대한 참조를 포함하는 참조 형식의 변수와 다르다.

기본적으로 할당, 메서드에 인수 전달, 메서드 결과 반환 시 변수 값이 복사된다.

값 형식 변수의 경우 해당 형식 인스턴스가 복사된다.

 

Value types and reference types are the two main categories of C# types. A variable of a value type contains an instance of the type. This differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on assignment, passing an argument to a method, and returning a method result, variable values are copied. In the case of value-type variables, the corresponding type instances are copied. The following example demonstrates

출처 : Value types - C# reference

 

 

 

예시

  • 기본 숫자형: int, float, double, decimal, long, short, byte
  • 논리형: bool
  • 문자형: char
  • 열거형: enum
  • 구조체: struct

 

C# 값 타입 vs C++ 값 타입

 

C# C++

구분 C# C++
값 타입 정의 구조체(struct), 열거(enum), 기본 숫자형 등값이 직접 저장됨 기본 자료형(int, float 등), struct, enum값이 직접 저장됨
메모리 위치 스택(기본적으로), 일부 상황에서 힙(예: 박싱) 스택(기본적으로), new 사용 시 힙
복사 방식 복사 시 값이 복사됨 (Deep Copy of value) 복사 시 값이 복사됨 (Deep Copy of value)

 

 

 

복사 동작 예시

int a = 10;
int b = a; // 값이 복사됨
b = 20;
Console.WriteLine(a); // 출력: 10

 

a와 b는 서로 독립적인 값을 가지므로, b를 변경해도 a는 영향을 받지 않는다.

“Operations on a value-type variable affect only that instance of the value type.”
→ 값 타입은 독립된 복사본을 가진다.
 출처: MS Docs

 

using System;

public struct MutablePoint
{
    public int X;
    public int Y;

    public MutablePoint(int x, int y) => (X, Y) = (x, y);

    public override string ToString() => $"({X}, {Y})";
}

public class Program
{
    public static void Main()
    {
        var p1 = new MutablePoint(1, 2);
        var p2 = p1;
        p2.Y = 200;
        Console.WriteLine($"{nameof(p1)} after {nameof(p2)} is modified: {p1}");
        Console.WriteLine($"{nameof(p2)}: {p2}");

        MutateAndDisplay(p2);
        Console.WriteLine($"{nameof(p2)} after passing to a method: {p2}");
    }

    private static void MutateAndDisplay(MutablePoint p)
    {
        p.X = 100;
        Console.WriteLine($"Point mutated in a method: {p}");
    }
}
// Expected output:
// p1 after p2 is modified: (1, 2)
// p2: (1, 200)
// Point mutated in a method: (100, 200)
// p2 after passing to a method: (1, 200)

위 예시에서 보이듯,

값 유형 변수에 대한 작업은 변수에 저장된 해당 값 유형의 인스턴스에만 영향을 미친다.

 

 

 

값 형식에 참조 형시그이 데이터 멤버가 포함된 경우,

값 형식 인스턴스를 복사할 때 참조 형식의 인스턴스에 대한 참조만 복사된다.

복사본과 원본 값 형식 인스턴스 모두 동일한 참조 형식 인스턴스에 엑서스할 수 있다.

( = 구조체 안에 참조 타입이 있을 경우 주의점 shallow copy 문제를 말한다.)

 

using System;
using System.Collections.Generic;

public struct TaggedInteger
{
    public int Number;
    private List<string> tags;

    public TaggedInteger(int n)
    {
        Number = n;
        tags = new List<string>();
    }

    public void AddTag(string tag) => tags.Add(tag);

    public override string ToString() => $"{Number} [{string.Join(", ", tags)}]";
}

public class Program
{
    public static void Main()
    {
        var n1 = new TaggedInteger(0);
        n1.AddTag("A");
        Console.WriteLine(n1);  // output: 0 [A]

        var n2 = n1;
        n2.Number = 7;
        n2.AddTag("B");

        Console.WriteLine(n1);  // output: 0 [A, B]
        Console.WriteLine(n2);  // output: 7 [A, B]
    }
}

 

값 유형의 종류와 유형 제약 조건

값 유형은 다음 두 종류 중 하나일 수 있다.

원문

A value type can be one of the two following kinds:

a structure type, which encapsulates data and related functionality

an enumeration type, which is defined by a set of named constants and represents a choice or a combination of choices.
📎 출처: MS Docs - Value Types

1. 사용자 정의 구조체(struct) 

struct는 값 타입이다.

함수에 인자로 전달되면 복사본이 전달된다.
즉 "데이터와 관련 기능을 캡슐화하는 구조 유형"에 속하는 것이다.

 

쉽게 말하자면,

  • 데이터(변수들)와 그 데이터를 처리하는 기능(메서드)를 하나로 묶은 타입
  • C#의 struct가 이에 해당
  • 클래스처럼 생겼지만, 값 타입이라는 차이점

2. Enumeration type (열거형 타입)

명명된 상수 집합으로 정의되고 선택 사항 또는 선택 사항의 조합을 나타내는 열거형

 

 쉽게 말하자면,

  • 미리 정해진 이름 있는 값들 중 하나(또는 몇 개)를 선택하게 해주는 타입
  • enum 키워드로 정의
  • 옵션이나 상태 등을 명확하게 표현할 때 유용

“선택 사항의 조합”이란?

  • enum에 [Flags] 특성을 붙이면 여러 값을 조합 가능

 

 

비트 연산으로 Read + Write를 동시에 표현할 수 있음

[Flags]
enum FileAccess
{
    Read = 1,
    Write = 2,
    Execute = 4
}

// 여러 값 조합
FileAccess access = FileAccess.Read | FileAccess.Write;

 

 

구조체 (struct) 데이터와 기능을 함께 가지는 값 타입 Point, DateTime, Vector2
열거형 (enum) 이름이 붙은 정수 상수들의 모음으로, 하나 또는 여러 상태를 표현 Color, FileAccess, DayOfWeek

 

 

참조 타입(Reference Type)

C#에는 두 가지 종류의 타입이 있다: 참조형(reference types)과 값형(value types)이다.

  • 참조형 변수는 데이터(객체)에 대한 참조(주소) 를 저장합니다.
  • 값형 변수는 데이터를 직접 포함합니다.

 

 

 

참조형의 경우, 두 개의 변수가 같은 객체를 참조할 수 있다.

따라서 한 변수에 대한 작업이 다른 변수가 참조하는 객체에도 영향을 줄 수 있다.

 

반면, 값형은 각 변수가 자기만의 데이터 복사본을 가지므로,
한 변수에 대한 작업이 다른 변수에 영향을 줄 수 없다.

 

 

다음 키워드는 참조형 타입을 선언할 때 사용

  • class
  • interface
  • delegate
  • record

C#에는 다음과 같은 내장 참조형있음

  • dynamic
  • object
  • string

 

예외
(except in the case of in, ref, and out parameter variables; see inref, and out parameter modifier).
(단, in, ref, out 매개변수로 전달된 경우는 예외입니다. 자세한 내용은 in, ref, out 매개변수 한정자 참고).

 

값 타입 참조 타입 비교

  값 타입 참조 타입
저장 위치 스택(Stack) 힙(Heap)
변수에 담기는 것 실제 값 참조(주소)
복사 시 값 복사 (완전한 복제) 주소 복사 (같은 객체 가리킴)
null 가능 여부 불가 (nullable 필요) 가능

 

 

 

  • 참조 형식 값 = new를 통해 만들어진 객체 그 자체
  • 참조 형식 변수 = 그 객체의 주소를 가진 변수

 

즉, 참조 변수는 객체를 "가리키고" 있고, 객체는 힙에 존재하는 “참조 형식 값”이다.

 

 

 

 

 

 

참고 사이트

Value types - C# reference

Reference types - C# reference

Types - C# language specification

 

박싱(Boxing)과 언박싱(Unboxing) 정리

 

박싱(Boxing)이란?

값 타입(Value Type)을 참조 타입(Reference Type)인 object 또는 해당 값 타입이 구현한 인터페이스 타입으로 변환하는 과정

 

“Boxing is the process of converting a value type to the type object or
to any interface type implemented by this value type.”
출저 : Boxing and Unboxing - Microsoft Docs

 

 

 

  • C#에서 int, float, bool 등은 값 타입입니다. 이는 메모리의 스택(stack)에 저장되고, 직접 값을 담는다.
  • object는 모든 타입의 부모인 참조 타입이며, 메모리의 힙(heap)에 저장되고, 값을 참조(주소)한다.
  • 박싱을 하면, 값 타입의 데이터를 힙에 새로 생성된 object 인스턴스에 복사해서 감싸게 된다.
int i = 123;
object o = i; // 암시적 박싱

 

 

 

"값 타입을 참조 타입(object나 인터페이스)으로 변환한다"는 의미

  • 예시: int i = 123; 는 값 타입
  • object o = i; 는 값 타입 i를 힙에 복사하고, 그 참조를 o가 가지게 되는 것
  • 즉, int → object 로 감싸짐

 

“Boxing a value type allocates an object instance on the heap and copies the value into the new object.”
출저 : Boxing - Microsoft Docs

 

언박싱이란?

object 또는 인터페이스 참조에서 원래의 값 타입으로 되돌리는 명시적 캐스팅 과정

 

“Unboxing extracts the value type from the object. Boxing is implicit; unboxing is explicit.”
출처: Boxing and Unboxing - Microsoft Docs

 

  • 이 과정은 명시적 형변환(explicit cast)이 필요
  • 참조 타입인 object에 박싱된 값 타입을 꺼낼 때 사용하는 과정
object o = 123;
int i = (int)o; // 명시적 언박싱

 

인터페이스 참조란?

인터페이스(interface)는 메서드나 속성의 "규약"만 정의하고,
실제 구현은 구현체가 갖는 추상적인 틀

 

interface IExample
{
    void DoSomething();
}

struct MyValueType : IExample
{
    public void DoSomething() { Console.WriteLine("Doing something"); }
}

 

여기서 MyValueType은 값 타입이지만, IExample 인터페이스를 구현하고 있다.
이제 박싱을 통해 IExample 타입 변수에 값을 담을 수 있다.

 

MyValueType value = new MyValueType();
IExample ex = value; // 박싱 발생, 힙에 object 생성

 

즉, 값 타입이지만 인터페이스를 구현하고 있다면, 해당 인터페이스 타입으로의 변환도 박싱

 

 

박싱과 언박싱의 성능 비용에 대한 Microsoft 공식 문서의 설명

1. 박싱(Boxing)

"박싱은 값 형식을 참조 형식으로 변환하는 과정으로,
새로운 객체를 힙에 할당하고 값을 복사해야 하므로 계산 비용이 많이 듭니다."

박싱은 값 형식을 object나 해당 값 형식이 구현한 인터페이스 타입으로 변환하는 과정 
이 과정에서 새로운 객체가 힙에 할당되고, 값이 복사되므로 추가적인 메모리와 CPU 자원이 필요로 함

2. 언박싱(Unboxing)

"언박싱은 참조 형식에서 값 형식으로 변환하는 과정으로, 캐스팅이 필요하며 이 역시 계산 비용이 많이 듭니다."

언박싱은 object나 인터페이스 타입에서 원래의 값 형식으로 변환하는 과정 
이 과정에서는 런타임에서 타입 검사가 수행되며, 값이 다시 복사되므로 추가적인 계산 비용이 발생

3. 성능 비교

"박싱은 단순한 참조 할당보다 최대 20배 더 오래 걸릴 수 있으며, 언박싱은 4배 더 오래 걸릴 수 있습니다."

박싱과 언박싱은 단순한 값 할당이나 참조 할당에 비해 상당한 성능 차이를 보일 수 있다.

특히 반복적인 작업이나 대량의 데이터를 처리할 때 이러한 차이는 더욱 두드러진다.

 

요약

  • 박싱(Boxing): 값 형식을 참조 형식으로 변환하는 과정으로, 힙에 새로운 객체를 할당하고 값을 복사하므로 성능 비용이 크다.
  • 언박싱(Unboxing): 참조 형식에서 값 형식으로 변환하는 과정으로, 타입 검사와 값 복사가 필요하므로 성능 비용이 발생한다.
  • 성능 영향: 박싱은 단순한 참조 할당보다 최대 20배, 언박싱은 4배 더 오래 걸릴 수 있다.

성능 최적화 팁

  • 제네릭(Generic) 컬렉션 사용: List<int>와 같은 제네릭 컬렉션을 사용하면 박싱과 언박싱을 피할 수 있다.
  • 값 형식 사용 최소화: 박싱이 자주 발생하는 경우, 값 형식의 사용을 최소화하거나 참조 형식으로 대체하는 것을 고려

 

 

박싱(Boxing) vs 언박싱(Unboxing) 비교 표

 

  박싱 (Boxing)  언박싱 (Unboxing)
정의 값 타입을 참조 타입 (object 또는 인터페이스)으로 변환 참조 타입 (object)에서 원래의 값 타입으로 변환
코드 예시 object o = i; int j = (int)o;
메모리 위치 힙 (Heap) 스택 (Stack)
암시성 암시적 변환 (implicit) 명시적 변환 (explicit)
성능 영향 힙 메모리 할당 + 값 복사 + GC 부담 → 비용 큼 타입 검사 + 값 복사 → 비용 발생
위험 C#에서는 값 타입을 object나 인터페이스 타입으로 변환하는 박싱은 컴파일러가 자동으로 허용
힙에 객체를 새로 생성하고, GC 대상
→  데이터를 처리할 때 성능이 급격히 저하

암묵적 박싱 발생으로 인한 예측 불가한 동작

특히 struct가 인터페이스를 구현하면, 인터페이스 변수에 담을 때마다 박싱이 발생


박싱 그 자체는 안전하지만, 언박싱을 잘못하면 박싱으로 인해 런타임 예외가 발생할 수도 있음.
잘못된 형변환 → InvalidCastException - null 언박싱 → NullReferenceException
피해야 할 경우 성능이 민감한 루프, 대량 데이터 구조에선 박싱 발생 시 성능 저하 가능 타입이 확실하지 않거나 null 가능성이 있을 경우 언박싱은 위험함

 

 

  • 박싱 자체는 안전하다. (컴파일 오류 없음, 예외 없음)
  • 하지만 실무에서는 성능 저하 및 언박싱 시 오류로 이어질 수 있는 잠재적 위험이 존재

 

 

박싱이 발생하는 대표적인 코드 패턴

 

값 타입 → object, interface로 변환 object o = 123;, IComparable c = 5; 등
object 기반 컬렉션에 값 타입 추가 ArrayList.Add(10);
인터페이스를 구현한 struct를 인터페이스 타입 변수에 할당 IMyInterface i = new MyStruct();
params object[]에 값 타입 인수 전달 string.Format("{0}", 123);
object.Equals() 또는 ToString() 호출 내부에서 박싱이 발생할 수 있음

 

 

박싱 발생을 탐지하는 방법

방법 1: Visual Studio 진단 도구

  • Visual Studio → 성능 프로파일러(Alt + F2)할당 추적(Allocation Tracking)
  • object 타입이 힙에 얼마나 생성되는지 확인 가능
  • System.Int32 → System.Object로 변환된 흔적이 보이면 박싱 발생한 것

방법 2: IL 코드 확인

박싱된 코드는 IL(중간 언어)에서 box 명령어로 확인 가능

interface IMyInterface { void DoSomething(); }
struct MyStruct : IMyInterface
{
    public void DoSomething() { }
}

void Main()
{
    IMyInterface m = new MyStruct(); // 박싱 발생!
}

위 코드를 IL로 보면 박싱이 발생했다는 명확한 증거를 남김

box MyStruct

 

 

3. 박싱을 회피하는 전략

제네릭(Generic) 사용

박싱의 대표적인 대안은 object 기반이 아닌 제네릭 기반 컬렉션 사용


 박싱 발생 코드 박싱 회피 코드
ArrayList list = new ArrayList(); list.Add(10); List<int> list = new List<int>();
 
  • List<object>는 int 추가 시 박싱 발생
  • List<int>는 박싱 없이 직접 저장됨

 

박싱 발생 예제

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<object> list = new List<object>();

        for (int i = 0; i < 5; i++)
        {
            list.Add(i); // 여기서 박싱 발생 (int → object)
        }

        foreach (object item in list)
        {
            Console.WriteLine(item); // Unboxing 없음, 하지만 힙 객체 접근
        }
    }
}

문제점

  • int는 값 타입
  • object는 참조 타입
  • int를 object로 넣으면서 박싱이 발생합니다 → 힙 메모리 사용, 성능 저하

 

리팩토링 버전: List<int> 사용 → 박싱 제거 예제

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> list = new List<int>();

        for (int i = 0; i < 5; i++)
        {
            list.Add(i); // ✅ 박싱 없음
        }

        foreach (int item in list)
        {
            Console.WriteLine(item); // 스택 기반 값 타입 그대로 처리
        }
    }
}

이점

  • int는 값 타입이므로 스택에서 직접 처리됨
  • 박싱/언박싱 없음, 힙 메모리 할당 없음
  • GC 부담 없음 → 성능 향상

 

 

 

struct 대신 class, 또는 반대로

 

상황 전략
struct가 인터페이스에 할당될 때 class로 바꾸면 박싱 없음
불필요하게 class 사용 중이고 할당 비용이 문제 struct로 바꾸고 제네릭 사용

 

문제 예제 예시 IComparable 인터페이스 사용 시 박싱 발생

struct MyNumber : IComparable
{
    public int Value;

    public int CompareTo(object obj)
    {
        return Value.CompareTo(((MyNumber)obj).Value);
    }
}

class Program
{
    static void Main()
    {
        MyNumber a = new MyNumber { Value = 5 };
        IComparable comp = a; // struct → interface로 박싱 발생
        Console.WriteLine(comp.CompareTo(new MyNumber { Value = 3 }));
    }
}

 

리팩토링 버전: Class로 변경

 

class MyNumber : IComparable
{
    public int Value;

    public int CompareTo(object obj)
    {
        return Value.CompareTo(((MyNumber)obj).Value);
    }
}

class Program
{
    static void Main()
    {
        MyNumber a = new MyNumber { Value = 5 };
        IComparable comp = a; // struct → interface로 박싱 발생
        Console.WriteLine(comp.CompareTo(new MyNumber { Value = 3 }));
    }
}

 

 

Span<T>, ref struct, stackalloc 등 활용

C#에서는 고성능 코드를 위해 다음과 같은 힙 할당을 회피하는 기능도 제공:

  • Span<T>: 스택 기반 배열 슬라이스
  • stackalloc: 스택에 직접 배열 생성
  • ref struct: 힙에 올릴 수 없는 구조체 (박싱 불가)
Span<int> numbers = stackalloc int[100]; // 힙 할당 없음

 

 

활용 이유

  • 힙 할당 없이 스택 기반 버퍼/데이터 처리
  • 값 타입을 직접 메모리에 접근하며 사용
  • GC 부담 줄이고 박싱 방지

 

예제: Span<T> + stackalloc 활용

using System;

class Program
{
    static void Main()
    {
        Span<int> numbers = stackalloc int[5]; // 스택에 배열 생성

        for (int i = 0; i < numbers.Length; i++)
        {
            numbers[i] = i * 10;
        }

        foreach (int n in numbers)
        {
            Console.WriteLine(n); // 힙 할당 없이 출력
        }
    }
}

 

설명

  • stackalloc은 스택에 메모리를 직접 할당
  • Span<T>는 박싱 없이 메모리에 직접 접근할 수 있는 ref struct
  • Span<T>는 ref struct이기 때문에 힙에 할당 불가, 따라서 박싱 자체가 불가능

제한 사항

  • Span<T>는 필드( 클래스 또는 구조체 안에 선언된 변수 )로 저장 불가
    클래스 객체는 힙에 올라가기 때문에, Span<T>를 필드로 만들면 힙에 저장되므로 금지
  • 비동기 메서드, Task 안에서 사용 불가
    await, 현재 작업을 멈추고, 다른 일을 하다가 결과가 준비되면 다시 이어서 실행
    await를 사용하면 메서드가 중단되고 재개되기 때문에, 원래 스택 메모리가 사라질 수 있
  • 왜냐하면 스택 메모리 생명 주기가 제한되어 있어서

Span<T> 안전하게 쓰려면?

  • 메서드 내부에서만 선언하고 즉시 사용
  • async/await, lambda, iterator, class 필드 안에서 사용

 

default interface method + 제네릭 제한 활용 

박싱 없이 인터페이스 메서드를 호출하려면

interface IProcessor<T> where T : struct
{
    void Process(ref T value); // ref를 통해 박싱 없이 처리 가능
}

 


목적

  • 값 타입에 대해 박싱 없이 인터페이스 메서드 제공
  • 제네릭 + ref 전달로 성능 최적화

예제: IProcessor<T> 인터페이스에 ref T 사용

using System;

interface IProcessor<T> where T : struct
{
    void Process(ref T value)
    {
        Console.WriteLine("Default implementation: " + value);
    }
}

struct MyStruct : IProcessor<MyStruct>
{
    public int X;

    public override string ToString() => $"MyStruct: {X}";
}

class Program
{
    static void Main()
    {
        MyStruct m = new MyStruct { X = 42 };
        CallProcessor(ref m);
    }

    static void CallProcessor<T>(ref T value) where T : struct, IProcessor<T>
    {
        value.Process(ref value); // 박싱 없이 인터페이스 호출
    }
}

 

 

 

설명

T : struct 제한 덕분에 값 타입만 허용됨 → 박싱 X

ref 전달 → 복사 X, 직접 전달

IProcessor<T> 인터페이스 메서드는 default interface implementation 사용 가능 (C# 8.0+)

호출 시 박싱 없이 ref T 타입으로 호출됨

 

 

요약    
1 object 기반 대신 제네릭 사용 List<T>, Dictionary<TKey, TValue>
2 struct → class 전환 고려 인터페이스 할당 시 박싱 주의
3 고성능 구조 활용 Span<T>, ref struct, stackalloc
4 제네릭 + 인터페이스 최적화 where T : struct, ref T 활용

 

'프로그래밍 언어 > C#' 카테고리의 다른 글

.NET 과 C#의 아키텍처  (4) 2025.05.17
IEnumerator<T> 인터페이스  (0) 2025.05.16
C# out 키워드  (0) 2024.07.27
프로퍼티, 델리게이트, 이벤트 간단 정리  (0) 2024.01.08
C#  (1) 2023.12.17