홈으로 돌아가기

.NET에서의 YandexGPT 통합 — 가이드

.NET 프로젝트에 YandexGPT 통합 가이드. 보안 클라이언트 초기화, 지수 백오프를 사용한 비동기 작업 처리, 이미지 유효성 검사 및 저장, Markdown을 안전한 HTML로 변환 다룸. 목표 — 개발자를 위한 프로덕션 준비 솔루션.

.NET 프로젝트에 YandexGPT를 올바르게 연결하는 방법
Advertisement 728x90

YandexGPT의 .NET 통합: 개발자를 위한 실용 가이드

YandexGPT는 단순한 클라우드 LLM 서비스가 아닙니다. 비동기 텍스트 및 이미지 생성을 지원하며, 엔터프라이즈 .NET 인프라와 원활하게 통합되는 완전 관리형 API입니다. 이 문서에서는 타임아웃, 작업 상태 관리, Base64 처리, 그리고 Markdown 출력 정제와 같은 실제 환경에서 직면하는 문제들을 해결하기 위한 생산 환경에 적합한 통합 구현 방법을 제공합니다. 특히, 생산 환경에서 가장 중요한 요소인 장애 허용성, 타입 안전성, 프롬프트 엔지니어링 제어, 그리고 보안된 자격 증명 저장에 중점을 둡니다.

클라이언트 아키텍처와 보안 초기화

MyGPTClient 클래스는 스레드 안전성을 보장하고 리소스 누수를 방지해야 합니다. 전역 HttpClient 사용은 잘못된 패턴입니다. 대신, 의존성 주입을 통해 등록된 IHttpClientFactory를 활용합니다:

// Program.cs
builder.Services.AddHttpClient<MyGPTClient>((sp, client) => {
    var config = sp.GetRequiredService<IConfiguration>();
    client.BaseAddress = new Uri("https://llm.api.cloud.yandex.net/");
    client.DefaultRequestHeaders.Authorization = 
        new System.Net.Http.Headers.AuthenticationHeaderValue(
            "Api-Key", config["Yandex:ApiKey"]);
});

이제 MyGPTClient 생성자는 직접 HttpClient를 생성하는 대신 IHttpClientFactory를 받아들입니다. 모델 URI(gpt://.../yandexgpt, art://.../yandex-art/latest)는 설정으로 이동되어, 재컴파일 없이도 개발/스테이징/프로덕션 환경 간 손쉽게 전환할 수 있습니다.

Google AdInline article slot

중요: ai.languageModels.user 역할은 Yandex Cloud 카탈로그 IAM 정책에서 명시적으로 할당되어야 합니다. CI/CD 환경에서는 최소 권한만 부여된 별도의 서비스 계정—즉, ai.*.user 권한만—을 사용해야 하며, editoradmin 역할은 필요 없습니다.

비동기 작업 처리: 폴링에서 재시도 로직까지

원래 WaitForTaskCompletionAsync 메서드는 고정된 Task.Delay(1000)을 사용하는 무한 루프를 채택했습니다. 하지만 이는 생산 환경에서는 용납될 수 없습니다. 동시 요청이 50건 이상 발생하면 API 한도를 초과하고 스레드가 과도하게 소모되기 때문입니다. 우리는 지수적 백오프와 재시도 제한을 적용한 적응형 폴링을 구현합니다:

private async Task<dynamic> WaitForTaskCompletionAsync(string taskId, int maxAttempts = 30)
{
    int attempt = 0;
    TimeSpan delay = TimeSpan.FromMilliseconds(500);

    while (attempt < maxAttempts)
    {
        try
        {
            var response = await _client.GetAsync($"operations/{taskId}");
            response.EnsureSuccessStatusCode();

            var responseBody = await response.Content.ReadAsStringAsync();
            var result = JsonConvert.DeserializeObject<dynamic>(responseBody);

            if (result?.done == true && result?.response != null)
                return result.response;

            if (result?.error != null)
                throw new InvalidOperationException($"API 오류: {result.error.message}");
        }
        catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
        {
            // 429 오류 처리 — 지연 시간 늘리기
            delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.5, 10_000));
        }
        catch (JsonException)
        {
            throw new InvalidOperationException("작업 상태 응답에 유효하지 않은 JSON");
        }

        await Task.Delay(delay);
        attempt++;
        delay = TimeSpan.FromMilliseconds(Math.Min(delay.TotalMilliseconds * 1.2, 5_000));
    }

    throw new TimeoutException($"작업 {taskId}가 {maxAttempts}번의 시도에도 완료되지 않았습니다");
}

이 솔루션은 두 가지 핵심 문제를 해결합니다. 첫째, 일시적인 서비스 불가능 시 API 과부하를 방지하고, 둘째, 시간 초과 시 명확한 예외를 던져 적절한 로깅과 알림을 가능하게 합니다.

