1. Unity와 메인 스레드
Unity는 대부분의 API가 thread-safe하지 않다.
따라서 Unity 객체를 다루는 코드는 반드시 메인 스레드에서 실행해야 한다.
메인 스레드에서만 안전한 작업
- GameObject 생성/삭제
- Transform 변경
- UI 변경 (Text, Image 등)
- Animator 조작
- Scene 관련 작업
- MonoBehaviour 접근
- UnityEngine.Object 관련 작업
백그라운드 스레드에서 가능한 작업
- 파일 읽기
- JSON 파싱
- 네트워크 요청 처리
- 데이터 가공
- AI 계산
- 경로 탐색
- 암호화/복호화
- 순수 C# 로직
2. EventBusManager에서 Thread 체크를 하는 이유
private readonly int _mainThreadId = Thread.CurrentThread.ManagedThreadId;
EventManager가 생성된 시점의 스레드 ID를 저장한다.
이 값은 이후 메인 스레드 판별 기준으로 사용된다.
public void Notify<T>(T eventData) where T : IEvent
{
if (Thread.CurrentThread.ManagedThreadId != _mainThreadId)
{
Debug.LogError("Main Thread에서만 Notify 가능");
return;
}
// handler 실행
}
핵심 이유
Notify는 단순한 알림 함수가 아니라
등록된 handler들을 즉시 실행하는 함수다.
즉 다음과 같은 구조가 된다.
Notify 호출
→ 구독된 handler 실행
→ handler 내부에서 UI, GameObject 접근 가능
따라서 Notify가 백그라운드 스레드에서 호출되면 handler도 백그라운드 스레드에서 실행된다.
그 결과 Unity API를 잘못된 스레드에서 호출하게 되어 오류, 크래시, 비정상 동작이 발생할 수 있다.
3. Task.Run과 백그라운드 스레드
Task.Run(() =>
{
LoadData();
eventManager.Notify(new DataLoadedEvent());
});
Task.Run은 작업을 ThreadPool의 작업자 스레드에서 실행한다.
즉 메인 스레드가 아닌 백그라운드 스레드에서 실행된다.
문제는 이 코드에서 Notify가 호출된다는 점이다.
백그라운드 스레드
→ Notify 호출
→ handler 실행
→ UI 접근
→ 문제 발생
4. 왜 Notify를 막는가
EventBusManager는 다음 상황을 방지하기 위해 방어 코드를 둔다.
백그라운드 스레드에서 Notify 호출
→ handler도 백그라운드에서 실행
→ Unity API 접근
→ 위험
따라서 구조적으로 다음 규칙을 강제한다.
Notify는 반드시 메인 스레드에서만 호출
5. async/await에서 주의할 점
async void LoadData()
{
var data = await HttpClient.GetAsync(...);
eventManager.Notify(new DataLoadedEvent(data));
}
await는 다음을 보장한다.
비동기 작업이 끝난 뒤 아래 코드가 실행된다
하지만 다음은 보장하지 않는다.
await 이후 코드가 반드시 메인 스레드에서 실행된다
이유는 다음과 같다.
- SynchronizationContext에 따라 실행 위치가 달라질 수 있음
- ConfigureAwait(false)를 사용하면 메인 스레드로 돌아오지 않음
- Task.Run과 함께 사용하면 백그라운드에서 이어질 수 있음
정리하면:
await는 실행 순서는 보장하지만
실행 스레드는 항상 보장하지 않는다
https://www.sysnet.pe.kr/2/0/13191
.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext
.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext [링크 복사], [링크+제목 복사], 조회: 25743 글쓴 사람 정성태 (techsharer at outlook.com) 홈페이지 첨부 파일 [sync_ctx_user_sample.zip] 부모글 보
www.sysnet.pe.kr
6. 안전한 구조 (큐를 통한 메인 스레드 처리)
백그라운드에서 바로 Notify하지 않고,
메인 스레드에서 처리하도록 넘기는 구조를 사용한다.
private readonly Queue<IEvent> pendingEvents = new Queue<IEvent>();
public void EnqueueNotify(IEvent eventData)
{
pendingEvents.Enqueue(eventData);
}
private void Update()
{
while (pendingEvents.Count > 0)
{
var eventData = pendingEvents.Dequeue();
Notify(eventData);
}
}
흐름
백그라운드 스레드
→ 데이터 처리
→ EnqueueNotify 호출
메인 스레드 (Update)
→ 큐에서 이벤트 꺼냄
→ Notify 실행
→ handler 실행 (안전)
멀티스레드 환경에서는 Queue 대신 ConcurrentQueue 사용이 권장된다.
7. Coroutine과 Thread의 차이
Coroutine은 멀티스레드가 아니다.
메인 스레드에서 실행되는 분할 실행 구조다.
Coroutine 특징
- 메인 스레드에서 실행
- yield를 기준으로 실행이 나뉨
- 여러 개가 동시에 도는 것처럼 보이지만 실제로는 순차 실행
Thread / Task와 차이
| 구분 | 실제 스레드 | Unity API 안전 |
| Coroutine | X | O |
| Task / Thread | O | X |
8. 정리
- Unity API는 메인 스레드에서만 안전하다
- Notify는 handler를 즉시 실행하므로 호출 스레드가 중요하다
- Task.Run은 백그라운드 스레드에서 실행된다
- await는 순서를 보장하지만 스레드를 항상 보장하지 않는다
- 따라서 Notify는 메인 스레드에서만 실행되도록 강제해야 한다
- 백그라운드 결과는 큐를 통해 메인 스레드에서 처리하는 구조가 안전하다
'유니티' 카테고리의 다른 글
| c# 심화 (0) | 2026.04.28 |
|---|---|
| 유니티 생성자, 소멸자 (0) | 2026.04.28 |
| unity TMP_TextUtilities 클릭한 글자 가져오기 (0) | 2026.04.23 |
| TextMeshPro(TMP) <link></link>, unity 텍스트 하이퍼링크 (0) | 2026.04.23 |
| AES-256 이란? (0) | 2026.04.01 |