iOS & Swift

Swift 오브젝트 풀 패턴(Object Pool Pattern) 완전정리 (플라이웨이트와 차이·재사용법)

게임 이펙트를 잔뜩 뿌리는 화면을 만들다 보면 프레임이 뚝뚝 끊기는 순간이 옵니다.

이석우iOS Developer5분 읽기
Swift 오브젝트 풀 패턴(Object Pool Pattern) 완전정리 (플라이웨이트와 차이·재사용법) 대표 이미지

게임 이펙트를 잔뜩 뿌리는 화면을 만들다 보면 프레임이 뚝뚝 끊기는 순간이 옵니다.

총알이나 파티클처럼 짧게 살고 금방 사라지는 객체를 계속 init으로 만들면, 인스트루먼트에서 메모리 그래프가 톱니처럼 튀는 걸 보게 됩니다.

이럴 때 꺼내 드는 게 바로 Swift 오브젝트 풀 패턴입니다.

결론부터 말씀드릴게요. 오브젝트 풀은 “객체를 매번 새로 만들지 않고 미리 만들어둔 걸 빌려 쓰고 반납하는” 재사용 기법이고 자주 헷갈리는 플라이웨이트와는 목적 자체가 다릅니다.

Swift 오브젝트 풀과 플라이웨이트, 한눈에 비교해봤어요
Swift 오브젝트 풀과 플라이웨이트, 한눈에 비교해봤어요

이 글에서는 오브젝트 풀이 뭔지, 플라이웨이트와 어떻게 다른지, 그리고 Swift로 실제 어떻게 짜는지까지 순서대로 풀어드릴게요.


오브젝트 풀 패턴이 뭔가요?

오브젝트 풀은 생성 비용이 비싼 객체를 미리 여러 개 만들어 “풀(pool)“에 담아두는 방식입니다.

필요할 때 새로 만드는 게 아니라 풀에서 하나 꺼내 쓰고, 다 쓰면 다시 풀에 반납합니다.

핵심은 재사용이에요. initdeinit을 반복하면서 생기는 메모리 할당·해제 비용을 줄이는 게 목적입니다.

오브젝트 풀의 한 문장 요약은 “만들지 말고 빌려 쓰고 돌려받아라”입니다.

주로 이런 상황에서 빛을 발합니다.

  • 총알, 파티클처럼 짧은 시간에 대량으로 생성·소멸되는 객체
  • 네트워크 연결이나 스레드처럼 생성 자체가 무거운 리소스
  • UITableViewCell의 재사용 큐처럼 화면에서 계속 갈아끼우는 뷰

사실 iOS 개발자라면 이미 오브젝트 풀을 쓰고 있었어요. dequeueReusableCell이 바로 애플이 만들어둔 오브젝트 풀이거든요.


플라이웨이트와 뭐가 다른가요?

둘 다 “객체를 아껴 쓴다”는 인상 때문에 헷갈리기 쉽습니다.

하지만 목적이 완전히 다릅니다.

오브젝트 풀은 같은 객체를 시간차를 두고 돌려쓰는 방식입니다. 지금 이 총알을 쓰고 반납하면, 다음 총알이 그 자리를 물려받아요.

플라이웨이트는 여러 곳에서 동시에 하나의 공유 객체를 참조하는 방식입니다. 예를 들어 숲에 나무 1만 그루를 그릴 때, 나무의 색·질감 같은 공통 데이터는 한 벌만 두고 위치 값만 따로 넘깁니다.

표로 정리하면 이렇게 나뉩니다.

구분 오브젝트 풀 플라이웨이트
목적 생성·해제 비용 절감 메모리 사용량 절감
재사용 방식 빌려 쓰고 반납 (시간 분할) 동시에 공유 (공간 분할)
상태 객체마다 고유 상태 유지 공유 상태 + 외부 상태 분리
대표 예시 셀 재사용, 파티클 폰트 글리프, 지도 아이콘

한 줄로 정리하면 오브젝트 풀은 “차례로 돌려쓰기”, 플라이웨이트는 “다 같이 나눠쓰기”입니다.

빌려 쓰고 반납하는 구조라 코드가 생각보다 단순합니다
빌려 쓰고 반납하는 구조라 코드가 생각보다 단순합니다

Swift로 오브젝트 풀 만드는 법

생각보다 구조는 단순합니다. 사용 가능한 객체를 담는 배열 하나, 그리고 빌려주고 돌려받는 메서드 두 개면 됩니다.

아래는 제네릭으로 짠 간단한 풀입니다.

final class ObjectPool<T> {
    private var available: [T] = []
    private let factory: () -> T

    init(factory: @escaping () -> T) { self.factory = factory }

    func acquire() -> T { available.popLast() ?? factory() }  // 없으면 새로 생성
    func release(_ item: T) { available.append(item) }        // 다 쓰면 반납
}

acquire()는 풀에 남은 게 있으면 꺼내 주고, 비었으면 그때만 새로 만듭니다.

release()로 반납하면 다음 요청이 그 객체를 재사용해요.

빌리고 쓰고 돌려주고, 이 순환이 전부예요
빌리고 쓰고 돌려주고, 이 순환이 전부예요

실제로 파티클에 적용하면 이런 흐름이 됩니다.

let pool = ObjectPool<Particle> { Particle() }

let p = pool.acquire()  // 풀에서 빌리기
p.reset(at: point)      // 상태 초기화가 중요!
// ...화면에서 다 쓴 뒤
pool.release(p)         // 반납

여기서 꼭 기억할 점 하나. 반납받은 객체는 이전 상태가 그대로 남아 있어요.

그래서 다시 빌려줄 때는 reset() 같은 초기화를 반드시 거쳐야 유령 데이터가 안 생깁니다.


오브젝트 풀, 언제 쓰고 언제 피할까?

좋다고 아무 데나 쓰면 오히려 독이 됩니다.

직접 써보고 정리한 기준이에요.

이럴 때 추천해요.

  1. 초당 수십 개 이상 생성·소멸이 반복될 때
  2. 객체 하나 만드는 비용이 눈에 띄게 클 때
  3. 메모리 그래프가 톱니처럼 튀어 GC(가비지 컬렉션)/ARC(Automatic Reference Counting, 자동 참조 계수) 부담이 보일 때

이럴 땐 다시 생각해보세요.

  • 가끔 한두 개만 만드는 가벼운 객체라면 풀 관리 비용이 더 큽니다
  • 반납을 깜빡하면 풀이 텅 비어 오히려 매번 새로 만들게 됩니다
  • 멀티스레드 환경이면 풀 접근에 락이나 큐 동기화가 필요해요

Swift는 값 타입(struct)이 많고 ARC가 꽤 효율적이라 무조건 풀부터 도입할 필요는 없습니다.

먼저 인스트루먼트로 측정하고 병목이 확인됐을 때 도입하는 순서를 권합니다.

측정해보니 메모리 그래프부터 확실히 잔잔해지더라고요
측정해보니 메모리 그래프부터 확실히 잔잔해지더라고요

오브젝트 풀은 생성 비용을 아끼는 재사용 패턴이고 플라이웨이트는 메모리를 아끼는 공유 패턴입니다.

둘의 차이만 확실히 잡아두면 상황에 맞는 카드를 꺼내 쓸 수 있어요.

측정부터 하고 필요할 때 딱 맞게 도입해보세요. 프레임이 한결 부드러워지는 순간을 꼭 경험하시길 바랍니다.

함께 보면 좋은 글