SharedArrayBuffer
SharedArrayBuffer는 자바스크립에서 메모리를 여러 스레드(Worker) 간에 공유할 수 있도록 고안된 객체로, 기존의 ArrayBuffer는 단일 스레드에서만 접근 가능한 반면, SharedArrayBuffer는 복수의 워커(또는 메인 스레드)가 동일한 물리 메모리를 직접 접근할 수 있게 해준다. 이로써 고성능 병렬처리를 위한 "공유 메모리" 방식의 프로그래밍이 가능해졌다.
다만, 여러 스레드가 동시에 같은 메모리 영역을 읽고 쓰는 구조가 되므로, 올바르게 사용하기 위해서는 동시성 제어(Atomics API)와 보안(COOP/COEP 설정) 등을 고려해야 한다.
1. SharedArrayBuffer의 기본 개념
1.1. 공유 가능한 메모리 버퍼
- ArrayBuffer와 유사하게, 이진 데이터를 담을 수 있는 버퍼 객체이다.
- 차이점은 여러 스레드(워커)가 동시에 접근 가능한 메모리라는 점이다.
1.2 TypedArray를 통해 읽고 쓰기
SharedArrayBuffer 자체는 단순히 "길이를 갖는 버퍼"일 뿐이다. 실제 데이터를 읽고 쓰려면 Int8Array, Int32Array, Float64Array 등의 TypedArray를 생성해야 한다.
const sharedBuffer = new SharedArrayBuffer(4) // 4바이트 버퍼
const sharedView = new Int32Array(sharedBuffer);
// 메모리에 직접 값 지정
sharedView[0] = 10;
console.log(sharedView[0]); // 10
1.3 다른 스레드와의 공유
- 메인 스레드에서 생성한 SharedArrayBuffer를 Web Worker(브라우저)나 Worker Threads(Node.js)에게 "메시지로 전달"할 때, 일반적으로 구조화된 복사(Structured Clone)로 보내진다.
- 구조화된 복사로 전달해도, SharedArrayBuffer 는 실제로는 복사되지 않고 동일한 메모리 영역에 대한 참조가 전달되므로, 워커에서 이 메모리를 똑같이 접근할 수 있게 된다.
2. 동시성 제어: Atomics API
여러 스레드가 동시에 같은 메모리를 읽고 쓸 경우, 경쟁 상태(Race Condition)가 발생할 수 있다. 이를 안정적으로 제어하기 위해 자바스크립트는 Atomics 객체를 제공한다.
2.1 원자적(Atomics) 읽고 쓰기
- Atomics.load(typedArray, index), Atomics.store(typedArray, index, value)
- 한 스레드가 읽기 또는 쓰기를 수행하는 동안 다른 스레드가 중간에 값을 건드리지 못하도록 보장한다.
2.2 원자적 연산
- Aomics.add, Aomics.sub, Aomics.and, Aomics.or, Aomics.xor, Aomics.exchange, Aomics.compareExchange
- 예: Aomics.add(sharedView, 0, 1)은 sharedView[0]를 원자적으로 1 증가시킨다.
2.3 wait와 notify
- Aomics.wait(typedArray, index, value, timeout) / Aomics.notify(typedArray, index, count)
- 특정 스레드가 조건이 충족될 때까지 대기(wait)하고, 다른 스레드가 조건을 만족시키면 깨워(notify) 동작을 재개할 수 있다.
- 이 로직을 통해 뮤텍스(Mutex)나 세마포어와 유사한 동기화 패턴을 구현할 수 있다.
// 메인 스레드 or Worker
const shreadBuffer = new SharedArrayBuffer(4);
const sharedView = new Int32Array(sharedBuffer);
// 다른 스레드(Worker)에서 sharedView를 전달 받았다고 가정
// 초기값 세팅
sharedView[0] = 0;
// 다른 스레드에서 동시에 다음 코드를 실행한다고 할 때,
// Aomics.add는 중간 경합 없이 원자적으로 값 증가
Aomics.add(sharedView, 0, 1);
3. 브라우저 환경: 보안 이슈와 COOP/COEP 설정
3.1 보안 정책(Cross-Origin Isolation)
과거 Meltdown과 Spectre CPU 취약점이 알려진 이후 브라우저에서 SharedArrayBuffer를 무조건 활성화하지 않도록 정책이 바뀌었다.
- 2020 ~ 2021년 이후 크로스 오리진 격리가 된 환경(COOP/COEP 헤더 설정)에 한해서만 SharedArrayBuffer 사용이 허용된다(또는 일부 브라우저는 특정 버전 이상의 Chrome에서만 제한적 지원).
- 이를 적용하려면, 서버에서 HTTP 응답 헤더를 설정해야 한다.
예를 들어, 다음과 같은 헤더를 추가해야 한다.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Nginx라면 다음과 같이 하면 된다.
location / {
# Cross-Origin Opener Policy
add_header Coross-Origin-Opener-Policy same-origin always;
# Cross-Origin Embedder Policy
add_header Cross-Origin-Embedder-Policy require-corp always;
# COEP를 require-corp 대신 credentialless로 쓸 수도 있다.
# add_header Cross-Origin-Embedder-Policy credentialless always;
}
3.2 사용 시나리오
- 이미지 프로세싱, 영상 편집, 머신러닝 추론, 실시간 데이터 시각화 등 고성능, 저지연이 필요한 작업에서 사용된다.
- 큰 파일을 여러 워커가 분할해 병렬로 처리한 뒤, 결과를 합치는 식의 로직을 구현할 수 있다.
- 단순히 메시지를 전달(복사)하는 것보다, 공유 메모리를 직접 쓰고 읽는 편이 더 빠를 수 있다.
4. Node.js 환경에서의 SharedArrayBuffer
Node.js 환경에서는 브라우저처럼 COOP/COEP 보안 정책이 필요하지는 않지만, Worker Threads에서 SharedArrayBuffer를 공유할 때에도 기본 구조는 동일하다.
// main.js
const { Worker } = require('worker_threads');
const sharedBuffer = new SharedArrayBuffer(4);
const sharedView = new Int32Array(sharedBuffer);
sharedView[0] = 100;
const worker = Worker('./worker.js', {}
workerData: sharedBuffer,
);
worker.on('message', (msg) => {
console.log('From worker:' + msg);
});
// worker.js
const { parentPort, workerData } = require('worker_threads');
const sharedView = new Int32Array(workderData);
// 공유 메모리 값 읽기
console.log('Shared value:', sharedView[0]);
// 원자적으로 값 업데이트
Atomics.add(sharedView, 0, 1);
parentPort.postMessage(`Updated value: ${sharedView[0]}`);
- 위 예시에서 메인 스레드에서 만든 SharedArrayBuffer를 워커에 전달한다. 워커는 sharedView[0]에 접근해 값을 읽고, Atomics.add로 변경한다.
- 메인 스레드와 워커가 "같은 메모리"를 사용하기 때문에, 변경된 결과는양쪽에서 동일하게 관찰할 수 있다.
5. SharedArrayBuffer 사용 시 주의사항
5.1 동시성 문제
- 여러 스레드가 같은 메모리 주소를 동시에 읽고 쓴다면, 경쟁 상태(Race Condition)가 발생할 수 있다.
- Atomics API로 각 접근을 원자적으로 수행해야 안전하다.
5.2 복잡도 증가
- 메시지 기반(worker.postMessage) 만으로도 많은 병렬 로직을 구현할 수 있는데, 굳이 SharedArrayBuffer를 사용할 경우, 동시성 제어 로직이 복잡해 질 수 있다.
- 성능이 필요한 상황에서만 사용하는 것이 좋다.
5.3 브라우저 지원 및 보안 설정
- 브라우저별 지원 상황을 확인해야 한다(최신 크롬/파이어폭스/사파리 등 대부분 지원하지만, COOP/COEP 설정이 필요).
- 크로스 오리진 리소스 로드가 필요한 경우, 헤더 설정과 리소스 정책을 면밀히 구성해야 한다.
5.4 성능 이점
- 대량 데이터를 메시지로 복사 전송하는 것보다, 공유 메모리에 바로 쓰고 읽는 방식이 훨씬 빠를 수 있다.
- 하지만 Worker 수가 늘어날수록 동기화 오버헤드(원자 연산, 스핀락 등)가 커질 수 있으므로, 적절히 설계해야 한다.
결론
SharedArrayBuffer는 CPU 집약적인 작업을 빠르게 처리해야 하는 고성능 애플리케이션에서 중요한 역할을 한다. 다만, 스레드 동기화에 대한 이해가 필수적이며, 브라우저의 보안 정책을 맞춰야 하는 번거로움이 있다.
필요한 기능(동시성 제어, 메시지 전달, 성능 요구 사항)과 보안 이슈를 충분히 고려한 뒤, SharedArrayBuffer + Atomics를 활용한 공유 메모리 모델을 도입하는 것이 좋다.