
우리가 Task에 맡기는 일이 항상 "이것 좀 해주세요!"로 끝나는 것은 아닙니다.
"데이터베이스에서 사용자 이름을 가져와 주세요!"처럼, 작업의 '결과물'이 필요한 경우가 많죠.
이번 글에서는 결과값을 반환하는 비동기 작업, Task<TResult>에 대해 알아보겠습니다.
<TResult>Task와 Task<TResult>의 차이점을 비유를 통해 생각해 봅시다.
Task(진동벨): '진동벨'이 울리면 '작업 완료'라는 사실만 알려줍니다.Task<TResult>(음료 교환권): 이 '음료 교환권'은 "작업이 완료되면TResult)을 돌려주겠다!"라는 약속이 담겨 있습니다.Task<TResult>는 제네릭 클래스로, 여기서 TResult는 비동기 작업이 완료되었을 때
반환될 값의 타입을 의미합니다. 예를 들어, 사용자 이름을 가져오는 작업이라면
Task<string>이 될 것이고, 사용자 목록을 가져온다면 Task<List<User>>가 되겠죠.
음료 교환권(Task<TResult>)을 받았으면 점원에게 주고 음료를 받아야 합니다.
Task<TResult>에서 결과값을 꺼내는 방법에도 종류가 있습니다.
await키워드는 Task<TResult>를 만나면, 작업이 완료될 때까지
비동기적으로 기다린 후, 알맹이(TResult)만 꺼내서 반환해 줍니다.
async Task DoSomethingAsync()
{
// GetUserNameAsync는 Task<string>을 반환하는 비동기 메서드
Task<string> userNameTask = GetUserNameAsync(123); // 'string 교환권'을 받음
// ... 다른 작업을 하다가 ...
// 교환권을 내고 'string' 결과물을 받을 때까지 비동기적으로 기다림
string userName = await userNameTask;
Console.WriteLine($"사용자 이름: {userName}");
}
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키워드를 최우선으로 사용하세요.
결과값을 반환하는 비동기 메서드를 직접 만들어 봅시다.
[코드]
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>로 변환해 줍니다.
Task와 Task<TResult>는 C# 비동기 프로그래밍의 양대 산맥입니다.
| 구분 | Task | Task<TResult> |
|---|---|---|
| 비유 | 진동벨 | 음료 교환권 |
| 반환값 | 없음 (void) | 있음 (TResult 타입) |
| 사용 목적 | 작업의 '완료' 여부만 중요할 때 | 작업의 '결과물'이 필요할 때 |
await 결과 | void (결과 없음) | TResult 타입의 값 |