.NET을 사용한 백그라운드 처리


많은 앱은 웹 요청의 컨텍스트 외부에서 백그라운드 처리를 해야 합니다. 이 가이드에서는 사용자가 번역할 텍스트를 입력하고 이전 번역 목록을 표시하는 웹 앱을 만듭니다. 사용자 요청이 차단되지 않도록 번역이 백그라운드 프로세스로 수행됩니다.

다음 다이어그램은 번역 요청 프로세스를 보여줍니다.

아키텍처 다이어그램입니다.

다음은 가이드 앱 작동 방법을 보여주는 이벤트 시퀀스입니다.

  1. 웹 페이지를 방문하여 Firestore에 저장된 이전 번역 목록을 확인합니다.
  2. HTML 양식을 입력하여 텍스트 번역을 요청합니다.
  3. 번역 요청이 Pub/Sub에 게시됩니다.
  4. 해당 Pub/Sub 주제에 구독된 Cloud Run 서비스가 트리거됩니다.
  5. Cloud Run 서비스는 Cloud Translation을 사용하여 텍스트를 번역합니다.
  6. Cloud Run 서비스는 결과를 Firestore에 저장합니다.

이 가이드는 Google Cloud를 사용한 백그라운드 처리에 관심이 있는 모든 사용자를 대상으로 합니다. Pub/Sub, Firestore, App Engine, Cloud Functions와 관련된 사전 경험은 필요하지 않습니다. 하지만 모든 코드를 이해하기 위해서는 .NET, 자바스크립트, HTML 관련 경험이 도움이 됩니다.

목표

  • Cloud Run 서비스 이해 및 배포
  • 앱을 사용해봅니다.

비용

이 문서에서는 비용이 청구될 수 있는 다음과 같은 Google Cloud 구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요. Google Cloud를 처음 사용하는 사용자는 무료 체험판을 사용할 수 있습니다.

이 문서에 설명된 태스크를 완료했으면 만든 리소스를 삭제하여 청구가 계속되는 것을 방지할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  3. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  4. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  5. Install the Google Cloud CLI.
  6. To initialize the gcloud CLI, run the following command:

    gcloud init
  7. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  8. Google Cloud 프로젝트에 결제가 사용 설정되어 있는지 확인합니다.

  9. Enable the Firestore, Cloud Run, Pub/Sub, and Cloud Translation APIs.

    Enable the APIs

  10. Install the Google Cloud CLI.
  11. To initialize the gcloud CLI, run the following command:

    gcloud init
  12. gcloud 구성요소를 업데이트합니다.
    gcloud components update
  13. 개발 환경을 준비합니다.

    .NET 개발 환경 설정

앱 준비

  1. 터미널 창에서 샘플 앱 저장소를 로컬 머신에 클론합니다.

    git clone https://github.com/GoogleCloudPlatform/getting-started-dotnet.git

    또는 zip 파일로 샘플을 다운로드하고 압축을 풀 수 있습니다.

  2. 백그라운드 작업 샘플 코드가 포함된 디렉터리로 변경합니다.

    cd getting-started-dotnet/BackgroundProcessing

