객체 배열(Object of Arrays, SoA) 패턴이 인터리브 배열을 이긴 이유 – JavaScript 성능 최적화 비밀
# 서론 – “왜 내 코드가 느려지는 걸까?”
JavaScript 로 게임 로직을 짜거나 대규모 데이터 처리를 할 때, 배열 안에 객체 형태(예: [{x:…, y:…}, …]) 로 데이터를 저장하면 직관적이고 코딩도 편합니다. 하지만 실제 실행 속도는 기대에 못 미치는 경우가 많습니다.
최근 Reddit에 올라온 “Why Object of Arrays (SoA pattern) beat interleaved arrays” 토론을 보면서 많은 개발자들이 Structure of Arrays (SoA), 즉 Object of Arrays 패턴이 메모리 접근과 CPU 캐시 활용 측면에서 훨씬 효율적이라는 사실을 확인했습니다.
이 글에서는 초보자도 이해할 수 있도록 왜 SoA가 인터리브(Interleaved) 배열보다 빠른지, 그리고 실제 프로젝트에 바로 적용할 수 있는 방법을 단계별로 살펴보겠습니다.
# 본론
# 1️⃣ 인터리브 배열이란? – 객체를 배열에 섞어 넣는 방식
js
// 인터리브( AoS, Array of Structures )
const particles = [
{ x: 10, y: 20, vx: 1, vy: -1 },
{ x: 15, y: 25, vx: 0, vy: 2 },
// …
];
장점: 가독성이 뛰어나고,
particles[i].x같은 접근이 직관적입니다.단점: 루프를 돌면서
x,y,vx,vy를 차례로 읽을 때, CPU는 메모리의 서로 다른 위치를 계속 뒤섞어 접근합니다. 이때문에 CPU 캐시 미스가 빈번히 발생해 성능이 저하됩니다.
# 2️⃣ Object of Arrays (SoA) 패턴 – 데이터를 열(column) 단위로 분리
js
// SoA (Structure of Arrays)
const particleX = new Float32Array(count);
const particleY = new Float32Array(count);
const particleVX = new Float32Array(count);
const particleVY = new Float32Array(count);
핵심 아이디어: 같은 속성값을 하나의 연속된 메모리 블록에 모아두어 연속 접근이 가능하게 함.
장점
CPU 캐시 효율 – 한 번에 여러
x값을 읽어 캐시 라인에 적재하면, 같은 루프 안에서x만 계속 사용할 때 캐시 히트율이 급상승합니다.SIMD 최적화 –
Float32Array같은 TypedArray는 브라우저 엔진이 자동으로 SIMD 명령어를 활용해 벡터 연산을 수행합니다.메모리 사용량 감소 – 객체 헤더(프로퍼티 이름 등) 오버헤드가 사라져 전체 메모리 풋프린트가 작아집니다.
# 3️⃣ 실제 성능 차이를 보여주는 간단 실험
아래 코드는 두 가지 구조를 10 000 000개의 파티클에 대해 100번 업데이트하는 벤치마크 예시입니다.
js
// 인터리브 버전
function updateAoS(particles) {
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
}
}
// SoA 버전
function updateSoA(x, y, vx, vy) {
for (let i = 0; i < x.length; i++) {
x[i] += vx[i];
y[i] += vy[i];
}
}
Chrome DevTools의 Performance 탭으로 측정하면, 동일한 하드웨어에서 SoA가 평균 2~3배 빠른 결과가 나옵니다. 특히 모바일 브라우저에서 캐시가 작아질수록 차이는 더욱 커집니다.
# 4️⃣ 언제 SoA를 선택해야 할까? – 적용 가이드
| 상황 | 권장 구조 | 이유 |
|------|-----------|------|
| 대량의 동일 속성 데이터 (예: 물리 엔진, 파티클, 이미지 픽셀) | Object of Arrays | 연속 메모리 접근 → 캐시 효율 ↑ |
| 데이터가 복잡하고 서로 다른 타입 (예: UI 컴포넌트 리스트) | Array of Objects | 가독성·유지보수성 ↑ |
| WebGL 혹은 Canvas와 연동 | TypedArray 기반 SoA | GPU에 바로 전달 가능, 버퍼 복사 최소화 |
| 데이터 변형이 드물고 읽기 중심 | SoA | 읽기 전용 루프에서 최적화 효과 극대화 |
# 5️⃣ SoA 전환 팁 – 기존 코드를 부드럽게 마이그레이션하기
- 속성 리스트 정의
js
const props = ['x', 'y', 'vx', 'vy'];
- 데이터 초기화 함수
js
function createSoA(count) {
const soa = {};
for (const p of props) soa[p] = new Float32Array(count);
return soa;
}
- 기존 AoS → SoA 변환
js
function convertAoSToSoA(aos) {
const count = aos.length;
const soa = createSoA(count);
for (let i = 0; i < count; i++) {
const p = aos[i];
for (const key of props) soa[key][i] = p[key];
}
return soa;
}
루프 로직 교체
기존
particles[i].x를particleX[i]로 바꾸면 끝!코드가 길어질 경우 ES6 모듈이나 함수 래퍼를 활용해 가독성을 유지하세요.
# 결론 – 지금 바로 SoA를 적용해 보세요!
JavaScript 성능 최적화는 작은 구조 변화에서 시작됩니다.
Structure of Arrays(Object of Arrays) 패턴은 CPU 캐시와 SIMD 활용을 통해 인터리브 배열보다 2~3배 빠른 실행 속도를 제공합니다.
대규모 데이터 처리, 게임 로직, WebGL 연동 등 성능이 중요한 프로젝트라면 지금 바로 SoA로 전환해 보세요.
실천 팁: 프로젝트에
benchmark.html파일을 하나 만들어, 기존 AoS와 SoA 각각의 실행 시간을 측정해 보세요. 직접 눈으로 확인하면 전환의 필요성을 팀원에게 설득하기 쉬워집니다.
# 📢 행동 유도
코드 스니펫을 직접 실행하고 성능 차이를 체감해 보세요.
이 글을 북마크하고, 팀 회의 때 SoA 적용 방안을 제안해 보세요.
더 많은 JavaScript 성능 최적화 팁을 원한다면, 댓글이나 구독을 통해 알려 주세요!
SEO 키워드: JavaScript 성능 최적화, Structure of Arrays, Object of Arrays