Base64 형식을 다루어야 하나요? 그러면 여러분에게 이 웹사이트가 딱 맞네요! 저희 웹사이트의 아주 편리한 온라인 도구를 사용하여 데이터를 인코딩하거나 디코딩해보세요.

C# (CSharp)에서의 Base64 디코딩: 완전한 가이드

한눈에 알아봅니다. 글자와 숫자의 강, 가끔 끼어드는 +나 /, 끝에 매달린 = 한두 개. API 응답, 이메일 첨부 파일, 설정 파일, JWT 중 어딘가에서 누군가는 바이너리 데이터를 텍스트에 담아 넣었고, 이제 여는 일은 당신의 몫입니다. C#에서 Base64를 디코딩하는 편인데, 첫 번째 좋은 소식은 프레임워크 밖의 무엇이 하나도 필요 없다는 것입니다. 이 디코더는 20년 넘게 System 네임스페이스에서 살아 왔고, 모든 최신 .NET 런타임은 여전히 이것을 함께 묶어 줍니다. 처음보다 옵션도 더 많고 성능도 나아졌죠.

이 사이트의 홈 페이지가 포맷을 전부 설명해 두므로, 간단히 복기만 하고 갑니다. 64개의 기호로 이루어진 알파벳에서 4자의 문자가 3바이트의 데이터를 싣고, 꼬리에 있는 = 한두 개가 남은 바이트를 표시합니다. 디코딩은 그 거래를 거꾸로 실행하므로, 결과는 입력 크기의 약 4분의 3입니다. 문제의 모양을 머리에 두고, 자, 몇 가지 패키지를 열어 봅시다.

디코더 패밀리: 당신의 선택지를 알아 두세요

첫 번째 예제에 앞서, 손이 닿는 모든 디코딩 API 패밀리를 한 번에 보여 드리고, 각각이 어떤 상황을 위해 만들어졌는지 짚겠습니다. 여기에 나열된 것들은 모두 .NET 런타임 자체의 일부입니다. 다만 더 오래된 프레임워크에서 쓰는 URL 안전 클래스는 작은 NuGet 패키지로 함께 들어오죠:

API 사용 가능 용도
Convert.FromBase64String(string) .NET Framework 1.1 (2003) 클래식 그 자체. 문자열 하나 들어가고, 새로 만든 byte[] 하나 나옵니다. 일반적인 공백은 건너뛰고, 그 외에는 예외를 던집니다.
Convert.FromBase64CharArray(char[], int, int) .NET Framework 1.1 (2003) 동일한 디코딩인데, 이미 손에 쥔 문자 버퍼의 한 조각에서 읽습니다.
Convert.TryFromBase64String, Convert.TryFromBase64Chars .NET Core 2.1 (2018) 예외 대신 부울 값을 돌려 주며, 당신이 제공한 스판에 씁니다. 신뢰할 수 없는 입력을 위한 다정한 경호원입니다.
System.Buffers.Text.Base64 .NET Core 2.1 (2018) 엄격한 스판 API: 예외 대신 상태 코드, 인플레이스 디코딩, 그리고 IsValid 사전 검사.
System.Buffers.Text.Base64Url .NET 9 (2024) URL 안전 알파벳(+과 / 대신 -와 _), 패딩이 있든 없든 다 소화합니다. .NET Framework 4.6.2 이상과 .NET Standard 2.0에서는: Microsoft.Bcl.Memory NuGet 패키지.
FromBase64Transform + CryptoStream .NET Framework 1.1 (2003) 스트리밍 디코딩: 파일에서 파일로, 네트워크에서 디스크로, 조각 조각, 페이로드 전체를 읽어들일 필요 없이.

프로젝트가 2018년 이후의 .NET 버전을 목표로 한다면, 위 네 줄은 박스 안에 이미 들어 있습니다. Base64Url은 .NET 9 이상, 아니면 그보다 오래된 버전에서는 Microsoft.Bcl.Memory 패키지가 필요합니다. 앞으로의 소식도 덧붙이면: 작성 시점에 프리뷰 중인 .NET 11 라이브러리는(일반 릴리스는 2026년 말 예정) 기존 타입에 Base64 편의 API와 오버로드를 더하며, 패밀리는 계속 커져 갑니다. 이 글의 다른 부분은 어디에서도 패키지를 요구하지 않습니다.

견실한 일꾼: Convert.FromBase64String

C#에서의 디코딩 생활의 90퍼센트는 하나의 호출입니다. 문자열을 주면, 안에 담아져 있던 바이트를 정확히 그대로 돌려 줍니다:

using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man

기억할 가치가 있는 세 가지 세부 사항이 있습니다. 첫째, 반환 값은 텍스트가 아니라 바이트입니다. byte[]이고, 디코더는 처음부터 끝까지 바이트를 향해 일하며, 이건 정확히 당신이 원하는 모습입니다. 페이로드가 문장일 수도, PNG일 수도, 인증서일 수도, 해시일 수도 있고, 그 어느 것도 특별 대우받아서는 안 되니까요. 바이트를 다시 읽을 수 있는 텍스트로 올리는 과정은 Encoding을 거치는 또 다른, 의도된 단계이고, 문자 집합 결정은 바로 그 단계에서 이루어집니다(아래에서 더 다룹니다). 둘째, 디코더는 호출할 때마다 디코딩된 길이에 딱 맞는 새 배열을 할당하므로, 여유 용량을 가진 버퍼를 건네는 일이 없습니다. 셋째, 계약은 작고 솔직합니다. 빈 문자열은 빈 배열로 디코딩되고, null 참조는 ArgumentNullException을, 유효하지 않은 Base64는 FormatException을 던집니다. 나머지는 전부 이 세 가지 규칙의 변주입니다.

용서하는 것과 거절하는 것

C# 디코더의 개성이 드러나는 순간이 바로 여기입니다. 그리고 꽤 뚜렷한 개성이에요. 디코더는 정확히 한 가지, 공백에 대해서는 관대하고, 그 외 모든 것에 대해서는 무자비합니다. 문자열 안에서 어디에 나타나든 정확히 네 문자를 건너뛰는 것입니다. 스페이스(U+0020), 탭(U+0009), 라인 피드(U+000A), 캐리지 리턴(U+000D)이죠. 이 정책은 이메일을 의도적으로 배려한 것입니다. 이메일에서는 Base64 페이로드가 76자 단위로 줄바꿈되어 도착하기 때문입니다. 덕분에 MIME 포장된 첨부 파일은 전처리 없이 디코딩됩니다. 64개의 기호로 이루어진 알파벳을 벗어난 것, 길이 규칙을 어기는 것, 패딩 자리가 잘못된 것, 어느 하나라도 있으면 예외를 받습니다. 여러 다른 입력으로 같은 디코더를 움직여 보면:

