안녕하세요. 블로그쥔장 입니다. 간만에 다시 키보드를 잡았네요. 시간 없다는 핑계로 이렇게 사이트를 내버려두기도 그렇고 뭐라도 해서 다시 활력을 가져보고자 다시 플로그질을 시작해 볼까 합니다. 항상 그렇듯이 기약은 없답니다. ㅎㅎ

앞으로 몇 차례 닷넷 프레임워크 4.5에 추가된 새로운 키워드인 async/await에 대해 다루려고 합니다. 닷넷 프레임워크 4.5가 나온지 한참도 더 되었지만 async/await 키워드에 대해 심도 깊게 다룬 글들은 별로 없어 보이는지라… 아는 척하기 좋겠다 싶어서 토픽으로 잡았습니다.

사실은 이 글은 2012년 월간 마이크로소프트웨어 1월, 2월에 연재 되었던 제 글을 바탕으로 한 것입니다. ^^

본론에 들어가기 전에…

async/await 키워드 시리즈의 첫 번째 글은 async/await 키워드가 등장한 배경을 살펴보는 글이 되겠다. 필자가 누누이 말했듯이 why에 대한 질문 없이 what, how를 터득하기 어렵기 때문이다. 다다음 글에나 등장하게 될 async/await 키워드 없이도 비동기 프로그래밍이 가능하다. 그럼에도 불구하고 왜 이따우 것들이 튀어나와 우리를 머리아프게 하는지 이해한다면 async/await를 더 잘 이용할 수 있을 것이다.

동기(Synchronous) 프로그래밍의 문제점

동기 프로그래밍이란 것은 어떤 작업(서버 호출, I/O 작업 등)을 수행하고 이 작업이 끝날 때까지 호출자(대개의 경우 호출 스레드이다)를 블록시키는 것을 말한다. 여러분이 지금까지 해왔던 대부분의 호출은 동기 호출이라고 보면 되겠다. 예를 들어 데이터베이스에서 데이터를 읽어 오는 메서드를 호출했고 이 메서드가 실제 데이터베이스에서 작업 수행을 완료한 후에 리턴된다면 이 호출은 동기 호출이 되는 것이다.

UI 상에서 동기적인 서버 호출이 어떠한 문제를 유발하는지 간단한 예제를 통해 살펴보도록 하겠다. 우리가 살펴볼 예제 어플리케이션은 그림 1과 같은 인터넷 상의 블로그(필자의 블로그이다)에서 RSS 피드를 읽어 표시하는 졸라 간단한 RSS 리더 어플리케이션이다. RSS 피드를 읽어 파싱 하는 작업은 WCF의 Syndication 지원을 사용할 것이다.그림1

[그림1] 간단한 RSS 리더 예제 어플리케이션

RSS 피드를 읽는 코드는 버튼의 클릭 이벤트 핸들러에 직접 구현했으며 리스트 1과 같다. 리스트 1의 코드는 대단히 간단하다. 먼저 RSS 피드를 읽는 서버 호출 작업이 수행 중임을 알리기 위해 Progress 컨트롤을 표시하고 WebRequest/WebResponse 클래스를 이용하여 RSS 피드를 읽는다. 읽어 들인 RSS 피드는 WCF의 System.ServiceModel.Syndication 네임스페이스에 존재하는 SyndicationFeed 클래스를 통해 파싱 하였고 그 결과를 리스트 박스에 추가한다. 리스트 1에 등장하는 FeedItem 클래스는 WCF의 SyndicationItem 클래스에서 필요한 부분만을 읽어 오는 엔티티 클래스이다. 상세한 내용은 이 글과 큰 관계가 없으므로 MSDN을 뒤져보기 바란다. (쫌!)

   1: private void button1_Click(object sender, EventArgs e)
   2: {
   3:     progressBar1.Visible = true;
   4:     listBox1.Items.Clear();
   5:  
   6:     WebRequest request = WebRequest.Create(BlogUri);
   7:     WebResponse response = request.GetResponse();
   8:     Stream stream = response.GetResponseStream();
   9:  
  10:     var reader = XmlReader.Create(stream);
  11:     var feed = SyndicationFeed.Load(reader);
  12:  
  13:     foreach (var feedItem in feed.Items)
  14:     {
  15:         var item = new FeedItem(feedItem);
  16:         listBox1.Items.Add(item);
  17:     }
  18:  
  19:     progressBar1.Visible = false;
  20: }

