Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) async/await

새벽_글쓴이 2025. 1. 21. 23:53
반응형

async/await 이란?

비동기 프로그래밍을 쉽게 구현할 수 있게 해주는 기능

 

유니티에서는 코루틴 대신 사용할 수 있는 좋은 대안이다

 


async의 핵심 정의

  • 메서드나 람다 표현식을 비동기 메서드로 지정하는 키워드
  • async가 붙은 메서드는 내부에서 await를 사용할 수 있음
  • 컴파일러에게 "이 메서드는 비동기적으로 실행될 수 있다"라고 알려주는 역할을 함
  • 반환 타입으로 Task, Task<T>, void를 사용할 수 있음

 

예시코드

public async Task DoSomethingAsync()
{
    // 비동기 코드 작성 가능
}

await의 핵심 정의

  • 비동기 작업이 완료될 때까지 현재 메서드의 실행을 일시 중단하는 키워드
  • await는 반드시 async 메서드 내부에서만 사용할 수 있음
  • 작업이 완료되면 await 이후의 코드가 실행됨
  • 비동기 작업의 결과를 동기적인 코드처럼 받을 수 있게 해줌

 

예시코드

public async Task<string> GetDataAsync()
{
    var result = await SomeAsyncOperation(); // 이 작업이 끝날 때까지 대기
    return result; // 작업이 완료되면 실행
}

Task란?

비동기적으로 실행될 작업을 나타내는 객체

쉽게 말하자면
지금 당장은 결과를 모르지만, 미래의 어떤 시점에 완료될 작업을 표현한다
Task는 작업의 상태 (실행 중, 완료, 실패 등)를 추적할 수 있다

 

간단한 예시

// string을 반환하는 비동기 작업
public async Task<string> GetMessageAsync()
{
    await Task.Delay(1000); // 1초 대기
    return "안녕하세요!";
}

// 아무것도 반환하지 않는 비동기 작업
public async Task ShowMessageAsync()
{
    await Task.Delay(1000); // 1초 대기
    Debug.Log("완료!");
}

제어흐름

Task가 await를 만나면 해당 메서드는 작업이 완료될 때까지 대기하지만,
스레드를 차단하지 않고 제어권을 호출자에게 반환한다.

이해를 돕기 위해 비교를 해보았다

 

일반적인 동기 처리 ( 차단 )
void DoSomething()
{
    Thread.Sleep(5000); // 5초 동안 현재 스레드가 멈춤
    Debug.Log("완료");  // 5초 후에 실행
}
// 이 메서드를 호출하면 프로그램이 5초 동안 완전히 멈춤

 

비동기 처리 ( 비차단 )
async Task DoSomethingAsync()
{
    await Task.Delay(5000); // 5초를 기다리지만, 프로그램은 계속 실행됨
    Debug.Log("완료");      // 5초 후에 실행
}
// 이 메서드를 호출해도 프로그램은 계속 실행됨

 

조금 더 자세한 실행순서
public async Task ProcessDataAsync()
{
    Debug.Log("1. 작업 시작");
    await Task.Delay(1000);  // 여기서 Task가 await를 만남
    Debug.Log("3. 작업 재개");  // 1초 후에 실행됨
}

public async Task MainMethodAsync()
{
    Debug.Log("시작");
    var task = ProcessDataAsync();  // ProcessDataAsync가 await를 만나면 여기로 제어가 돌아옴
    Debug.Log("2. 다른 작업 실행");  // ProcessDataAsync가 대기하는 동안 실행됨
    await task;  // ProcessDataAsync의 완료를 기다림
    Debug.Log("끝");
}

 

실행결과
시작
1. 작업 시작
2. 다른 작업 실행
3. 작업 재개

 

즉, 스레드를 차단하지 않는다는 것
시간이 오래 걸리는 작업을 실행하면서도 프로그램의 다른 부분들은
정상적으로 계속 동작할 수 있다는 의미이다.

 

실제 사용 예시

public async void StartGame()
{
    Debug.Log("게임 시작!");
    
    // 리소스를 로딩하는 동안에도 플레이어는 로딩 화면을 볼 수 있고
    // 프로그램은 계속 응답 가능한 상태를 유지합니다
    await LoadResourcesAsync(); 
    
    Debug.Log("리소스 로딩 완료!");
}

효율적인 사용방법 및 주의사항

1. async void 사용을 피해야 하는 패턴

async void는 예외 처리가 불가능해서 애플리케이션이 갑자기 종료된다는 등 위험하다
// 나쁜 예시
public async void SaveData()  // void라서 에러 추적 불가
{
    await WriteToFile();     // 여기서 에러나면?
}

// 좋은 예시
public async Task SaveData()  // Task를 사용해서 에러 처리 가능
{
    try 
    {
        await WriteToFile();
    }
    catch (Exception e)
    {
        Debug.LogError("파일 저장 실패: " + e.Message);
        // 에러 처리 가능
    }
}

