캐싱과 버퍼

박지예·2026년 7월 4일

공부2026

목록 보기
5/20

캐싱 (Caching)

캐시(Cache) 라고 하는 좀 더 빠른 메모리 영역으로 데이터를 가져와서 접근하는 방식.

예를 들어, 속도가 느린 하드 디스크의 데이터를 메모리로 가지고 와서 메모리 상에서 읽기 쓰기를 수행하는 것을 "데이터를 메모리에 캐싱한다" 라고 한다.

캐시는 데이터의 지역성이라는 특성을 이용해서 성능 개선을 달성한다.
지역성은 공간지역성과 시간지역성이 존재하는데,
공간 지역성은 한번 접근한 데이터의 인근에 저장되어 있는 데이터가 다시 접근할 가능성이 높은 특성을 의미한다
시간 지역성은 한번 접근한 데이터가 가까운 시간내에 다시 접근될 가능성이 높다는 의미다.

이러한 데이터 접근의 지역성이라는 특성을 이용해서 자주 접근될 데이터를 더 빠른 속도의 메모리 상에 가지고 와서 연산을 수행하여 성능을 높이는 것이 캐싱의 목표다.

반복 계산 / 조회 비용을 없애기 위해 결과를 한 번 구해서 재사용 하는 것

Unity 개발 시에 어떻게 활용 ?

1. 컴포넌트/참조 캐싱

// 나쁜 예: Update마다 GetComponent 호출 (내부적으로 탐색 비용 발생)
void Update() {
    GetComponent<Rigidbody>().AddForce(Vector3.up);
}

// 좋은 예: Awake/Start에서 한 번만 캐싱
Rigidbody rb;
void Awake() { rb = GetComponent<Rigidbody>(); }
void Update() { rb.AddForce(Vector3.up); }

2. 오브젝트 풀링(object Pooling)
생성과 삭제는 메모리 할당&해제 비용이 크고 GC를 유발하는데 자주 생성 소멸하는 오브젝트는 미리 만들어 놓고 SetActive로 껐다 켰다 하면서 재사용한다.

3. Shader Property ID 캐싱

// 매번 문자열 해싱 비용 발생
material.SetFloat("_Alpha", value);

// 캐싱해서 정수 비교로 처리
static readonly int AlphaID = Shader.PropertyToID("_Alpha");
material.SetFloat(AlphaID, value);

꼭 쉐이더가 아니더라도 const or readonly로 선언했던 변수들을 다시 사용하는 것도 이 상황에 해당된다고 본다.

또는

// 매 호출마다 새 객체 생성 → GC 압박
yield return new WaitForSeconds(1f);

// static으로 캐싱해서 재사용
static readonly WaitForSeconds wait1s = new WaitForSeconds(1f);
yield return wait1s;

new로 생성하는 waitforseconds들도 캐싱해서 사용할 수 있다

**4. 데이터/에셋 캐싱

  • 서버 응답(JSON) 원격 이미지, Addressables로 로드한 에셋을 메모리 딕셔너리나 로컬 파일(PlayerPrefs, 파일 시스템 등) 에 저장해서 재요청/재로드를 방지

버퍼 (buffer)

버퍼링은 버퍼(buffer) 라고 하는 메모리 영역에 데이터를 모았다가 사용하는 동작을 의미한다.
버퍼를 사용하는 이유로는 크게 3가지를 꼽아볼 수 있다.

  1. 보내는 쪽과 받는 쪽의 속도차이에서 오는 비효율을 극복하기 위함이다.
    cpu를 통해 데이터를 다루다가 다시 디스크로 쓰기 연산을 하려고 할 때, 매번 디스크 드라이브에 데이터를 넘기고 디스크로 쓰여지기를 기다린다면 CPU는 메모리에 비해 속도가 매우 느린 디스크의 동작을 항상 기다리게 된다. 버퍼링을 하게 되면, 디스크로의 작업을 버퍼에 '모아서' 한번에 수행하기 때문에 보다 효율적으로 수행할 수 있다.

  2. 서로 다른 자료 전송 사이즈가 다른 상황을 극복하기 위함이다.
    작은 데이터들을 하나에 모아서(버퍼링) 하나의 블록으로 전달 할 수 있다.

  3. 데이터 입출력의 의미를 명확하게 하기 위해 사용된다.
    특정 데이터가 생성되고 다른 쪽으로 이동한 상황에서,
    buffer에 저장하지 않고 보내면
    받은 쪽 입장은 생성된 시점의 데이터로 받아야 하는지 전달받은 시점의 데이터로 받아야 하는지 불확실하다 .
    buffer에 담아서 보내면 buffer에 담은 시점을 기준으로 입출력을 시행할 수 있다.

