C# (CSharp)에서의 Base64 인코딩: 완전한 가이드
바이트를 손에 쥐었습니다. JSON 응답 안에서 옮겨야 할 PNG, URL에 들어맞아야 할 토큰, 글자와 숫자만 받는 시스템으로 곧 들어갈 한 줄의 텍스트. 손안의 byte[]와 그것이 반드시 건너야 할 채널 사이 어딘가에서, C#은 Base64 인코더 메뉴를 내밀고, 그 사이에서 고르는 일이 바로 이 분야의 진짜 실력입니다. 클래식 원라인러는 2003년부터 프레임워크에 있었고, 스판 기반과 URL 안전 옵션은 최신 런타임과 함께 왔으며, 각각은 크기, 줄바꿈, 알파벳에 대해 서로 다른 약속을 합니다. 이 글은 메뉴를 전부 거닐며, 인코더에게 부탁되는 모든 실제 일을 위한 작동 예제를 갖습니다.
이 사이트의 홈 페이지가 포맷을 깊이 있게 설명해 두었으니, 먼저 집안 규칙을 한숨에 정리합니다: 인코더는 바이트 3개를 받아 64기호 알파벳에서 문자 4개를 쓰고, 꼬리에 = 한두 개로 패딩하므로, 당신의 데이터는 도착한 것보다 약 33퍼센트 뚱뚱해진 채로 나갑니다. 그 숫자, 어떤 코드도 아니라, 이 글에서 가장 중요한 사실이며, 아래 모든 이야기는 그 대가를 합리적으로 치우는 방법에 관한 것입니다.
인코더 메뉴: 도구를 고르세요
.NET 세계의 인코딩 API 패밀리 전체를, 각각이 어떤 상황을 위해 만들어졌는지와 함께 보여 드릴게요. 전부 런타임 자체에 들어 있습니다. 다만 더 오래된 프레임워크에서 쓰는 URL 안전 클래스는 작은 NuGet 패키지로 함께 들어오죠:
| API | 사용 가능 | 용도 |
|---|---|---|
Convert.ToBase64String(byte[]) |
.NET Framework 1.1 (2003) | 클래식 그 자체. 배열 통째로 들어가고, 패딩된 문자열 나옵니다. 옵션도, 예상 밖도 없습니다. |
Convert.ToBase64String(byte[], int, int) |
.NET Framework 1.1 (2003) | 더 큰 배열의 한 조각을, 먼저 밖으로 복사하지 않고 인코딩합니다. |
Convert.ToBase64String(byte[], Base64FormattingOptions) |
.NET 2.0 (2005) | 다이얼이 달린 클래식: 원하면 76자마다 줄바꿈을 넣습니다. MIME 방식이죠. |
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) |
.NET Core 2.1 (2018) | 스판 버전: 버퍼로의 뷰를 인코딩하며, 배열 복사도, 조각 할당도 없습니다. |
Convert.ToBase64CharArray(byte[], int, int, char[], int) |
.NET Framework 1.1 (2003) | 당신이 가진 문자 버퍼에 쓰고, 몇 개의 문자를 썼는지 돌려 받습니다. |
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) |
.NET Core 2.1 (2018) | 예외 대신 부울 값: 버퍼에 들어맞으면 인코딩하고, 그렇지 않으면 false를 보고합니다. |
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace |
.NET Core 2.1 (2018) | 엄격한 스판 패밀리: 예외 대신 상태 코드, 그리고 이미 가진 버퍼의 인플레이스 팽창. |
System.Buffers.Text.Base64Url.EncodeToString와 형제들 |
.NET 9 (2024) | URL 안전 알파벳, 패딩 없이 출력합니다. .NET Framework 4.6.2 이상과 .NET Standard 2.0에서는: Microsoft.Bcl.Memory NuGet 패키지. |
ToBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | 스트리밍: 파일이 흐르는 대로 인코딩하며, 페이로드 전체를 메모리에 안고 있지 않습니다. |
프로젝트가 2018년 이후의 .NET 버전을 목표로 한다면, 위 일곱 줄과 맨 아래의 스트리밍 쌍은 박스 안에 이미 들어 있습니다. Base64Url은 .NET 9 이상, 아니면 그보다 오래된 버전에서는 Microsoft.Bcl.Memory 패키지가 필요합니다. 앞으로의 소식도 덧붙이면: 작성 시점에 프리뷰 중인 .NET 11 라이브러리는(일반 릴리스는 2026년 말 예정) 기존 타입에 Base64 편의 API와 오버로드를 더하며, 메뉴는 계속 커져 갑니다. 이 글의 다른 패키지는 필요하지 않습니다.
표준 호출: Convert.ToBase64String
C#에서의 인코딩 생활의 90퍼센트는 하나의 호출입니다. 바이트를 주면, 그것을 싣고 다니는 문자열을 돌려 줍니다:
using System;
using System.Text;
string text = "Man";
byte[] bytes = Encoding.UTF8.GetBytes(text);
string packed = Convert.ToBase64String(bytes);
Console.WriteLine(packed);
// TWFu
두 단계 모양에 주목하세요. 이것이 C#에서 가장 흔한 "왜 내 Base64가 안 맞는 거지" 질문에 대한 답이니까요. string을 직접 받는 오버로드는 없습니다. 그리고 그것은 의도된 것입니다: C# 문자열은 UTF-16이라, 당신이 "이 텍스트를 인코딩해 줘"라고 했을 때 어느 바이트를 뜻하는지 프레임워크가 추측하기를 거부합니다. 먼저 Encoding.UTF8.GetBytes(또는 데이터의 실제 문자 집합)으로 바이트 표현을 고르고, 그때서야 Base64 단계가 일어납니다. 클래식 패밀리의 나머지는 같은 호출의 더 허리 졸인 버전입니다: (byte[], int, int) 오버로드는 버퍼의 한 조각을 조각을 밖으로 복사하지 않고 인코딩하고, 스판 오버로드는 ReadOnlySpan<byte>에서 같은 일을 합니다. 데이터가 더 큰 읽기 버퍼로 향한 윈도우일 때 바로 그 도구죠. 클래식 인코더의 또 다른 성질은 담백하게 말해 둘 가치가 있습니다: 실패하지도, 묻지도 않습니다. 언제나 표준 알파벳을 출력하고, 언제나 패딩을 포함하며, 같은 입력에는 언제나 같은 문자열을 줍니다. 그래서 Base64 문자열은 그것을 만들어 낸 바이트의 신뢰할 만한 지문입니다.
76자 문제: 줄바꿈과 Base64FormattingOptions
클래식 인코더에는 다이얼이 하나 있고, .NET 2.0부터 있었죠: Base64FormattingOptions. InsertLineBreaks로 설정하면 인코더는 출력 76자마다 줄바꿈을 넣습니다. MIME 규격이 이메일 첨부 파일에 쓰는 줄 길이죠. None으로 설정하거나, 옵션 없는 오버로드를 쓰면, 길고 끊기지 않는 문자열 하나가 나옵니다:
using System;
byte[] bytes = new byte[90];
string plain = Convert.ToBase64String(bytes);
string wrapped = Convert.ToBase64String(bytes,
Base64FormattingOptions.InsertLineBreaks);
Console.WriteLine(plain.Length); // 120
Console.WriteLine(wrapped.Length); // 122, 76번째 문자 뒤에 줄바꿈 하나 추가
그 다이얼에 관한 실무상 중요한 세부 사항은 두 가지입니다. 첫째, 넣는 줄바꿈은 윈도우 쌍, 캐리지 리턴-라인 피드입니다. 단순 라인 피드가 아니죠. 그래서 줄바꿈된 출력은 \r\n 시퀀스를 포함하고, 나중에 \n만 제거해 문자열을 "정리"하는 코드는 데이터 속에 숨어 있는 떠돌이 캐리지 리턴을 남겨 둡니다. 둘째, 줄바꿈은 인코딩된 출력 76자마다 일어납니다. 그래서 MIME 표준은 76자 또는 78자의 줄 한계를 가진 이메일 전송이 Base64 4자 그룹을 줄 사이에 나눠 놓을 일이 결코 없음을 보장할 수 있었죠: 76은 4의 배수라, 모든 줄이 그룹 경계에서 끝나니까요. 이메일 바디, PEM 스타일 텍스트 블록, 레거시 메일 파이프라인이 실어 나를 어떤 것을 만들어 낼 때는 줄바꿈된 형태가 필요합니다. 그 외 모든 곳에서는 줄바꿈 없는 형태가 필요합니다: JSON 페이로드, URL 토큰, API 응답, 그리고 놀라움을 싫어하는 엄격한 파서가 디코딩할 파일까지요. 그리고 JWT 안에는 줄바꿈된 형태를 절대 원하지 마세요. 그 규격은 줄바꿈, 공백, 심지어 패딩까지 명시적으로 금하고 있거든요.
출력을 소유하기: char 버퍼와 Try API
가끔 문자열이 목표가 아니라, 버퍼가 목표입니다. 고정 크기 문자 배열에 덧붙이거나, 프로토콜 프레임에 쓰거나, 그냥 런타임이 당신 대신 출력을 할당하는 것이 싫거나. 그런 순간들을 위해 인코더는 1.1 시대부터 char 버퍼 모드를, 스판 시대부터 Try 모드를 갖고 있습니다. char 버퍼 메서드는 당신이 제공한 배열에 쓰고 몇 개의 문자를 썼는지 알려 주므로, 버퍼 크기를 정하는 일은 당신의 일입니다. 표준 라이브러리는 크기를 정하는 공식까지 건네 주죠:
using System.Buffers.Text;
using System.Text;
byte[] bytes = Encoding.ASCII.GetBytes("Man");
char[] buffer = new char[Base64.GetMaxEncodedToUtf8Length(bytes.Length)];
int written = Convert.ToBase64CharArray(bytes, 0, bytes.Length, buffer, 0);
string packed = new string(buffer, 0, written);
Console.WriteLine(packed);
// TWFu
Try 형제는 스판에서 같은 일을 하며 부울로 답합니다. 입력을 당신의 대상 스판에 인코딩하고, 문자 수를 out-인자로 보고하며, 대상이 너무 작으면 아무것도 쓰지 않고 false를 돌려 줍니다. 마지막 성질이 신뢰할 수 없는 입력 크기와 함께 써도 안전하게 만듭니다: 실패한 호출에서 반쯤 채워진 버퍼를 받는 일은 없으니까요:
using System;
byte[] bytes = { 1, 2, 3 };
char[] buffer = new char[4];
if (Convert.TryToBase64Chars(bytes, buffer, out int written,
Base64FormattingOptions.None))
{
Console.WriteLine(new string(buffer, 0, written));
// AQID
}
else
{
Console.WriteLine("Buffer too small, nothing was written.");
}
System.Buffers.Text.Base64의 엄격한 스판 패밀리에겐, 부울 대신 OperationStatus 계약으로 같은 모양이 존재합니다: EncodeToUtf8는 당신이 가진 바이트 스판을 채우고, 상태를 통해 끝냈는지, 공간이 떨어진 건지, 더 많은 입력이 필요한 건지 알려 주며, EncodeToUtf8InPlace는 바이너리 데이터가 이미 당신을 기꺼이 커져 주려는 버퍼에 앉아 있을 때 손이 가는 것입니다: 인코딩은 데이터를 부풀리므로, 메서드는 Base64 텍스트를 같은 버퍼의 끝 위에 쓰고, 결과가 얼마나 긴지 보고합니다. 이 모든 것은 크기에 관한 하나의 규칙을 공유합니다: n개의 입력 바이트의 출력은 패딩을 포함해 언제나 4 * ceil(n / 3)문자이며, GetMaxEncodedToUtf8Length와 Base64Url.GetEncodedLength 헬퍼가 그 산수를 구현합니다 - 후자는 패딩 없는 길이용이며, 언제나 패딩된 크기와 같거나 짧으니까요. 그래서 크기는 헬퍼에서 정하고, 기억해 둔 상수로 절대 정하지 마세요.
URL 안전 인코더: Base64Url
표준 알파벳에는 URL이 싫어하는 문자가 둘 있습니다. 쿼리 문자열의 +는 폼 파싱 규칙에 따라 늘 스페이스로 디코딩되고, /와 =도 경로나 파라미터에 올라려면 퍼센트 인코딩이 필요합니다. RFC 4648 5절에서 정의한 Base64의 URL 안전 변형은 +와 /를 어디에서도 이스케이프가 필요 없는 -와 _로 바꾸고, 꼬리의 = 패딩을 선택 사항으로 만듭니다. .NET 9부터 런타임에는 이를 위한 전용 클래스 System.Buffers.Text.Base64Url이 있고, 처음에 사람들을 놀라게 하는 행동 하나를 갖습니다: 패딩을 아예 출력하지 않는다는 것입니다:
using System.Buffers.Text;
byte[] bytes = { 1, 2 };
string classic = Convert.ToBase64String(bytes);
string urlSafe = Base64Url.EncodeToString(bytes);
Console.WriteLine(classic); // AQI=
Console.WriteLine(urlSafe); // AQI
그 차이가 바로 전부입니다. JWT 세그먼트, 업로드 식별자, 쿼리 문자열의 토큰, URL 경로의 값: 전부 패딩 없는 URL 안전 형태를 원하고, Base64Url.EncodeToString은 그것을 바로 줍니다. 알파벳과 패딩을 그 포맷들이 지정한 대로 처리해 주죠. 이 클래스는 패밀리를 전부 갖습니다: 문자열로, char 스판으로, UTF-8 바이트 스판으로 인코딩, 그리고 버퍼 크기를 정하는 GetEncodedLength와 들어오는 입력을 검증하는 IsValid까지요. 프로젝트가 오래된 런타임에서 돌라면, 그 클래스를 .NET Framework 4.6.2 이상으로 백포트하기 위해 Microsoft가 발행한 Microsoft.Bcl.Memory 패키지를 추가하세요:
dotnet add package Microsoft.Bcl.Memory
패키지를 쓸 수 없다면, 손수 만든 버전은 클래식 인코더에 리플레이스 두 개와 트리밍 하나를 더한 것이며, 수많은 C# 코드베이스에서 마주치게 될 것입니다:
using System;
using System.Text;
byte[] bytes = Encoding.UTF8.GetBytes("Hello World!");
string packed = Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
Console.WriteLine(packed);
// SGVsbG8gV29ybGQh, URL 안전이고 패딩 없음
그 체인에서 작업 순서는 주목할 만합니다: 문자 교환은 표준 출력에서 일어나고, 패딩 트리밍은 마지막에 합니다. 먼저 트리밍하면 아무것도 바뀌지 않으면서 코드를 읽기 어렵게 만들고, 트리밍 후에 교환하면 그래도 작동하지만, 섬세한 버그가 태어나는 길이거든요. 토큰, 식별자, URL에서 살게 될 모든 것은 이 모양을 쓰세요. +, /, =가 완전히 안락하게 머무는 이메일 바디, JSON 페이로드, 파일에는 표준 알파벳을 남겨 두세요.
인코더에게 먹이기: 문자열, 문자 집합, 인코딩 선택
텍스트에서 시작하는 모든 인코딩 일은 같은 고요한 결정으로 시작합니다: 이 텍스트는 어느 바이트가 될 것인가? Base64 단계는 결정적이고 무죄지만, 그 앞의 Encoding 단계가 출력을 갈라놓는 곳이며, 그 갈림은 침묵 속에 일어날 수 있습니다. UTF-8은 현대 웹의 기본 가정이며, 여기에서 맞는 기본값입니다: 모든 언어를 왕복하고, 다른 모든 플랫폼이 당신의 페이로드를 디코딩할 때 가정할 것이 바로 그것이며, Encoding.UTF8이 한 번의 호출로 주는 것이니까요:
using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
Console.WriteLine(packed);
// aMOpbGxvIOS4lueVjA==
자, 다른 문자 집합을 거쳐 인코딩된 같은 문자를 지켜 보세요. 그리고 문자 집합이 붙지 않은 "같은 텍스트"가 왜 잘 정의된 것이 아닌지 보고:
using System;
using System.Text;
string euro = "\u20ac";
string asUtf8 = Convert.ToBase64String(Encoding.UTF8.GetBytes(euro));
string asLatin1 = Convert.ToBase64String(
Encoding.GetEncoding("ISO-8859-1").GetBytes(euro));
Console.WriteLine(asUtf8); // 4oKs
Console.WriteLine(asLatin1); // Pw==
같은 유로 기호에 Base64 문자열 둘, 둘 다 완벽하게 유효하며, 오직 하나만이 저쪽에서 유로 기호로 디코딩됩니다. 가장 넓은 폭발 반경의 함정은 Encoding.Default입니다: Windows의 .NET Framework에서는 시스템의 ANSI 코드 페이지이고, .NET (Core)에서는 UTF-8이라, Encoding.Default로 인코딩하는 프로그램은 2010년 머신과 2025년 머신에서 다른 Base64를 만들고, 두 출력 모두 자기 집 플랫폼에서는 "정확히" 디코딩됩니다. 디코딩된 페이로드가 발음 기호가 깨진 글자로 가득히 도착했다면, 원래 인코딩이 디코딩이 가정했던 것보다 다른 문자 집합을 쓴 것이며, 수정은 파이프의 이쪽, 즉 작성한 팀보다 오래 살 코드에서 양 방향으로 인코딩을 명시적으로 고정하는 것입니다. 그리고 타입 시스템 자체에 관한 마지막 노트: C# 문자열은 UTF-16이라, 만약 원본 UTF-16 코드 단위를(Encoding.Unicode.GetBytes을 호출해) 인코더에게 건네면, 모든 ASCII 문자가 두 바이트를 쓰고, 당신의 출력은 이득 없이 두 배로 커집니다. 저쪽의 디코더가 그것을 당신의 원본 문자열의 바이트가 아니라 UTF-16 텍스트로 읽기 때문이죠. Base64는 당신이 건네는 바이트를 싣고, 그것이 무슨 뜻인지 신경 쓰지 않습니다.
파일: 디스크에서 문자열로
파일은 가장 흔한 인코딩 페이로드이자 가장 관대한 대상입니다. 문자 집합 문제가 없기 때문이죠: 디스크 위의 바이트가 데이터이며, 인코더는 그것이 단어를 이끄는지 파형을 이끄는지 신경 쓰지 않습니다. 왕복은 읽기, 인코딩, 쓰기이며, 유일한 진짜 결정은 결과가 어디로 가느냐입니다:
using System.IO;
byte[] bytes = File.ReadAllBytes("photo.png");
string packed = Convert.ToBase64String(bytes);
File.WriteAllText("photo.b64", packed);
Console.WriteLine(packed.Length + " characters for "
+ bytes.Length + " bytes of image.");
크기 계산이 전체 이야기이며, 전송을 고르기 전에 하는 가치가 있습니다. 1메가바이트 파일은 Base64 문자 1,333,336개가 되고, C# 문자열은 문자 하나당 두 바이트를 저장하므로, 그 인코딩된 결과는 문자열로서 메모리에서 약 2.7 메가바이트를 차지합니다. 10 메가바이트 파일은 26 메가바이트의 관리 메모리에 앉아 있는 13 메가바이트 문자열이 됩니다. 사진이나 설정 블롭이라면 그 어느 것도 문제가 되지 않으며, 페이로드가 영상이면 아래 스트리밍 인코더를 써야 할 아주 좋은 이유입니다. 위 패턴은 메모리에 넉넉히 들어 맞는 모든 것에 손이 가는 것이며, 모든 "JSON 바디에 파일을 Base64로 업로드" 기능도 조용히 쓰고 있는 바로 그 패턴입니다: 파일을 읽고, 인코딩하고, 문자열을 JSON에 넣고, API 레이어가 일을 하도록 하는 것.
웹의 이미지: 데이터 URI 만들기
인코딩된 이미지의 가장 눈에 띄는 소비자는 웹이며, 웹의 "문서 안에 사는 이미지" 포맷이 바로 데이터 URI입니다: data: 스킴 뒤에 MIME 타입, ;base64 플래그, 쉼표, 그리고 인코딩된 바이트. C#에서 하나를 만들어 내는 일은 문자열 연결이며, 모든 진짜 일은 인코더가 합니다:
using System.IO;
byte[] png = File.ReadAllBytes("logo.png");
string packed = Convert.ToBase64String(png);
string dataUri = "data:image/png;base64," + packed;
Console.WriteLine(dataUri.Substring(0, 30));
// data:image/png;base64,iVBORw0K
그 출력의 iVBORw0KGgo 접두어는 유용한 체크포인트입니다: 8바이트 PNG 시그니처의 Base64 형태이기 때문에, 당신이 인코딩하는 어떤 PNG든 그렇게 시작하고, 그렇지 않은 PNG 데이터 URI는 PNG가 아닙니다. 이 패턴에는 실용적인 노트 세 가지가 함께합니다. 첫째, 데이터 URI는 3분의 1 불어난 이미지의 완전한 사본으로 당신의 HTML이나 CSS에 임베드되므로, 네트워크 요청을 영구적인 페이지 무게와 바꾸는 것입니다. 4 KB 파비콘에는 득이 되는 거래이고, 4 MB 히어로 이미지에는 손해이며, 인코더는 33퍼센트에 대해 흥정하지 않습니다. 둘째, 이미지가 크다면 인코딩 전에 크기를 줄이거나 재압축하세요. 원본의 모든 바이트가 페이지에 나타나니까요. 셋째, 사용자에 노출되는 HTML에서 사용자가 제공한 SVG에는 주의하세요. SVG는 스크립트를 싣을 수 있으므로, 그걸 임베드하는 것 - 인라인으로든, <object>/<embed>로든 - 은 고전적인 XSS 표면입니다. 단순한 <img> 데이터 URI는 현대 브라우저에서 그것을 실행하지 않지만, 같은 마크업이 그 맥락에서 재사용되면 실행합니다. 데이터 URI 안의 PNG, JPEG, GIF, WebP는 무해하며, SVG만이 무해하지 않습니다.
손으로 JWT 조립하기
JSON Web Token을 처음부터 만들어 보는 것은 통과 의례이며, 조각들이 짧으므로 C#에서는 대부분의 언어보다 나은 의례입니다. JWT는 점으로 이어진 base64url 세그먼트 세 개입니다: 인코딩된 헤더, 인코딩된 페이로드, 그리고 서명. 첫 둘은 UTF-8 JSON 문서이며, 서명은 점으로 이어진 첫 두 세그먼트 위에 계산됩니다. 조립 전체는 이렇습니다. 서명은 대체물로 넣었는데, 암호학 단계는 Base64 이야기가 아니라 당신의 서명 키에 속하기 때문이죠:
using System;
using System.Buffers.Text;
using System.Text;
string headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
string payloadJson = "{\"sub\":\"42\",\"name\":\"Ada\"}";
string header = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(headerJson));
string payload = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payloadJson));
string signature = "c2lnbmF0dXJl"; // 진짜 HMAC 또는 ECDSA 값의 대체물
string jwt = header + "." + payload + "." + signature;
Console.WriteLine(jwt);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl
Base64Url.EncodeToString의 두 성질이 그 예제에서 조용한 일을 합니다. URL 안전 알파벳을 출력하므로 토큰에 +도 /도 나올 수 없고, 패딩을 생략하므로 =도 결코 나오지 않습니다. JWS 규격이 요구하는 바로 그것이며, 도움이 없다면 Convert.ToBase64String이 하지 않을 바로 그 일이죠. .NET 9 이전 런타임이면, 같은 일은 표준 인코더에 URL 안전 섹션의 손질 체인을 얹어 돌아갑니다: 인코딩, 두 문자 교환, 패딩 트리밍. 세그먼트 순서는 서명에 중요합니다. 서명은 header에 점을 더하고 payload를 더한 것을 단순 ASCII 바이트로 계산하므로, 먼저 두 세그먼트를 조립한 뒤, JSON을 다시 포맷한 버전이 아니라 그 정확한 결합체에 서명하세요. 그리고 날카롭게 유지할 경계: 사용자가 닿을 수 있는 것에 대해서는 아예 손으로 JWT를 조립하지 마세요. System.IdentityModel.Tokens.Jwt 패키지가 조립, 서명, 검증, 만료 처리를 당신을 대신해 하며, 그 base64url 처리는 정확히 이 알파벳과 패딩 규칙입니다. 손 조립은 테스트와 데모, 그리고 라이브러리가 정확히 무엇을 하는지 알아야 할 날을 위한 것입니다.
HTTP 헤더: 기본 인증
Base64는 기본 인증 스킴으로 순수 HTTP에 등장하며, 인코딩 쪽은 이 프로토콜에서 가장 짧은 헤더 빌더 중 하나입니다: 사용자 이름과 비밀번호를 콜론으로 붙이고, 결과를 UTF-8로 인코딩하고, Base64로 만들고, 스킴 이름을 접두로 붙입니다:
using System;
using System.Text;
string user = "ada";
string password = "s3cret";
string credentials = user + ":" + password;
string header = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials));
Console.WriteLine(header);
// Basic YWRhOnMzY3JldA==
문자 집합이 까다로운 부분입니다: RFC 7617은 호환성을 위해 기본 인증 스킴의 기본 문자 집합을 정의하지 않은 채, 권고 사항인 UTF-8 힌트만 제공하지만, 바로 그것이 모든 현대적인 서버가 기대하는 것이므로, 발음 기호가 있는 사용자 이름은 플랫폼 기본값이 무엇이든 Encoding.UTF8를 거쳐야 하며, 그렇지 않으면 서버가 다른 바이트 문자열을 디코딩해 로그인을 거부합니다. Base64 단계가 헤더의 유일한 인코딩입니다: 결과를 퍼센트 인코딩하지 말고, URL 인코딩하지도 말고, Base64를 이중으로 하지 마세요. 그 "도움" 같은 추가 단계 하나하나가 알려진 버그이며, 이중 인코딩이 가장 흔합니다. 자격 증명이 이미 Base64로 만든 레이어에서 미리 인코딩되어 도착하기 때문이고, 두 번째 인코딩은 그럴듯해 보이다가 서버에서 조용히 실패하는 헤더를 만들어 내니까요. 스킴 자체에 관한 경고 두 가지입니다. 보안 섹션에서 희석될 테니 여기 두고 갑니다: 기본 인증은 비밀번호를 명령 하나면 읽을 수 있는 형태로 전송하므로, TLS 위에서만 허용할 만하고, 그것조차 대부분의 API 작업에는 잘못된 도구라서, 베어러 토큰과 JWT가 그 자리를 차지했습니다. 이 모든 일에서 인코더의 임무는 작고 솔직한 것입니다: 콜론으로 붙인 자격 증명을 헤더에 안전한 문자열로 바꾸는 것, 그것뿐.
이메일: MIME과 ToBase64Transform이 줄바꿈하지 않는 이유
이메일은 Base64의 역사적 고향이며, 여전히 76자 줄 규칙이 나온 곳입니다: MIME 규격은 인코딩된 바디를 76자마다 줄바꿈하고 줄 사이에는 CRLF를 넣어서, 어떤 SMTP 홉도 다시 줄바꿈할 이유가 없게 합니다. C#은 이 일을 위한 인코더 둘을 주며, 둘은 서로 다른 약속을 합니다. 하나를 고르기 전에 이해할 가치가 있는 부분이죠. 첫 번째는 줄바꿈 섹션에서 본 InsertLineBreaks를 붙인 클래식 Convert.ToBase64String이며, 정확히 MIME 모양입니다: 76자에 CRLF로 줄바꿈되어, Content-Transfer-Encoding: base64 헤더 아래에 붙여 넣기만 하면 됩니다. 두 번째는 스트리밍 사촌 ToBase64Transform인데, 여기가 놀라운 부분입니다: 줄바꿈을 넣지 않습니다. 그 용도의 모드도, 옵션도, 생성자 플래그도 없으며, 출력은 줄바꿈 없는 긴 스트림 하나입니다:
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("photo.png");
using MemoryStream destination = new MemoryStream();
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);
encoder.CopyTo(destination);
Console.WriteLine(destination.Length + " characters, no line breaks");
그래서 실용적인 규칙은 이렇습니다: 작은 규모에서 중간 규모의 이메일 페이로드는 바이트를 읽고 줄바꿈하는 클래식 인코더를 쓰세요. MIME 모양이 바로 나오니까요. 큰 첨부 파일은 메모리를 평탄하게 유지하려고 ToBase64Transform으로 스트리밍하고, 전송이 정말로 76자 줄을 원한다면 결과를 스스로 줄바꿈하세요. 그룹 경계에서 출력을 나누는 것이죠(76자마다-줄바꿈 섹션이 설명했듯이 항상 그룹 경계가-). 변환기가 줄바꿈 없이 남는 것은 맞는 일입니다: 입력을 바이트 3개의 그룹으로 처리하며, 줄바꿈은 파이프에서 바이트를 문자로 변환하는 레이어가 아니라, 전송을 아는 레이어에 속하는 포맷 결정이니까요.
스트리밍: 두 번 읽지 않고 큰 파일 인코딩
페이로드가 영상이거나, 백업이거나, 문자열로 안고 있으면 부끄러운 어떤 것이면, 스트리밍 인코더가 전체 해결책입니다. 패턴은 디코딩 쪽 스트리밍의 거울입니다: 소스 파일 위의 CryptoStream, 읽기 모드의 ToBase64Transform, 그리고 대상으로의 CopyTo. 파일이 흘러 들어오고, Base64가 흘러 나가며, 프로세스가 안고 있는 유일한 메모리는 스트림이 내부적으로 쓰는 버퍼입니다:
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("video.mp4");
using FileStream target = File.Create("video.b64");
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);
encoder.CopyTo(target);
Console.WriteLine("Wrote " + target.Length + " characters.");
이 패턴에 관한 사실 두 가지는 넣어 둘 가치가 있습니다. 첫째, 출력 크기는 입력 크기로 완전히 결정됩니다: 3바이트당 4문자. 그래서 바이트 하나가 흘러가기 전에 대상 공간을 예약하거나, content-length 헤더의 길이를 미리 계산하거나, 디스크 할당량을 책정할 수 있습니다. 둘째, 변환기는 바이트 3개의 그룹으로 입력을 기대하며, CryptoStream이 그 정렬을 당신을 대신해 처리합니다: 파일이 스쳐 지나가는 동안 변환기가 원하는 것을 정확히 먹여 주죠. 만약 TransformBlock으로 변환기를 직접 움직여야 한다면, 3의 배수로 먹이고, 꼬리 - 패딩 문자 하나나 둘을 가진 마지막 부분 그룹이 되는 남은 한두 바이트 - 는 TransformFinalBlock이 비우게 두세요. 대부분의 애플리케이션에서는 CopyTo 형태가 당신이 영원히 쓸 전부이며, 그것이 바로 메모리 한계 아래에서 잘 행동하는 형태입니다. 그리고 바로 거기가 큰 파일이 좋아서 사는 곳이니까요.
설정, 환경 변수, 데이터베이스
C# 애플리케이션에서 또 흔한 인코딩 작업은 저장 작업입니다: 시크릿이나 바이너리 블롭을 텍스트만 받는 곳에 넣는 것이죠. 환경 변수는 눈에 보이는 예입니다. 환경 변수는 정의상 문자열이니까요:
using System;
using System.Text;
string secret = "p@ssw0rd+and/symbols";
string packed = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
Environment.SetEnvironmentVariable("SECRET_B64", packed);
string back = Encoding.UTF8.GetString(
Convert.FromBase64String(Environment.GetEnvironmentVariable("SECRET_B64")));
Console.WriteLine(back == secret);
// True
데이터베이스에서는 같은 아이디어가 보통 이렇습니다: 텍스트 컬럼에 저장되어야 하는 byte[] 프로퍼티. Entity Framework Core에는 정확히 이를 위한 내장 기전이 있습니다: 읽기와 쓰기마다 당신의 인코딩과 디코딩 함수를 실행하는 값 변환기입니다:
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
그 변환기가 데이터베이스 통합의 전부입니다: C# 코드는 byte[]를, 컬럼은 Base64 문자열을 보며, 왕복은 호출 지점에서 보이지 않습니다. 이 섹션에는 경고 두 가지가 함께해야 합니다. 첫째, 컬럼이 33퍼센트 세금을 냅니다: 인코딩된 길이에 맞춘 너비의 텍스트 컬럼은 같은 너비의 바이너리보다 3분의 1 적은 데이터를 담으므로, 고정 너비의 컬럼이 있으면 Base64 길이에 맞춰 크기를 정하세요. varchar(max)나 그와 동등한 것이면, 세금은 청구 문제일 뿐입니다. 둘째, 그리고 이것은 계속 돌아오는 부분입니다: 설정 파일의 Base64는 모양이지 방패가 아닙니다. 값을 한 줄에 머물게 하고, 텍스트 에디터의 눈치를 피해 주지만, 파일을 읽을 수 있는 사람에게는 명령 하나만큼의 거리에 읽힐 수 있는 상태입니다. 시크릿은 진짜 보호가 필요합니다: 시크릿 저장소, 키 볼트, 최소한 파일 권한. 그리고 Base64는 시크릿이 설정에 앉아 있는 동안 입는 전송 포맷일 뿐입니다.
명령줄에서
모든 인코더는 15줄짜리 콘솔 삶을 받을 자격이 있으며, C# 쪽은 기쁩니다: 출력이 표준 출력을 위해 만들어진 단순 문자열이니까요. 도구 전체는 이렇습니다: 파일 경로나 표준 입력을 받아, 인코딩하고, 어떤 셸 파이프라인이든 가져갈 수 있게 터미널로 Base64를 씁니다:
using System;
using System.IO;
using System.Text;
string input = args.Length > 0
? File.ReadAllText(args[0])
: Console.In.ReadToEnd();
byte[] bytes = Encoding.UTF8.GetBytes(input);
Console.WriteLine(Convert.ToBase64String(bytes));
한 번 빌드해 두면, .NET 인코더의 행동을 특히 원하는 날을 위해 셸 자신의 base64 유틸리티 옆에 앉아 있게 됩니다: 같은 알파벳, 같은 패딩, 그리고 파이프가 건네는 것이 무엇이든 C# 런타임의 UTF-8 처리. 바이너리 파일에는 File.ReadAllText 대신 File.ReadAllBytes를 쓴 같은 뼈대가 전부이며, 출력이 파일의 텍스트 해석이 아니라 파일의 정확한 바이트를 설명합니다. 도구는 좋은 탐침이기도 합니다: 파일을 통과시키고, 출력을 디코딩 편의 디코더를 다시 통과시킨 뒤, 두 파일을 diff하세요. 파이프 양쪽이 모든 바이트에서 일치함을 확인하는 기분 좋은 끝에서 끝 검사입니다.
패딩, 또는 꼬리의 등호
Base64 문자열의 마지막 = 문자들은 포맷의 장부이며, C#의 인코더들은 그것에 대해 의견이 다릅니다. 특정하고 흔한 상호 운용 버그의 근원이죠. 클래식 Convert.ToBase64String은 언제나 패딩합니다. 짝을 이루는 클래식 디코더가 언제나 그것을 기대하기 때문이죠. Base64Url.EncodeToString은 결코 패딩하지 않습니다. 대상인 URL 안전 소비자, JWT와 토큰 API가 언제나 컴팩트한 형태를 기대하기 때문이죠. 당신의 출력이 반대쪽 기대를 가진 세계로 건너갈 때, 해법은 산수이며, 디코딩 편이 반대 방향에 대해 보여 준 바로 그 산수입니다:
using System;
string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded); // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, URL 안전 소비자가 원하는 형태
string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored); // AQI=, 클래식 디코더가 원하는 형태
(4 - length % 4) % 4 공식이 패딩 우주의 전부입니다: 길이가 4의 배수에 떨어지도록 0, 1, 2자를 더하고, 바깥쪽 나머지 연산이 이미 패딩된 입력이 추가로 얻는 일을 막아 줍니다. 패딩에 관한 경고 두 가지입니다. 좋은 뜻의 코드가 여기서 틀어지니까요. =를 데이터로 다루지 마세요: 정보는 싣지 않으므로, 이미 패딩을 포함한 문자열을 페이로드인 것처럼 인코딩하거나, 쿼리 문자열 안에서 =를 %3D로 URL 인코딩하는 것, 둘 다 보기에는 맞고 디코딩은 틀린 출력을 만들어 내는 방법입니다. 그리고 패딩이 표준 = 대신 다른 문자로, 어떤 오래된 시스템에서는 점으로 적힌 레거시 페이로드의 작은 패밀리에 주의하세요: 받으시는 값에서 패딩이 기대되는 자리에 점이 쓰인다면, 디코딩 전에 =로 정규화하거나, 패딩 없이 URL 안전 경로를 거치세요.
얼마나 빠르게 도는지
최신 .NET의 Base64 인코딩은 빠르며, 흥미로운 부분은 CPU 이야기가 아니라 메모리 이야기입니다. 런타임의 구현은 하드웨어가 지원하는 곳에서 SIMD 벡터 명령어로 최적화되어 있고, 수 메가바이트 입력은 일반 데스크톱 머신에서 한 자릿수에서 두 자릿수 초반 밀리초 만에 인코딩됩니다. 당신이 쓰게 될 어떤 애플리케이션에서든 인코더가 사실상 무료일 만큼 빠르죠. 코드를 실제로 바꾸는 성능 조언은 모양에 관한 것입니다. 출력은 C# 문자열이며, C# 문자열은 문자 하나당 두 바이트를 저장하므로, 인코딩된 결과의 메모리 비용은 입력 바이트당 약 2.7바이트입니다 (입력 바이트 3개당 문자 4개, 문자당 2바이트). 페이로드가 메가바이트 단위라면 알아 둘 가치가 있는 숫자죠. 루프 안에서 수천 개의 작은 페이로드를 인코딩할 때, 호출마다 새 관리 문자열을 할당하는 문자열 API보다, 재사용하는 버퍼에 쓰는 스판 API와 char 버퍼 API를 선호하세요. 큰 파일 하나를 인코딩할 때, 문자열을 통째로 건너뛰고 스트리밍 변환을 쓰세요. CopyTo라면 스트림 버퍼에 일 작업 집합을 담아 둘 것을, 13 메가바이트 문자열을 안고 있는 문자당 2바이트의 비용은 순전한 낭비이니까요. 그리고 MIME 줄바꿈된 출력을 만들어 낼 때, 줄바꿈 패스는 데이터에 대한 두 번째 왕복이라는 점을 기억하세요. 그래서 전송이 필요할 때만, 기본값으로 줄바꿈하지 마세요.
보안에 대한 대화
Base64의 인코더 쪽에는 보안 교훈이 하나 있고, 디코더 쪽의 역수입니다: 읽을 수 있는 데이터를 드러내기로 선택하는 쪽은 당신이며, 포맷은 당신을 막지 않습니다. Base64는 인코딩이지 암호화가 아닙니다. 키도, 알고리즘도, 어떤 형태의 비밀도 갖지 않으며, 당신의 ToBase64String 호출의 출력은 어느 머신에서, 어떤 언어로, 누구나 명령 하나 거리에 입력을 알아볼 수 있습니다. 그래서 첫 번째 규칙은 당신이 인코딩할 것을 고르는 것에 관한 것입니다: 비밀번호, 토큰, 시크릿을 Base64로 "보호된" 설정 파일에 넣지 마세요. 그 보호는 정확히 디코딩 호출 한 번 깊이에 불과하고, 설정을 읽는 사람은 그 명령을 갖고 있거든요. 값이 시크릿이어야 한다면 진짜 보호가 필요하며, Base64는 텍스트 필드에 앉아 있는 동안 입는 모양일 뿐입니다.
두 번째 교훈은 채널에 관한 것이며, 이 글이 만들어 내는 것들에 특화되어 있습니다. 기본 인증 헤더는 어떤 프록시든, 어떤 로그든, 어떤 미들박스든 읽을 수 있는 형태로 비밀번호를 싣므로, 이 스킴은 TLS 위에서만 허용할 만하고, 레거시 통합 바깥에서는 대부분 쓸모가 없어졌습니다. HTML의 데이터 URI는 이미지를 싣고, 이미지가 사용자가 제공한 SVG라면 SVG가 싣는 것을 싣게 됩니다. 그래서 데이터 URI 안의 SVG 사례는 어떤 사용자 콘텐츠와 같은 주의를 필요로 하죠. 그리고 URL 안의 Base64 값은, 문자 그대로, URL 안에 있습니다. 즉 브라우저 히스토리, 서버 접근 로그, 리페러 헤더, 프록시 캐시에 있다는 뜻이므로, 비공개로 남아야 할 토큰은 패딩이 있든 없든 쿼리 문자열에 속하지 않습니다. 인코더는 세 경우 모두에서 자신의 솔직한 임무를 수행하며, 바이트를 들고 다니기 안전한 문자열로 바꾸고 있습니다. 보안은 당신이 무엇을, 어디에 싣는지에 있으며, 포맷은 대부분의 것보다 나은 전달자이지만, 전달자이지 금고는 아니니까요.
C# 인코더가 빠지는 함정
C# 코드 인코딩 쪽에서 계속 나타나는 함정들입니다. 그리고 그 하나하나에 모두 프레임워크 작동 방식에 구체적인 원인이 있습니다:
- 당신이 고르지 않은 문자 집합.
Encoding.Default로 문자열을 인코딩하면 .NET Framework(Windows ANSI 코드 페이지)와 .NET(UTF-8)에서 다른 Base64가 됩니다. 두 출력 모두 유효하고, 각각 자기 집 플랫폼에서는 "정확히" 디코딩되며, 같은 바이트가 아닙니다. 인코딩을 명시적으로 고정하세요. - 이중 인코딩. 입력이 이미 Base64였다면(인코딩된 값을 인코딩한 설정, 입력을 재인코딩하는 API), 인코더는 지시받은 것을 정확히 하며 Base64의 Base64를 만들어 냅니다. 결과는 그럴듯해 보이며, 한 층씩 디코딩됩니다. 그래서 디코딩 두 번이 걸려야 고쳐지는 버그가 프로덕션에서 발견되는 것이죠.
- 잘못된 자리의 줄바꿈. CRLF 쌍을 가진 MIME 줄바꿈된 형태가 JSON 문자열, JWT 세그먼트, URL 파라미터에 떨어지면, 엄격한 소비자는 알리지도 않은 공백에 걸립니다. 메일에는 줄바꿈하고, 그 외 모든 곳에는 그대로 두세요. 그리고 남의 줄바꿈을 벗길 때는
\n뿐 아니라\r도 벗기세요. - URL의 표준 알파벳. 쿼리 문자열의
+는 폼 파싱 규칙에 따라 스페이스로 디코딩되므로, URL에 넣은 표준 Base64 값은 더하기 부호가 있던 자리에 글자 그대로 돌아옵니다. URL 안전 알파벳을 쓰거나, 값을 통째로 퍼센트 인코딩하세요. 둘 다 결코 하지 마세요. - 패딩 불일치. 당신의 출력이 패딩되어 있고 소비자는 컴팩트를 원하거나, 그 반대이거나, 어느 쪽도 틀리지 않았습니다 - 그저 의견이 다를 뿐이죠. 해법은 패딩 섹션의 산수로, 소비자의 기대를 아는 쪽에 적용하는 것입니다. 보통은 토큰을 쓰는 쪽이죠.
- 책정되지 않은 메모리. 인코딩된 문자열은 메모리에서 문자당 두 바이트이므로, 10 MB 파일은 관리 메모리에서 대략 27 MB로 달리는 1300만 자 문자열이 되며, 그런 문자열을 하나씩 만들어 내는 루프는 원인이 눈에 보이지 않는 할당 변동으로 프로파일러에 나타납니다. 버퍼는 길이 헬퍼로 크기를 정하고, 큰 것은 스트리밍하고, 핫 루프에서 버퍼를 재사용하세요.
- 줄바꿈하지 않는 변환기.
ToBase64Transform은 긴 줄 하나를 출력합니다. 그것을 거쳐 "MIME 준비 완료" 첨부 파일을 스트리밍한 뒤 메일로 보내는 코드는, 어떤 전송이든 그룹 한가운데서 다시 줄바꿈할 12만 자 줄을 만들어 내며, 그것은 바로 76자 규칙이 막으려고 설계된 손상입니다. - 인코딩의 인코딩. "데이터가 이미 텍스트인데"라는 이유로 Base64 문자열을 인코더에 넣으면 두 번째 층이 생깁니다. 인코더는 입력이 Base64처럼 보인다는 것을 알지 못하고, 신경도 쓰지 않습니다. 문자열이 마침 몇 개의 문자를 갖고 있든 그것만 인코딩하고, 저쪽의 디코더는 당신의 데이터를 기대하는 자리에 Base64 문자열을 받게 됩니다.
인코더가 성장한 모습: 버전 투어
API의 인코딩 쪽에는 자기만의 타임라인이 있고, 두 번째 .NET 릴리스부터 현재 프리뷰 중인 릴리스까지 이어집니다:
- .NET Framework 1.1, 2003년 4월.
Convert.ToBase64String과ToBase64CharArray가 도착합니다. 클래식 패밀리 전체가 한 릴리스에, 조각 오버로드까지 이미 포함되었는데, 2003년 API치고는 작은 예언의 기적입니다. - .NET 2.0, 2005.
Base64FormattingOptions와InsertLineBreaks값이 패밀리에 합류하며, MIME 줄바꿈을 프레임워크로 끌어들이고, 이메일 코드에서 손수 쓴Substring루프의 시대를 끝냅니다. - .NET Core 2.1, 2018. 스판 시대.
Convert가 스판 기반 인코딩과TryToBase64Chars를 갖게 되고, 새System.Buffers.Text.Base64클래스가OperationStatus계약과 인플레이스 팽창과 함께 도착합니다. 제로 할당 세계를 위해 만들어진 것이죠. - .NET 5, 2020. 16진수 형제들(
Convert.ToHexString과 친구들)이 출시됩니다. 같은 디자인 패턴을 16기호 알파벳에 적용한 것이며, 변환 클래스 패턴이 하우스 스타일이 됩니다. - .NET 7, 2022.
X509Certificate2.ExportCertificatePem이 프레임워크가 당신을 대신해 PEM을 만들게 합니다. 아머 마커, 64자 줄바꿈, Base64 바디까지 포함해서, 손수 인증서 포맷팅 코드의 한 무리가 조용히 퇴역합니다. - .NET 9, 2024년 11월.
System.Buffers.Text.Base64Url이 수년간의 커뮤니티 요청 끝에 박스에 들어옵니다.Microsoft.Bcl.Memory패키지가 .NET Framework 4.6.2 이상으로 백포트하고, JWT 코드가 늘 손수 만들어 왔던 패딩 제거 동작도 함께죠. - .NET 11, 작성 시점에 프리뷰 중. 2026년 말에 나올 것으로 예상되는 다음 릴리스가 기존 타입에 Base64 편의 API와 오버로드를 더하며, 더 편한 표면으로의 행진을 계속합니다.
포맷 자체는 더 오래된 전기를 갖고 있으며, 그것이 C# API가 지금의 모습을 갖게 된 이유입니다. 지금은 MIME Base64라고 부르는 것의 첫 표준화된 사용은 1987년의 Privacy-Enhanced Mail 프로토콜(RFC 989)이며, MIME 규격은 1993년에 76자 줄바꿈 형태를 고정했고, 2006년의 RFC 4648은 C#이 2024년에야 일급 인코더를 갖게 된 URL 안전 변형을 포함해, 이 포맷에 현대적이고 알파벳을 아는 규격을 줍니다. 30년간의 이메일과 웹 관례가 바로 줄바꿈, 패딩, 두 알파벳이 전부 존재하는 이유이며, C# 인코더는 그 셋이 만나는 곳입니다.
작은 경이들
- 4문자 최소한. 가장 작을 수 있는 비어 있지 않은 Base64 출력은 4문자입니다. 포맷이 1바이트를 줘도 4문자 그룹으로 생각하니까요. 무엇이든 1바이트는 글자 두 개와
=두 개로 인코딩되며, 그 모양 - 패딩 모자를 쓴 데이터 문자 두 개 - 은 설정과 토큰에서 알아 보기 시작하게 될 지문입니다. - NUL은 환영. 인코더는 바이트의 뜻에 대해 의견이 없으므로, 제로로 가득한 버퍼는
A문자의 벽으로 기분 좋게 인코딩되고, NUL 바이트를 온전히 가진 바이너리 파일은 하나도 잃지 않고 왕복합니다. "문자열은 바이너리를 담을 수 없다"는 불안은 인코더의 것이 아니라, 문자열을 결코 보지 않는 타입 시스템의 문자열 쪽에 속합니다. - 기능으로서의 결정성. 같은 바이트, 같은 옵션, 언제나 같은 문자열. 타임스탬프도, 랜덤 솔트도, 변이도 없습니다. 그래서 Base64 문자열은 파일 내용물에 쓸 만한 대충 만든 지문입니다: 같은 Base64를 가진 두 파일은 같은 파일이며, 확인은 문자열 비교일 뿐이죠.
- 문자당 두 바이트, 무료. C# 문자열은 UTF-16이라, Base64 출력의 모든 문자가 관리 메모리에서 두 바이트를 차지합니다. 인코더는 그것을 알리지 않고, 길이 프로퍼티는 보고하지 않으며, 1300만 자 문자열은 그저 26 MB입니다. 페이로드가 클 때 머릿속에 두고 갈 숫자죠.
- 유산으로 온 CRLF. MIME 줄바꿈은 코드가 Linux에서 도는 경우에도 캐리지 리턴-라인 피드 쌍을 넣습니다. 규칙이 플랫폼이 아니라 이메일 규격에서 왔기 때문이죠. 인코더는 변환기이면서 동시에 역사학자이며, 2026년 머신 위에서 1993년의 줄 끝을 보존합니다.
- 첫날부터의 조각 오버로드.
ToBase64String(byte[], int, int)은 2003년부터, 스판이 그 생각을 유행하게 만든 것보다 15년 앞서, 더 큰 배열로 향한 윈도우를 인코딩해 왔습니다. 1.1 시대의 API 설계자들은 진짜 버퍼를 보고 오프셋-길이 형태를 추가했고, 데이터가 더 큰 읽기의 한 섹션이라면 지금도 맞는 선택입니다. - 64자 인증서 줄. PEM은 76이 아니라 64자에 줄바꿈하며,
ExportCertificatePem은 그것을 알고 그에 맞게 줄바꿈합니다. 인증서 작업에서 "프레임워크가 하게 두라"가 맞는 조언이 되는 조용한 세부 사항 중 하나가죠. 줄바꿈 너비 둘, 포맷 패밀리 하나, 그리고 프레임워크가 그것들을 바르게 구분합니다. - 알파벳 둘, 이름 둘. 64개의 값은 API의 한 부분에서는 "standard"로, 다른 부분에서는 "URL-safe"로 불리며, 정확히 두 문자에서 다릅니다: 62번과 63번 슬롯. 한쪽에는
+과/, 다른 쪽에는-와_, 그리고 이 글의 모든 상호 운용 버그는 누가 두 쪽을 같다고 전제한 순간에 삽니다.
다시 원점으로
이것이 인코더 편이며, 당신이 결정을 내리는 곳입니다: 알파벳, 패딩, 줄바꿈, 문자 집합, 버퍼. 다른 방향, 남의 Base64를 받는 방향 - 그들의 패딩 선택, 그들의 줄바꿈, 그들의 알파벳, 그들의 토큰을 가진 - 은 고통의 대부분이 사는 곳입니다. 페이로드와는 흥정할 수 없으니까요. C#에서 Base64 디코딩, 클래식 원라인러부터 스판과 URL 안전 패밀리까지는 아래에 링크된 짝편에서 깊이 다루며, 두 편 사이에는 이 분야 전체가 당신의 작업 기억에 들어맞습니다. 이토록 오래되고, 이토록 작은 포맷의 의의이니까요.
마지막 업데이트: 2026-09-08