대부분의 데이터 교환은 UTF-8로 이루어집니다. C# 11/.NET 8에서는 u8 접미사를 통해 UTF-8 바이트 시퀀스를 바로 얻을 수 있어 문자열 변환 비용을 줄일 수 있습니다. 이 글은 UTF-8 리터럴과 Span 기반 API를 활용해 성능을 끌어올리는 실용 팁을 정리합니다.
1. UTF-8 리터럴(u8) 핵심 개념
문자열 리터럴 뒤에 u8을 붙이면 ReadOnlySpan<byte>를 얻습니다. 이 값은 힙 할당 없이 UTF-8 바이트로 즉시 사용 가능합니다.
using System;
class Program
{
static void Main()
{
ReadOnlySpan<byte> hello = "안녕하세요"u8;
Console.WriteLine(hello.Length); // UTF-8 바이트 길이
// API에 바로 전달
Console.WriteLine(CountNewLines("첫줄\n둘째줄"u8));
}
static int CountNewLines(ReadOnlySpan<byte> utf8)
{
int count = 0;
foreach (byte b in utf8)
{
if (b == (byte)'\n') count++;
}
return count;
}
}
주의사항: ReadOnlySpan<byte>는 ref struct이므로 박싱/필드 저장이 불가합니다. 필요 시 byte[]로 복사해 영속화합니다.
// 영속 저장이 필요할 때
static readonly byte[] GreetingBytes = "안녕하세요"u8.ToArray();
2. 문자열 ↔ UTF-8 변환 최적화(Span, stackalloc)
문자열을 UTF-8로 인코딩할 때는 미리 크기를 계산하고 Span 버전을 사용하면 추가 할당을 줄일 수 있습니다.
using System;
using System.Text;
static byte[] ToUtf8Bytes(string s)
{
int byteCount = Encoding.UTF8.GetByteCount(s);
Span<byte> buffer = byteCount <= 256 ? stackalloc byte[byteCount] : new byte[byteCount];
int written = Encoding.UTF8.GetBytes(s.AsSpan(), buffer);
return buffer[..written].ToArray(); // 필요 시만 배열로 변환
}
UTF-8 바이트를 문자열로 디코딩할 때도 Span 기반 API로 불필요한 중간 배열을 피할 수 있습니다.
using System;
using System.Text;
static string FromUtf8(ReadOnlySpan<byte> utf8)
{
int charCount = Encoding.UTF8.GetCharCount(utf8);
Span<char> charBuf = charCount <= 256 ? stackalloc char[charCount] : new char[charCount];
int written = Encoding.UTF8.GetChars(utf8, charBuf);
return new string(charBuf[..written]);
}
3. 문자열 대신 UTF-8 바이트로 파싱/검색
텍스트를 굳이 string으로 변환하지 않고 바이트 상태에서 처리하면 비용을 크게 절약할 수 있습니다. .NET 8의 SearchValues를 활용하면 다중 구분자 검색이 빠릅니다.
using System;
using System.Buffers;
static int CountDelimiters(ReadOnlySpan<byte> utf8)
{
var delims = SearchValues.Create(stackalloc byte[] { (byte)',', (byte)';' });
int count = 0;
ReadOnlySpan<byte> rest = utf8;
while (true)
{
int idx = rest.IndexOfAny(delims);
if (idx < 0) break;
count++;
rest = rest[(idx + 1)..];
}
return count;
}
// 사용 예
int c = CountDelimiters("a,b;c"u8);
문자 의미 단위(코드포인트) 단위 처리가 필요하면 Rune을 사용합니다.
using System;
using System.Text;
foreach (var rune in "이모지🙂".EnumerateRunes())
{
Console.WriteLine(rune); // UTF-16 surrogate를 안전하게 건너뜀
}
4. 파일/네트워크 I/O에서 UTF-8 설정
파일 저장 시 BOM을 피하려면 UTF8Encoding(false)를 명시합니다. 네트워크 전송은 바이트 중심으로 처리해 불필요한 변환을 줄이세요.
using System.IO;
using System.Text;
// BOM 없는 UTF-8 파일 쓰기
using var w = new StreamWriter("data.txt", append: false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
w.WriteLine("데이터");
// HttpClient JSON 전송
using var client = new System.Net.Http.HttpClient();
var content = new System.Net.Http.StringContent("{\"a\":1}", Encoding.UTF8, "application/json");
var resp = await client.PostAsync("https://api.example.com", content);
5. u8 리터럴 실전 패턴
프로토콜 상수, 헤더 이름, 고정 토큰 등은 u8로 정의하고 바이트 기반 파서에 직접 전달합니다. string이 필요한 API에만 최소한으로 변환하세요.
// 프로토콜 상수 (필드 대신 프로퍼티 사용 가능)
static ReadOnlySpan<byte> CrLf => "\r\n"u8;
// 파서에 직접 전달
bool EndsWithCrLf(ReadOnlySpan<byte> data)
{
return data.Length >= 2 && data[^2] == (byte)'\r' && data[^1] == (byte)'\n';
}
6. 성능 팁과 주의사항
1) 작은 버퍼는 stackalloc으로, 큰 버퍼는 배열로. 2) 문자열 생성은 최후의 수단으로. 3) 잘못된 UTF-8 입력을 고려해 DecoderFallback을 설정하거나 예외로 처리합니다. 4) u8 리터럴은 ReadOnlySpan<byte>이므로 필드에 저장할 수 없습니다. 필요하면 byte[]로 복사하세요.
using System.Text;
// 잘못된 입력 시 예외 발생하도록 설정
var utf8Strict = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
정리: u8 리터럴과 Span 기반 Encoding API를 조합하면 문자열-바이트 간 변환을 최소화하고, 파싱/검색을 바이트 레벨에서 처리하여 성능을 크게 개선할 수 있습니다.
'C#' 카테고리의 다른 글
| C# PeriodicTimer로 주기적 작업 구현하기 (0) | 2026.07.28 |
|---|---|
| C# Random.Shared와 안전한 난수 생성 전략 (0) | 2026.07.27 |
| C# init 접근자를 활용한 불변 객체 설계 (0) | 2026.07.26 |
| C# required 키워드로 필수 초기화 강제하기 (0) | 2026.07.26 |
| C# CallerArgumentExpression으로 검증 메시지 자동화 (0) | 2026.07.24 |