게임 프로그래밍 메모리 누수 해결 가이드 2026

profile_image
작성자 메모리분석가 지후
댓글 0건 조회 5회

프레임은 멀쩡한데 게임이 무거워지는 이유

메모리 누수는 FPS보다 늦게 드러납니다

게임 프로그래밍에서 가장 피곤한 문제는 처음 5분 동안 재현되지 않는 버그입니다. 시작 직후에는 144FPS로 부드럽게 돌아가지만, 30분 플레이 후 로딩이 길어지고 텍스처 전환이 버벅이며, 결국 특정 맵에서 크래시가 나는 상황이라면 메모리 누수를 먼저 의심해야 합니다.

특히 개인 포트폴리오나 테크 데모를 만드는 개발자는 렌더링 효과, 물리, AI, 사운드보다 메모리 수명 관리가 뒤로 밀리기 쉽습니다. 하지만 채용 담당자나 동료 개발자가 빌드를 오래 실행했을 때 안정성이 무너지면, 코드 품질에 대한 신뢰도도 같이 흔들립니다.

  • 증상 1: 같은 스테이지를 반복 진입할수록 RAM 사용량이 계속 증가합니다.
  • 증상 2: 오브젝트를 삭제했는데 충돌 콜백이나 이벤트가 계속 호출됩니다.
  • 증상 3: 씬 전환 후에도 텍스처, 메시, 사운드 버퍼가 해제되지 않습니다.
  • 증상 4: 디버그 빌드에서는 괜찮지만 릴리즈 빌드에서만 불규칙하게 크래시가 납니다.
팁: 메모리 문제는 “한 번에 큰 폭으로 터지는 버그”보다 “작은 할당이 해제되지 않고 누적되는 버그”인 경우가 많습니다. 10분 단위가 아니라 1시간 단위로 관찰해야 실제 문제가 보입니다.

Will Perone처럼 게임 프로그래밍, 수학 라이브러리, 개발 포트폴리오를 함께 다루는 사이트라면 단순한 문법 설명보다 실제 런타임에서 어떻게 고장을 찾고 줄일 것인가가 더 중요합니다. 이 글은 C/C++ 기반 엔진, Unity 네이티브 플러그인, 커스텀 math 라이브러리, 소규모 게임 엔진 프로젝트에 모두 적용할 수 있는 문제 해결 흐름으로 구성했습니다.

가장 흔한 원인: 소유권이 불분명한 객체

누가 만들고 누가 해제하는지 코드에 보이나요?

메모리 누수의 출발점은 대개 객체 소유권이 불명확한 코드입니다. 예를 들어 EnemyManager가 Enemy를 만들고, Scene이 Enemy 목록을 들고 있으며, PhysicsWorld가 충돌 포인터를 참조하고, UI가 체력바 대상으로 같은 객체를 바라본다면 삭제 책임이 흐려집니다.

이때 개발자는 “씬이 끝나면 알아서 정리되겠지”라고 생각하지만, 실제로는 이벤트 리스너나 콜백 람다가 객체 참조를 붙잡고 있을 수 있습니다. C++에서는 raw pointer가 남고, C#에서는 이벤트 구독이 해제되지 않아 GC 대상에서 빠지는 일이 흔합니다.

  1. 생성 위치를 기록합니다. new, malloc, CreateTexture, LoadMesh, AddComponent 같은 호출을 추적합니다.
  2. 해제 책임자를 정합니다. Scene, ResourceManager, EntityRegistry 중 하나가 최종 소유자가 되어야 합니다.
  3. 참조자는 소유하지 않게 만듭니다. 렌더러나 물리 시스템은 ID, weak reference, handle을 사용하도록 분리합니다.
  4. 씬 종료 시 검증합니다. 활성 엔티티 수, 리소스 핸들 수, 이벤트 구독 수가 0으로 내려가는지 로그로 확인합니다.

RAII와 스마트 포인터를 과신하지 마세요

현대 C++에서는 std::unique_ptrstd::shared_ptr가 기본 도구처럼 쓰입니다. 하지만 shared_ptr를 무분별하게 쓰면 순환 참조가 생기고, unique_ptr를 컨테이너에 넣어도 외부 raw pointer가 오래 살아남으면 use-after-free가 됩니다.

