IT개발

C#의 Boxing / Unboxing 정리

HyochulLab 2025. 3. 11. 12:08

1. 박싱(Boxing)이란?

**박싱(Boxing)**은 **값 타입(Value Type)을 참조 타입(Reference Type)**인 object 또는 인터페이스 타입으로 변환하는 과정입니다.

1.1 왜 박싱이 일어날까?

  • C#에서 값 타입(예: int, float, struct)은 스택(Stack)에 할당되고, 참조 타입(예: class, object, string)은 힙(Heap)에 할당됩니다.
  • 어떤 이유로든 값 타입을 object나 인터페이스로 다뤄야 하는 상황이 생기면, .NET은 내부적으로 그 값을 힙 영역에 새롭게 복사(메모리 박스)하여 참조를 만들고, 이 참조를 반환합니다.
    • 예: int i = 123; object o = i; → i는 힙 메모리에 들어가 있는 박스화된 123을 가리키는 object가 됩니다.

1.2 박싱 예시 코드

int num = 42;          // 값 타입
object boxedNum = num; // 박싱 발생: int -> object
  • num은 스택 영역에 42를 담고 있음
  • boxedNum을 만들 때 힙에 새로운 공간이 생기고, 42가 그 공간에 복사됨
  • boxedNum은 object로서 해당 힙 객체(‘박스’)를 가리킵니다.

2. 언박싱(Unboxing)이란?

**언박싱(Unboxing)**은 박싱된 객체(참조 타입)에서 다시 값 타입을 꺼내오는 과정입니다.

  • 박싱의 반대 작업으로, object 변수에 들어 있는 박싱된 값을 다시 원래의 값 타입으로 변환해야 할 때 일어납니다.

2.1 언박싱 예시 코드

object boxedNum = 42; // 박싱
int unboxedNum = (int)boxedNum; // 언박싱
  • boxedNum은 힙 메모리에 박스된 42를 가리키는 object
  • (int)boxedNum을 통해 힙에 저장된 값을 다시 스택으로 복사해서 unboxedNum 변수에 할당

2.2 주의사항: 캐스팅 오류

object boxed = 42;    // int를 박싱
short s = (short)boxed; // InvalidCastException 발생
  • boxed 안에 실제로는 int 형식의 값이 들어 있으므로, short로 언박싱하면 예외가 발생합니다.
  • 언박싱은 원래 박싱했던 타입(또는 호환되는 형식)으로만 가능합니다.

3. 박싱/언박싱 시 성능 이슈

박싱과 언박싱은 다음과 같은 추가 비용이 발생합니다.

  1. 힙 할당/해제 오버헤드
    • 박싱 시 힙에 새로운 객체가 생성되고, 가비지 컬렉터가 이를 추적해야 합니다.
    • 언박싱도 힙에서 값을 다시 읽어 스택에 복사하는 과정을 거칩니다.
  2. 런타임 타입 체크
    • 언박싱 시점에, 실제 힙에 들어 있는 타입이 캐스팅 대상과 맞는지 확인합니다.

Tip: 박싱과 언박싱이 빈번하게 일어나면 GC(가비지 컬렉션) 부담이 커져 성능 저하가 생길 수 있습니다. 특히, 성능이 중요한 루프나 실시간 작업에서 주의가 필요합니다.


4. 박싱/언박싱을 피하는 방법

  1. 제너릭(Generic) 활용
    • 예: List<int> 대신 과거 C# 1.0 시절에는 ArrayList(object 기반)를 많이 썼고, int를 저장할 때마다 박싱이 일어났습니다.
    • 제너릭 컬렉션을 사용하면 타입에 안전하며 박싱/언박싱 없는 원시 값 타입의 저장이 가능합니다.
    // 구버전, 비권장: ArrayList 사용
    ArrayList arr = new ArrayList();
    arr.Add(10); // 박싱 발생
    int val = (int)arr[0]; // 언박싱
    
    // 제너릭, 권장: List<int> 사용
    List<int> list = new List<int>();
    list.Add(10);   // 박싱 없음
    int val2 = list[0]; // 언박싱 없음
    
  2. string.Format, Console.WriteLine 등에 매개변수 타입 주의
    • string.Format("{0}", someInt) → 내부적으로 object 파라미터를 받으므로 박싱 발생 가능
    • C# 6.0 이상에서 string interpolation($"Value: {someInt}")을 사용해도 내부적으로는 박싱이 일어날 수도 있습니다(특히 구조체나 IFormattable 등).
    • 꼭 필요하다면 ToString() 오버라이드를 사용하거나 IFormattable을 적절히 구현하여 박싱 없이 처리되도록 할 수 있습니다(고급 주제).
  3. Enum의 박싱 방지
    • 열거형(Enums)을 object로 다루거나, 인터페이스 파라미터에 전달하면 박싱이 발생
    • 필요하면 Convert.ToInt32(enumValue)처럼 정수 변환을 직접 명시하여 처리할 수 있습니다.
  4. boxing-free API 활용
    • 일부 .NET API는 오버로드로 object를 받지 않고 제너릭 메서드를 제공해 박싱을 피할 수 있게 해줍니다.
    • 예: string.Join<T>(), StringBuilder.Append<T>() (일부 버전부터 지원) 등

5. 실제 사례

5.1 수많은 박싱이 발생하는 코드

// 예: 대규모 로그 시스템 - 다양한 숫자 타입 로그를 남김
public void LogValues(params object[] values)
{
    foreach(var v in values)
    {
        Console.WriteLine(v);
    }
}

// 사용 예시
for(int i = 0; i < 10000; i++)
{
    LogValues(i, i*2, i*3.14f);
}
  • LogValues(int, int, float) → 모두 object 파라미터 배열로 변환 (박싱)
  • 반복문이 1만 번 도니까, 박싱 연산이 3만 번 발생
  • 이로 인해 GC 빈번 호출 가능, 성능 저하 위험

개선 방안

  • string 형태로 미리 변환(.ToString())해서 넘기거나, 제너릭이나 별도 오버로드를 활용해 object가 아닌 타입으로 받는 방법 고려

6. 정리

**박싱(Boxing)/언박싱(Unboxing)**은 값 타입과 참조 타입 간 변환에서 자연스럽게 일어나는 동작이지만,

  • 무분별하게 사용하면 불필요한 힙 할당성능 저하를 야기할 수 있습니다.
  • 최신 C# 환경에서는 제너릭을 활용한 타입 안정성이 잘 보장되므로, 박싱을 최소화하는 구조를 설계하세요.

핵심 포인트

  1. 박싱: 값 타입 → 참조 타입 변환 (힙 할당 발생)
  2. 언박싱: 박싱된 객체 → 원래의 값 타입 추출
  3. 성능 고려: 빈번한 박싱/언박싱 → GC 부담 증가, 런타임 캐스팅 검증
  4. 해결책:
    • 제너릭 컬렉션(List, Dictionary<TKey,TValue>) 사용
    • interface, object 기반 호출 최소화
    • 필요하다면 맞춤형 오버로드 또는 ToString()으로 사전 변환

코드 최적화와 유지보수성 사이에서 적절한 균형을 유지하되, 박싱/언박싱이 잦은 부분은 프로파일링을 통해 실제 성능에 미치는 영향을 확인하고 최적화하는 것이 좋습니다.