-
제네릭 - 박싱/언박싱 - 스택/힙카테고리 없음 2024. 11. 22. 13:33
제네릭이란?
C#의 제네릭(Generic)은 데이터 타입을 미리 지정하지 않고
코드 작성 시 타입을 파라미터화하여 다양한 데이터 타입을 처리할 수 있게 하는 기능이다.
타입 매개변수 (T)를 기반으로 지정타입과 호환되는 타입만 처리하도록 하고,
그렇지 않으면 컴파일 에러를 발생시킨다.
(사실 T(Type)는 관례적으로 사용하는 거고, 아무렇게나 이름 붙일 수 있다)
제네릭의 사용 예시
제네릭 클래스
public class GenericClass<T> { private T data; public void SetData(T value) { data = value; } public T GetData() { return data; } } // 사용 var intInstance = new GenericClass<int>(); intInstance.SetData(42); Console.WriteLine(intInstance.GetData()); // 출력: 42 var stringInstance = new GenericClass<string>(); stringInstance.SetData("Hello"); Console.WriteLine(stringInstance.GetData()); // 출력: Hello
제네릭 메서드
public class Utility { public static void PrintType<T>(T input) { Console.WriteLine($"입력 값: {input}, 타입: {typeof(T)}"); } } // 사용 Utility.PrintType(42); // 출력: 입력 값: 42, 타입: System.Int32 Utility.PrintType("Hello"); // 출력: 입력 값: Hello, 타입: System.String
제네릭 인터페이스
public interface IRepository<T> { void Add(T item); T Get(int id); } public class Repository<T> : IRepository<T> { private List<T> items = new List<T>(); public void Add(T item) { items.Add(item); } public T Get(int id) { return items[id]; } } // 사용 var stringRepo = new Repository<string>(); stringRepo.Add("Test"); Console.WriteLine(stringRepo.Get(0)); // 출력: Test
이외에도 구조체, 델리게이트, 이벤트 등 다양한 곳에서 사용가능하다.
사실 우리는 제네릭을 알게모르게 사용해봤다.
List<int> iList = new List<int>();
List<string> sList = new List<string>();
Dictionary<string, float> dic = new Dictionary<string, float>();
이런게 다 System.Collections.Generic 네임스페이스가 가진 제네릭 기반 컬렉션 클래스이다.그래서 제네릭 왜 씀?
제네릭이 등장하기 이전에..
C#의 제네릭은 C# 2.0과 함께 등장했는데, 제네릭 등장 이전에는 비제네릭 컬렉션인 ArrayList와 Hashtable을 사용했다. 이들은 데이터를 내부적으로 object 타입으로 저장한다.
object는 C#에서 모든 데이터 타입의 부모 타입으로, 데이터가 값 형식이든 참조 형식이든 타입에 상관없이 데이터를 저장하고 처리할 수 있다.
.NET 타입 계층구조를 보면 object가 모든 데이터 타입의 부모 클래스라는 의미를 명확히 알 수 있다.System.Object ├── System.ValueType (값 형식의 부모) │ ├── int │ ├── float │ ├── bool │ ├── char │ ├── double │ ├── decimal │ ├── struct (사용자 정의 값 형식) │ │ ├── DateTime │ │ ├── Guid │ ├── enum │ ├── ConsoleColor │ ├── DayOfWeek │ ├── 사용자 정의 열거형 │ ├── 참조 형식 ├── string ├── Array ├── 사용자 정의 클래스 ├── System.Delegate (델리게이트의 부모) ├── System.Exception (예외 클래스의 부모)
값 형식(Value Type)은 내부적으로 System.ValueType에서 파생되고, System.ValueType은 object를 상속받는다. 참조 형식은 직접적으로 object를 상속받는다. (object도 참조 형식이다)
다시 돌아와서, 비제네릭 컬렉션이 모든 데이터를 참조 형식인 object로 데이터를 저장한다는 것은, 여기에 값 형식을 추가하거나 가져올 때 박싱/언박싱이 필요하다는 것을 말한다.박싱/언박싱은 또 뭔데?
박싱(Boxing)은 값 형식 데이터를 참조 형식으로 변환하는 과정을 말하고,
언박싱(Unboxing)은 참조 형식 데이터를 값 형식으로 변환하는 과정을 말한다.그냥 값-참조 바꾸면 되는건데 왜 문제가 생김?
값 형식의 데이터는 스택에 저장되고, 참조 형식의 데이터는 힙에 저장되기 때문에
박싱(값->참조)은 힙에 메모리 할당을 새로 해야하고,
언박싱(참조->값)은 힙에서 스택으로 복사하는 비용이 발생한다.값 형식과 참조 형식을 스택과 힙에 따로 저장하는 이유는 뭐야?
그걸 알기 위해서는 또 스택 영역과 힙 영역에 대한 특성을 알아야 한다.
자세한 정리는 다른 글에서 하고 여기서는 간단하게 특징만 알아보겠다.
스택의 특징- LIFO(Last In First Out) 구조로 데이터를 관리, 데이터를 빠르게 추가 제거할 수 있음.
- 정적 메모리 영역으로, 메모리 크기가 컴파일 시점에 고정됨.
- 메모리 할당과 해제가 매우 빠름.힙의 특징
- 동적으로 메모리를 관리함. 필요할 때 메모리를 요청(할당)하고 사용이 끝나면 반환(해제)함.
- 데이터 크기가 가변적일 수 있고, 프로그램 실행 중 메모리를 동적으로 관리 가능.
- 참조를 통해 접근(데이터를 힙에 저장하고, 데이터에 대한 주소를 스택에 저장해 스택으로 접근)
- 메모리 할당과 해제에 시간이 오래 걸림.
참조 형식 데이터(문자열, 배열, 객체)는 크기가 가변적일 수 있다.
힙은 동적으로 메모리를 관리하므로, 참조 형식 같이 크기가 가변적인 데이터를 저장하는 데 적합하다.
스택은 빠르다. 크기 변하지 않는 값 형식의 경우 스택에 저장하는 것이 좋다.
그래서 값 형식의 데이터는 스택에, 참조 형식의 데이터는 힙에 데이터가 저장된다.다시 돌아와서 박싱/언박싱이 일어나면 왜 안좋은지 알아보자
값 형식과 참조 형식의 저장방식이 다르기 때문에,
비제네릭 클래스(ArrayList, Hashtable)에 값 형식을 저장/반환 할 때 박싱/언박싱이 발생한다.
이런 박싱/언박싱 과정은 성능의 저하를 일으킨다.
ArrayList list = new ArrayList(); // 값 형식 추가: 박싱 발생 int number = 42; // 값 형식 list.Add(number); // 박싱 (값 형식 → 참조 형식) // 값 가져오기: 언박싱 발생 int retrievedNumber = (int)list[0]; // 언박싱 (참조 형식 → 값 형식)
이렇게 값 형식을 비제네릭 클래스에 저장하면, 내부적으로는 아래와 같은 동작이 발생한다.
- number라는 값 형식 데이터(42)가 힙 메모리에 복사됨.
- 힙 메모리에 저장된 값을 참조하는 object 타입의 참조가 생성됨.
- list.Add()는 이 참조를 저장함.
반대로 참조 형식을 값 형식으로 가져오려면 아래와 같은 동작이 발생한다.
- 참조 형식(object)이 힙 메모리에서 값을 가져옴.
- 힙에 있는 데이터를 스택으로 복사.
- 명시적으로 캐스팅((int))하여 원래 값 형식으로 변환.
박싱 과정에서 힙 메모리를 할당해야하고, 언박싱 과정에서 힙의 데이터를 스택에 복사하는 작업이 필요하므로 성능이 저하된다. 반복적으로 이런 작업을 해야한다면 성능 문제가 심각해질 수 있다.
또한, 박싱/언박싱 과정은 컴파일러가 타입을 체크하지 못하기 때문에 타입 안정성이 떨어진다.
object obj = 42; // 박싱 string text = (string)obj; // InvalidCastException 발생
언박싱 시 잘못된 타입으로 캐스팅하면 컴파일시에는 오류를 찾을 수 없기 때문에 런타임 오류가 발생한다.
제네릭은 타입 매개변수(T)를 사용하여 컴파일 시점에 데이터 타입을 고정하여
이런 박싱/언박싱 과정 없이 데이터를 저장/관리 할 수 있게 해준다.
그래서 성능과 타입 안정성이 높아졌고, C# 2.0에서 제네릭이 도입된 이후 잘 사용하고 있다.
정리하자면..
제네릭은 데이터 타입에 의존하지 않고 재사용 가능한 클래스, 메서드, 인터페이스, 델리게이트 등을 작성할 수 있도록 지원하는 기능이다. 제네릭은 타입 매개변수(T)를 사용하여 컴파일 시점에 데이터 타입을 고정하기 때문에 타입 안정성이 올라가고, 값 형식을 저장할 때 박싱/언박싱이 발생하지 않아 성능이 향상된다.
*근데 C#말고 다른 언어에는 제네릭 같은거 없음?
C는 그런거 없고 C++은 std에서 유사한 기능을 제공함.