게임 오브젝트 구조에서는 “모두 shared_ptr로 감싸기”보다 수명 단위를 먼저 나누는 편이 좋습니다. 씬 단위로 사라지는 객체, 게임 전체에서 유지되는 리소스, 한 프레임만 필요한 임시 데이터, 에디터에서만 필요한 디버그 데이터가 서로 다른 규칙을 가져야 합니다.

  • Scene-owned: 캐릭터, 투사체, 트리거 볼륨처럼 씬 종료와 함께 사라지는 객체
  • Global resource: 공용 셰이더, 기본 머티리얼, 폰트, 입력 매핑 데이터
  • Frame allocator: 충돌 후보 목록, 렌더 큐, 임시 행렬 배열처럼 프레임 끝에 일괄 폐기 가능한 데이터
  • Tool-only: 에디터 선택 상태, 디버그 라인, 프로파일링 라벨

기초를 다시 점검하고 싶다면 Fundamentals of C/C++ Game Programming 관련 서적처럼 타깃 기반 개발 관점의 자료를 참고하면 포인터, 빌드, 런타임 환경을 함께 이해하는 데 도움이 됩니다.

리소스 로딩 문제를 찾는 단계별 점검법

텍스처와 메시가 계속 쌓이는 패턴

게임 프로그래밍에서 메모리 누수처럼 보이는 문제 중 상당수는 사실 리소스 캐시 정책 문제입니다. 같은 PNG 파일을 씬 진입 때마다 새 텍스처로 올리고, 파일 경로 문자열이 조금씩 달라 캐시 키가 다르게 잡히면 GPU 메모리와 시스템 메모리가 동시에 증가합니다.

예를 들어 assets/character/hero.png./assets/character/hero.png를 다른 키로 취급하면 같은 이미지가 두 번 로딩됩니다. 대소문자 구분이 있는 플랫폼에서는 Hero.png와 hero.png가 별도 리소스가 될 수 있어 윈도우 개발 환경에서는 멀쩡하다가 배포 환경에서 문제가 드러나기도 합니다.

  1. 리소스 키를 정규화합니다. 경로 구분자, 대소문자, 상대 경로를 로딩 직전에 통일합니다.
  2. 참조 카운트를 로그로 남깁니다. Load, Retain, Release 시점에 리소스 ID와 카운트를 출력합니다.
  3. 씬 전환 스냅샷을 비교합니다. 전환 전후 텍스처 수, 메시 수, 오디오 버퍼 수를 표로 비교합니다.
  4. GPU 리소스 해제를 분리 확인합니다. CPU 객체가 해제되어도 그래픽 API 핸들이 남아 있을 수 있습니다.

리소스 매니저 체크리스트

리소스 매니저를 만들 때 가장 흔한 실수는 “중복 로딩 방지”까지만 구현하고 “언제 제거할지”를 설계하지 않는 것입니다. 포트폴리오 프로젝트라도 캐시 정책은 반드시 있어야 하며, 최소한 수동 언로드와 씬 범위 언로드는 제공하는 편이 좋습니다.

아래 표처럼 리소스 유형별 해제 기준을 분리하면 디버깅이 쉬워집니다. 특히 수학 라이브러리나 렌더링 데모를 공개하는 개발자라면, 데모가 짧게만 동작하는 것이 아니라 반복 실행에도 안정적인지 보여주는 것이 중요합니다.

  • 텍스처: 마지막 참조가 사라지고 다음 프레임 경계에서 GPU 핸들을 해제합니다.
  • 메시: 정적 메시와 동적 메시를 나누고, 동적 버퍼는 풀링 여부를 기록합니다.
  • 사운드: 짧은 효과음은 캐시 유지, 긴 스트리밍 음원은 씬 종료 시 닫습니다.
  • 셰이더: 전역 캐시로 유지하되 핫리로드 시 이전 프로그램 핸들을 반드시 삭제합니다.
실무형 규칙: “로드한 곳에서 바로 해제”보다 “소유권을 가진 시스템에서 일관되게 해제”하는 구조가 장기적으로 안전합니다. 디버깅 로그도 그 시스템 한곳에 모입니다.