[리스트1] 동기(synchronous) 버전의 RSS 피드 읽기 코드

리스트 1의 코드는 단순한 동기 서버 호출을 통해 구현되어 있기 때문에 코드의 내용이 직관적이며 어렵지 않다. 하지만 이 코드는 서버 호출을 수행하는 동안 UI가 업데이트 되지 않는 문제를 가지고 있다. 다시 말해, ProgressBar가 화면 상에 나타나지도 않을뿐더러 ProgressBar의 애니메이션도 나타나지 않는다. 다시 말해 졸라 구린 UX를 제공한다고 보면 되겠다. 다행스럽게 서버로부터 응답 시간이 빠르다면 문제가 없겠지만 서버가 느리게 응답하는 경우 사용자들은 프로그램을 닫아버리거나 다른 어플리케이션을 구동하는 등의 행동을 나타낼 것이며 이는 곧 어플리케이션의 경쟁력을 크게 저하시키는 것이 될 것이다. (아 쓰바 이시키 디진거여? 아니여?)

Windows UI Processing

동기 프로그래밍 모델, 즉 호출 결과를 반환하기 전까지 호출자를 블록시켜버리는 동기 프로그래밍 모델이이 Windows UI와 무슨 상관이 있길래 프로그래스 바도 움직이지 않고, 심지어 윈도우 이동도 안 되는 등의 문제를 유발하는 것일까?

Windows 운영체제의 UI는 윈도우 메시지(Windows Message)라 불리는 이벤트에 의해 처리되며, 모든 사용자 입력(마우스, 키보드)은 윈도우 메시지로 변환되어 어플리케이션에 전달된다. 이 윈도우 메시지는 메시지 루프(message loop) 혹은 메시지 펌프(message pump)라 불리는 일련의 반복문(loop statement)에 의해 처리된다. Windows Form, WPF, Silverlight, Windows Store Apps 등은 모두 이 메시지 루프가 닷넷 프레임워크 혹은 WinRT 내부에 구현되어 있기 때문에 대부분의 상황에서 개발자는 대개 이에 대해 신경 쓰지 않아도 된다. 하지만 리스트 1의 코드와 같이 이벤트 핸들러 내에서 오랜(?) 시간 동안 작업을 수행하면, 이 메시지 루프가 수행되지 않아 UI가 멈추는 현상(freezing)이 발생하게 된다.

그림2

[그림2] 윈도우 메시지와 메시지 루프, 그리고 이벤트 핸들러 (만지면 커져요)

그림 2는 윈도우 메시지가 처리되는 상황을 간략하게 보여주고 있다. 사용자 입력이나 화면 업데이트 등의 작업이 필요하면 윈도우 시스템은 메시지 큐에 윈도우 메시지를 삽입한다. 어플리케이션은 메시지 루프에서 지속적으로 윈도우 메시지를 꺼내어 이 메시지를 사용하여 각 이벤트 핸들러를 호출한다. 실제 메시지 디스패치는 이보다 훨씬 복잡한 ‘윈도우 프로시저’를 통해 이벤트 핸들러를 호출하지만 논리적으로 그림 2와 같이 작동한다고 생각해도 큰 문제는 없다. 그림 2에서 유의해야 할 사항은 메시지 루프가 이벤트 핸들러를 호출할 때 동기적인 호출을 수행한다는 것이다. 다시 말해 이벤트 핸들러가 제어를 반환하지 않고 오랜 작업을 수행하면 메시지 루프가 수행되지 않게 되고 사용자 입력이나 화면 업데이트 메시지가 처리되지 않게 됨에 유의하자.

