[Unity] ConcurrentQueue는 왜 필요할까?

ChangBeom·2026년 1월 7일

Unity

목록 보기
18/20
post-thumbnail

멀티스레드 환경에서 가장 자주 마주치는 문제 중 하나는
"컬렉션을 여러 스레드에서 동시에 접근해도 괜찮은가?"이다.

싱글 스레드 환경에서는 전혀 문제가 없던 코드가
스레드가 하나만 늘어나도 갑자기 예외를 뿜거나,
더 무서운 경우에는 조용히 잘못된 결과를 만들어낸다.

ConcurrentQueue는 이런 문제를 해결하기 위해 등장한 컬렉션이다.
하지만 이걸 단순히 "Thread-safe한 Queue"로만 이해하면
정작 중요한 맥락을 놓칠 수 있기 때문에 이번 글을 통해 정리해보려 한다.


[1. Queue는 원래 안전하지 않다.]

C#의 Queue<T>는 스레드 안전(Thread-safe)하지 않다.

  • A 스레드에서 Enqueue
  • B 스레드에서 Dequeue

이런 상황이 동시에 발생하면 컬렉션의 내부 상태는 언제든 깨질 수 있다.

그래서 멀티스레드 코드에서는 보통 이런 패턴이 등장한다.

lock(_lock)
{
	queue.Enqueue(item);
}

문제는 이 방식이 점점 코드를 갉아먹는다는 점이다.

  • lock 범위가 넓어질수록 성능은 떨어지고
  • lock을 빼먹는 순간 바로 버그가 생기며
  • 코드 곳곳에 동기화 코드가 퍼지며 가독성이 매우 떨어진다.

자료구조 하나 쓰기 위해 동기화를 계속 의식해야 하는 상태

이 자체가 이미 부담이 된다.


[2. ConcurrentQueue란 무엇인가?]

ConcurrentQueue<T\>
동시 접근을 전제로 설계된 Queue이다.

여러 스레드에서 동시에 Enqueue/Dequeue를 호출해도 내부적으로 안전하게 처리된다.

그래서 외부에서 lock을 걸 필요가 없다.

ConcurrentQueue<int> queue = new ConcurrentQueue<int>();

queue.Enqueue(1);

if(queue.TryDequeue(out int value))
{
	// 안전하게 처리
}

여기서 중요한 건
ConcurrentQueue에는 Dequeue()가 없고
항상 Try 계열 메서드만 제공된다는 점이다.


[3. 왜 항상 TryDequeue일까?]

ConcurrentQueue는 API 설계 단계에서부터
이 전제를 깔고 있다.

"지금 이 순간에도 상태는 바뀔 수 있다."

다른 스레드가 이미 데이터를 가져갔을 수도 있고,
내가 확인한 직후 큐의 상태가 달라질 수도 있다.

그래서 ConcurrentQueue는
확실함을 약속하지 않고, 가능성만 제공한다.

이 지점에서 일반 Queue와 사고방식이 갈린다.


[4. 그런데 ConcurrentQueue는 .NET 4.0부터 아닌가?]

ConcurrentQueue를 처음 접했을 때,
다음과 같은 의문이 들었다.

"ConcurrentQueue는 .NET 4.0부터인데,
Unity는 .NET 2.0이라 어차피 못 쓰는거 아니야?"

이말은 과거에는 맞았고, 지금은 틀린 얘기이다.

예전 Unity는

  • .NET 2.0 Subset 기반이었고
  • System.Collections.Concurrent 네임스페이스 자체가 없었다.

그래서 당시에는

  • ConcurrentQueue
  • ConcurrentDictionary
  • Task 기반 코드

를 사용할 수 없었다.

이 때문에 "Unity에서는 Concurrent 컬렉션을 못 쓴다"는 인식이 오래 남아 있었다.

하지만 지금 Unity는 다르다.

API Compatibility Level이 .NET Standard 2.0 이상이면
System.Collections.Concurrent를 사용할 수 있다.
(현재 Unity에서는 .NET Standard 2.1이 권장된다.)

즉, 현재 Unity에서는 ConcurrentQueue를 문제없이 사용할 수 있다.


[5. 그럼 Unity에서는 언제 ConcurrentQueue를 사용할까?]

Unity에서 멀티스레드를 고민하기 시작하는 순간, 항상 다음과 같은 제약에 부딪힌다.

  • UnityEngine API는 메인 스레드 전용
  • GameObject, Transform 접근은 백그라운드 스레드에서 금지

즉,
백그라운드 스레드에서는 계산만 가능하고,
결과를 화면에 반영하는 작업은
반드시 메인 스레드로 돌아와야 한다.

그래서 Unity에서 멀티스레드를 쓰면
자연스럽게 이런 구조가 만들어진다.

  • 백그라운드 스레드에서 무거운 계산 수행
  • 계산 결과를 임시로 보관
  • 메인 스레드의 Update에서 하나씩 처리

이 지점에서 선택지가 생긴다.

"이 결과를 어떻게 메인 스레드로 넘길 것인가?"

lock으로 감싼 Queue를 직접 만들 수도 있지만,
그 순간부터 동기화 책임은 전부 개발자의 몫이 된다.

그래서 Unity에서는
스레드 간 전달용 버퍼로 ConcurrentQueue를 선택하게 되는 것이다.

ConcurrentQueue는

  • 여러 스레드에서 안전하게 넣을 수 있고
  • 메인 스레드에서 부담 없이 꺼내 쓸 수 있으며
  • Unity의 메인 스레드 제약과도 잘 맞는다

즉, Unity에서 ConcurrentQueue는
"멀티스레드를 쓰기 시작하면 자연스럽게 만나게 되는 선택지"에 가깝다.


[6. 그럼 무조건 ConcurrentQueue를 사용하면 될까?]

이건 아니라고 할 수 있다.

ConcurrentQueue는 만능이 아니다.

  • 쌓이는 속도를 제어하지 않으면 메모리를 계속 먹고
  • Count 같은 값은 신뢰하기 어렵고
  • 정확한 상태 동기화가 필요한 로직에는 어울리지 않는다.

따라서 ConcurrentQueue는
다음과 같은 성격의 작업에 가깝다.

"정확함보다는 안전한 전달이 필요한 경우"


[7. 정리]

ConcurrentQueue는
단순히 Queue + Thread-safe가 아니다.

  • 멀티스레드 환경을 전제로 하고
  • 상태가 언제든 바뀔 수 있음을 인정하며
  • 그에 맞는 API 사용을 강제하는 컬렉션이다.

그리고 중요한 사실 하나.

과거 Unity는 ConcurrentQueue를 사용할 수 없었지만,
현재 Unity는 ConcurrentQueue를 사용할 수 있다.

문법보다 먼저
사고방식이 바뀌어야 하는 컬렉션,
그게 ConcurrentQueue이다.

0개의 댓글