Google AdInline article slot

출력 정제: Markdown에서 세미틱 HTML로

YandexGPT는 형식이 일관되지 않은 Markdown을 반환합니다: #, ##, *, 뿐만 아니라 임의의 문자(////, ----, ****)도 포함됩니다. 단순한 정규 표현식 교체만으로는 충분하지 않습니다. 우리는 Markdig 라이브러리를 기반으로 한 파서를 구현하여 화이트리스트 모드로 안전한 변환을 지원합니다:

public string ConvertToHtml(string markdown)
{
    if (string.IsNullOrWhiteSpace(markdown)) return string.Empty;

    var pipeline = new MarkdownPipelineBuilder()
        .UseAdvancedExtensions()
        .UseEmojiAndSmiley()
        .Build();

    // 1차 머리글 제거 — 글의 주제를 중복함
    var cleaned = Regex.Replace(markdown, @"^#{1,6}\s+.*$", "", RegexOptions.Multiline | RegexOptions.IgnoreCase);

    // 위험한 요소 비활성화
    var html = Markdown.ToHtml(cleaned, pipeline);
    return SanitizeHtml(html); // HtmlSanitizer를 통해 구현
}

private string SanitizeHtml(string html)
{
    var sanitizer = new HtmlSanitizer();
    sanitizer.AllowedTags.Clear();
    sanitizer.AllowedTags.UnionWith(new[] { "p", "br", "strong", "em", "ul", "ol", "li", "h2", "h3" });
    sanitizer.AllowedAttributes.Clear();
    return sanitizer.Sanitize(html);
}

이렇게 하면 출력이 유효하고 안전한 HTML로 변환되며, <script>, <iframe>, 또는 인라인 스타일이 포함되지 않습니다.

이미지 생성: Base64 검증과 저장

GenerateImageAsync 메서드는 엄격한 MIME 유형 및 크기 검증이 필요합니다. Base64 문자열은 손상되거나 JPEG가 아닌 데이터를 포함할 수 있습니다. 우리는 다음과 같은 검증을 추가합니다:

Google AdInline article slot
private bool IsValidBase64Image(string base64String)
{
    if (!base64String.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase) ||
        !base64String.Contains(";base64,"))
        return false;

    var data = base64String.Substring(base64String.IndexOf(',') + 1);
    if (!IsValidBase64String(data)) return false;

    try
    {
        var bytes = Convert.FromBase64String(data);
        using var ms = new MemoryStream(bytes);
        using var img = Image.Load(ms);
        return img.Metadata.DecodedImageOrientation == ExifOrientation.Undefined ||
               img.Width > 100 && img.Height > 100;
    }
    catch
    {
        return false;
    }
}

저장은 IWebHostEnvironment.WebRootPath/Resources/ArticleImg 경로에 이루어집니다 — ASP.NET Core의 표준 정적 파일 경로입니다. 파일 이름은 Guid.NewGuid() 대신 Path.GetRandomFileName()을 사용해 .jpg 확장자로 생성하여 고부하 상황에서도 충돌을 방지합니다.

주요 핵심 사항

  • 보안: API 키는 하드코딩되지 않고 IConfiguration을 통해 전달됩니다. 서비스 계정에는 ai.*.user 역할만 부여됩니다.
  • 신뢰성: 폴링은 지수적 백오프와 재시도 제한을 적용해 구현됩니다. 모든 HTTP 오류는 명시적으로 처리됩니다.
  • 호환성: 클라이언트는 IHttpClientFactory를 사용하여 Polly를 통해 재시도와 회로 차단을 지원합니다.
  • 출력 청결: Markdown에서 HTML로의 변환은 정규 표현식 대신 MarkdigHtmlSanitizer를 통해 이루어집니다.
  • 성능: 이미지는 WebRootPath에 저장되어 URL을 통해 미들웨어 없이 접근할 수 있습니다.

테스트를 위해서는 /foundationModels/v1/completionAsync/operations/{id}를 에뮬레이트하는 모ック 서버(예: WireMock.NET)를 사용하는 것이 좋습니다. 이를 통해 done: true, done: false, error, 429 Too Many Requests 등 모든 상태를 커버할 수 있습니다.

전체 코드는 DI 등록, 단위 테스트, 그리고 컨트롤러 사용 예제를 포함하여 dotnet-yandexgpt-sample 리포지토리에서 확인하실 수 있습니다. 생산 환경에서는 Yandex API 호출 추적을 위해 Application Insights를 반드시 구성하세요 — 지연과 오류를 진단하는 데 매우 중요합니다.

— Editorial Team

Advertisement 728x90

다음 읽기