인스턴스와 공유 메모리
어떤 요청이 메모리를 공유하는지, 로컬 캐시가 언제 유용한지, 영속·공유 상태는 어디에 저장해야 하는지 설명합니다.
인스턴스 하나가 여러 요청을 처리합니다
앱 인스턴스는 실행 중인 서버 사본 하나입니다. 나이트로쉽은 같은 인스턴스에 여러 요청을 동시에 보낼 수 있습니다. 요청마다 새 컨테이너를 시작하지 않습니다.
Node 서버 프로세스에서 모듈 최상위에 선언한 변수나 전역 변수는 요청 하나가 아니라 프로세스에 속합니다. 요청 핸들러 밖에서 만든 Map은 같은 프로세스가 처리하는 다른 요청도 읽거나 바꿀 수 있으며, 나중에 들어온 요청도 재사용할 수 있습니다. 앱이 여러 프로세스를 시작한다면 각 프로세스의 모듈 메모리는 별개입니다.
서로 다른 인스턴스는 리전이 같아도, 달라도 실시간으로 메모리를 공유하지 않습니다. 한 인스턴스에서 Map을 바꿔도 다른 인스턴스의 Map은 바뀌지 않습니다.
로컬 메모리를 쓰기 좋은 경우
캐시가 비었을 때 다시 수행해도 괜찮은 작업에 로컬 메모리를 쓰세요.
- 설정, JWT 서명을 검증하기 위해 임포트한 공개 키, 컴파일한 템플릿처럼 처리 비용이 높고 특정 사용자에 종속되지 않는 데이터.
- 필요할 때 새 연결을 열 수 있는 데이터베이스·HTTP 연결 풀.
원본 데이터는 다른 곳에 보관하세요. 캐시에 값이 없으면 원본에서 읽어야 하며, 앞선 요청이 이미 캐시를 채웠을 것이라고 가정하면 안 됩니다.
예시: 임포트한 JWT 검증 키 재사용하기
아래 Node/TypeScript 모듈은 신뢰하는 키 서비스에서 PEM 공개 키를 가져와 한 번 임포트합니다. 같은 URL을 요청하는 동시 요청은 로딩 중인 Promise를 공유하고, 이후 요청은 임포트한 키를 재사용합니다. 로딩에 실패한 항목은 지우므로 다음 호출에서 원본을 다시 읽을 수 있습니다.
import { createPublicKey, type KeyObject } from "node:crypto";
const keys = new Map<string, Promise<KeyObject>>();
export function getVerificationKey(keyUrl: string): Promise<KeyObject> {
const cached = keys.get(keyUrl);
if (cached) return cached;
const loading = (async () => {
const response = await fetch(keyUrl);
if (!response.ok) throw new Error(`Key load failed: ${response.status}`);
return createPublicKey(await response.text());
})().catch((error) => {
keys.delete(keyUrl);
throw error;
});
keys.set(keyUrl, loading);
return loading;
}미리 설정한 소수의 신뢰할 수 있는 공개 키 URL로만 호출하세요. 요청에서 받은 임의의 URL을 넘기면 안 됩니다. 호출자의 토큰과 인증 결과는 요청 내부 변수에 두고, 이 캐시에는 재사용할 공개 키만 저장하세요.
기준이 되는 원본은 키 서비스입니다. 이 예시는 성공한 항목을 프로세스가 살아 있는 동안 유지합니다. 키가 교체될 수 있다면 만료 시간이나 명시적인 무효화를 추가하고, 키 종류가 늘어날 수 있다면 항목 수 제한과 퇴거 정책을 두세요. 메모리에 있다는 이유로 오래된 키를 계속 사용하면 안 됩니다.
스케일링과 메모리의 수명
컴퓨트 설정은 실행할 수 있는 인스턴스 수와 인스턴스별 자원을 정합니다.
| 설정 | 메모리에 미치는 영향 |
|---|---|
regions | 선택한 컴퓨트 리전에서 앱을 실행할 수 있습니다. 리전 사이에서 로컬 캐시가 동기화되지는 않습니다. |
minInstances | 현재 프로덕션 배포가 각 리전·워커 풀에서 유지할 최소 실행 인스턴스 수입니다. 프리뷰에는 적용되지 않습니다. 인스턴스를 실행 상태로 유지한다고 데이터의 영속성이 보장되는 것은 아닙니다. |
maxInstances | 프로덕션·프리뷰 모두에 적용되는 배포별, 리전·워커 풀별 인스턴스 상한입니다. 인스턴스가 늘면 독립적인 로컬 캐시도 늘어납니다. 1로 설정해도 모든 리전이 공유하는 캐시 하나가 생기지는 않습니다. |
tier | 인스턴스마다 티어의 메모리 한도가 적용되며, 컴퓨트 옵션 API는 이를 memoryMiB로 표시합니다. 캐시, 앱 코드, 그 밖의 프로세스 내부 메모리 할당이 이 한도를 함께 사용합니다. |
minInstances: 0은 최소 실행 인스턴스 수를 요청하지 않았다는 뜻입니다. 트래픽이 없으면 나이트로쉽이 인스턴스를 일시 정지(freeze)하거나 체크포인트를 만들거나 제거할 수 있고, 일시 정지된 웜 인스턴스를 유지할 수도 있습니다. 스냅샷 복원으로 메모리가 유지될 수는 있지만, 영속 저장소는 아닙니다.
앱 프로세스 재시작, 새 배포나 재배포, 스케일 다운, 다른 노드로의 이동 후에는 캐시가 비어 있을 수 있다고 생각하고 설계하세요. 크래시나 메모리 부족(OOM)으로 프로세스가 종료되어도 메모리를 잃을 수 있습니다. 이전 인스턴스의 캐시가 남아 있어야만 요청을 처리할 수 있는 구조로 만들면 안 됩니다.
제한 없는 캐시는 티어의 메모리 한도를 소모해 OOM을 일으킬 수 있습니다. 항목 수와 각 항목의 크기를 제한하고, 요청 처리와 연결 풀에 쓸 메모리도 남겨 두세요.
컴퓨트 설정을 반영하는 방법은 배포 관리, 사용 가능한 리전과 티어는 컴퓨트 설정 API를 참고하세요.
요청 상태를 전역 변수에 넣지 마세요
현재 사용자 ID, 인증 정보, 토큰, 요청 본문, 응답 객체를 모듈 최상위 변수나 전역 변수에 보관하지 마세요. 다른 요청이 이를 덮어쓰거나 읽어 한 사용자의 데이터가 다른 사용자의 응답으로 유출될 수 있습니다.
공유되는 가변 객체도 주의해야 합니다. 단일 Node 프로세스에서도 비동기 핸들러는 await에서 실행이 엇갈릴 수 있습니다. 두 요청이 같은 값을 읽은 뒤 각각 수정한 값을 쓰면 업데이트가 덮어써질 수 있습니다. 요청별 값은 핸들러의 지역 변수와 인자로 전달하세요. 여러 요청에 걸친 업데이트의 정확성이 중요하다면 메모리에서 읽고 수정해 쓰는 방식 대신 데이터베이스 트랜잭션이나 적절한 공유 저장소의 원자적 연산을 쓰세요.
공유·영속 상태를 저장할 곳
기록, 세션처럼 앱의 기준이 되는 데이터는 외부 데이터베이스에 저장하세요. 모든 인스턴스가 같은 캐시, 요청 횟수 제한 카운터, 작업 조율 상태를 써야 한다면 외부 Redis 서비스나 적절한 공유 저장소를 쓰고, 용도에 맞는 영속성과 원자적 연산을 선택하세요. 업로드한 파일은 프로세스 메모리가 아니라 객체 스토리지에 저장하세요.
연결 설정과 자격 증명은 환경 변수로 전달하세요. 로컬 Map을 공유 원본 앞에 두어 작업을 줄일 수는 있지만, 그 Map이 비어 있어도 앱이 동작해야 합니다.