C# ObjectPool과 ArrayPool 활용
왜 풀링이 필요한가
섹션 제목: “왜 풀링이 필요한가”짧은 생명주기의 대형 객체를 반복 생성하면 GC가 자주 발생합니다. 풀링은 객체를 재사용해 할당과 GC 부담을 줄입니다.
ArrayPool<T>
섹션 제목: “ArrayPool<T>”임시 배열을 스택처럼 빌리고 반납합니다. 대용량 임시 버퍼에 적합합니다.
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);ObjectPool<T> (.NET 6+)
섹션 제목: “ObjectPool<T> (.NET 6+)”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);}커스텀 ObjectPool 구현
섹션 제목: “커스텀 ObjectPool 구현”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);IDisposable 패턴 풀
섹션 제목: “IDisposable 패턴 풀”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");DI와 통합 (ASP.NET Core)
섹션 제목: “DI와 통합 (ASP.NET Core)”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 프레셔가 높은 핫 패스에서 우선 적용 고려