간단한 비동기 패턴 생성하기 [#2]

닷넷 프레임워크에서는 성능과 효율성을 고려하는 작업와 입/출력 작업들을 대체로 비동기 모델을 사용하여 지연없이 작업을 처리할 수 있도록 하고 있습니다. 비동기를 구현하는 방식은 여러가지가 있지만 이러한 비동기 방식을 구현하는 방법 중 가장 손쉬우면서도 확실한 방법을 하나 소개합니다. 바로 대리자를 이용하는 것입니다. 지금 소개하는 방법은 다른 메서드와의 인과 관계가 너무 복잡하지 않으면서도 처리 시간이 상대적으로 오래 걸리는 거의 모든 유형의 메서드에 대하여 적용이 가능합니다.

1. 비동기 패턴으로 다시 디자인할 함수의 시그니처를 준비하고 이 시그니처와 일치하는 대리자를 선언합니다.

시그니처는 함수의 반환 형식, 함수의 속성, 함수가 받아들이는 인자와 같이 함수의 이름을 제외한 나머지 모든 요소들을 이르는 용어입니다. 단, 시그니처의 범위에서 메타 데이터와 어트리뷰트에 의한 속성 (System.Attribute)은 제외됩니다. 이러한 시그니처의 정보와 일치하는 대리자를 클래스의 정의 안에 포함하고 접근 범위를 정의합니다. 대체로 이렇게 생성하는 대리자를 직접 노출할 필요가 없으므로 private 제한자로 캡슐화합니다.

2. BeginX 함수를 만듭니다.

BeginX 함수에서 X는 기존의 동기 방식의 함수 이름을 사용하면 됩니다. 예를 들어 Convert라는 이름의 함수였다면 BeginConvert가 될 것입니다. 그리고 이 함수는 다음의 규칙을 따라야 합니다.

* BeginX 함수의 반환 형식은 IAsyncResult입니다.
* BeginX 함수의 처음에 오는 인자들은 X 함수의 인자들과 같은 구성이어야 합니다.
* 다만 BeginX 함수의 가장 마지막 인자로는 언제든지 AsyncCallback 형식이 와야 합니다.

만약 가변 인자를 사용하여야 한다면 두 번째와 세 번째 규칙을 조금 다르게 적용하여 AsyncCallback 형식을 앞으로 당기거나 가변 인자가 아닌 일반 배열 인자 (System.Object의 배열)로 변경하여야 합니다. 가변 인자 뒤에는 다른 인자가 올 수 없다는 점에 따른 부분입니다.

BeginX 함수를 프로그래밍할 때는 다음과 같은 순서를 기초로 합니다.

* 1단계에서 만든 대리자를 사용하여 기존의 X 함수를 포장합니다. 즉, 다음과 같습니다.


DelegateX oDelegate = new DelegateX(X);

* 대리자 객체의 메서드 중 BeginInvoke 메서드를 호출합니다. 이 때 BeginInvoke 메서드에서 필요로하는 인자들이 기존의 동기 함수 호출을 위하여 사용했던 인자와 유사한 것을 알 수 있으며 그대로 지정할 수 있다는 것을 확인할 수 있습니다. 그리고 BeginInvoke 함수의 반환 객체인 IAsyncResult를 그대로 반환하면 BeginX 함수의 작업은 끝납니다.

그리고 BeginX 함수의 반환 형식이 IAsyncResult인 이유는 간단한데, IAsyncResult가 바로 예약된 비동기 작업에 대한 핸들 역할을 하기 때문입니다. 이 객체를 사용하여 결과를 통지받거나 작업을 통제할 수 있습니다.

3. EndX 함수를 만듭니다.

EndX 함수에서 X는 BeginX 함수에서의 X와 마찬가지로 기존의 동기 방식의 함수 이름을 사용하면 됩니다. 예를 들어 Convert라는 이름의 함수였다면 EndConvert가 될 것입니다. 그리고 이 함수는 다음의 규칙을 따라야 합니다.

* EndX 함수의 반환 형식은 원래의 X 함수의 반환 형식과 같아야 합니다.
* EndX 함수의 인자들 중에 반드시 IAsyncResult 형식이 있어야 합니다. 별다른 작업이 필요하지 않으면 IAsyncResult 형식의 인자만 필요합니다.

EndX 함수는 특별한 일이 아니면 대체로 AsyncCallback 대리자에 의하여 실행될 함수에 의하여 호출됩니다. 따라서 EndX 함수를 임의로 호출하는 것은 올바르지 않습니다.

EndX 함수에서 해야 할 일은 간단합니다. 인수로 받아들인 IAsyncResult의 AsyncState 속성을 통하여 가져올 수 있는 “비동기 작업의 시작점”을 확인하고 이 시작점으로 하여금 비동기 작업을 완료하도록 요구하는 것입니다. 여기서 AsyncState 속성으로 유효한 객체를 가져올 수 있으려면 우리가 호출했었던 BeginInvoke 메서드의 가장 끝에 오는 state 매개 변수에 적절한 객체를 지정했어야 합니다.

AsyncState는 Object 형식이므로 다시 형변환을 거쳐서 우리가 원하는 형식으로 재구성하거나 리플렉션을 통한 간접 호출을 이용하는 등의 방법으로 비동기 작업을 완료하도록 요청하는 것입니다. 이 때 반환되는 값이나 객체를 함수의 결과로 내보내면 되는 것입니다.

4. 예제: 일반 비트맵 이미지를 흑백 비트맵 이미지로 변환하기

* 동기 및 비동기 함수군



public static Bitmap ConvertAsMono(Bitmap oBitmap)
        {
            const float fThreshold = 1.0f / 2.0f;
            PixelFormat nFormat = PixelFormat.Format32bppArgb;
            Rectangle oRect = new Rectangle(0, 0, oBitmap.Width, oBitmap.Height);
            Bitmap oMonoBitmap = new Bitmap(oRect.Width, oRect.Height, nFormat);


            for (int i = 0; i < oBitmap.Width; i++)
            {
                for (int j = 0; j < oBitmap.Height; j++)
                {
                    oMonoBitmap.SetPixel(i, j,
                        oBitmap.GetPixel(i, j).GetBrightness() > fThreshold ? Color.White : Color.Black);
                }
            }


            return oMonoBitmap;
        }


        [Serializable()]
        private delegate Bitmap BitmapConversionDelegate(Bitmap oBitmap);


        public static IAsyncResult BeginConvertAsMono(Bitmap oBitmap, AsyncCallback oResultCallback)
        {
            BitmapConversionDelegate oOperationDelegate = new BitmapConversionDelegate(ConvertAsMono);


            return oOperationDelegate.BeginInvoke(
                oBitmap,
                oResultCallback,
                oOperationDelegate);
        }


        public static Bitmap EndConvertAsMono(IAsyncResult oAsyncResult)
        {
            return ((BitmapConversionDelegate)oAsyncResult.AsyncState).EndInvoke(oAsyncResult);
        }


* BeginConvertAsMono 사용 예


AsyncCallback oCallback = new AsyncCallback(this.ImageLoadCompleted);
BitmapUtilities.BeginConvertAsMono(pictPreview.Image, oCallback);

* EndConvertAsMono 사용 예


private void ImageLoadCompleted(IAsyncResult oAsyncResult)
        {
            this.pictPreview.Image = BitmapUtilities.EndConvertAsMono(oAsyncResult);
            this.pictPreview.Invoke(
                new Action<Cursor>(delegate(Cursor oCursor) { this.pictPreview.Cursor = oCursor; }),
                Cursors.Default);
        }

5. 결론

이와 같은 방법을 사용하면 동기 함수와 비동기 함수의 구현을 코드에 큰 변화를 주지 않고도 구현할 수 있으므로 매우 효율적입니다. 또한 닷넷 프레임워크 내부 구현을 재사용하는 것이므로 확실하다고 볼 수 있습니다.

댓글 남기기