unsafe 코드는 CLR의 메모리 안전 장치를 우회해 포인터로 직접 메모리를 다루게 합니다. 관리 코드만으로는 어려운 고성능 연산, 네이티브 상호운용, 바이너리 파싱에 유용합니다. 다만 잘못 사용하면 크래시/데이터 손상이 일어날 수 있으니 반드시 범위를 최소화하고 검증하며 사용합니다.
1. 프로젝트에서 unsafe 활성화
// .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
// CLI 빌드
csc /unsafe Program.cs
// 또는
dotnet build -p:AllowUnsafeBlocks=true
2. 기본 문법 요약
using System;
class Program
{
static void Main()
{
unsafe
{
int x = 10;
int* p = &x; // 주소 연산자 &
int v = *p; // 역참조 *
*p = 42; // 값 쓰기
Console.WriteLine(v); // 10
Console.WriteLine(x); // 42
// 구조체 포인터와 -> 연산자
Point pt = new Point { X = 3, Y = 4 };
Point* pp = &pt;
pp->X = 7;
Console.WriteLine(pp->Y); // 4
}
}
}
struct Point
{
public int X;
public int Y;
}
포인터 산술은 동일한 요소 크기 단위로 이동합니다. 예: p + 1은 sizeof(T)만큼 이동합니다.
3. fixed로 관리 배열 고정(pinning) 후 접근
GC는 객체를 이동시킬 수 있으므로, 관리 힙 객체의 주소를 얻어 쓰려면 fixed로 고정해야 합니다.
using System;
class Program
{
static unsafe long Sum(int[] data)
{
long sum = 0;
fixed (int* p = data) // 배열을 고정하고 첫 요소 포인터 획득
{
for (int i = 0; i < data.Length; i++)
sum += p[i]; // 인덱서 문법 허용
}
return sum;
}
static unsafe void Fill(byte[] data, byte value)
{
fixed (byte* p = data)
{
for (int i = 0; i < data.Length; i++)
p[i] = value;
}
}
}
4. stackalloc으로 빠른 임시 버퍼 만들기
stackalloc은 스택에 고정 크기 버퍼를 할당합니다. 함수가 끝나면 자동 해제되며 GC 부담이 없습니다.
using System;
class Program
{
static unsafe byte[] MakeSequence(int count)
{
byte* buf = stackalloc byte[count];
for (int i = 0; i < count; i++)
buf[i] = (byte)i;
var managed = new byte[count];
fixed (byte* pManaged = managed)
{
Buffer.MemoryCopy(buf, pManaged, managed.Length, count);
}
return managed;
}
}
stackalloc 버퍼 크기는 과도하게 키우지 않습니다(스택 오버플로우 위험). 큰 버퍼는 ArrayPool 등 다른 방법을 고려합니다.
5. 고성능 메모리 복사/변환
using System;
class Program
{
static unsafe void FastCopy(byte[] src, byte[] dst, int count)
{
if (count < 0 || count > src.Length || count > dst.Length)
throw new ArgumentOutOfRangeException(nameof(count));
fixed (byte* pSrc = src)
fixed (byte* pDst = dst)
{
Buffer.MemoryCopy(pSrc, pDst, dst.Length, count);
}
}
// 재해석 캐스팅: 같은 크기의 값 형식 간 바이트 레벨 접근
static unsafe uint ReinterpretToUInt(int value)
{
int* p = &value;
return *(uint*)p;
}
}
재해석 캐스팅은 엔디언, 정렬에 민감합니다. 교차 플랫폼을 고려해야 합니다.
6. 구조체의 고정 크기 버퍼
unsafe struct에서 fixed 버퍼를 선언하면 헤더 없는 연속 메모리 필드를 가질 수 있습니다.
using System;
unsafe struct Packet
{
public int Length;
public fixed byte Data[256];
}
class Program
{
static unsafe void UsePacket()
{
Packet p;
p.Length = 4;
// fixed 버퍼는 포인터를 얻어 접근
fixed (byte* d = p.Data)
{
for (int i = 0; i < p.Length; i++)
d[i] = (byte)(i + 1);
for (int i = 0; i < p.Length; i++)
Console.Write(d[i]);
}
}
}
7. 안전 가이드와 체크리스트
- 범위를 최소화: 필요한 코드에만 unsafe 키워드/블록을 씁니다.
- 고정 최소화: fixed로 핀 상태를 짧게 유지합니다(긴 pinning은 GC 단편화 유발).
- 경계 검사: 길이/오프셋을 엄격히 검증합니다. 오버플로우를 주의합니다.
- 플랫폼 고려: 포인터 크기(32/64비트)와 정렬, 엔디언을 고려합니다. 포인터/크기엔 IntPtr, UIntPtr, nint, nuint를 활용합니다.
- 대안 우선: 가능한 경우 Span<T>/ReadOnlySpan<T>, MemoryMarshal, BinaryPrimitives 등 안전한 API를 먼저 고려합니다.
8. 언제 쓰면 좋은가
- 핫패스의 바운드 체크 제거, 대량 메모리 조작이 필요한 경우
- 네이티브 라이브러리와 포인터 기반 인터페이스로 상호운용할 때
- 프로토콜/파일 포맷의 바이너리 파싱 최적화
unsafe는 마지막 수단입니다. 먼저 안전한 방법을 검토하고, 측정으로 필요성을 확인한 뒤, 작은 단위로 도입하고 철저히 테스트합니다.
'C#' 카테고리의 다른 글
| C# BackgroundWorker로 백그라운드 작업 처리 (0) | 2026.04.22 |
|---|---|
| C# 호출자 정보 특성(Caller Info Attributes) 활용 (0) | 2026.04.21 |
| C# 구조체(Struct) 심층 분석 (1) | 2026.04.21 |
| C# 메모리 누수 방지를 위한 약한 참조(WeakReference) 사용 (1) | 2026.04.20 |
| C# 이벤트 접근자(add/remove) 커스터마이징 (1) | 2026.04.20 |