관련 예시를 찾긴했는데 이해하지 못해서 풀어서 쓴다

예시
예를 들어, Unix 프로세스가 Write() 시스템 콜을 사용하여 디스크 쓰기 요청을 했다고 가정해보자. write() 시스템 콜은 애플리케이션에서 데이터를 저장하고 있는 버퍼의 내용을 커널의 버퍼로 복사를 해준다. 그리고 커널의 버퍼에 복사된 데이터는 디스크로 쓰여지게 된다. 만일 커널 버퍼를 두지 않는다면? 커널은 넘겨받은 애플리케이션의 버퍼 주소를 읽어서 디스크로 쓰려고 할 것이다. 커널이 디스크로 데이터를 쓰기 전에 애플리케이션이 버퍼의 내용을 바꾼다면, 커널은 애플리케이션이 마지막에 바꾼 데이터를 디스크로 써야할까 아니면 write() 시스템 콜이 호출된 시점의 데이터를 디스크로 내려야 할까?

처리 속도 불일치 해결 및 흐름 제어

버퍼에 담긴 데이터는 처리된 후 폐기된다.

Unity 개발 시에 어떻게 활용 ?

  1. 랜더링 관련 버퍼
  • Frame Buffer / Render Target : 화면에 그려질 픽셀 데이터를 담는 버퍼

  • Depth Buffer : 깊이 판정, 마스킹 용

  • Double Triple Buffering : 화면 찢어짐(tearing) 방지를 위해 그리는 버퍼와 보여주는 버퍼를 분리

  • Command Buffer : CommandBuffer 클래스로 랜더링 명령을 모아뒀다가 한 번에 GPU에 제출 (커스텀 렌더 이펙트에 자주 사용)

  • ComputeBuffer : GPU와 대량 데이터를 주고 받을 때 (파티클 등)

  1. 네트워크 버퍼
  • 소켓 송수신 패킷을 바로바로 처리하지 않고 버퍼에 모았다가 파싱 (TCP 스트림은 메시지 경계가 없어서 특히 중요 - 버퍼에서 완전한 메시지 단위를 잘라내야 한다)
  • 멀티플레이어 게임에서 서버에서 온 스냅샷(순간적인 데이터)을 버퍼에 쌓아두고 보간하며 재생
  1. 입력 버퍼(Input Buffering)
    액션/격투 게임에서 사용자가 입력한 커맨드를 짧은 시간 동안 버퍼에 저장해뒀다가, 다음 유효한 타이밍에 실행 (ex: 점프 직전 눌러도 착지하자마자 점프되게)

  2. 순환 버퍼
    고정 크기 배열을 재사용하며 오래된 데이터를 덮어쓰는 구조.
    로그 기록, 리플레이 시스템, 이동 히스토리 등에 사용.
    힙 할당없이 고정 메모리로 반복 데이터를 다룰 수 있어 GC 친화적이다.

  3. String Builder = 문자열 버퍼

// 매번 새 문자열 생성 → GC 압박
string result = "";
for (int i = 0; i < 100; i++) result += i;

// 버퍼에 누적 후 한 번에 변환
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.Append(i);
string result = sb.ToString();

한줄 요약

캐싱 : 이미 확정된 결과를 재계산, 재요청 방지로 저장
버퍼 : 아직 처리 전인 데이터를 속도 완충을 위해서 저장


그동안 운영체제에 중요성을 못 느끼고 있었는데, 단순한 구조 구현 뿐 아니라 그 너머의 system이나 tool을 만들려면 관련 지식도 어느정도 알고 있어야 한다고 생각이 바뀌었다.
모르는 건 차근히 공부해가자

profile
게임 클라이언트 개발자

0개의 댓글