JavaScript의 미래, 그리고 C#의 현재 – 비동기 프로그래밍

Promise

얼마전 JavaScript의 promise 구현에 대한 멋진 글을 읽었습니다. 마음 같아서는 당장이라도 한국어로 번역해 널리 퍼뜨리고 싶을 정도로 내용이 알차고 이해하기 쉽게 쓰여진 글이었지만 제 허접한 영어 실력이 허락하지 않았습니다. 다행스럽게도 이미 뛰어나신 분들이 번역 작업에 착수한 것으로 알고있습니다. 곧 많은 국내 프로그래머들에게 제공되겠죠.

많은 내용 중에도 개인적으로 유독 눈길이 가는 부분이 있었는데 바로 ‘Bonus round: Promises and Generators’ 섹션이었습니다. 현재 ES6(ECMAScript 6 Specification)에 포함되기 위해 실험단계에 있는 generator라는 기능이 소개되는데, 새로운 키워드인 yield를 사용해 함수의 코드 진행을 제어하는 기법입니다. 이 기능을 이용해 비동기 코드를 마치 동기식 프로그램처럼 작성할 수 있습니다. 많은 분들이 이 멋진 기능에 눈길이 가셨을텐데요, 저 역시 마찬가지입니다. 하지만 저같은 경우는 눈길이 갔던 이유가 하나 더 있습니다. 바로 C#이 가진 기능과 너무도 닮아있었기 때문입니다.

yield

전체 포스트에 대한 내용은 이미 말씀드린 것처럼 곧 국문 버전이 공개될 것이라 기대되고요, 저는 generator를 이용한 비동기 코드에 대해서 좀 더 얘기해보고 싶습니다. 아직 ES6 generator wiki의 내용을 다 이해하지는 못했지만 아마 앞으로 JavaScript에서도 C#처럼 iterator를 작성할 수 있을 듯 합니다. 멋진 일이죠. 다시 promise 얘기로 돌아가면, ‘Bonus round: Promises and Generators’ 섹션에서는 이전에 보여준 비동기 코드를 generator를 사용해 동기적 프로그램을 만들 듯 다시 작성합니다. 코드를 한 번 보겠습니다.