TranslateWorker 서비스 이해

  • 이 서비스는 Firestore 및 Translation과 같은 여러 종속 항목을 가져오는 것으로 시작됩니다.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    
  • 핸들러 호출 간에 재사용될 수 있도록 Firestore 및 Translation 클라이언트가 초기화됩니다. 이렇게 하면 호출할 때마다 새 클라이언트를 초기화할 필요가 없으므로, 실행 시간이 빨라집니다.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<FirestoreDb>(provider =>
            FirestoreDb.Create(GetFirestoreProjectId()));
        services.AddSingleton<TranslationClient>(
            TranslationClient.Create());
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    
  • Translation API는 문자열을 선택된 언어로 번역합니다.

    var result = await _translator.TranslateTextAsync(sourceText, "es");
    
  • 컨트롤러의 생성자는 Firestore 및 Pub/Sub 클라이언트를 받습니다.

    Post 메서드는 Pub/Sub 메시지를 파싱하여 번역할 텍스트를 가져옵니다. 메시지 ID를 번역 요청의 고유한 이름으로 사용하여 중복 번역을 저장하지 않도록 합니다.

    using Google.Cloud.Firestore;
    using Google.Cloud.Translation.V2;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Logging;
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace TranslateWorker.Controllers
    {
        /// <summary>
        /// The message Pubsub posts to our controller.
        /// </summary>
        public class PostMessage
        {
            public PubsubMessage message { get; set; }
            public string subscription { get; set; }
        }
    
        /// <summary>
        /// Pubsub's inner message.
        /// </summary>
        public class PubsubMessage
        {
            public string data { get; set; }
            public string messageId { get; set; }
            public Dictionary<string, string> attributes { get; set; }
        }
    
        [Route("api/[controller]")]
        [ApiController]
        public class TranslateController : ControllerBase
        {
            private readonly ILogger<TranslateController> _logger;
            private readonly FirestoreDb _firestore;
            private readonly TranslationClient _translator;
            // The Firestore collection where we store translations.
            private readonly CollectionReference _translations;
    
            public TranslateController(ILogger<TranslateController> logger,
                FirestoreDb firestore,
                TranslationClient translator)
            {
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                _firestore = firestore ?? throw new ArgumentNullException(
                    nameof(firestore));
                _translator = translator ?? throw new ArgumentNullException(
                    nameof(translator));
                _translations = _firestore.Collection("Translations");
            }
    
            /// <summary>
            /// Handle a posted message from Pubsub.
            /// </summary>
            /// <param name="request">The message Pubsub posts to this process.</param>
            /// <returns>NoContent on success.</returns>
            [HttpPost]
            public async Task<IActionResult> Post([FromBody] PostMessage request)
            {
                // Unpack the message from Pubsub.
                string sourceText;
                try
                {
                    byte[] data = Convert.FromBase64String(request.message.data);
                    sourceText = Encoding.UTF8.GetString(data);
                }
                catch (Exception e)
                {
                    _logger.LogError(1, e, "Bad request");
                    return BadRequest();
                }
                // Translate the source text.
                _logger.LogDebug(2, "Translating {0} to Spanish.", sourceText);
                var result = await _translator.TranslateTextAsync(sourceText, "es");
                // Store the result in Firestore.
                Translation translation = new Translation()
                {
                    TimeStamp = DateTime.UtcNow,
                    SourceText = sourceText,
                    TranslatedText = result.TranslatedText
                };
                _logger.LogDebug(3, "Saving translation {0} to {1}.",
                    translation.TranslatedText, _translations.Path);
                await _translations.Document(request.message.messageId)
                    .SetAsync(translation);
                // Return a success code.
                return NoContent();
            }
    
            /// <summary>
            /// Serve a root page so Cloud Run knows this process is healthy.
            /// </summary>
            [Route("/")]
            public IActionResult Index()
            {
                return Content("Serving translate requests...");
            }
        }
    }
    

TranslateWorker 서비스 배포

  • BackgroundProcessing 디렉터리에서 PowerShell 스크립트를 실행하여 서비스를 빌드하고 Cloud Run에 배포합니다.

    PublishTo-CloudRun.ps1

PublishTo-CloudRun.ps1 스크립트 이해

PublishTo-CloudRun.ps1 스크립트는 서비스를 Cloud Run에 게시하고 TranslateWorker 서비스가 악용되지 않도록 보호합니다. 서비스가 새로 추가되는 모든 연결을 허용한 경우 누구나 번역 요청을 컨트롤러에 게시하여 비용을 발생시킬 수 있습니다. 따라서 Pub/Sub의 POST 요청만 수락하도록 서비스를 설정합니다.