입력 결과
"TWFu" Man(3바이트)로 디코딩됩니다.
"TWF\nu" (가운데 줄바꿈) Man으로 디코딩됩니다. 공백은 디코더에게 보이지 않습니다.
"TWFu\u00A0" (끝에 붙이지 않는 공백) FormatException. 건너뛰는 공백은 위에서 나열한 네 문자뿐이며, 붙이지 않는 공백(NBSP)은 그중 하나도 아닙니다.
"TWE" (길이 3, 4의 배수가 아님) FormatException. 공백을 제외한 페이로드 길이는 4의 배수여야 합니다.
"TWFu=" (데이터 뒤의 추가 패딩) FormatException. 패딩은 최대 두 개, 그리고 맨 끝에만 올 수 있습니다.
"-_88" (URL 안전 알파벳) FormatException. 표준 디코더가 아는 것은 표준 알파벳의 64문자뿐입니다.
null ArgumentNullException: Value cannot be null. (Parameter 's')

기억할 특이점이 하나 더 있습니다. 어떤 포맷 범죄를 저지르든 돌아가는 에러 메시지는 단 하나로, The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters입니다. 메시지는 세 가지 가능한 원인을 모두 나열할 뿐, 당신이 걸린 것은 어느 원인인지도, 어디인지도 알려 주지 않습니다. 실패한 페이로드를 디버깅 중이라면, 문자를 세고, 알파벳을 확인하고, 그 순서로 패딩을 확인하세요.

예외 없이 디코딩하기: Try API

예외를 동력으로 쓰는 제어 흐름도 정당한 패턴이지만, 대량이거나 신뢰할 수 없는 입력에는 Try 패밀리가 더 시민 의식 있는 선택입니다. .NET Core 2.1에서 추가되었으며, 두 가지 맛이 있습니다: 문자열에서 읽는 것과 문자 스판에서 읽는 것. 둘 다 당신이 제공한 버퍼에 쓰고, 그중 얼마를 채웠는지 보고합니다:

using System;
using System.Text;
string payload = "TWFu"; // 어떤 페이로드든, 유효하든 아니든
Span<byte> buffer = stackalloc byte[4096];
if (Convert.TryFromBase64String(payload, buffer, out int written))
{
  string text = Encoding.UTF8.GetString(buffer[..written]);
  Console.WriteLine(text);
}
else
{
  Console.WriteLine("Not a valid Base64 payload.");
}

두 가지 행동이 Try 변종을 완전히 다른 종처럼 느끼게 합니다. 잘못된 입력은 예외를 던지기는커녕 false를 돌려 주므로, 잘못된 페이로드가 쏟아져 들어와도 대가는 예외가 아니라 분기 하나입니다. 주의할 점 하나: null 입력은 계약의 일부가 아니라 - ArgumentNullException을 던지므로 - Try 경호는 깨진 페이로드를 덮어 줄 뿐, 아예 없을 수도 있는 값은 여전히 앞선 null 검사가 필요합니다. 동생 메서드 Convert.TryFromBase64Chars는 ReadOnlySpan<char>에서 같은 일을 하며, 페이로드가 더 큰 문자 버퍼 안에 살고 먼저 부분 문자열을 잘라내기가 귀찮을 때 유용합니다. 출력 버퍼는 넉넉하게 잡으세요. 디코딩된 길이는 (공백을 제외한) 입력 길이의 4분의 3을 넘지 않으며, out-인자 written가 정확히 얼마가 나왔는지 알려 줍니다.

System.Buffers.Text.Base64를 이용한 스판 기반 디코딩

할당을 하나하나 세거나, 디코더가 실패를 던지기보다 설명해 주기를 원할 때, System.Buffers.Text.Base64 클래스가 그 도구입니다. .NET Core 2.1부터 표준 라이브러리에 들어 있는 정적 클래스로, 관리되는 배열 대신 스판에서 일합니다. 디코딩 메서드는 네 가지 기분을 가진 OperationStatus 값을 돌려 줍니다: Done(성공), DestinationTooSmall(당신의 버퍼가 너무 작음), NeedMoreData(입력이 아직 4의 배수가 아니므로 계속 읽으라는 뜻), 그리고 InvalidData(이건 Base64가 아님). 마지막 부울 파라미터 isFinalBlock가 바로 그 둘을 구분합니다. 더 들어올 입력이 있는지 디코더에게 알려 주는 것이죠. 클래스 자신의 헬퍼로 크기를 잡은 원샷 형태입니다:

using System.Buffers;
using System.Buffers.Text;
using System.Text;
string payload = "TWFu";
byte[] input = Encoding.ASCII.GetBytes(payload);
byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(input.Length)];
OperationStatus status = Base64.DecodeFromUtf8(input, output,
  out int consumed, out int written, isFinalBlock: true);
if (status == OperationStatus.Done)
{
  Console.WriteLine(Encoding.UTF8.GetString(output.AsSpan(0, written)));
  // Man
}

이 클래스의 멤버 두 개는 단락 하나를 더 받을 자격이 있습니다. 첫째는 IsValid로, 디코딩하지 않고 페이로드를 검증합니다. 바이트 스판과 문자 스판 맛이 있으며, 어느 오버로드는 판결과 함께 디코딩된 길이를 보고해 줍니다. 그래서 한 번의 검사로 버퍼 크기를 잡을 수 있습니다:

using System.Buffers.Text;
string payload = "TWFu";
if (Base64.IsValid(payload, out int decodedLength))
{
  Console.WriteLine("Valid, decodes to " + decodedLength + " bytes.");
  // Valid, decodes to 3 bytes.
}
else
{
  Console.WriteLine("Rejecting payload before allocating anything.");
}

둘째는 DecodeFromUtf8InPlace로, Base64 텍스트가 이미 당신의 버퍼 안에 앉아 있고 덮어 써도 상관없을 때 씁니다. 디코딩은 데이터를 줄이므로, 결과는 같은 버퍼의 맨 앞에 쓰이고, 메서드는 그 길이를 보고합니다:

using System.Buffers;
using System.Buffers.Text;
using System.Text;
byte[] data = Encoding.ASCII.GetBytes("TWFu");
OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int written);
if (status == OperationStatus.Done)
{
  Console.WriteLine(Encoding.ASCII.GetString(data, 0, written));
  // Man, 이제 같은 버퍼의 앞 세 바이트에 살고 있다
}

주머니에 넣어 둘 행동 하나: 이 클래스도 네 가지 일반적인 공백 문자(스페이스, 탭, 라인 피드, 캐리지 리턴)를 건너뜁니다. 그래서 줄바꿈된 페이로드도 똑같이 잘 디코딩됩니다. 중요한 부분에서는 엄격합니다: 공백을 제외한 길이가 4의 배수가 아닌 페이로드는 마지막 블록일 때 InvalidData가 되며, 표준 알파벳 밖의 문자는 그 자리에서 거부됩니다. 이 클래스 어디에도 조용한 치환 작업은 없습니다.

URL 안전 Base64: Base64Url 클래스

