C#의 HashSet이란 무엇인가
Unity 개발을 하다 다음과 같은 코드를 만들었다.
private readonly HashSet<Renderer> currentOccluders = new();
이 코드를 보면서 가장 먼저 든 생각은, 내가 기존에 써왔던 자료구조와 얼마나 비슷한가 하는 점이었다.
자체 엔진을 개발할 때는 unordered_set, unordered_map을 사용했고, 언리얼에서는 TSet, TMap을 주로 봐왔기 때문에, C#의 HashSet이 어떤 개념인지부터 정리할 필요가 있었다.
결론부터 말하면 HashSet<T>는 중복 없는 값들의 집합을 해시 기반으로 관리하는 자료구조다.
개념적으로는 C++의 std::unordered_set<T>나 언리얼의 TSet<T>와 매우 유사하다.
즉 HashSet은 목록을 순서대로 관리하기 위한 컨테이너가 아니라, 어떤 값이 집합 안에 존재하는지를 빠르게 판단하고 중복 없이 관리하기 위한 자료구조라고 보면 된다.
HashSet은 어떤 자료구조인가
HashSet<T>는 해시 기반 집합 자료구조다.
값을 저장할 때 해시를 사용하고, 동일한 값이 이미 있는지 빠르게 검사할 수 있다.
중요한 점은 같은 값을 여러 번 넣으려고 해도 하나만 유지된다는 것이다.
즉 다음과 같은 특성을 가진다.
- 중복을 허용하지 않는다
- 순서를 보장하지 않는다
- 포함 여부 검사에 강하다
- 삽입과 제거가 빠른 편이다
카메라 오클루전처럼 이번 프레임에 어떤 오브젝트가 시야를 가리고 있는지 관리하는 상황에서는 이 특성이 그대로 장점이 된다.
List와는 무엇이 다른가
List<T>와 HashSet<T>는 목적이 다르다.
List<T>는 순서 있는 목록을 다룰 때 적합하다.
인덱스로 접근할 수 있고, 같은 값을 여러 번 넣을 수도 있다.
반면 HashSet<T>는 순서가 중요하지 않고, 어떤 값이 이미 존재하는지가 중요할 때 적합하다.
같은 값을 여러 번 추가해도 하나만 남고, Contains()를 통해 특정 값이 집합 안에 있는지 빠르게 확인할 수 있다.
즉 List는 목록이고, HashSet은 집합이다.
카메라 가림 처리에서는 목록보다 집합의 의미가 훨씬 더 정확하다.
C++의 unordered_set과 어떤 점이 비슷한가
C++ 경험이 있다면 HashSet<T>는 거의 std::unordered_set<T>에 대응한다고 보면 된다.
공통점은 다음과 같다.
- 해시 기반 자료구조다
- 중복 없는 원소 집합을 관리한다
- 포함 여부 검사에 강하다
- 순서를 보장하지 않는다
즉 C++에서 unordered_set을 사용해 본 경험이 있다면, C#의 HashSet도 거의 같은 개념으로 받아들여도 무방하다.
이름만 다를 뿐, 목적과 사용 감각은 상당히 비슷하다.
언리얼의 TSet과는 어떤 점이 비슷한가
언리얼의 TSet과도 개념적으로 매우 유사하다.
TSet 역시 중복 없는 원소 집합을 관리하고, 특정 값이 현재 집합 안에 포함되어 있는지를 빠르게 확인할 때 사용된다.
Unity C#의 HashSet도 정확히 같은 방향의 문제를 해결하기 위해 쓰인다.
즉 자료구조의 역할만 놓고 보면 다음과 같이 대응시킬 수 있다.
- HashSet<T> ↔ std::unordered_set<T> ↔ TSet<T>
이렇게 보면 기존에 알고 있던 자료구조 감각을 거의 그대로 가져와도 된다.
unordered_map이나 TMap과는 무엇이 다른가
여기서 함께 구분해야 하는 것이 있다.
HashSet은 집합(수학적 의미의..)이고, unordered_map이나 TMap은 맵이다.
즉
- HashSet<T>는 값만 저장한다
- Dictionary<TKey, TValue>는 키와 값을 쌍으로 저장한다
예를 들어 오클루전 처리 코드에서
private readonly Dictionary<Renderer, Material[]> materialCache = new();
이 부분은 HashSet이 아니라 맵 구조다.
개념적으로는 C++의 std::unordered_map이나 언리얼의 TMap과 더 가깝다.
대응 관계를 정리하면 이렇게 된다.
- HashSet<T> ↔ std::unordered_set<T> ↔ TSet<T>
- Dictionary<TKey, TValue> ↔ std::unordered_map<K, V> ↔ TMap<K, V>
즉 HashSet은 집합, Dictionary는 맵으로 이해하면 된다.
HashSet이 카메라 오클루전 처리에 잘 맞는 이유
카메라 오클루전 처리에서는 매 프레임 이런 판단이 필요하다.
- 지금 이 렌더러가 플레이어를 가리고 있는가
- 더 이상 가리지 않게 되었는가
- 가리고 있으면 반투명 처리해야 하는가
- 가리지 않으면 원래 상태로 복구해야 하는가
이 과정에서 중요한 것은 어떤 렌더러가 현재 집합에 포함되어 있느냐이다.
순서나 중복 여부는 중요하지 않다.
그래서 HashSet<Renderer>는 이 문제에 매우 잘 맞는다.
currentOccluders.Add(renderer);
이렇게 하면 같은 렌더러가 여러 번 감지되어도 하나만 유지된다.
currentOccluders.Contains(renderer)
이렇게 하면 특정 렌더러가 현재 가림 상태인지 빠르게 판단할 수 있다.
즉 오클루전 처리에서 HashSet은 현재 가리는 대상의 집합을 표현하는 역할을 한다.
HashSet은 내부적으로 어떻게 동작하는가
HashSet은 해시값을 기반으로 동작한다.
어떤 값을 집합에 넣으면, 그 값의 해시를 계산하고 내부 저장 구조 안에서 적절한 위치를 찾아 넣는다.
같은 값이 이미 존재하는지도 해시 기반으로 확인한다.
이 방식 덕분에 평균적으로 삽입, 삭제, 포함 여부 검사가 빠르다.
그래서 집합의 존재 여부를 자주 체크해야 하는 상황에 잘 맞는다.
유니티에서처럼 매 프레임 카메라 가림 대상을 갱신해야 하는 코드에서는 이 장점이 더 잘 드러난다.
Renderer를 HashSet에 넣는다는 것은 무엇을 의미하는가
여기서 HashSet<Renderer>는 렌더러 객체 참조를 저장한다.
즉 지금 플레이어를 가리고 있는 것이 어떤 벽의 렌더러인지를 객체 단위로 기억하는 것이다.
같은 렌더러를 두 번 넣으려고 해도 하나만 유지되므로,
중복 감지를 신경 쓰지 않고 집합처럼 다룰 수 있다.
이 점은 실제 오클루전 처리에서 꽤 중요하다.
같은 벽이 여러 히트 결과로 들어오더라도, 최종적으로는 이 벽이 현재 가림 상태에 포함되는지만 알면 되기 때문이다.
HashSet을 사용할 때 주의할 점
HashSet은 강력하지만, 목적에 맞게 써야 한다.
첫 번째로 순서를 기대하면 안 된다.
이 자료구조는 집합이지 목록이 아니므로, 몇 번째 원소인지 같은 개념은 의미가 없다.
두 번째로 중복 저장이 필요하다면 맞지 않는다.
같은 값을 여러 번 보관해야 하는 경우에는 List나 다른 구조가 더 적합하다.
세 번째로 비교 기준이 중요하다.
Unity 객체 참조처럼 동일성 비교가 명확한 경우에는 큰 문제가 없지만, 직접 만든 타입을 넣는 경우에는 해시와 동등성 비교 방식을 이해할 필요가 있다.
정리
HashSet<T>는 C#에서 중복 없는 값의 집합을 관리하기 위한 해시 기반 자료구조다.
Unity에서는 카메라 오클루전 처리, 감지 시스템, 상호작용 대상 관리처럼 특정 값이 현재 포함되어 있느냐를 빠르게 확인해야 하는 상황에서 자주 유용하다.
핵심만 정리하면 다음과 같다.
- HashSet<T>는 집합 자료구조다
- 중복을 허용하지 않는다
- 순서를 보장하지 않는다
- 포함 여부 검사에 강하다
- C++의 std::unordered_set<T>와 매우 비슷하다
- 언리얼의 TSet<T>와도 비슷하다
- Dictionary는 unordered_map, TMap 쪽에 대응된다
즉 HashSet은 새로운 개념이라기보다, 이미 알고 있던 집합 자료구조가 C#과 Unity 문법으로 표현된 형태라고 보는 편이 훨씬 자연스럽다.
그리고 카메라 오클루전 코드에서 HashSet<Renderer>를 사용한 이유는 명확하다.
이번 프레임에 플레이어를 가리고 있는 렌더러 집합을 중복 없이 빠르게 관리하기 위해서다.
'Unity - '한 우산 아래'' 카테고리의 다른 글
| 맵툴 제작기 1 - Unity Editor에서 쿼터뷰 퍼즐 게임용 Map Tool 만들기 (0) | 2026.04.08 |
|---|---|
| [한 우산 아래] 캐릭터를 가리는 벽 처리 - Layer 분리와 Material 기반 Occlusion Fade 구현 (0) | 2026.04.01 |
| C#의 foreach와 var를 C++과 비교해보기 (0) | 2026.04.01 |
| Unity - Dictionary (0) | 2026.04.01 |
| Unity LayerMask (0) | 2026.04.01 |