Windows Forms 어플리케이션은 정확하게 윈도우 메시지에 의해 모든 사용자 입력과 화면 갱신 등의 작업이 수행된다. 반면 WPF 나 Silverlight, Windows Store Apps 들은 윈도우 메시지를 일부 사용하지만 화면 구성 요소들에 대한 업데이트 등은 자체적인 디스패치 메커니즘을 사용한다. 이 메커니즘 역시 디스패처가 지속적으로 수행되어야만 사용자 입력 및 화면 갱신 작업이 원활하게 작동한다. 다시 말해, 이벤트 핸들러에서 오랜 시간 동안 어떤 작업을 수행하면 디스패처가 수행되지 않게 되고 사용자 입력이나 화면 갱신은 이루어지지 않는다는 말이 되겠다.

이러한 이유에서 UI를 가진 클라이언트 어플리케이션들은 오랜 시간이 소요될지도 모르는 서버 호출에 대해서 비동기 호출을 수행하고 UI 업데이트를 수행하거나 곧바로 제어를 반환하여 메시지 루프가 지속적으로 윈도우 메시지를 처리하도록 해야만 한다는 것이다.

What’s the Problem with Synchronous Server Call?

무엇이 문제인지 다시 [리스트1]의 코드로 되돌아가 보자. 사용자가 조회 버튼을 클릭하면 버튼 이벤트 핸들러가 호출될 것이다. 그리고 웹 서버로부터 RSS 피드를 읽어 들이는 서버 호출(라인 7: GetResponse 메서드 호출)을 수행한다. 웹 서버가 응답을 늦게 보내게 되면 서버 호출 다음 코드는 수행되지 않는다.

이 말은 곧 그림2에서의 메시지 루프가 수행되지 않음을 의미한다. 메시지 루프는 GetResponse 메서드 호출이 반환되고 이벤트 핸들러가 종료되어야만 다시 윈도우 메시지들을 꺼내어 처리를 수행하게 되는 것이다. 메시지 루프가 처리해야 하는 윈도우 메시지는 윈도우의 이동, 크기 변화, 화면 업데이트(ProgressBar 업데이트 포함) 등을 포함한다. 따라서 웹 서버로부터 응답이 오지 않는 한 해당 어플리케이션의 UI는 변경되지 않게 되는 것이다.

이러한 동기적인 작업들의 대부분은 UX에 좋지 못한 영향을 미친다. 다시 말해 사용자에게 아무런 피드백을 주지 않고 마냥 기다리게 하는 것은 현대적인 UX와 관계가 멀다는 얘기가 되겠다. 이런 경향을 최초로 강력하게 반영한 개발 환경이 바로 Ajax나 Silverlight이다. 이들은 서버/파일 작업을 수행하는 작업들 중 동기 호출을 단 하나도 존재하지 않는다. UI를 잠기게 만드는 동기 호출을 아예 제공하지 않음으로써 보다 나은 UX를 제공하도록 강요하고 있는 것이다.

다음 글에는…

이번 글은 간단히 이 정도로만 마치고 다음 글에서 전통적인 비동기 프로그래밍을 간략하게 살펴보도록 하겠다. 다음 글에서는 전통적인 비동기 프로그래밍 모델(APM; Asynchronous Programming Model)을 살펴보고 이 APM의 문제가 무엇인지를 살펴볼 것이다. 그 다음 글에서 전통적인 APM 문제를 해결하는 async/await 키워드를 살펴보게 될 것이다. 물론 항상 그러하듯이 언제 다음 글이 올라올지는 미지수이다…
(이 사이트는 다 나쁜데 쥔장의 싸가지가 없는 게 가장 나쁘다!)


경고 : 이 글을 무단으로 복제/스크랩하여 타 게시판, 블로그에 게시하는 것은 허용하지 않습니다.