게임 개발 컨퍼런스와 산업 흐름을 살펴보면 최적화와 툴링은 늘 반복되는 핵심 주제입니다. 용어 배경은 GDC 지식백과 설명에서도 확인할 수 있으며, 개인 프로젝트라도 프로파일링 습관을 갖추면 결과물의 완성도가 달라집니다.

수학 라이브러리에서 생기는 숨은 할당 줄이기

Vector와 Matrix 연산이 가벼워 보인다는 착각

Will Perone 사이트의 키워드에 math가 포함된 만큼, 게임 프로그래밍에서 수학 라이브러리는 단순 보조 코드가 아니라 성능과 안정성의 중심입니다. 벡터, 행렬, 쿼터니언 연산은 매 프레임 수천 번 이상 호출되기 때문에 작은 할당 하나도 빠르게 누적됩니다.

문제는 수학 타입이 “작은 값 객체”처럼 보여도 구현에 따라 임시 객체를 많이 만들 수 있다는 점입니다. operator+와 operator*를 과하게 연결하거나, 행렬 배열을 매번 동적 할당하거나, SIMD 정렬 요구사항을 무시하면 성능 저하와 메모리 오류가 함께 발생합니다.

  • 임시 객체 폭증: a + b + c 같은 표현식에서 중간 값이 반복 생성될 수 있습니다.
  • 정렬 문제: SIMD 타입은 16바이트 또는 32바이트 정렬이 필요할 수 있습니다.
  • 동적 배열 남용: 스켈레톤 팔레트, 카메라 행렬 목록을 매 프레임 새로 만들면 힙 압박이 커집니다.
  • API 경계 복사: 렌더러, 물리, 애니메이션 시스템 사이에서 큰 구조체를 값으로 넘기면 비용이 커집니다.

할당 없는 수학 코드를 만드는 기준

수학 라이브러리의 핵심은 멋진 문법보다 예측 가능한 비용입니다. Transform 계산, 카메라 뷰 행렬, 충돌 판정용 벡터 연산은 가능하면 스택 기반 값 타입으로 유지하고, 동적 메모리 할당은 명시적인 컨테이너 계층에서만 발생하도록 제한하는 것이 좋습니다.

예를 들어 Matrix4는 내부에 float[16] 또는 SIMD 레지스터 형태를 두고, 연산 결과는 값으로 반환하되 컴파일러의 RVO와 인라이닝을 기대할 수 있게 단순하게 둡니다. 반대로 “편리한 동적 크기 Matrix”를 게임 루프 안에서 사용하면 일반적인 엔진 코드에서는 과한 비용이 됩니다.

  1. 프로파일링 지점을 만듭니다. math 연산 자체보다 호출되는 루프와 데이터 개수를 함께 봅니다.
  2. 힙 할당을 금지할 구역을 정합니다. Update, Render, PhysicsStep 안에서는 new가 나오지 않게 검사합니다.
  3. 정렬 테스트를 추가합니다. SIMD 타입을 vector에 넣을 때 allocator가 올바른지 확인합니다.
  4. 복사 비용을 측정합니다. 큰 구조체는 const reference 또는 span 형태로 넘기는지 점검합니다.

이 접근은 “빠른 코드”만을 위한 것이 아닙니다. 할당 위치가 줄어들면 메모리 누수 원인도 줄어들고, 디버거에서 추적해야 할 경로가 짧아집니다. 포트폴리오 코드 리뷰에서도 이런 설계는 개발자의 기본기를 보여주는 강한 신호가 됩니다.

디버깅 도구로 원인을 좁히는 실전 절차

재현 조건부터 숫자로 고정합니다

메모리 누수를 잡을 때 “가끔 느려집니다”라는 설명은 충분하지 않습니다. 먼저 어떤 입력, 어떤 맵, 몇 분 플레이, 몇 번의 씬 전환 후 문제가 생기는지 숫자로 고정해야 합니다. 그래야 개선 전후를 비교할 수 있고, 수정이 실제로 효과가 있었는지 판단할 수 있습니다.

