제네릭이란
C# 2.0부터 지원하는 제네릭은 일반화(Generalization)이라고 부른다. 일반화는 특수한 개념에서 공통된 개념을 찾아 묶는 것을 칭한다. 예를 들면 토끼, 고양이, 사자는 모두 동물이라는 공통된 개념으로 묶을 수 있다. 이런 일반화를 프로그래밍 상에서 제네릭( 일반화를 이용하는 프로그래밍 기법 )이라고 부른다. 앞선 컬렉션 포스팅에서 컬렉션의 단점( 박싱, 언박싱으로 인한 좋지 못한 성능 )을 보완하기 위해 등장하였다.
제네릭은 메서드(함수)나 클래스를 작성할 때 데이터 형식(Type)을 지정하지 않고, 실제 사용하는 시점에서 데이터 형식을 지정할 수 있도록 하는 기능이다. 그래서 재사용성이 높고, 컴파일 시점에서 타입 체크를 하기 때문에 안정적이다. 또한 코드의 가독성과 유지 보수성을 향상시킬 수 있다.
그럼 제네릭에 대해 알아보기 전에 제네릭을 사용하지 않았을 때 코드를 예시로 알아보자
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericEx : MonoBehaviour
{
public void Copy(int[] source, int[] target)
{
for (int i = 0; i < source.Length; i++)
{
target[i] = source[i];
}
}
// 메소드 이름이 Copy로 똑같아도 매개변수의 자료형이 다르면 같은 이름으로 사용할 수 있다. -> 메소드(함수) 오버로드
public void Copy(float[] source, float[] target)
{
for (int i = 0; i < source.Length; i++)
{
target[i] = source[i];
}
}
public void Copy(int[] source, string[] target)
{
for (int i = 0; i < source.Length; i++)
{
target[i] = source[i].ToString();
}
}
}
위 코드는 제네릭을 사용하지 않았을 때 코드이다. 처음에 int[]을 매개변수로 갖는 Copy()를 구현했다. 그리고 사용하다보니 float[]을 매개변수로 받고싶다면 코드를 새로 작성해야하는 한다. 코드를 이런식으로 짜면 어떤 문제점이 있을까?
이렇게 다른 타입을 받아야할 때마다 코드를 새롭게 작성해야하는 불편함과 더불어 코드가 난잡해지는 상황이 발생한다.
즉, 유지 보수가 힘들어진다.
그렇다면 제네릭을 사용하지 않고 코드를 줄여보자
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericEx : MonoBehaviour
{
public void Copy(object[] source, object[] target)
{
for (int i = 0; i < source.Length; i++)
{
target[i] = source[i];
}
}
}
이렇게 만들면 편하게 모든 자료형을 받을 수 있고, 코드도 깔끔해져 해결책이 될 수도 있다. 그러나 object 타입 쓴다는 건
박싱, 언박싱의 문제가 있기 때문에 성능상으로 정말 좋지 못하다.
그럼 이제 제네릭를 활용하여 코드를 변경해보자
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericEx2 : MonoBehaviour
{
// <T> -> 형식 매개변수 : 제네릭 자료형을 나타낸다. (자료형을 알려주기 위한 매개변수)
public void Copy<T>(T[] source, T[] target)
{
for (int i = 0; i < source.Length; i++)
{
Debug.Log(target[i] = source[i]);
}
}
void Start()
{
int[] sourceArray = { 1, 2, 3, 4, 5 };
int[] targetArray = new int[sourceArray.Length];
Copy<int>(sourceArray, targetArray);
string[] strSourceArray = {"하나", "둘", "셋", "넷", "다섯"};
string[] strTargetArray = new string[strSourceArray.Length];
Copy<string>(strSourceArray, strTargetArray);
}
}
Object를 T로 대체해서 코드를 변경했다. 이점이 뭘까? 플레이를 하고 실질적으로 함수를 호출할 때 매개변수에 필요한 자료형을 넣어주면 그 자료형으로 컴파일이 된다. 또한 Object타입을 쓰지 않기 떄문에 박싱, 언박싱이 일어나지 않아 효율적인 성능을 자랑한다.
우리가 유니티에서 다른 컴포넌트의 정보를 가져올 때 사용하는 GetComponent<>() T를 사용한다.
지금까지 제네릭 메소드에 대해 알아봤다.
제네릭 클래스
이제 제네릭 클래스에 대해 살펴보자
public class ReturnInt
{
private int value;
public int Return()
{
return value;
}
}
public class ReturnFloat
{
private float value;
public float Return()
{
return value;
}
}
void Start()
{
ReturnInt returnInt = new ReturnInt();
ReturnFloat returnFloat = new ReturnFloat();
}
위 코드를 보면 자료형 빼고선 함수의 로직이 같다는 것을 알 수 있다. 제네릭 클래스로 변환해보자
public class ReturnGeneric<T>
{
private T value;
public T Return()
{
return value;
}
}
void Start()
{
// 형식 매개변수를 int형으로 쓰는 제네릭 클래스
ReturnGeneric<int> intGeneric = new ReturnGeneric<int>();
ReturnGeneric<float> floatGeneric = new ReturnGeneric<float>();
}
제네릭 메소드 구현과 비슷하게 (접근제어자) (class) (클래스이름) (<T>) 로 구현한다. 이렇게 제네릭 클래스를 이용하면 뭐가 좋을까? 뭔가 좀 복잡해 보이긴 하지만 제네릭을 사용하지 않았던 기존 코드에서는 자료형을 추가하고 싶을 때 클래스를 또 만들어야하는 불편함이 있다. 그치만 제네릭 클래스를 활용하면 이 클래스 하나로 원하는 자료형에 맞게 인스턴스를 생성할 수 있다. T에는 잡다한 것을 다 넣을 수 있지만, 내가 원하는 값만 넣을 수 있도록 제약을 걸 수 있다.
where 키워드, 제약조건
where T : struct는 값 타입인 형식에 대한 제약 조건이다.
where T : class는 참조 타입인 형식에 대한 제약 조건이다.
where T : new()는 매개 변수가 없는 기본 생성자를 가지는 형식에 대한 제약 조건이다.
where T : ParentClass는 파생된 형식에 대한 제약 조건이다. 무조건 ParentClass를 상속해야한다.
where T : ISomeInterface는 ISomeInterface 인터페이스를 구현하는 형식에 대한 제약 조건이다.
where T : struct, ISomeInterface는 값 타입 형식 + 인터페이스 형식에 대한 제약 조건이다.
사용 예시
public class ReturnGeneric<T> where T : struct // 값 타입에 대한 제약
{
}
where 키워드는 타입 매개 변수(T)가 만족해야 할 조건을 명시할 수 있다. 코드를 예를 들자면
제네릭 클래스를 struct 제약을 걸어 작성한다면 참조 타입인 ReturnFloat는 제약으로 지정할 수 없다. 마찬가지로 참조 타입 제약을 걸게 된다면 int, float처럼 값 타입 형식은 지정할 수 없다.
제네릭 형식 제약 조건 공식 문서 참조 : https://learn.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
where(제네릭 형식 제약 조건) - C# reference
where(제네릭 형식 제약 조건) - C# 참조
learn.microsoft.com
우리가 실질적으로 유니티에서 제네릭을 활용할 때 System.Collections.Generic을 많이 사용한다. 이것은 이전에 포스팅했던 C# 1.0 컬렉션 (stack, Queue, ArrayList, Hashtable 등) 을 제네릭화 시킨 것이라고 보면 된다.
[Unity C#] C# 1.0 컬렉션
컬렉션이란같은 성격을 가지는 데이터 모음을 담는 자료 구조로 사용하기 편리하지만 박싱, 언박싱이 일어나기 떄문에 성능상으론 좋지 않은 편이다. 그렇다면 왜 만들었을까? 컬렉션은 C# 1.0에
routine96.tistory.com
활용 예시
컬렉션에서 배웠던 ArrayList, Hashtable, Stack, Queue를 제네릭화 해보자
ArrayList 일반화
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericEx3 : MonoBehaviour
{
ArrayList arrayList = new ArrayList();
void ArrayListEx()
{
arrayList.Add("A");
arrayList.Add(6);
arrayList.Add(true);
}
// ArrayList의 일반화
// List<T>
// 형식 매개변수가 int형이기 때문에 int형만 지정할 수 있다.
List<int> list = new List<int>();
void listEx()
{
list.Add(1);
list.Add(2);
list.Add(3);
}
}
ArrayList는 Object타입으로 모든 형식을 받을 수 있었다(박싱 언박싱이 일어난다). List<T> 지정된 타입의 형식 매개변수만 넣을 수 있다. 나머지 Hashtable, Stack, Queue 또한 일반화해서 사용한다면 지정된 타입의 형식 매개변수를 이용해 작성한다.
Hashtable 일반화
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GenericEx3 : MonoBehaviour
{
void Start()
{
// Hashtable
Hashtable hash = new Hashtable();
hash["Book"] = "책";
hash["Cook"] = "요리";
hash[1.0f] = 15;
hash[5092] = true;
Debug.Log(hash["Book"]);
Debug.Log(hash["Cook"]);
Debug.Log(hash[1.0f]);
Debug.Log(hash[5092]);
// Hashtable 일반화
// Dictionary<T, value>
Dictionary<string, int> dic = new Dictionary<string, int>();
dic["하나"] = 1;
dic["둘"] = 2;
dic["셋"] = 3;
Debug.Log(dic["하나"]);
Debug.Log(dic["둘"]);
Debug.Log(dic["셋"]);
}
}
Dictionary는 게임에서 많이 사용하는 자료 구조이다.
stack 일반화
// stack
Stack stack = new Stack();
stack.Push(1); // 1 삽입 -> 현재 stack : 1
stack.Push(2); // 2 삽입 -> 현재 stack : 1 2
stack.Push(3); // 3 삽입 -> 현재 stack : 1 2 3
stack.Pop(); // 제일 마지막에 삽입된(제일 위에 있는) 값 3 추출 -> 현재 stack : 1 2
stack.Peek(); // 제일 위에 있는 값이 무엇인지 확인 2 -> 현재 stack : 1 2
stack.Pop(); // 2 추출 -> 현재 stack : 1
stack.Pop();
// stack 일반화
// stack<T>
Stack<int> stack2 = new Stack<int>();
stack2.Push(1);
stack2.Push(2);
stack2.Push(3);
stack2.Pop();
stack2.Peek();
stack2.Pop();
stack2.Pop();
Queue 일반화
// Queue
Queue queue = new Queue();
queue.Enqueue(1); // 1 삽입 -> 현재 queue : 1
queue.Enqueue(2); // 2 삽입 -> 현재 queue : 1 2
queue.Enqueue(3); // 3 삽입 -> 현재 queue : 1 2 3
queue.Dequeue(); // 제일 처음에 삽입된 값 1 추출 -> 현재 queue : 2 3
queue.Peek(); // 제일 처음 값 확인 -> 현재 queue : 2 3
queue.Dequeue(); // 2 추출 -> 현재 queue : 3
// Queue 일반화
// Queue<T>
Queue<int> queue2 = new Queue<int>();
queue2.Enqueue(1); // 1 삽입 -> 현재 queue : 1
queue2.Enqueue(2); // 2 삽입 -> 현재 queue : 1 2
queue2.Enqueue(3); // 3 삽입 -> 현재 queue : 1 2 3
queue2.Dequeue(); // 제일 처음에 삽입된 값 1 추출 -> 현재 queue : 2 3
queue2.Peek(); // 제일 처음 값 확인 -> 현재 queue : 2 3
queue2.Dequeue(); // 2 추출 -> 현재 queue : 3
위와 같이 사용 방법은 모두 동일하다. 형식에 맞게 지정된 자료형만 사용 가능하다.
오늘은 간단하게 제네릭에 대해 알아보았다. 제네릭은 실제 코드를 작성할 때 활용도가 매우높은 걸로 알고 있다. 개념은 이해했지만 자주 사용해보지 않아서 익숙하지 않다. 천천히 응용을 해보면서 친해져보자
동영상 강의 참조 : https://www.youtube.com/watch?v=gQvTCxcyt_k&t=1162s
'Unity C# > 개념 및 문법 정리' 카테고리의 다른 글
[Unity C#] 객체 지향 _ 캡슐화(접근 제한자, 프로퍼티) (1) | 2025.01.16 |
---|---|
[Unity C#] 객체 지향 (클래스, 추상화) (1) | 2025.01.13 |
[Unity C#] C# 1.0 컬렉션 (1) | 2025.01.10 |
[Unity C#] 형변환 (Type Casting) (0) | 2025.01.10 |
[Unity C#] 메모리 구조와 GC(Garbage Collection) (1) | 2025.01.09 |