스크립트는 다음을 수행합니다.

  1. dotnet publish를 사용하여 로컬에서 앱을 빌드합니다.
  2. Cloud Build를 사용하여 앱을 실행하는 컨테이너를 빌드합니다.
  3. Cloud Run에 앱을 배포합니다.
  4. 프로젝트가 Pub/Sub 인증 토큰을 만들 수 있도록 합니다.
  5. Pub/Sub 구독 ID를 나타내는 서비스 계정을 만듭니다.
  6. 서비스 계정에 TranslateWorker 서비스를 호출할 수 있는 권한을 부여합니다.
  7. Pub/Sub 주제 및 구독을 생성합니다.

    # 1. Build the application locally.
    dotnet publish -c Release
    
    # Collect some details about the project that we'll need later.
    $projectId = gcloud config get-value project
    $projectNumber = gcloud projects describe $projectId --format="get(projectNumber)"
    $region = "us-central1"
    
    # 2. Use Google Cloud Build to build the worker's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-worker `
        TranslateWorker/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-worker --region $region --platform managed `
        --image gcr.io/$projectId/translate-worker --no-allow-unauthenticated
    $url = gcloud beta run services describe translate-worker --platform managed `
        --region $region --format="get(status.address.hostname)"
    
    # 4. Enable the project to create pubsub authentication tokens.
    gcloud projects add-iam-policy-binding $projectId `
         --member=serviceAccount:service-$projectNumber@gcp-sa-pubsub.iam.gserviceaccount.com `
         --role=roles/iam.serviceAccountTokenCreator
    
    # 5. Create a service account to represent the Cloud Pub/Sub subscription identity.
    $serviceAccountExists = gcloud iam service-accounts describe `
        cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com 2> $null
    if (-not $serviceAccountExists) {
        gcloud iam service-accounts create cloud-run-pubsub-invoker `
            --display-name "Cloud Run Pub/Sub Invoker"
    }
    
    # 6. For Cloud Run, give this service account permission to invoke
    # translate-worker service.
    gcloud beta run services add-iam-policy-binding translate-worker `
         --member=serviceAccount:cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com `
         --role=roles/run.invoker --region=$region
    
    # 7. Create a pubsub topic and subscription, if they don't already exist.
    $topicExists = gcloud pubsub topics describe translate-requests 2> $null
    if (-not $topicExists) {
        gcloud pubsub topics create translate-requests
    }
    $subscriptionExists = gcloud pubsub subscriptions describe translate-requests 2> $null
    if ($subscriptionExists) {
        gcloud beta pubsub subscriptions modify-push-config translate-requests `
            --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    } else {
        gcloud beta pubsub subscriptions create translate-requests `
            --topic translate-requests --push-endpoint $url/api/translate `
            --push-auth-service-account cloud-run-pubsub-invoker@$projectId.iam.gserviceaccount.com
    }
    
    

TranslateUI 서비스 이해

TranslateUI 서비스는 최근 번역을 표시하고 새 번역에 대한 요청을 승인하는 웹페이지를 렌더링합니다.

  • StartUp 클래스는 ASP.NET 앱을 구성하고 Pub/Sub 및 Firestore 클라이언트를 생성합니다.

    using Google.Apis.Auth.OAuth2;
    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Net.Http;
    
    namespace TranslateUI
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddSingleton<FirestoreDb>(
                    provider => FirestoreDb.Create(GetFirestoreProjectId()));
                services.AddSingleton<PublisherClient>(
                    provider => PublisherClient.CreateAsync(new TopicName(
                        GetProjectId(), GetTopicName())).Result);
                services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                    app.UseHsts();
                }
    
                app.UseHttpsRedirection();
                app.UseStaticFiles();
                app.UseCookiePolicy();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
    
        }
    }
    
  • 색인 핸들러 Index는 Firestore에서 기존의 모든 번역을 가져오고 ViewModel을 목록으로 채웁니다.

    using Google.Cloud.Firestore;
    using Google.Cloud.PubSub.V1;
    using Google.Protobuf;
    using Microsoft.AspNetCore.Mvc;
    using System.Diagnostics;
    using System.Linq;
    using System.Threading.Tasks;
    using TranslateUI.Models;
    
    namespace TranslateUI.Controllers
    {
        public class HomeController : Controller
        {
            private readonly FirestoreDb _firestore;
            private readonly PublisherClient _publisher;
            private CollectionReference _translations;
    
            public HomeController(FirestoreDb firestore, PublisherClient publisher)
            {
                _firestore = firestore;
                _publisher = publisher;
                _translations = _firestore.Collection("Translations");
            }
    
            [HttpPost]
            [HttpGet]
            public async Task<IActionResult> Index(string SourceText)
            {
                // Look up the most recent 20 translations.
                var query = _translations.OrderByDescending("TimeStamp")
                    .Limit(20);
                var snapshotTask = query.GetSnapshotAsync();
    
                if (!string.IsNullOrWhiteSpace(SourceText))
                {
                    // Submit a new translation request.
                    await _publisher.PublishAsync(new PubsubMessage()
                    {
                        Data = ByteString.CopyFromUtf8(SourceText)
                    });
                }
    
                // Render the page.
                var model = new HomeViewModel()
                {
                    Translations = (await snapshotTask).Documents.Select(
                        doc => doc.ConvertTo<Translation>()).ToList(),
                    SourceText = SourceText
                };
                return View(model);
            }
    
            [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
            public IActionResult Error()
            {
                return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
            }
        }
    }
  • 새 번역은 HTML 양식을 제출하여 요청됩니다. 번역 요청 핸들러는 요청을 검증하고 메시지를 Pub/Sub에 게시합니다.

    // Submit a new translation request.
    await _publisher.PublishAsync(new PubsubMessage()
    {
        Data = ByteString.CopyFromUtf8(SourceText)
    });
    

TranslateUI 서비스 배포

  • BackgroundProcessing 디렉터리에서 PowerShell 스크립트를 실행하여 서비스를 빌드하고 Cloud Run에 배포합니다.

    ./PublishTo-CloudRun.ps1

PublishTo-CloudRun.ps1 스크립트 이해

PublishTo-CloudRun.ps1 스크립트는 Cloud Run에 앱을 게시합니다.

스크립트는 다음을 수행합니다.

  1. dotnet publish를 사용하여 로컬에서 앱을 빌드합니다.
  2. Cloud Build를 사용하여 앱을 실행하는 컨테이너를 빌드합니다.
  3. Cloud Run에 앱을 배포합니다.

    # 1. Build the application locally.
    dotnet publish -c Release
    # 2. Use Google Cloud Build to build the UI's container and publish to Google
    # Container Registry.
    gcloud builds submit --tag gcr.io/$projectId/translate-ui `
        TranslateUI/bin/Release/netcoreapp2.1/publish
    
    # 3. Run the container with Google Cloud Run.
    gcloud beta run deploy translate-ui --region $region --platform managed `
        --image gcr.io/$projectId/translate-ui --allow-unauthenticated
    
    

앱 테스트

PublishTo-CloudRun.ps1 스크립트를 성공적으로 실행한 후 번역을 요청합니다.

  1. PublishTo-CloudRun.ps1 스크립트의 마지막 명령어는 UI 서비스의 URL을 알려줍니다. 터미널 창에서 TranslateUI 서비스의 URL을 찾으세요.

    gcloud beta run services describe translate-ui --region $region --format="get(status.address.hostname)"
  2. 브라우저에서 이전 단계에서 확인한 URL로 이동합니다.

    빈 번역 목록과 새 번역 요청 양식이 포함된 페이지가 있습니다.

  3. 번역할 텍스트 필드에 번역할 텍스트를 입력합니다(예: Hello, World.).

  4. 제출을 클릭합니다.

  5. 페이지를 새로고침하려면 새로고침 을 클릭합니다. 번역 목록에 새 행이 있습니다. 번역이 표시되지 않으면 몇 초 후 다시 시도합니다. 그래도 번역이 표시되지 않으면 앱 디버깅에 대한 다음 섹션을 참조하세요.

앱 디버깅

Cloud Run 서비스에 연결할 수 없거나 새로운 번역이 표시되지 않으면 다음 사항을 확인하세요.

  • PublishTo-CloudRun.ps1 스크립트가 성공적으로 완료되었고 오류가 출력되지 않았는지 확인합니다. 오류가 있으면(예: message=Build failed) 오류를 수정하고 다시 실행해 봅니다.

  • 로그에서 오류를 검사합니다.

    1. Google Cloud Console에서 Cloud Run 페이지로 이동합니다.

      Cloud Run 페이지로 이동

    2. 서비스 이름 translate-ui를 클릭합니다.

    3. 로그를 클릭합니다.

삭제

이 가이드에서 사용된 리소스 비용이 Google Cloud 계정에 청구되지 않도록 하려면 리소스가 포함된 프로젝트를 삭제하거나 프로젝트를 유지하고 개별 리소스를 삭제하세요.

Google Cloud 프로젝트 삭제

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Cloud Run 서비스 삭제

  • 이 가이드에서 만든 Cloud Run 서비스를 삭제합니다.

    gcloud beta run services delete --region=$region translate-ui
    gcloud beta run services delete --region=$region translate-worker

다음 단계