[C#] 박싱(Boxing)과 언박싱(Unboxing)이란?

Running boy·2023년 8월 11일
0

컴퓨터 공학

목록 보기
28/36

C#에서 스택 메모리 영역과 힙 메모리 영역을 설명할 때 항상 나오는 개념이 박싱, 언박싱이다.
이전 포스트에서 우선 스택과 힙에 대해 복습하고 읽어보자.

박싱(Boxing)/언박싱(Unboxing)

값 형식의 데이터를 참조 형식으로 변환하는 것을 박싱이라고 하며, 그 반대를 언박싱이라고 한다.
이에 대해 유의할 점 두가지가 있다.

첫번째는 단순히 스택에서 힙으로의 값 복사가 박싱은 아니라는 것이다.
클래스의 필드로 값 형식이 선언됐다고 생각해보자.
객체는 힙 메모리에 할당되므로 그 필드 역시 힙의 연속된 메모리에 존재할 것이다.
그렇다고 그 값 형식의 필드가 박싱된 것은 아니다.
반대로 그 필드의 값을 참조 형식으로 변환한다면 그것 역시 박싱이다.
힙 메모리 내부에서도 박싱이 일어날 수 있다는거다.

두번째는 참조 형식을 값 형식으로 바꾸는게 무조건 언박싱은 아니라는 것이다.
언박싱은 박싱된 데이터를 다시 값 형식으로 돌려놓는 것이다.
즉 박싱이 있어야 언박싱이 있을 수 있다.

박싱/언박싱을 왜 사용할까?

대표적인 참조 형식에는 'object'가 있다.
Object 클래스는 모든 타입의 어버이이다.
즉 모든 타입은 object 타입으로 형변환을 할 수 있다는 것이고, int, struct와 같은 값 형식의 타입도 object 타입으로 형변환이 가능하다는 말이다.

아래 코드는 박싱과 언박싱의 예시이다.

class Program
{
    static void Main(string[] args)
    {
        BoxingTest(1, "2", '3');
    }

    static void BoxingTest(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] is int)
            {
                int n = (int)args[i];
                Console.WriteLine(n + " is int");
            }
            else if (args[i] is char)
            {
                char c = (char)args[i];
                Console.WriteLine("'" + c + "' is char");
            }
            else if (args[i] is string) // string은 애초에 참조 형식이므로 따지고보면 박싱이 아니긴 하다.
            {
                string s = (string)args[i];
                Console.WriteLine('"' + s + "\" is string");
            }
        }
    }
}
1 is int
"2" is string
'3' is char

값 형식의 타입은 BoxingTest의 인자로 넘겨지면서 object 타입으로 박싱된다.
그리고 원래의 타입으로 다시 언박싱되어 사용된다.

BoxingTest 메서드는 object 타입을 매개변수로 받는다.
이는 모든 타입을 인자로 활용할 수 있음을 뜻한다.
데이터를 사용할 때 원래의 타입으로 돌려놓는 과정이 필요하지만 이를 잘 사용하면 상당히 범용적인 코드가 탄생한다.
하지만 여기까지만 보고 바로 본인의 코드에 박스를 쌓으러 가진 말자.


박싱/언박싱은 과연 좋은 것일까?

최적화 관점에서 박싱과 언박싱은 지양해야 되는 기법이다.
사실 박싱과 언박싱은 내부적으로 상당한 오버헤드를 감수하고 사용해야 된다.

박싱은 1) 힙 영역에 새로운 메모리를 할당하고, 2) 스택의 값을 힙 메모리로 복사한 뒤, 3) 힙 메모리의 주소 값을 갖는 새로운 스택 메모리를 할당하는 과정을 거친다.
박싱은 딱 봐도 값 하나 옮기는데 메모리 참조를 엄청 많이 하는데, 이로 인해 시간적 오버헤드가 발생한다.

언박싱의 경우도 비슷하다.
하지만 언박싱은 새로운 문제를 야기하는데, 바로 가비지를 생성한다는 것이다.
즉 언박싱은 그 자체로도 오버헤드가 있지만 가비지를 생성함으로 인해 GC를 동작시키는 잠재적 오버헤드까지 가진 셈이다.

마이크로소프트도 박싱과 언박싱의 사용을 자제하는 편이다.
Console.WriteLine 메서드만 봐도 그렇다.
박싱과 언박싱을 사용한다면 object 타입의 매개변수를 받고 ToString 메서드만 호출해도 될 구조다.
하지만 실제로 WriteLine은 수많은 타입에 대해 오버로딩 되어있다.


박싱의 편리함을 가져가면서 오버헤드를 줄이는 방법

C# 2.0부터 제네릭(Generic)을 지원함으로써 박싱과 언박싱의 사용을 줄일 수 있게 됐다.
제네릭에 관한 내용은 별도의 포스트에 정리했다.

foreach문 대신 for문을 쓰라던데?

foreach문 내부에서 박싱이 일어나기 때문에 foreach문은 최적화 관점에서 안좋다고 한다.
하지만 해당 이슈는 이미 예전에 리펙토링을 통해 해결됐으며 현재 둘의 성능적 차이는 거의 없다고 한다.

이런 이슈가 발생한 이유는 struct인 Enumerator가 여러 interface를 상속받는데, interface의 메서드를 호출하는 과정에서 Enumerator가 struct인지 class인지 알 수가 없어서 전부 참조 형식으로 간주했기 때문이다.


참고 자료
시작하세요! C# 10 프로그래밍 - 정성태
Foreach에 관하여

profile
Runner's high를 목표로

0개의 댓글