2026년 기준으로도 기본 원칙은 변하지 않습니다. 운영체제 도구, 엔진 프로파일러, AddressSanitizer, Valgrind, Visual Studio Diagnostic Tools, Xcode Instruments, RenderDoc, GPUView 같은 도구 중 프로젝트 환경에 맞는 것을 골라 반복 가능한 측정 루틴을 만드는 것이 핵심입니다.

  1. 기준 시나리오 작성: 게임 실행, 메뉴 진입, 전투 5분, 씬 전환 10회처럼 순서를 고정합니다.
  2. 메모리 스냅샷 저장: 시작 직후, 10분 후, 30분 후, 종료 직전 데이터를 비교합니다.
  3. 할당 콜스택 확인: 증가량이 큰 타입과 생성 함수를 우선 추적합니다.
  4. 수정 후 같은 절차 반복: 다른 조건으로 테스트하면 개선 여부를 판단하기 어렵습니다.

로그는 적게, 하지만 추적 가능하게

게임 개발자가 자주 하는 실수는 로그를 너무 많이 찍거나, 반대로 중요한 식별자가 빠진 로그를 남기는 것입니다. “Texture loaded”라는 로그는 도움이 거의 없습니다. “Texture loaded id=grass_01 path=assets/terrain/grass.png ref=3 size=2048x2048”처럼 원인을 좁힐 수 있어야 합니다.

디버그 빌드에서는 리소스 생성 시점의 파일명, 라인 번호, 스레드 ID, 프레임 번호를 함께 남기면 좋습니다. 단, 릴리즈 빌드에서는 성능과 보안 문제를 고려해 상세 로그를 끄거나 샘플링해야 합니다.

  • 좋은 로그: 객체 ID, 소유 시스템, 생성 프레임, 해제 프레임, 참조 수가 있습니다.
  • 나쁜 로그: 성공/실패만 있고 어떤 객체인지 알 수 없습니다.
  • 위험한 로그: 매 프레임 수천 줄을 출력해 문제를 더 악화시킵니다.
  • 추천 방식: 누수 의심 타입만 필터링하고, 씬 종료 시 요약 리포트를 출력합니다.

팀 프로젝트라면 기획자와 개발자가 같은 재현 절차를 공유해야 합니다. 역할 용어가 궁금하다면 기획자 지식백과 항목을 참고할 수 있지만, 실제 현장에서는 “문제 상황을 누구나 같은 순서로 재현할 수 있게 쓰는 문서”가 훨씬 중요합니다.

다시 터지지 않게 막는 코드 구조와 리뷰 기준

자동 테스트보다 먼저 필요한 런타임 감시

메모리 누수는 단위 테스트만으로 완전히 막기 어렵습니다. 렌더링, 입력, 씬 전환, 리소스 캐시, 스레드 작업이 함께 얽힐 때 발생하기 때문입니다. 그래서 게임 프로그래밍에서는 테스트와 함께 런타임 감시 장치를 넣어두는 편이 현실적입니다.

가장 간단한 방법은 디버그 빌드에서 전역 할당 카운터와 리소스 카운터를 두는 것입니다. 씬 진입 전후 수치가 일정 범위 이상 차이 나면 경고를 띄우고, 개발자 콘솔에서 현재 살아 있는 리소스 목록을 확인할 수 있게 만들면 원인 추적 시간이 크게 줄어듭니다.

  • 씬 종료 검증: 남은 Entity, Component, Texture, Mesh, AudioClip 수를 출력합니다.
  • 프레임 할당 경고: Update와 Render 구간에서 힙 할당이 발생하면 표시합니다.
  • 리소스 예산: 플랫폼별 메모리 상한을 정하고 초과 시 빨간색 경고를 냅니다.
  • 핸들 유효성 검사: 이미 해제된 리소스 ID 접근을 즉시 감지합니다.

코드 리뷰에서 바로 보는 질문들

포트폴리오나 오픈소스 게임 프로젝트를 리뷰할 때는 그래픽 효과보다 수명 관리 코드를 먼저 보는 편이 좋습니다. 화려한 셰이더가 있어도 씬 전환마다 메모리가 증가하면 완성도가 낮아 보입니다. 반대로 작은 데모라도 리소스 수명과 오류 처리가 명확하면 개발자의 신뢰도가 올라갑니다.