try {
  let story = yield getJSON('story.json');
  addHtmlToPage(story.heading);

  let chapterPromises = story.chapterUrls.map(getJSON);

  for (let chapterPromise of chapterPromises) {
    let chapter = yield chapterPromise;
    addHtmlToPage(chapter.html);
  }

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

네, 비동기 코드 맞습니다. 놀랍게도 비동기적으로 진행되는 프로그램이지만 어떤 callback 함수 정의도, then 함수 호출도 보이지 않습니다. 대신 yield라는 JavaScript 사용자들에게 낮선 키워드가 두 번 등장합니다. 위 코드가 하는 일을 간단히 정리해보죠.

  1. story.json 웹 리소스를 비동기적으로 가져와 story 변수에 담습니다.
  2. story.chapterUrls 배열의 각 요소에 지정된 모든 챕터 데이터를 비동기적으로 가져오는 promise 배열을 만듭니다. 각 웹 요청 작업은 브라우저에 의해 병렬적으로 실행됩니다.
  3. 병렬적으로 수행된 각 promise 작업의 결과를 promise의 순서대로(병렬적 작업 결과가 순서대로! 이것이 promise의 힘이죠.) 브라우저 화면에 보여줍니다.
  4. 각 챕터를 보여주는 작업이 모두 완료되면 완료 메시지를 출력합니다.
  5. 지금까지의 과정 중 어느 프레임, 어느 코드에서라도 오류가 발생하면 하나의 공통된 catch 블록에서 화면에 오류 내용을 보여줍니다.

일련의 프로세스와 코드를 비교해 보면 비동기 작업이 시작되고 종료되는 부분에 yield 키워드가 들어가 있는 것을 알아차릴 수 있습니다. 이 yield 키워드가 코드 진행을 제어하는 역할을 하는 것이죠. 이 흐름의 변화는 반복문에 포함될 수도 있고 try 블록에 포함되어 서로 다른 프레임에서 발생하는 오류를 단일 코드로 처리하는 것도 가능케 합니다.

await

그런데 제가 분명 위 코드를 보고 C#을 떠올렸다고 말씀 드렸습니다. 비슷한 일을 하는 콘솔 프로그램을 C#으로 작성해 보겠습니다.

try
{
  dynamic story = await GetJson("story.json");
  Console.WriteLine(story.heading);

  var chapterTasks = ((JArray)story.chapterUrls).Select(GetJson);

  foreach (var task in chapterTasks.ToList())
  {
    dynamic chapter = await task;
    Console.WriteLine(chapter.html);
  }

  Console.WriteLine("All done");
}
catch (Exception exception)
{
  Console.WriteLine("Argh, broken: {0}", exception.Message);
}

제가 왜 미래의 JavaScript 코드를 보고 지금의 C#을 떠올렸는지 이해가 되실 겁니다. 이 C# 코드는 출력 대상이 콘솔이라는 것을 제외하면 JavaScript 프로그램과 정확히 같은 일을 합니다. 뿐만아니라 작성된 코드의 모습이 매우 유사합니다. 사실 코드가 이 정도로 유사한 것은 dynamic과 Linq 역할도 큽니다. 정적 형식을 기반으로한 컴파일 언어이면서도 일부 동적 언어의 특징을 수용하는 현재의 C#을 사용하면 json 데이터를 새로운 클래스를 정의하지 않고 마치 JavaScript 개체를 다루듯 사용할 수 있으며 JavaScript 배열의 map 함수와 Linq의 Select 연산은 ‘지연된 실행(delayed execution)’ 메커니즘(이것 때문에 foreach 구문에서 ToList() 메서드를 사용한 것입니다. ToList() 메서드가 없으면 병렬성이 사라집니다. 직접 해보세요!)을 제외하면 매우 유사합니다. 전체 C# 프로그램 소스는 여기에 있습니다.

하지만 제가 지금 강조하고 싶은 것은 서로 너무나도 다른 특징을 가진 두 프로그래밍 언어가 비동기 프로그래밍을 매우 유사한 방법으로 풀어내고 있다는 점입니다. JavaScript 코드의 yield 키워드가 자리한 정확한 위치에 C# 코드에는 await 키워드가 있습니다. C#은 IAsyncResult 인터페이스를 이용해 처리하던 비동기 프로그래밍을 promise 구현제가 포함된 TPL(Task Parallel Library)을 통해 한 단계 개선한 뒤 얼마 지나지 않아 await 키워드를 추가함으로써 비동기 코드 작성 방법을 완전히 바꿔버렸습니다. 디버깅 경험은 정말 극적으로 향상됩니다. await 구문에 중단점을 설정하고 Step Over를 해보세요! 그런데 C#과 태생적인 목적도, 기본 특성도 매우 다른 언어인 JavaScript가 이와 거의 유사한 비동기 프로그래밍 해법을 제공할 계획을 가지고 있는 것입니다.

Promise 그 이후

분명 promise가 프로그래머를 callback 지옥으로부터 구제해주는 것은 사실입니다. JavaScript 개발 팀이 꾸려지면 전 항상 ‘우리는 Q 또는 when을 사용합시다.’라고 설득합니다. 하지만 여전히 동기적 코드 작성과 비교하면 더 깊은 사고를 필요로하고 가독성이 낮으며 디버깅이 수월하지 않습니다. 이 문제를 해결하기 위해 현재의 C#과 미래의 JavaScript는 획기적인 비동기 프로그래밍 인프라를 제공합니다. 물론 그 저변에는 promise가 있으며 여전히 promise에 대한 이해는 중요합니다.

반복해서 강조하지만 동기적 코드를 작성하는 것처럼 비동기 코드를 작성하는 프로그래밍 경험은 프로그래머에게 높은 생산성을 제공합니다. 단일 스레드 언어인 JavaScript에 있어서 비동기 프로그래밍은 더욱 중요한 요소입니다. 더군다나 JavaScript는 더 이상 웹 클라이언트 전용 언어가 아닙니다. Node.js 환경에서 네트워크 서버를 개발해 본 프로그래머들은 데이터베이스, 메시지 큐, 웹 API, 파일 시스템 등에 대한 입출력이 복합적으로 연관된 코드를 작성할 때 소위 callback 지옥을 한 번쯤은 경험했을 거라 생각됩니다. 여기에 대한 하나의 해법이 promise이며 이제 곧 더 편리하고 효율적인 promise 사용법이 제공될 예정입니다. 너무나 기대되는 일입니다.

개선점

여기에 더해 generator를 이용한 JavaScript 비동기 프로그래밍은 C#의 await가 아직 해결 하지 못하는 상황을 처리해 냅니다. 아래 C# 코드는 컴파일 시 오류가 발생합니다.

try
{
  await AsyncOperation1();
}
catch
{
  await AsyncOperation2();
}

발생하는 오류는 다음과 같습니다.

Error 1 Cannot await in the body of a catch clause ...

catch 블록은 await 구문을 포함할 수 없다는 내용입니다. 저같은 경우는 아직 개발하면서 catch 블록에 await 구문을 사용해야 할 일이 없었지만 분명 경우에 따라 마주할 수 있는 상황이라고 생각됩니다. 실제로 이 문제에 대한 의문과 불만들이 개발자 커뮤니티에 종종 등장합니다. 오류를 해결하는 코드를 작성해 보겠습니다.

bool hasError = false;
try
{
  await AsyncOperation1();
}
catch
{
  hasError = true;
}
if (hasError)
  await AsyncOperation2();

봐줄만 한가요? 이 경우에는 그럴 수도 있습니다. 하지만 안타깝게도 finally 블록에도 await 코드는 작성할 수 없으며, 다양한 예외 형식에 대해 여러 개의 catch 블록이 필요하다면 문제는 심각해질 수 있습니다. 이런 경우는 생각하기도 싫기 때문에 예제 코드를 만들지는 않겠습니다.

반면 JavaScript의 generator는 catch 블록과 finally 블록에도 yield 구문 작성을 허용합니다. 예를 들어 아래와 같은 JavaScript 코드는 오류 없이 기대했던 그대로 동작합니다.

try {
  yield asyncOperation1();
}
catch (err) {
  yield asyncOperation2();
}

프로그래밍 언어들은 서로 영향과 자극을 주고 받으며 발전해왔습니다. C#의 await 구문도 가까운 미래에는 catch 블록과 finally 블록에게서 배척당하지 않게될 것이라 기대합니다.

결론

await 키워드가 추가된 이후 C# 프로그래머들은 정말 재미있고 편리하게 비동기 코드를 작성할 수 있었습니다. 분기문, 반복문, using 블록, try 블록 등과 함께 사용하는 await 구문은 C# 프로그래머의 사고를 보다 비즈니스 중심적일 수 있게 했고 동기적 코드와 유사한 디버깅 경험을 제공했습니다. 이제 다음 버전의 JavaScript는 이보다 조금 더 개선된 비동기 프로그래밍 인프라를 제공할 예정입니다. 벌써부터 여태껏 작성해왔던 JavaScript 코드의 개선 작업이 기다려집니다.

  • Chrome에서 chrome://flags/에 들어가 실험용 JavaScript를 사용하도록 설정하면 개발자 도구에서 generator를 미리 경험해 볼 수 있습니다.

  • 이 포스트에 등장한 C# 프로그램은 너무나 유명한 패키지, RestSharpJson.NET을 사용합니다.

Advertisements

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중