콘텐츠로 이동

C# ObjectPool과 ArrayPool 활용

짧은 생명주기의 대형 객체를 반복 생성하면 GC가 자주 발생합니다. 풀링은 객체를 재사용해 할당과 GC 부담을 줄입니다.

임시 배열을 스택처럼 빌리고 반납합니다. 대용량 임시 버퍼에 적합합니다.

using System.Buffers;
// 공유 풀 (스레드 안전)
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096); // 실제로는 4096 이상 반환될 수 있음
try {
int read = stream.Read(buffer, 0, buffer.Length);
Process(buffer.AsSpan(0, read));
} finally {
ArrayPool<byte>.Shared.Return(buffer, clearArray: false); // clearArray: 민감 데이터면 true
}

커스텀 풀:

var pool = ArrayPool<int>.Create(maxArrayLength: 1024, maxArraysPerBucket: 50);
int[] arr = pool.Rent(100);
pool.Return(arr);
using Microsoft.Extensions.ObjectPool;
var policy = new DefaultPooledObjectPolicy<StringBuilder>();
var pool = new DefaultObjectPool<StringBuilder>(policy, maximumRetained: 10);
StringBuilder sb = pool.Get();
try {
sb.Clear();
sb.Append("hello ");
sb.Append("world");
string result = sb.ToString();
} finally {
pool.Return(sb);
}
public sealed class SimplePool<T> where T : class {
private readonly ConcurrentBag<T> _bag = new();
private readonly Func<T> _factory;
private readonly Action<T>? _reset;
public SimplePool(Func<T> factory, Action<T>? reset = null) {
_factory = factory;
_reset = reset;
}
public T Rent() => _bag.TryTake(out var obj) ? obj : _factory();
public void Return(T obj) {
_reset?.Invoke(obj);
_bag.Add(obj);
}
}
// 사용
var pool = new SimplePool<List<int>>(
factory: () => new List<int>(64),
reset: list => list.Clear());
var list = pool.Rent();
list.Add(1); list.Add(2);
pool.Return(list);
public struct PooledObject<T> : IDisposable where T : class {
private readonly SimplePool<T> _pool;
public readonly T Value;
public PooledObject(SimplePool<T> pool) {
_pool = pool;
Value = pool.Rent();
}
public void Dispose() => _pool.Return(Value);
}
// using으로 자동 반납
using var pooled = new PooledObject<StringBuilder>(sbPool);
pooled.Value.Append("hello");
Program.cs
builder.Services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
builder.Services.AddSingleton<ObjectPool<StringBuilder>>(sp => {
var provider = sp.GetRequiredService<ObjectPoolProvider>();
return provider.CreateStringBuilderPool();
});
// 컨트롤러에서 주입
public class MyController {
private readonly ObjectPool<StringBuilder> _pool;
public MyController(ObjectPool<StringBuilder> pool) => _pool = pool;
public string Format() {
var sb = _pool.Get();
try {
sb.Clear();
sb.Append("result");
return sb.ToString();
} finally { _pool.Return(sb); }
}
}
// 벤치마크 결과 예시 (1M iterations)
// new byte[4096]: ~50ms, GC 빈번
// ArrayPool.Rent/Return: ~5ms, GC 없음
  • ArrayPool<T>.Shared.Rent/Return으로 임시 배열 재사용 — 가장 간단한 풀링
  • ObjectPool<T>로 StringBuilder, List 등 재사용 비용이 있는 객체 관리
  • clearArray: true는 민감 데이터(암호, 토큰) 처리 시 필수
  • IDisposable 래퍼로 using 블록에서 자동 반납 패턴 구현
  • GC 프레셔가 높은 핫 패스에서 우선 적용 고려