같은 64개의 값에 두 번째 알파벳이 존재하며, C# 웹 작업에서는 그와 끊임없이 마주하게 됩니다. 표준 알파벳에서 62번과 63번 값은 +과 /인데, 이 두 문자는 URL에서 사고를 부립니다. 쿼리 문자열의 +는 늘 스페이스로 디코딩되곤 하고, /와 =도 각기 퍼센트 인코딩이 필요합니다. RFC 4648 5절은 이를 -와 _로 교체해서 해결합니다. 이 둘은 어떤 URL 문맥에서도 특별한 뜻을 갖지 않으며, 꼬리의 = 패딩도 선택 사항이 됩니다. 이 결과가 base64url입니다. JWT, API 토큰, 파일 업로드 ID, 수많은 URL의 알파벳이죠(YouTube의 11자 영상 식별자도 패딩 없는 base64url입니다).

.NET 9부터 표준 라이브러리는 이를 위한 전용 클래스를 함께 묶어 줍니다: System.Buffers.Text.Base64Url. Base64 클래스의 URL 안전 쌍둥이로, 자기만의 디코딩, 검증, 길이 헬퍼를 갖습니다:

using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC

클래식 API가 그 예제에서 못했을 것에 주목하세요. 같은 세 바이트는 표준 알파벳에서 +//8으로 인코딩되며, Convert.FromBase64String("+//8")는 작동합니다. 하지만 Convert.FromBase64String("-__8")는 예외를 던지는데, URL 안전 문자가 그의 알파벳 밖이라 그렇습니다. 그리고 base64url 페이로드는 패딩 없이 오는 경우가 많은데, 클래식 디코더도 이를 거부합니다. 4자 완정 그룹을 고집하기 때문이죠. Base64Url 클래스는 이 문제의 두 변종을 내장 기능으로 처리합니다: TWE(세 문자, 패딩 없음)를 두 바이트 Ma로 디코딩하고, TWE=도 똑같이 잘 디코딩합니다.

프로젝트가 오래된 런타임에서 돌라면, 실용적인 길은 두 가지입니다. .NET Framework 4.6.2 이상에서는 Microsoft.Bcl.Memory NuGet 패키지를 추가합니다. Microsoft가 Base64Url을(몇몇 다른 최신 타입과 함께) 백포트하기 위해 특별히 발행한 패키지죠:

dotnet add package Microsoft.Bcl.Memory

아니면, 패키지 하나 없이도, 페이로드를 클래식 디코더에게 넘기기 전에 정규화합니다: URL 안전 문자를 표준 쌍둥이로 되돌리고, 부족한 패딩을 채워 주는 것이죠. 이 작은 헬퍼는 C# 코드에서 가장 흔한 손수 만든 base64url 디코더이며, .NET Framework 1.1 이후 모든 런타임에서 작동하므로 알아 둘 가치가 있습니다:

using System;
using System.Text;
string segment = "TWE";
segment = segment.Replace('-', '+').Replace('_', '/');
segment += new string('=', (4 - segment.Length % 4) % 4);
byte[] bytes = Convert.FromBase64String(segment);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// Ma

(4 - length % 4) % 4 공식이 패딩 산수의 전부입니다. 길이가 4의 배수에 떨어지도록 =을 0개, 1개, 2개를 더하고, 바깥쪽 나머지 연산이 이미 패딩된 입력이 추가로 붙는 일을 막아 줍니다.

바이트에서 말로: 텍스트, 유니코드, 문자 집합

디코딩이 주는 것은 바이트이며, 바이트는 완전히 중립적인 것입니다. 원본 저자가 어떤 문자 집합을 썼는지 Base64는 아무 정보도 싣지 않으므로, 어떤 문자 집합으로 읽을지 고르는 순간에만 비로소 "텍스트"가 됩니다. 그리고 그 선택은 당신의 몫입니다. 실무로는 이겁니다: 이유가 없다면 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);
byte[] decoded = Convert.FromBase64String(packed);
string restored = Encoding.UTF8.GetString(decoded);
Console.WriteLine(restored == original);
// True: h\u00e9llo \u4e16\u754c가 완벽하게 왕복한다

세심한 함정은 바이트가 유효한 UTF-8이 아닌 경우에 일어납니다. 페이로드가 실제로 Latin-1이거나, 바이너리이거나, 그저 깨져 있을 때요. 기본적으로 .NET의 UTF-8 디코더는 잘못된 모든 시퀀스를 유니코드 교체 문자(U+FFFD)로 바꾸고 그대로 넘어갑니다. 예외도, 경고도 없습니다. 데이터는 그냥 사라져, 당신의 데이터베이스에서 물음표가 됩니다. 그런 일이 일어났는지 알고 싶다면, 엄격한 폴백으로 인코딩을 만드세요. 조용한 대체를 소리를 내는 DecoderFallbackException으로 바꿔 줍니다:

