[궁금시리즈] 5-1. 비동기 프로그래밍은 왜 필요할까?
프로그램은 기본적으로 코드를 위에서 아래로 순서대로 실행한다. 예를 들어
Console.WriteLine("1");
Console.WriteLine("2");
Console.WriteLine("3");
결과는 항상
1
2
3
이다. 앞의 코드가 끝나야 다음 코드가 실행된다. 이러한 방식을 동기(Synchronous)실행이라고 한다. 대부분의 코드는 이 방식으로 동작하며, 특별한 문제가 없다. 하지만 어떤 작업은 예상보다 훨씬 오래 걸린다. 이때 동기 방식은 큰 문제를 만들 수 있다.
오래 걸리는 작업은 무엇이 있을까?
다음과 같은 작업을 생각해 보자.
- 파일 읽기
- 데이터베이스 조회
- 웹 API 요청
- 이미지 다운로드
- 서버와 통신
예를 들어
string html = DownloadWebPage();
다운로드가 3초 걸린다면 프로그램은 3초 동안 아무것도 하지 못한다. 즉,
다운로드 시작
↓
3초 대기
↓
다운로드 완료
↓
다음 코드 실행
이 된다.
사용자 입장에서는 어떤 일이 일어날까?
윈도우 프로그램을 생각해 보자. 버튼을 눌렀는데
다운로드 시작
↓
3초 동안 멈춤
↓
다시 움직임
이 된다면 사용자는
“프로그램이 멈췄나?” 라고 생각할 것이다. 실제로 프로그램이 종료된 것은 아니지만, UI를 처리하는 스레드가 다운로드가 끝날 때까지 기다리고 있기 때문에 화면도 함께 멈춰 보인다.
게임에서도 같은 문제가 발생한다
게임에는 메인 루프(Game Loop)가 있다.
입력 처리
↓
게임 로직
↓
렌더링
↓
반복
만약 이 루프 안에서
LoadLargeFile();
처럼 오래 걸리는 작업을 수행하면 그 시간 동안
입력
×
렌더링
×
애니메이션
×
물리 계산
×
모든 것이 멈춘다. 그래서 게임에서는 무거운 작업을 메인 루프에서 직접 수행하지 않도록 설계한다.
해결 방법은 무엇일까?
오래 걸리는 작업 때문에 전체 프로그램이 기다리는 것이 문제라면, 그 작업을 기다리는 동안 다른 일을 하면 된다. 예를 들어
다운로드 시작
↓
사용자 입력 처리
↓
화면 그리기
↓
다운로드 완료
↓
결과 사용
이런 방식이라면 사용자는 프로그램이 멈췄다는 느낌을 받지 않는다. 이러한 개념이 바로 비동기(Asynchronous) 프로그래밍이다.
비동기는 동시에 실행하는 것일까?
많은 사람들이
“비동기 = 여러 작업을 동시에 실행하는 것”
이라고 생각한다. 하지만 이것은 정확한 표현이 아니다. 비동기의 핵심은
오래 걸리는 작업을 기다리는 동안 현재 스레드를 묶어 두지 않는 것이다.
즉, 반드시 여러 작업이 동시에 실행되어야 하는 것은 아니다. 예를 들어
파일 읽기 요청
↓
운영체제가 파일을 읽는 동안
↓
현재 스레드는 다른 작업 수행
↓
읽기 완료
↓
이후 작업 계속
처럼 동작할 수도 있다. 중요한 것은 기다리는 시간을 효율적으로 사용하는 것이다.
비동기와 멀티스레드는 같은 것일까?
이 부분은 가장 많이 오해하는 내용이다. 많은 개발자가
비동기
=
새로운 스레드 생성
이라고 생각한다. 하지만 그렇지 않다.
예를 들어
await File.ReadAllTextAsync(path);
를 호출했다고 해서 새로운 스레드가 반드시 만들어지는 것은 아니다. 작업의 종류에 따라
- 운영체제의 비동기 I/O를 사용할 수도 있고
- 스레드 풀을 사용할 수도 있으며
- 기존 스레드에서 이어서 실행될 수도 있다.
즉, 비동기와 멀티스레드는 서로 다른 개념이다.
그렇다면 Thread는 언제 사용할까?
CPU를 많이 사용하는 작업은
큰 이미지 압축
영상 인코딩
복잡한 수학 계산
처럼 실제 계산이 오래 걸린다. 이런 작업은 다른 CPU 코어에서 계산하는 것이 효과적이다. 반면
파일 읽기
네트워크 통신
데이터베이스 조회
는 대부분 CPU가 계산하는 시간이 아니라 외부 장치를 기다리는 시간이 대부분이다. 이런 경우에는 비동기가 훨씬 효율적이다. 즉,
- CPU를 많이 사용하는 작업 → 병렬 처리
- 기다리는 시간이 긴 작업 → 비동기 처리
라는 기준으로 생각하면 이해하기 쉽다.
실제 .NET에서는 어떻게 동작할까?
.NET은 비동기 프로그래밍을 위해
async
await
Task
를 제공한다. 예를 들어
string text = await File.ReadAllTextAsync(path);
처럼 작성하면 파일을 읽는 동안 현재 스레드를 불필요하게 점유하지 않고, 읽기가 완료되면 나머지 코드를 이어서 실행한다. 이처럼 async와 await는 복잡한 비동기 흐름을 일반적인 순차 코드처럼 작성할 수 있도록 도와주는 언어 기능이다.
다음 글부터는 이 기능들이 내부적으로 어떻게 동작하는지 하나씩 살펴보겠다.
마무리
비동기 프로그래밍이 등장한 이유는 프로그램을 더 빠르게 만들기 위해서가 아니라, 오래 걸리는 작업 때문에 다른 작업까지 멈추는 문제를 해결하기 위해서이다.
특히 파일 입출력, 네트워크 통신, 데이터베이스 조회처럼 기다리는 시간이 긴 작업에서는 비동기를 사용하면 프로그램의 응답성을 크게 향상시킬 수 있다. 중요한 것은 비동기를 “새로운 스레드를 만드는 기술”로 이해하는 것이 아니라, 기다리는 시간을 효율적으로 활용하는 방법으로 이해하는 것이다. 다음 글에서는 Task는 무엇이며 왜 등장했을까?를 살펴보며 비동기 프로그래밍의 핵심 타입을 알아보겠다.
핵심 정리
- 동기 프로그래밍은 이전 작업이 끝날 때까지 다음 작업을 수행하지 않는다.
- 오래 걸리는 작업은 프로그램 전체의 응답성을 떨어뜨릴 수 있다.
- 비동기 프로그래밍은 기다리는 동안 현재 스레드를 효율적으로 활용하기 위해 등장했다.
- 비동기가 반드시 새로운 스레드를 생성하는 것은 아니다.
- CPU 연산이 많은 작업과 대기 시간이 긴 작업은 서로 다른 방식으로 처리하는 것이 효과적이다.
- .NET은 Task, async, await를 통해 비동기 프로그래밍을 지원한다.