2. ConfigureAwait 패턴

UI 작업이 필요없는 경우 성능을 향상시킬 수 있다
// UI 업데이트가 필요한 경우
public async Task UpdatePlayerUI()
{
    var data = await GetPlayerData();  // UI 스레드로 돌아옴
    playerNameText.text = data.name;   // UI 업데이트
}

// UI 업데이트가 필요없는 경우
public async Task ProcessGameData()
{
    var data = await GetPlayerData().ConfigureAwait(false);  // 아무 스레드나 사용
    ProcessData(data);  // UI 작업 없음
}

3. 병렬 처리 패턴

여러 독립적인 작업을 동시에 실행할 수 있다
// 느린 방법
public async Task LoadGameAsync()
{
    await LoadMapAsync();      // 3초
    await LoadCharacterAsync(); // 2초
    await LoadItemsAsync();    // 1초
    // 총 6초 걸림
}

// 빠른 방법
public async Task LoadGameAsync()
{
    var mapTask = LoadMapAsync();      // 시작
    var characterTask = LoadCharacterAsync(); // 시작
    var itemsTask = LoadItemsAsync();    // 시작
    
    await Task.WhenAll(mapTask, characterTask, itemsTask);
    // 총 3초만 걸림 (가장 오래 걸리는 작업 기준)
}

4. 작업 취소 패턴

오래 걸리는 작업을 중간에 취소할 수 있게 해준다
public class GameManager : MonoBehaviour
{
    private CancellationTokenSource _cts;

    public async Task LoadLevelAsync()
    {
        _cts = new CancellationTokenSource();
        
        try 
        {
            await LoadResourcesAsync(_cts.Token);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("로딩이 취소되었습니다");
        }
    }

    public void CancelLoading()
    {
        _cts?.Cancel();  // 로딩 취소
    }
}

5. 데드락 방지 패턴

동기와 비동기 코드를 잘못 섞으면 프로그램이 완전히 멈출 수 있다
// 데드락 발생 코드
void Start()
{
    var task = LoadDataAsync();
    task.Wait();  // 여기서 멈춤!
}

// 올바른 코드
async void Start()
{
    await LoadDataAsync();  // 정상 작동
}

6.async 람다 패턴

이벤트 핸들러에서 비동기 작업을 처리할 때 유용하다
// UI 버튼에 비동기 이벤트 연결
saveButton.onClick.AddListener(async () => 
{
    saveButton.interactable = false;  // 버튼 비활성화
    await SaveGameAsync();            // 저장 작업
    saveButton.interactable = true;   // 버튼 다시 활성화
});

데드락이란?

두 개 이상의 작업이 서로가 가진 리소스를 기다리면서 영원히 멈춰있는 상태

 

간단 예시
// 데드락 발생 상황
public class DeadlockExample : MonoBehaviour
{
    private async Task<string> GetDataAsync()
    {
        await Task.Delay(1000);
        return "데이터";
    }

    private void Start()
    {
        // 메인 스레드에서 실행
        var task = GetDataAsync();     // 비동기 작업 시작
        var result = task.Result;      // 여기서 데드락! 
    }
}

 

위 코드에서 데드락이 발생하는 이유

  1. Start() 메서드에서 GetDataAsync()를 호출
  2. GetDataAsync() 안에서 await Task.Delay(1000)를 만남
  3. await를 만났으니 제어권이 Start() 메서드로 돌아옴
  4. Start() 메서드에서는 task.Result를 기다림
  5. 하지만 Result를 받으려면 GetDataAsync()가 완료되야함
  6. GetDataAsync()는 await 이후의 코드를 실행해야 하는데,
    이를 위해서는 Start() 메서드의 스레드(메인 스레드)가 필요함
  7. 그러나 메인 스레드는 이미 Result를 기다리고 있는 상태임
  8. 데드락 상황 발생!

 

해결방법

private async void Start()
{
    // await를 사용하여 데드락 방지
    var result = await GetDataAsync();
}

 

이를 실행 흐름을 순서대로 나타내보면 아래와 같은 결과가 나온다

 

private async void Start()
{
    Debug.Log("1. Start 시작!");
    var result = await GetDataAsync(); // GetDataAsync로 이동
    Debug.Log("4. 최종 result: " + result); // "데이터" 출력
}

private async Task<string> GetDataAsync()
{
    Debug.Log("2. GetDataAsync 시작!");
    await Task.Delay(1000);  // 1초 대기
    Debug.Log("3. 1초 지남, 데이터 반환!");
    return "데이터";
}

 

실행결과
1. Start 시작!
2. GetDataAsync 시작!
(1초 대기)
3. 1초 지남, 데이터 반환!
4. 최종 result: 데이터
반응형