RAM: 휘발성 메모리와 CPU-저장장치 사이의 속도 격차 해소
RAM(Random Access Memory)은 전원이 유지되는 동안 임의 위치에 O(1) 시간으로 접근 가능한 휘발성 저장장치다. CPU 레지스터와 캐시보다 느리지만 SSD/HDD보다 수백~수만 배 빠르며, 이 속도 격차 때문에 메모리 계층구조(memory hierarchy)와 캐시 지역성(locality)이 소프트웨어 성능 설계의 핵심 변수가 된다. 이
1. 개념
RAM(Random Access Memory)은 저장된 데이터의 물리적 위치와 무관하게 동일한 시간에 임의 접근(random access)이 가능한 휘발성(volatile) 저장장치다. 전원이 꺼지면 저장된 내용이 사라진다는 점에서 비휘발성(non-volatile)인 SSD, HDD와 구분된다.
RAM은 크게 두 종류로 나뉜다.
- DRAM(Dynamic RAM): 커패시터(capacitor) 하나와 트랜지스터 하나로 비트 하나를 저장한다. 커패시터는 시간이 지나면 전하가 누설되므로 주기적으로 재충전(refresh)해야 값을 유지한다. 밀도가 높고 저렴해서 메인 메모리로 쓰인다.
- SRAM(Static RAM): 플립플롭(flip-flop) 구조(트랜지스터 4~6개)로 비트를 저장한다. 재충전이 필요 없고 훨씬 빠르지만 면적을 많이 차지하고 비싸다. CPU 캐시(L1, L2, L3)에 쓰인다.
일반적으로 "RAM"이라고 하면 메인 메모리로 쓰이는 DRAM(정확히는 DDR SDRAM)을 의미한다.
2. 왜 사용하는가
CPU는 명령어와 데이터를 처리하려면 저장장치에서 값을 읽어야 한다. HDD는 기계적 헤드 이동 때문에 접근 지연이 밀리초(ms) 단위, SSD도 플래시 메모리 특성상 마이크로초(µs) 단위 지연이 발생한다. CPU 클럭 주기가 나노초(ns) 이하인 것을 고려하면 이 저장장치들은 CPU 입장에서 압도적으로 느리다.
RAM은 전기적 신호만으로 데이터를 읽고 쓰기 때문에 접근 지연이 수십~수백 나노초 수준이다. 이 덕분에 실행 중인 프로그램의 코드와 데이터를 RAM에 올려두고 CPU가 빠르게 주고받을 수 있다. 즉 RAM은 "CPU만큼 빠르지는 않지만 디스크보다는 훨씬 빠른" 중간 지점 역할을 하며, 이것이 메모리 계층구조가 존재하는 이유다.
3. 동작 원리
메모리 계층구조(memory hierarchy)
컴퓨터는 속도와 비용이 상충되는 여러 저장장치를 계층으로 배치한다.
레지스터 (CPU 내부) → L1 캐시 → L2 캐시 → L3 캐시 → RAM → SSD/HDD
가장 빠름, 가장 작음 가장 느림, 가장 큼CPU가 데이터를 요청하면 먼저 캐시를 확인하고(캐시 히트, cache hit), 없으면 RAM을 확인하고, RAM에도 없으면(페이지 폴트, page fault) 디스크에서 가져온다. 계층이 내려갈수록 용량은 커지지만 접근 지연은 늘어난다.
DRAM 셀 동작
DRAM 셀은 워드라인(word line)과 비트라인(bit line)으로 제어된다. 특정 행(row)의 워드라인을 활성화하면 해당 행의 커패시터 전하가 비트라인으로 흘러나오고, 이를 센스 앰프(sense amplifier)가 증폭해서 0/1을 판별한다. 이 과정을 읽으면 커패시터의 전하가 소모되므로 읽은 직후 재기록(rewrite)해야 하고, 읽지 않아도 전하가 자연 누설되므로 보통 64ms 주기로 전체 셀을 재충전(refresh)한다.
행/열 접근과 지역성
DRAM은 행(row) 단위로 데이터를 활성화한 뒤 열(column) 주소로 특정 비트를 골라낸다. 같은 행 안에서 연속으로 접근하면(행 버퍼 히트, row buffer hit) 빠르지만, 행을 바꿔가며 접근하면(행 버퍼 미스) 매번 새로 활성화해야 해서 느려진다. 이 특성이 소프트웨어의 캐시 지역성(cache locality) 최적화와 직결된다. 연속된 메모리 주소를 순차적으로 접근하는 코드가 임의 접근보다 빠른 근본 이유가 여기에 있다.
4. 코드 예제
같은 크기의 2차원 배열을 행 우선(row-major)으로 순회할 때와 열 우선(column-major)으로 순회할 때의 시간 차이를 측정한다. 파이썬의 리스트는 메모리 배치를 직접 제어하기 어려우므로 array 모듈로 연속된 메모리 블록을 흉내낸다.
import time
from array import array
N = 2000
# 1차원 배열로 2차원 배열을 흉내낸다 (행 우선 저장, row-major layout)
data = array('d', [0.0] * (N * N))
def row_major_sum(data, n):
total = 0.0
for i in range(n):
base = i * n
for j in range(n):
total += data[base + j] # 연속된 메모리 주소 접근
return total
def col_major_sum(data, n):
total = 0.0
for j in range(n):
for i in range(n):
total += data[i * n + j] # 매번 n칸씩 건너뛰는 접근
return total
start = time.perf_counter()
row_major_sum(data, N)
print(f"행 우선 순회: {time.perf_counter() - start:.4f}초")
start = time.perf_counter()
col_major_sum(data, N)
print(f"열 우선 순회: {time.perf_counter() - start:.4f}초")CPython은 인터프리터 오버헤드가 커서 캐시 효과가 절대적인 실행 시간을 지배하지는 않지만, C나 Rust처럼 저수준으로 컴파일되는 언어에서는 이 차이가 수 배까지 벌어진다. 캐시 지역성의 실제 크기를 확인하려면 numpy처럼 연속 메모리 버퍼를 다루는 라이브러리로 비교하는 것이 정확하다.
# 현재 시스템의 RAM 사용량 확인 (Linux)
free -h
# 프로세스별 실제 물리 메모리 사용량(RSS) 확인
ps -o pid,comm,rss --sort=-rss | head -n 10
# /proc/meminfo에서 상세 정보 확인
cat /proc/meminfo | head -n 55. 시간 복잡도 또는 성능 특성
RAM의 임의 접근은 이론적으로 O(1)이다. 다만 실제 하드웨어에서는 계층별 지연 시간에 뚜렷한 차이가 있다. 아래는 일반적인 서버급 x86 CPU 기준의 근사치이며, 정확한 수치는 CPU 세대와 메모리 규격(DDR4/DDR5)에 따라 달라진다.
| 계층 | 접근 지연 시간(근사치) | |---|---| | L1 캐시 | 약 1ns | | L2 캐시 | 약 3~10ns | | L3 캐시 | 약 10~20ns | | 메인 메모리(RAM) | 약 60~100ns | | SSD(NVMe) | 약 수십 µs | | HDD | 약 수 ms |
L1 캐시와 RAM 사이에는 대략 100배, RAM과 SSD 사이에는 대략 1000배 정도의 지연 시간 차이가 존재한다. 이 격차 때문에 캐시 미스율을 낮추는 것이 성능 최적화에서 가장 효과가 큰 요소 중 하나다.
6. 실무 사용 사례
데이터베이스 버퍼 캐시: PostgreSQL은 디스크 I/O를 줄이기 위해 자주 쓰이는 페이지를 RAM에 캐싱한다.
-- 현재 shared_buffers 설정 확인 (RAM 중 DB가 캐시로 쓰는 크기)
SHOW shared_buffers;
-- 보통 전체 RAM의 25% 정도로 설정하는 것이 관례다
ALTER SYSTEM SET shared_buffers = '4GB';인메모리 캐시: Redis, Memcached는 데이터를 디스크가 아닌 RAM에 저장해서 조회 지연을 마이크로초 단위로 줄인다.
컨테이너 메모리 제한: 컨테이너 환경에서는 RAM 사용량을 제한해두고, 이를 초과하면 OOM Killer가 프로세스를 강제 종료한다.
# Docker 컨테이너 메모리 제한 설정
docker run -m 512m --memory-swap 512m my-appNode.js 힙 크기 제한: V8 엔진은 기본 힙 크기 제한이 있어 RAM이 충분해도 애플리케이션이 제한에 걸릴 수 있다.
node --max-old-space-size=4096 server.js7. 주의할 점
- 캐시 미스(cache miss) 비용: 데이터 구조가 메모리에 흩어져 있으면(연결 리스트, 해시맵의 체이닝 등) 배열보다 캐시 미스가 잦아 실제 성능이 이론적 시간복잡도보다 나쁠 수 있다.
- 메모리 누수(memory leak): JavaScript의 클로저에 붙잡힌 참조, 해제되지 않은 이벤트 리스너, 순환 참조 등으로 가비지 컬렉터가 회수하지 못하는 메모리가 누적되면 결국 RAM이 고갈된다.
- 스왑(swap) 발생: 물리 RAM이 부족하면 운영체제가 디스크를 가상 메모리로 사용하는데, 이때 접근 지연이 RAM 대비 수천 배로 늘어나 애플리케이션 응답 시간이 급격히 나빠진다.
- NUMA(Non-Uniform Memory Access): 멀티소켓 서버에서는 CPU마다 가까운 RAM과 먼 RAM이 나뉘어 있어, 다른 소켓에 붙은 메모리에 접근하면 지연 시간이 늘어난다. 고성능 서버 애플리케이션에서는 NUMA 토폴로지를 고려한 스레드/메모리 바인딩이 필요할 수 있다.
- false sharing: 멀티스레드 환경에서 서로 다른 변수가 같은 캐시 라인(보통 64바이트)에 있으면, 한 스레드의 쓰기가 다른 스레드의 캐시를 무효화시켜 불필요한 성능 저하가 발생한다.
8. 핵심 정리
RAM은 휘발성이지만 전기적 신호만으로 동작해서 디스크보다 수백~수천 배 빠른 임의 접근을 제공하며, 이 덕분에 메모리 계층구조에서 CPU와 디스크 사이의 완충 역할을 한다. DRAM은 커패시터 기반이라 저렴하고 밀도가 높은 대신 주기적 재충전이 필요하고, 행 단위 접근 구조 때문에 순차 접근이 임의 접근보다 유리하다. 실무에서는 캐시 지역성을 살린 자료구조 선택, 인메모리 캐싱, 스왑 방지를 위한 적절한 메모리 사이징이 성능에 직접적인 영향을 준다.