리뷰 기준은 복잡할 필요가 없습니다. “이 객체는 누가 소유하는가?”, “해제 시점은 어디인가?”, “실패하면 어떤 상태가 남는가?”, “같은 리소스를 두 번 로딩하지 않는가?”라는 질문만 꾸준히 적용해도 많은 문제를 초기에 잡을 수 있습니다.

  1. 소유권 주석이 필요한가? 코드 구조만으로 소유자가 보이지 않으면 설계가 흐릴 수 있습니다.
  2. 순환 참조가 가능한가? Entity와 Component, UI와 Model 사이의 양방향 참조를 확인합니다.
  3. 예외나 실패 경로가 안전한가? 로딩 중 실패했을 때 이미 만든 리소스를 정리하는지 봅니다.
  4. 플랫폼 차이를 고려했는가? 파일 경로, 정렬, 그래픽 API 해제 규칙은 환경마다 다를 수 있습니다.

실전에서 가장 효과적인 습관은 작은 체크를 자동화하는 것입니다. 빌드 후 10회 씬 전환 테스트를 실행하고, 메모리 증가량이 기준치를 넘으면 실패 처리하는 스크립트를 두면 사람이 놓치는 문제를 줄일 수 있습니다. 개발자 개인 사이트에 올릴 포트폴리오라면 이 자동 검증 로그를 README에 짧게 남기는 것도 좋은 신호입니다.

자주 묻는 질문과 빠른 점검표

GC가 있는 언어도 메모리 누수가 생기나요?

네, 생깁니다. C#이나 JavaScript처럼 GC가 있는 환경에서도 이벤트 구독, 정적 컬렉션, 타이머, 클로저, 네이티브 플러그인 핸들이 객체를 계속 붙잡으면 메모리가 회수되지 않습니다. Unity 프로젝트에서 씬을 바꿨는데 이전 씬의 매니저가 살아 있다면 대부분 이런 참조 문제가 숨어 있습니다.

또한 GC가 회수해 주는 것은 관리 메모리 중심입니다. 텍스처, 렌더 타깃, 네이티브 버퍼, 오디오 핸들처럼 엔진이나 운영체제 리소스와 연결된 객체는 명시적인 Dispose, Release, Destroy 패턴이 필요할 수 있습니다. “GC가 있으니 괜찮다”는 생각은 게임 런타임에서는 위험합니다.

  • Unity: 이벤트 해제, Addressables Release, Destroy 호출 누락을 확인합니다.
  • Unreal: UObject 참조, 스마트 포인터 종류, 비동기 로딩 핸들을 확인합니다.
  • 커스텀 C++ 엔진: allocator, resource handle, graphics API destroy 호출을 확인합니다.
  • 웹 게임: WebGL texture delete, audio node 연결 해제, animation loop 중복 등록을 확인합니다.

지금 바로 실행할 15분 진단 루틴

시간이 부족하다면 아래 순서만 따라 해도 원인의 범위를 크게 줄일 수 있습니다. 특히 면접용 게임 데모, GitHub 포트폴리오, 개인 엔진 샘플을 준비 중이라면 이 루틴은 릴리즈 전 필수 점검에 가깝습니다.

  1. 게임을 실행하고 시작 메모리를 기록합니다. 작업 관리자나 프로파일러로 기준값을 잡습니다.
  2. 같은 씬을 10번 재시작합니다. 매번 메모리가 증가하면 씬 소유 객체나 리소스 캐시를 봅니다.
  3. 전투나 시뮬레이션을 10분 반복합니다. 투사체, 파티클, 사운드, AI 임시 데이터가 남는지 확인합니다.
  4. 종료 직전 살아 있는 리소스를 출력합니다. 이름과 생성 지점이 없는 항목부터 로깅을 보강합니다.
  5. 수정 후 같은 루틴을 다시 실행합니다. 증가량이 멈췄는지 숫자로 비교합니다.

게임 프로그래밍 메모리 누수 해결은 특별한 도구 하나로 끝나는 작업이 아닙니다. 소유권 설계, 리소스 캐시 정책, 수학 라이브러리의 할당 습관, 재현 가능한 프로파일링 루틴이 함께 맞아야 합니다. 이 기준을 프로젝트 초반부터 적용하면 나중에 성능 최적화나 포트폴리오 polish 단계에서 훨씬 적은 비용으로 안정적인 결과를 얻을 수 있습니다.

게임 프로그래밍 메모리 누수 해결 가이드 2026

댓글목록

등록된 댓글이 없습니다.