using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // FF FE 바이트, 유효하지 않은 UTF-8
Encoding strictUtf8 = Encoding.GetEncoding(
  "utf-8",
  new EncoderExceptionFallback(),
  new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// FF FE가 UTF-8 시퀀스가 아니므로 DecoderFallbackException이 발생

실패하는 것보다 살아남기를 원하는 페이로드에는, 대체 폴백이 더 다정한 선택입니다. 그리고 대체할 텍스트는 스스로 고를 수 있습니다:

using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // FF FE 바이트, 유효하지 않은 UTF-8
Encoding forgivingUtf8 = Encoding.GetEncoding(
  "utf-8",
  EncoderFallback.ReplacementFallback,
  new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// 침묵하는 U+FFFD 대체 대신 [bad][bad]

C#만의 역사 교훈이 하나 더 있습니다: Encoding.Default는 런타임마다 다른 뜻을 갖습니다. Windows의 .NET Framework에서는 시스템의 ANSI 코드 페이지(종종 Windows-1252)이고, .NET (Core)에서는 BOM 없는 UTF-8입니다. 그래서 Encoding.Default를 거쳐 페이로드를 왕복시키는 코드는 2010년 머신에서는 이렇게, 2025년 머신에서는 저렇게 다른 바이트를 만들어 낼 수 있고, Base64는 당신이 건네는 바이트가 무엇이든 기꺼이 인코딩합니다. 발음 기호가 깨진 글자로 가득한 디코딩된 문자열을 본다면, Encoding.Default부터 먼저 들여다보세요.

파일과 바이너리 페이로드

파일은 가장 단순한 디코딩 대상입니다. 문자 집합 문제가 전혀 없기 때문이죠: 디코딩한 바이트가 그 자체로 파일이며, 제로 바이트를 포함해 바이트 하나하나가 그대로 파일입니다. 패턴은 호출 두 개와 파일 하나이며, 이미지 업로드부터 백업 도구까지 어디서나 등장합니다:

using System.IO;
string b64 = File.ReadAllText("payload.b64");
byte[] original = Convert.FromBase64String(b64);
File.WriteAllBytes("restored.bin", original);
Console.WriteLine("Restored " + original.Length + " bytes.");

실용적인 노트 두 가지입니다. 파일에 공백이나 줄바꿈이 들어 있을 수 있다면(텍스트 파일인 이상 거의 확실합니다), 클래식 디코더가 공짜로 처리해 줍니다. 앞서 보았듯이요. 그리고 페이로드가 크다면, 아예 문자열을 거치지 마세요. 파일에서 문자열로 올리는 단계를 건너뛰고 스트림에서 바로 디코딩하는 것이 다음 섹션의 내용입니다. 텍스트이고 문자 집합도 마침 아는 디코딩된 페이로드는, 파일 예제가 곧 전체 해결책이며, 문자 집합 섹션의 Encoding.UTF8.GetString 단계를 디코딩과 사용 사이에 딱 끼워 넣으면 됩니다.

스트림에서 디코딩하기: FromBase64Transform

Convert 메서드는 문자열에 들어 맞는 페이로드를 위해 설계되어 있고, 공식 문서도 분명히 그렇게 말합니다: 스트리밍 데이터에는 변환 클래스를 쓰라고요. FromBase64Transform은 .NET Framework 1.1(2003)년부터 System.Security.Cryptography의 일부였으며, 데이터를 흐르면서 변환해 주는 프레임워크의 범용 파이프인 CryptoStream에 꽂힙니다. 파일에서 파일로의 디코딩 전체는 4줄의 세팅입니다:

using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("payload.b64");
using FromBase64Transform transform =
  new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using CryptoStream reader = new CryptoStream(source, transform, CryptoStreamMode.Read);
using FileStream target = File.Create("payload.bin");
reader.CopyTo(target);
Console.WriteLine("Done, " + target.Length + " bytes written.");

생성자는 모드를 받는데, 두 모드는 이름을 알아 둘 가치가 있습니다. IgnoreWhiteSpaces(기본값, 클래식 디코더의 공백 정책과 일치)는 스트림이 흐르는 동안 네 가지 일반적인 공백 문자를 건너뜁니다. 이메일 포장이거나 줄바꿈이 가득한 페이로드에 원하는 것이죠. DoNotIgnoreWhiteSpaces는 엄격합니다. 만나는 첫 알파벳 밖 문자에서 FormatException을 던지는데, 페이로드에 들어온 여분의 공백을 무시하기보다 버그로 다루고 싶을 때 원하는 것이죠. 겉을 벗겨 보면, 변환기는 입력을 4문자씩의 그룹으로 처리하고 각 그룹이 만들어 내는 3바이트를 돌려 주며, 꼬리는 TransformFinalBlock가 처리합니다. 직접 그 메서드를 호출할 일은 드뭅니다. CryptoStream이 대신해 주니까요. 하지만 4단위 그룹이라는 사실은 중요합니다. 변환기에 직접 먹여야 한다면 4의 배수 단위로 먹여야 하며, 그렇지 않으면 마지막 부분 그룹이 마지막 블록에 그대로 남아 있게 됩니다.

JWT: 세 개의 세그먼트, 하나의 점

JSON Web Token은 C# 웹 개발에서 트래픽이 가장 높은 base64url 페이로드이며, 그 모양은 속을 정도로 간단합니다: 점으로 나뉜 세 개의 세그먼트. 첫 번째는 인코딩된 헤더, 두 번째는 인코딩된 페이로드(클레임이라고도 함), 세 번째는 서명입니다. JWS 규격에 따라, 첫 둘은 모두 패딩 없는 UTF-8 JSON 문서의 base64url입니다. 나눠서 디코딩하는 일은 C# 두 줄이면 됩니다:

using System;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl";
string[] parts = jwt.Split('.');
string headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
string payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
using JsonDocument doc = JsonDocument.Parse(payloadJson);
Console.WriteLine(doc.RootElement.GetProperty("name").GetString());
// Ada

.NET 9 이전 런타임에서는, 같은 일을 URL 안전 섹션의 정규화 헬퍼를 거쳐 합니다: -와 _를 +와 /로 되돌리고, 세그먼트를 4의 배수까지 패딩한 뒤 Convert.FromBase64String으로 디코딩합니다. 두 방법 모두 같은 JSON을 줍니다. 목표 프레임워크에 맞는 쪽을 골라 쓰세요.

날카롭게 유지할 경계가 하나 있습니다: JWT를 디코딩하는 것과 JWT를 검증하는 것은 다릅니다. 위 디코딩은 쓰레기 서명을 가진 토큰의 클레임도 기꺼이 읽어 드립니다. 서명은 첫 두 세그먼트에 대한 별개의 암호학적 검사이니까요. 프로덕션 토큰 작업에서는 아예 손으로 파싱하지 마세요. System.IdentityModel.Tokens.Jwt 패키지(Microsoft.IdentityModel 계열)는 파싱, 검증, 만료 처리를 하나로 처리해 주며, 그 base64url 처리는 정확히 이 섹션이 설명하는 알파벳입니다. 디버깅과 작은 유틸리티에는 손으로 디코딩하고, 사용자가 닿을 수 있는 모든 것에는 라이브러리로 검증하세요.

데이터 URI와 임베디드 이미지

data: URI를 받는 것이 업무인 C# 코드는 정정한 무리입니다. HTML, CSS, 수많은 웹 API가 바이너리 콘텐츠를 인라인으로 임베드하려고 그걸 쓰니까요. RFC 2397에서 표준화한 이 스킴의 모양은 data:[mediatype][;base64],payload입니다: 첫 쉼표 이전은 전부 메타데이터(MIME 타입과 ;base64 플래그)이고, 그 이후는 전부 페이로드입니다. ;base64 플래그가 있으면 페이로드는 Base64 문자열이며, 쉼표에서 끊는 것이 전체 파싱입니다:

using System;
using System.Text;
string dataUri = "data:image/png;base64,iVBORw0KGgo=";
int comma = dataUri.IndexOf(',');
string mediaType = dataUri[..comma];        // data:image/png;base64
string b64 = dataUri[(comma + 1)..];        // iVBORw0KGgo=
byte[] imageBytes = Convert.FromBase64String(b64);
Console.WriteLine(imageBytes.Length);
// 8: PNG 시그니처 바이트 89 50 4E 47 0D 0A 1A 0A

예제의 iVBORw0KGgo= 접두어는 8바이트 PNG 매직 넘버의 Base64 형태이며, 유용한 지문입니다. 진짜 PNG의 데이터 URI는 전부 그렇게 시작하므로, 신뢰할 수 없는 HTML을 파싱할 때 빠른 건전성 확인이 됩니다. C# 개발자를 위한 실용적인 노트 두 가지입니다. 첫째, Uri 클래스는 .NET에서 데이터 URI를 기본적으로 이해합니다: new Uri("data:text/plain;base64,TWFu")는 아무 문제 없이 파싱되고 Scheme == "data"를 보고합니다. 그래서 코드에서 URI로 라우팅을 한다면, 데이터 URI가 파이프라인에 나타날 테니 어떻게 다룰지 결정하여 두세요. 둘째, 데이터 URI가 실제로 무엇인지 기억하세요: 파일의 완전한 사본이 3분의 1 불어난 채, 당신의 문서 안에 앉아 있는 것입니다. 4 KB 파비콘이라면 괜찮지만, 4 MB 로고라면 고통입니다. 그래서 당신이 그걸 만드는 쪽이라면(인코딩 편이 그쪽을 다룹니다), 인코딩 전에 이미지의 크기를 먼저 맞출 일입니다.

HTTP: 기본 인증과 API 왕래

Base64는 API 작업을 하면 꼭 만지는 곳, 최소 하나, HTTP에 짜여 있습니다: Basic 인증 스킴이죠. 클라이언트는 Authorization: Basic 뒤에, 콜론으로 붙인 username:password의 Base64 인코딩을 보냅니다. 서버 쪽에서는 들어오는 헤더를 디코딩하는 일이 그래서 이렇습니다: Basic 접두어를 벗기고, 디코딩하고, 첫 콜론에서 나누는 것:

using System;
using System.Text;
string header = "Basic YWRhOnMzY3JldA==";
string encoded = header["Basic ".Length..].Trim();
string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int colon = credentials.IndexOf(':');
string user = credentials[..colon];
string password = credentials[(colon + 1)..];
Console.WriteLine(user);      // ada
Console.WriteLine(password);  // s3cret

UTF-8 단계는 보이는 것보다 중요합니다: RFC 7617은 실제로 문자 집합을 고정하지 않습니다. 호환성을 위해 기본값을 정의하지 않은 채, 권고 사항인 UTF-8 힌트만 허용할 뿐이죠. 하지만 바로 그 힌트를 모든 현대적인 서버가 기대하므로, 발음 기호가 있는 사용자 이름은 Latin-1로 읽은 같은 이름과 다른(그리고 정확한) 바이트 문자열을 만듭니다. Basic 인증의 디코딩 쪽은 이 패턴의 단순한 끝입니다. ASP.NET Core에서는 보통 원본 헤더가 아니라 인증 핸들러를 통해 그와 만나지만, 밑에서 돌아가는 것은 바로 같은 디코딩 로직이며, API 서버를 가짜로 만들 때 쓰는 통합 테스트에 꼭 필요한 바로 그런 코드입니다. 거울 이미지인 작업, 즉 클라이언트 쪽에서 헤더를 만들어 내는 일은 인코딩 쪽에서는 한 줄이고, 인코딩 편에 전체 예제가 있습니다.

이메일: MIME과 줄바꿈된 페이로드

이메일은 Base64가 명성을 쌓은 곳이며, 여전히 C# 서비스들이 받는 페이로드의 많은 것이 여기에서 옵니다. SMTP는 원래 7비트 프로토콜이라, 바이너리 첨부 파일은 그대로 날아갈 수 없습니다. MIME 규격(RFC 2045)은 Content-Transfer-Encoding: base64 헤더를 달고 Base64로 인코딩하라고 하고, 출력을 76자마다 줄바꿈하며, 줄 사이에는 캐리지 리턴-라인 피드 쌍을 넣습니다. 그래서 진짜 첨부 파일 바디는 76자 줄들이 세운 기둥처럼 보입니다. C#에 좋은 소식은, 클래식 디코더가 그걸 읽는 법을 이미 안다는 것입니다: 문자열의 어디에 있는 공백이든 건너뛰기 때문에, 줄바꿈 포함, 포장된 바디 전체를 그대로 넘겨 줘도 줄바꿈이 처음부터 없었던 것처럼 디코딩합니다:

using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan

문자열이 아니라 스트림으로 도착하는 페이로드는, 공백 무시 모드의 FromBase64Transform가 스트리밍 옷을 입은 같은 이야기입니다. 그리고 바디를 디코딩하는 것보다 더 많은 것이 필요할 때, MIME 구조를 걷고, 헤더를 파싱하고, 중첩된 multipart 섹션을 다루고, 진짜 .eml 파일에서 모든 첨부 파일을 빼내야 할 때, C# 생태계의 답은 MimeKit 패키지입니다. .NET의 표준 MIME 라이브러리로, Base64와 quoted-printable 콘텐츠 전송 인코딩을 내부에서 처리하며, "바디만 디코딩하면 돼"가 더 이상 당신의 문제를 설명하지 못하는 순간 손이 가야 할 도구입니다. 프레임워크 자신의 MailMessage 클래스는 단순한 첨부 파일을 대신 디코딩해 줍니다만, 현대 기준으로 그 MIME 지원은 의도적으로 소박합니다.

PEM 인증서

PEM은 TLS 세계의 아머 포맷입니다: RFC 7468에서 규정한 대로, -----BEGIN CERTIFICATE-----와 -----END CERTIFICATE----- 마커 사이의 Base64 바디로, 64자마다 줄바꿈됩니다. C# 개발자는 모든 HTTPS 엔드포인트 뒤의 인증서 파일로 그와 마주칩니다. 그리고 여기의 디코딩 이야기는 기대보다 좋습니다. .NET 6부터 프레임워크가 Base64 바디까지 통째로 PEM을 대신 파싱해 주니까요:

using System.IO;
using System.Security.Cryptography.X509Certificates;
string pem = File.ReadAllText("server.pem");
X509Certificate2 certificate = X509Certificate2.CreateFromPem(pem);
Console.WriteLine(certificate.Subject);
// CN=server.example.com

그 안에 수동 Base64는 어디에도 없습니다: CreateFromPem이 마커를 찾고, 바디를 벗기고, 디코딩한 뒤 살아 있는 인증서를 돌려 줍니다. (인프라가 그렇게 건네 준다면, 이 패밀리에는 개인 키용과 인증서-키 결합 형태용 형제도 있습니다.) 오래된 런타임이거나, 아머 안에 있는 원본 DER 바이트가 필요하면, 수동 버전은 벗기고 디코딩하는 두 단계이며, 같은 패턴이 어떤 PEM 아머든 통하므로 알아 둘 가치가 있습니다:

using System;
using System.Text;
string pem = File.ReadAllText("server.pem");
string body = pem
  .Replace("-----BEGIN CERTIFICATE-----", "")
  .Replace("-----END CERTIFICATE-----", "")
  .Replace("\r", "")
  .Replace("\n", "");
byte[] der = Convert.FromBase64String(body);
Console.WriteLine(der.Length);
// 아머 안의 DER 인증서 길이

이 구석의 함정은 전부 공백입니다: 대부분의 인증서 도구가 만든 PEM 파일은 CRLF 줄 끝을 갖고 있으므로, 디코딩 전에 줄바꿈 문자뿐 아니라 \r와 \n 둘 다 벗기세요. 그리고 인증서 바디를 개인 키 바디와 혼동하지 마세요. 마커도 다르고 내용도 다르며, 이 하나에는 디코더도 당신을 구해 주지 못합니다.

설정, 환경 변수, 데이터베이스

C# 애플리케이션에서 Base64의 세 번째 집은 저장입니다: 설정 파일, 환경 변수, 데이터베이스 컬럼. 모든 곳에서 패턴은 같습니다. 바이너리나 시크릿 값은 들어올 때 문자열로 인코딩되고, 나갈 때 다시 바이트로 디코딩됩니다. 환경 변수는 가장 눈에 띄는 예입니다. 텍스트만 담을 수 있으니까요:

using System;
using System.Text;
string? encoded = Environment.GetEnvironmentVariable("API_KEY_B64");
if (encoded == null)
{
  throw new InvalidOperationException("Set the API_KEY_B64 environment variable first.");
}
byte[] keyBytes = Convert.FromBase64String(encoded);
string apiKey = Encoding.UTF8.GetString(keyBytes);
Console.WriteLine(apiKey.Length + " characters of API key, ready to use.");

데이터베이스에서는 같은 아이디어가 보통 이렇습니다: 이식성을 위해 텍스트 컬럼에 저장하고 싶은 byte[] 프로퍼티. Entity Framework Core에는 정확히 이를 위한 내장 기전이 있습니다: 읽기, 쓰기마다 당신의 인코딩과 디코딩 함수를 투명하게 실행하는 값 변환기입니다:

using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
  .Property(a => a.ImageData)
  .HasConversion(
    v => Convert.ToBase64String(v),
    v => Convert.FromBase64String(v));

그 변환기 하나가 데이터베이스 통합의 전부입니다: ImageData는 당신의 C# 코드에서 byte[]로 그대로 있고, 데이터베이스는 Base64 문자열을 봅니다. 이 섹션에는 경고 두 가지가 함께해야 합니다. 첫째, 3바이트당 4문자라는 세금 때문에, 주어진 너비의 컬럼은 원본 바이너리보다 인코딩된 텍스트로 약 3분의 1 적은 데이터를 담습니다. 그래서 고정 너비의 컬럼이라면 인코딩된 길이에 맞춰 크기를 정하세요. 둘째, 그리고 이것이 보안 관련입니다: 설정 파일의 Base64는 값을 한 줄에 두는 편의일 뿐, 값에 대한 보호가 아닙니다. 설정 파일을 읽을 수 있는 사람은 명령 하나면 키를 디코딩할 수 있으므로, 진짜 시크릿은 시크릿 저장소에 속하며, 거기의 Base64는 그저 전송 포맷일 뿐입니다.

페이로드가 클 때

Base64 디코딩에는 인코딩이 없는 기쁜 성질이 있습니다: 출력은 언제나 입력보다 작으며, 그 정도가 4분의 3입니다. 10 메가바이트 텍스트 페이로드는 약 7.5 메가바이트의 바이트로 디코딩되므로, 디코딩은 인코딩처럼 당신의 메모리를 부풀릴 일이 결코 없습니다. 버퍼 크기를 미리 잡아야 한다면, 계산은 두 호출 중 하나로 좁혀집니다: 엄격한 스판 클래스에는 Base64.GetMaxDecodedFromUtf8Length, 클래식 API에는 단순 계산 length / 4 * 3, 그리고 입력이 줄바꿈된 경우 공백을 위한 여분. (헬퍼는 디코딩된 최대 가능 길이를 돌려 줍니다: 진짜 길이가 그것과 같은 경우는 마지막 그룹에 패딩이 없을 때이며, 패딩 문자 하나나 둘로 끝나면 그만큼 1바이트, 2바이트 짧습니다.)

하지만 페이로드가 정말로 크다면, 정답은 더 큰 버퍼가 아니라 버퍼가 아예 없는 것입니다: 문자열을 통째로 건너뛰고, 스트림 섹션에서 본 대로 FromBase64Transform이 원본에서 대상으로 디코딩을 스트리밍하게 하는 것이죠. 지켜야 할 규칙은 4단위 그룹 정렬 하나뿐입니다: Base64 스트림은 4문자의 배수에서(공백을 계산한 뒤)만 자를 수 있으므로, 변환기에 직접 먹여야 한다면 4의 배수 크기의 청크로 읽고, 나머지는 TransformFinalBlock가 비우게 두세요. 수백 메가바이트 미만이라면, 원샷 디코딩은 충분히 빠릅니다. 이것은 필요이 아니라 최적화이며, 다만 스트리밍 형태가 메모리 한계 아래에서 잘 행동하는 쪽이기도 하고, 바로 그것이 큰 페이로드가 좋아서 사는 환경이니까요.

터미널의 디코더

어떤 언어에도, 15줄짜리 콘솔 프로그램이 명령줄 도구가 되는 만족스러운 순간이 있고, C#의 Base64 디코더는 그 일을 하기에 좋은 후보입니다. 표준 입력에서 읽기 때문에 셸 파이프에 그대로 끼워 쓸 수 있으니까요. 도구 전체는 이렇습니다: 파이프에서(또는 인자에서) Base64 페이로드를 읽고, 디코딩하고, 원본 바이트를 파일로 씁니다:

using System;
using System.IO;
using System.Text;
string input = args.Length > 0 ? File.ReadAllText(args[0]) : Console.In.ReadToEnd();
byte[] bytes = Convert.FromBase64String(input.Trim());
File.WriteAllBytes("output.bin", bytes);
Console.Error.WriteLine("Wrote " + bytes.Length + " bytes to output.bin.");

한 번 빌드해 두면, .NET 런타임의 디코더를 특히 원하는 날을 위해 셸 자신의 base64 유틸리티 옆에 앉아 있게 됩니다: 파일을 통과시키고, 다른 도구와 연쇄시키고, 그러면 엄격한 C# 검증 규칙(공백 관대, 알파벳 엄격, 패딩 엄격)이 당신의 파이프라인 일부가 됩니다. 그곳의 Trim()은 조용한 일을 하고 있습니다. 텍스트 에디터가 좋아서 붙여 주는 꼬리쪽 줄바꿈을 잡아 주는데, 솔직히 말하면 디코더는 어차피 무시했을 테지만요. API 로그에서 점점 더 많이 나타나는 URL 안전 페이로드는, URL 안전 섹션의 Base64Url 디코딩을 쓴 같은 뼈대가 전부입니다.

속도: 기대해 볼 수 있는 것

최신 .NET의 Base64는 빠르며, 점점 더 빨라지고 있습니다. Convert 메서드와 System.Buffers.Text 클래스, 둘 다의 런타임 구현은 하드웨어가 지원하는 곳에서 SIMD 벡터 명령어로 최적화되어 있으며, 사이클당 많은 문자를 처리합니다. 실무적으로는, 수 메가바이트 페이로드가 일반 데스크톱 머신에서 한 자릿수에서 두 자릿수 초반 밀리초 만에 디코딩된다는 뜻이며, 당신이 쓰게 될 어떤 애플리케이션에서든 Base64 디코딩은 사실상 무료인 만큼 빠른 속도입니다. 그래서 실용적인 성능 조언은 디코더 자체가 아니라 당신의 코드 모양에 관한 것입니다. 잘못된 입력이 가능하고 예외가 비쌀 핫 패스에서는 Try 메서드나 상태를 돌려 주는 스판 메서드를 선호하세요. 루프 안에서 수천 개의 작은 페이로드를 디코딩할 때, 호출마다 새 배열을 할당하기보다 인플레이스 API와 스판 API로 버퍼를 재사용하세요. 그리고 같은 페이로드를 두 번 디코딩하지 마세요: 한 번이 비용이며, 이미 디코딩한 필드를 두 번째로 디코딩하는 것은 순전한 낭비로, 프로파일에서 신비로운 두 번째 Base64 스파이크로 나타납니다.

보안: Base64가 하지 않는 것

Base64에 관한 가장 중요한 보안 사실은, 초보자가 가장 자주 놓치는 것입니다: 이것은 인코딩이지 암호화가 아닙니다. Base64 문자열은 누구나, 어떤 도구로든, 찰나 만에 읽을 수 있고, 이 글 전체가 보여주었듯이 C#에서 그것을 읽는 일은 한 줄입니다. Base64에는 키도, 알고리즘 파라미터도, 악용할 약점도 없습니다. 처음부터 아무것도 숨기려 한 적 없으니까요: 이것은 전송 포맷이며, 텍스트 전용 채널에서 바이너리가 살아남게 하는 방법입니다. 그에 따라 대하세요. 비밀번호, 토큰, 시크릿을 Base64로 "보호된" 설정 파일에 넣지 마세요. 그 보호는 정확히 Convert.FromBase64String 호출 한 번 깊이에 불과하기 때문입니다. 값이 시크릿이어야 한다면 진짜 보호(시크릿 매니저, 암호화된 저장소, 최소한 운영체제 접근 제어)가 필요하며, 거기의 Base64는 그저 전송 포맷일 뿐입니다.

두 번째 보안 노트는 당신의 디코딩 경로에 관한 것입니다. 디코딩하는 모든 페이로드는 반증이 입증될 때까지 신뢰할 수 없는 입력이며, 설계해 두어야 할 두 가지 실패 모드는 소리 나는 쪽(잘못된 입력: 클래식 API가 FormatException으로 답하는데, 이를 잡아 500이 아니라 400으로 바꿔야 함)과 조용한 쪽(유효한 Base64인데, 디코딩된 바이트가 기대한 것이 아닌 것: UTF-8이 아니거나, 요청한 파일 타입이 아니거나, 예산보다 길거나)입니다. 신뢰하기 전에 검증하세요: 할당하기 전에 IsValid나 Try 계열로 길이를 확인하고, 바이트를 이미지나 인증서 파서에 넘기기 전에 기대하는 시그니처(PNG 매직, PKCS 헤더)와 대조하고, 버퍼 크기는 디코딩 이후가 아니라 디코딩 전에 인코딩된 길이에서부터 정하세요. Base64는 올바른 형태의 것이라면 무엇이든 디코딩합니다; 올바른 형태가 당신의 애플리케이션에 무엇을 뜻하는지 결정하는 것은 당신의 일입니다.

물리기 전에 알아 둘 가치 있는 함정

실제 코드에서 계속 나타나는 C#만의 함정들입니다. 그리고 그 하나하나에 모두 프레임워크 작동 방식에 구체적인 원인이 있습니다:

  • 문자열을 거치는 바이너리. C#의 string은 UTF-16 코드 단위 시퀀스이며, 디코딩된 Base64는 그렇지 않습니다. 디코딩한 바이트를 문자열 변수에 쑤 넣는 순간(디코딩한 PNG의 Console.WriteLine, 바이너리와의 문자열 연결, "텍스트"를 직렬화하는 JSON 라이브러리), 하류 어딘가가 그것을 망가뜨립니다. 디코딩한 바이너리는 실제로 바이트를 원하는 곳에 도착할 때까지 byte[]에 담아 두세요.
  • Encoding.Default 갈림길. Encoding.Default로 디코딩한 바이트를 읽는 코드는 .NET Framework(Windows ANSI 코드 페이지)와 .NET(UTF-8)에서 다른 텍스트를 만듭니다. 같은 페이로드, 두 가지 다른 출력, 예외는 없습니다. 인코딩을 명시적으로 고정하세요.
  • JWT 세그먼트와 클래식 디코더. 원본 JWT 세그먼트를 Convert.FromBase64String에 먹이면 한 번에 두 가지 방식으로 실패합니다: -/_ 문자는 표준 알파벳 밖이고, 부족한 패딩은 길이 규칙을 어기거든요. 먼저 정규화하거나, Base64Url을 쓰세요.
  • 보이는 공백과 보이지 않는 공백. 디코더는 스페이스, 탭, 라인 피드, 캐리지 리턴을 건너뛰고, 그 외에는 아무것도 건너뛰지 않습니다. 페이로드에 붙이지 않는 공백, 유니코드 줄 구분자, 세로 탭이 섞여 있으면(몇몇 웹 페이지에서 복사-붙여넣기 후 살아남는 것들), 결과는 무시하는 것이 아니라 FormatException입니다.
  • 모든 범죄에 하나의 에러 메시지. 클래식 디코더의 FormatException은 어느 규칙이 어겨졌고 어디였는지를 말해 주지 않습니다. 길이를 확인하고, 알파벳을 확인하고, 패딩을 확인하는 순서로 디버깅하세요. 아니면 부울 답을 위해 TryFromBase64String과 IsValid로 바꾸세요.
  • 침묵하는 UTF-8 대체. Encoding.UTF8.GetString는 잘못된 바이트 시퀀스를 불평 없이 U+FFFD로 바꿉니다. 페이로드가 유효한 UTF-8이 아닐 수 있다면, 문자 집합 섹션의 엄격한 폴백을 쓰지 않으면, 일이 일어난 몇 주 후에야 사라진 데이터를 조사하게 됩니다.
  • 잘못된 자리에서의 스트림 자르기. Base64 스트림은 4문자의 배수에서만 자를 수 있습니다. 다른 경계에서 스트리밍 디코딩을 청크하면, 마지막 부분 그룹이 TransformFinalBlock에 도착하는데, 거기서는 그것이 정당한 것이거나 당신의 정렬 계산이 깨지거나 둘 중 하나입니다.
  • PEM 줄 끝. 인증서 파일은 CRLF를 갖고 있습니다. 아머를 수동으로 벗길 때 \n뿐 아니라 \r도 벗기지 않으면, 당신의 "디코딩된" DER의 첫 줄은 데이터 바이트의 옷을 입은 캐리지 리턴이 됩니다.
  • 더블 인코딩. 페이로드가 당신에게 도달했을 때 이미 Base64였다면(Base64 문자열을 Base64로 만든 설정, 다른 인코더의 출력을 인코딩한 API), 디코딩 한 번은 당신의 데이터가 아니라 Base64를 더 줍니다. 왕복은 인코딩 수만큼의 디코딩 후에야 닫히며, 그 버그의 인코더 쪽은 인코딩 편에서 다룹니다.

C#에서 Base64의 짧은 역사

C#의 Base64 이야기는 .NET 플랫폼이 성장한 이야기이기도 하며, 대부분의 사람이 기대하는 것보다 깁니다:

  • .NET Framework 1.1, 2003년 4월. Convert.FromBase64String과 형제들이 도착합니다. 그리고 API를 지금까지도 정의하는 설계를 갖고 옵니다: 알파벳에 엄격하고, 네 공백 문자에 관대하며, 오류에는 직설적이죠. 그 뒤 20년의 대부분, 이 한 메서드가 C#의 "그" Base64 디코더였습니다.
  • .NET 2.0, 2005. Base64FormattingOptions 열거형이 Convert에 합류하며, MIME 스타일의 줄바꿈을 인코딩 쪽에 가져옵니다(그리고 대응하는 공백 관용성을 디코딩 쪽에도-이미 조용히 작동하던 곳으로요).
  • .NET Core 2.1, 2018. 스판 시대. Convert가 Try 메서드와 스판 기반 인코딩을 갖게 되고, 새 System.Buffers.Text.Base64 클래스가 OperationStatus 계약, 인플레이스 디코딩, IsValid와 함께 도착합니다. 메모리에 집중한 재작성의 제로 할당 세계를 위해 만들어진 것이죠.
  • .NET 5, 2020. 16진수 형제들(Convert.ToHexString과 친구들)이 출시됩니다. Base64의 같은 디자인 패턴을 16기호 알파벳에 적용한 것인데, 변환 클래스 패턴이 이미 하우스 스타일이 되었음을 보여주는 신호입니다.
  • .NET 6, 2021. X509Certificate2.CreateFromPem이 PEM을 일급 입력으로 만들고, 수동 아머 벗기기 코드의 정정한 무리가 최신 런타임에서 선택 사항이 됩니다.
  • .NET 9, 2024년 11월. System.Buffers.Text.Base64Url이 수년간의 커뮤니티 요청 끝에 드디어 박스에 들어오고, Microsoft.Bcl.Memory 패키지가 여전히 모든 것을 돌리는 레거시 코드베이스를 위해 .NET Framework 4.6.2 이상으로 백포트합니다.
  • .NET 11, 작성 시점에 프리뷰 중. 2026년 말에 나올 것으로 예상되는 다음 릴리스가 기존 타입에 Base64 편의 API와 오버로드를 더하며, 더 편한 표면으로의 느린 행진을 계속합니다.

기억해 둘 가치가 있는 점: 인코딩 자체는 이 모든 것보다 훨씬 더 오래되었습니다. 지금은 MIME Base64라고 부르는 것의 첫 표준화된 사용은 1987년의 Privacy-Enhanced Mail 프로토콜(RFC 989)이며, MIME은 1993년에 76자 줄바꿈 형태를 표준화했고, 2006년의 RFC 4648은 이 포맷에 URL 안전 변형을 포함해 현대적이고 알파벳을 아는 규격을 줍니다. C#은 그것을 전부 상속했습니다: 30년 된 이메일 포맷에서 마주치는 줄바꿈과 패딩 특이점은 전부, C# 디코더가 흡수하도록 설계된 특이점입니다.

호기심 끄는 C# 사실

  • 가장 작은 스모크 테스트. "TWFu"는 Man으로 디코딩됩니다. 세 바이트, 패딩 없음, 변명 없음. C#에서 Base64 디버깅의 헬로 월드이며, 네 문자로 해피 패스 전체를 시험합니다.
  • 우편 역사를 가진 디코더. 공백 관용성은 구현의 사고가 아닙니다. MIME에서 상속된 설계 결정이죠: CRLF 쌍을 다 포함해, 76자 줄바꿈된 이메일 바디 전체가 Convert.FromBase64String의 유효한 단일 인자입니다. 이 디코더는 30년간 이메일이 써 온 포맷을 씹어 먹도록 지어졌습니다.
  • 하나의 에러, 세 가지 원인. 클래식 FormatException 메시지는 보고할 수 있는 세 가지 실패 모드(잘못된 문자, 패딩 과다, 패딩 위치 오류)를 전부 나열할 뿐, 어느 것이 터졌는지는 말하지 않습니다. API 표면에서 다중 선택 문제처럼 작동하는 유일한 에러 메시지입니다.
  • 조금 거짓말하는 네임스페이스. System.Buffers.Text는 텍스트 처리에 관한 것처럼 들리지만, 실제로는 바이너리에서 텍스트로 변환하는 일의 일반적 집입니다: 숫자와 날짜를 UTF-8로 바로 파싱하는 Utf8Parser와 Utf8Formatter가 Base64 클래스 바로 옆에서 삽니다.
  • 패딩은 패밀리의 한쪽에서는 선택 사항. Base64Url 클래스는 AQIDBA(여섯 문자, 패딩 없음)와 AQIDBA==(패딩 있는 같은 바이트)를 같은 네 바이트로 디코딩하는 반면, 클래식 디코더는 패딩된 형태만 받아들입니다. 디코더 둘, 계약 둘, 런타임 하나.
  • 존재하지 말았어야 할 문자열. C# 문자열은 합법적으로 NUL 바이트를 담을 수 있으므로, 디코딩한 바이너리의 Encoding.UTF8.GetString는 콘솔, 당신의 CSV 기록기, 그리고 지구상 JSON 라이브러리의 절반이 각각 다르게 다룰 제어 문자로 가득한 "문자열"을 만들어 낼 수 있습니다. 타입 시스템은 허용합니다. 생태계는 대부분 허용하지 않죠.
  • 건강한 상태의 1.1 유물. Convert.FromBase64CharArray는 2003년 4월 이후 같은 세 파라미터 시그니처를 갖고, 제네릭스 혁명, 스판 혁명, URL 안전 혁명을 오버로드 하나 없이 살아남았습니다. C#의 char 배열 시대는 사라지지 않았습니다. 그저 쉬고 있는 것이죠.
  • 열한 문자, 여덟 바이트. YouTube의 영상 식별자는 패딩 없는 base64url입니다: 8바이트로 디코딩되는 11문자. Base64Url.GetMaxDecodedLength(11)가 8을 알려 주고, 디코딩은 한 줄입니다. 그런 종류의 것을 쓰는 사람이면, 하루를 끝내는 기분 좋은 방법이죠.

다른 방향

이것이 디코더 편이며, 고통의 대부분이 사는 곳입니다. 디코딩은 남의 데이터를 마주하는 곳이니까요: 그들의 패딩 선택, 그들의 줄바꿈, 그들의 알파벳, 그들의 토큰. 반대 방향, 즉 자신의 바이트를 Base64에 싸 넣는 일은 더 고요한 문제입니다. 할 결정의 세트도, 함정의 세트도 자기만의 것이죠. C#에서 Base64 인코딩, 76자 문제부터 URL 안전 토큰까지는 아래에 링크된 짝편에서 깊이 다루며, 무엇을 찾아야 할지 알기만 하면 짧고 만족스러운 읽을거리입니다.

마지막 업데이트: 2026-09-08

관련 문서: C# (CSharp)에서의 Base64 인코딩: 완전한 가이드