[로봇활용_14주차] C# Task<TResult>

최윤호·2025년 11월 8일
post-thumbnail

결과물을 돌려주는 비동기 약속

우리가 Task에 맡기는 일이 항상 "이것 좀 해주세요!"로 끝나는 것은 아닙니다.
"데이터베이스에서 사용자 이름을 가져와 주세요!"처럼, 작업의 '결과물'이 필요한 경우가 많죠.
이번 글에서는 결과값을 반환하는 비동기 작업, Task<TResult>에 대해 알아보겠습니다.

1)Task vs Task<TResult>

TaskTask<TResult>의 차이점을 비유를 통해 생각해 봅시다.

  • Task(진동벨): '진동벨'이 울리면 '작업 완료'라는 사실만 알려줍니다.
  • Task<TResult>(음료 교환권):'음료 교환권'은 "작업이 완료되면
    '아메리카노'라는 결과물(TResult)을 돌려주겠다!"라는 약속이 담겨 있습니다.

Task<TResult>는 제네릭 클래스로, 여기서 TResult는 비동기 작업이 완료되었을 때
반환될 값의 타입을 의미합니다. 예를 들어, 사용자 이름을 가져오는 작업이라면
Task<string>이 될 것이고, 사용자 목록을 가져온다면 Task<List<User>>가 되겠죠.

2)어떻게 결과를 꺼낼까?

음료 교환권(Task<TResult>)을 받았으면 점원에게 주고 음료를 받아야 합니다.
Task<TResult>에서 결과값을 꺼내는 방법에도 종류가 있습니다.

await 키워드 (권장되는 방법)

await키워드는 Task<TResult>를 만나면, 작업이 완료될 때까지
비동기적으로 기다린 후, 알맹이(TResult)만 꺼내서 반환해 줍니다.

async Task DoSomethingAsync()
{
    // GetUserNameAsync는 Task<string>을 반환하는 비동기 메서드
    Task<string> userNameTask = GetUserNameAsync(123); // 'string 교환권'을 받음

    // ... 다른 작업을 하다가 ...

    // 교환권을 내고 'string' 결과물을 받을 때까지 비동기적으로 기다림
    string userName = await userNameTask;

    Console.WriteLine($"사용자 이름: {userName}");
}

.Result 속성 (위험한 방법!)

Task<TResult>에는 .Result라는 속성이 있습니다. 이 속성에 접근하면
결과값을 직접 꺼낼 수 있지만, 만약 작업이 아직 완료되지 않았다면,
작업을 완료될 때까지 현재 스레드가 그대로 멈춰버립니다. (블로킹 현상)

비유: 음료가 아직 나오지도 않았는데, 교환권을 들고 카운터 앞에서 나올 때까지
움직이지 않고 버티는 것과 같습니다. 뒤에 있는 손님들은 아무것도 못 하겠죠?

// ❌나쁜 예: UI 스레드나 중요한 스레드에서 절대 사용하지 마세요!
static void DoSomethingSync()
{
    Task<string> userNameTask = GetUserNameAsync(123);

    // 작업이 끝날 때까지 현재 스레드는 여기서 멈춘다! (UI 먹통의 원인)
    string userName = userNameTask.Result;

    Console.WriteLine($"사용자 이름: {userName}");
}

.Result속성은 스레드를 블로킹하므로, 데드락(Deadlock)의 주요 원인이 됩니다.
await키워드를 최우선으로 사용하도록 코드를 설계하고, .Result속성은 피하세요.

Task<TResult>에서 .GetAwaiter().GetResult()메서드를 사용하는 방법도
마찬가지로 블로킹 현상이 발생합니다. await키워드를 최우선으로 사용하세요.

3)C# 코드로 직접 만들기

결과값을 반환하는 비동기 메서드를 직접 만들어 봅시다.

[코드]

using System;
using System.Threading.Tasks;

public class User
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

class Program
{
    static async Task Main()
    {
        Console.WriteLine("데이터베이스에서 사용자 정보를 가져옵니다...");

        // GetUserFromDBAsync는 Task<User>를 반환한다.
        // await는 Task<User>에서 User 객체를 꺼내준다.
        User user = await GetUserFromDBAsync(1);

        Console.WriteLine($"조회된 사용자: ID={user.Id}, Name={user.Name}");
    }

    // User 객체를 결과로 반환하는 비동기 메서드
    public static async Task<User> GetUserFromDBAsync(int userId)
    {
        Console.WriteLine("DB 조회 시작...");
        // 실제로는 DB에 연결하고 쿼리를 실행하는 I/O 바운드 작업이 들어감
        await Task.Delay(2000); // 2초간 DB 조회를 시뮬레이션

        Console.WriteLine("DB 조회 완료!");

        // User 객체를 반환하면, 컴파일러가 async 키워드를 보고 Task<User>로 감싸서 반환합니다.
        // 기존의 return Task.FromResult(new User { ... });에서 코드가 간결해집니다.
        return new User { Id = userId, Name = "홍길동" };
    }
}

[실행 결과]

데이터베이스에서 사용자 정보를 가져옵니다...
DB 조회 시작...
DB 조회 완료!
조회된 사용자: ID=1, Name=홍길동

여기서 반환 타입은 Task<User>이지만, 실제 return문에서는 User객체를 반환합니다.
컴파일러는 async키워드를 보고 return new User(...)Task<User>로 변환해 줍니다.

4)정리 및 다음 단계

TaskTask<TResult>는 C# 비동기 프로그래밍의 양대 산맥입니다.

구분TaskTask<TResult>
비유진동벨음료 교환권
반환값없음 (void)있음 (TResult 타입)
사용 목적작업의 '완료' 여부만 중요할 때작업의 '결과물'이 필요할 때
await 결과void (결과 없음)TResult 타입의 값
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글