이번 편의 결과물: 배포된 notice-board에 짧은 시간 안에 조회수 API로 여러 번 요청을 보내면, 응답에 찍힌 인스턴스 식별자와 조회수 값이 인스턴스별로 따로 늘어나는 현상을 재현합니다. · 다루는 개념: 모듈 스코프 Map으로 만든 조회수 저장소, 응답에 인스턴스 식별자(process.pid) 노출, 반복 요청으로 여러 컴퓨트 인스턴스 유도
02편에서 컴퓨트 인스턴스가 트래픽에 따라 여러 개로 늘어나고, 각 인스턴스의 메모리가 서로 격리된다는 원리를 배웠습니다. 이 원리는 Next.js가 관리하는 캐시(06~08편)뿐 아니라, 애플리케이션이 직접 관리하는 상태에도 똑같이 적용됩니다. 이번 편에서는 공지사항 조회수라는 흔한 기능을 예로 들어, 그 상태가 인스턴스마다 따로 노는 것을 직접 확인합니다.
이 편에서 만드는 파일
notice-board/
├── lib/
│ └── view-counts.js (+, 모듈 스코프 Map 기반 조회수 저장소)
└── app/
└── api/
└── notices/
└── [id]/
└── views/
└── route.js (+, 조회수 증가 API. 인스턴스 식별자 포함 응답)개념 정리
조회수는 Next.js 캐시가 아니라 애플리케이션 상태다
06~08편에서 다룬 데이터 캐시·전체 라우트 캐시는 Next.js가 fetch·revalidate 옵션을 보고 알아서 관리합니다. 조회수는 다릅니다. “공지사항을 열람할 때마다 1을 더한다”는 로직은 애플리케이션 코드가 직접 관리하는 상태이고, Next.js 캐시 API를 전혀 거치지 않습니다. 이번 편은 이 상태를 가장 단순한 방법인 모듈 스코프 Map으로 구현합니다.
모듈 스코프 Map도 02편의 표 그대로다
| 저장 방식 | 인스턴스 간 공유 여부 |
|---|---|
모듈 스코프 변수(전역 객체, Map 등) | 공유되지 않는다 |
lib/view-counts.js에 선언한 Map은 그 모듈을 불러온 컴퓨트 인스턴스의 메모리 안에만 존재합니다. 인스턴스가 여러 개면, 같은 공지사항의 조회수를 물어도 인스턴스마다 다른 숫자가 돌아옵니다.
응답에 인스턴스 식별자를 함께 실어 보내기
process.pid(현재 프로세스 번호)를 응답에 함께 담으면, 어느 인스턴스가 이 요청에 응답했는지 구분할 수 있습니다. 조회수 값과 인스턴스 식별자를 나란히 놓고 보면, “이 조회수는 어느 인스턴스의 Map에 있던 값인가”를 바로 알 수 있습니다.
| 상황 | 로컬 개발 서버 | Amplify 배포 환경 |
|---|---|---|
| 실행 중인 인스턴스 수 | 1개(항상 같은 프로세스) | 트래픽에 따라 1개–여러 개 |
| 조회수 Map | 항상 하나 | 인스턴스마다 따로 존재 |
| 재현 가능 여부 | 불가능(인스턴스가 하나뿐) | 가능(짧은 시간에 요청을 몰아야 함) |
이 표 때문에 이번 편의 재현 실습은 로컬이 아니라 04편에서 배포한 Amplify URL을 대상으로 합니다.
실습
1. 조회수 저장소 작성
// lib/view-counts.js
const viewCounts = new Map();
export function bumpViewCount(noticeId) {
const current = viewCounts.get(noticeId) ?? 0;
const next = current + 1;
viewCounts.set(noticeId, next);
return next;
}공지사항 ID별로 센 조회수를 Map에 저장합니다. 데이터베이스도, 파일도 아닙니다. 이 모듈을 불러온 프로세스가 살아있는 동안만 메모리에 남습니다.
2. 조회수 증가 API 작성
// app/api/notices/[id]/views/route.js
import { NextResponse } from 'next/server';
import { bumpViewCount } from '../../../../../lib/view-counts.js';
export async function POST(request, { params }) {
const { id } = await params;
const views = bumpViewCount(id);
const instanceId = process.pid;
return NextResponse.json(
{ id, views, instanceId },
{ headers: { 'x-instance-id': String(instanceId) } },
);
}호출할 때마다 조회수를 1 늘리고, 그 값과 함께 지금 이 요청을 처리한 인스턴스의 process.pid를 응답 본문과 x-instance-id 헤더에 담아 돌려줍니다.
3. 배포하고 반복 요청 보내기
코드를 GitHub에 푸시해 04편에서 연결한 Amplify Hosting 자동 배포를 실행합니다. 배포가 끝나면 터미널에서 실제 Amplify URL에 짧은 간격으로 요청을 몰아 보냅니다. 순차 요청은 같은 인스턴스로 몰릴 가능성이 높으므로, &로 백그라운드 실행해 동시에 여러 요청이 나가게 합니다.
for i in $(seq 1 20); do
curl -s -X POST "https://main.d1234567890abc.amplifyapp.com/api/notices/1/views" &
done
wait각 줄은 {"id":"1","views":3,"instanceId":41823} 형태로 출력됩니다. 결과를 순서대로 아래처럼 표에 옮겨 적습니다.
| 요청 순서 | 인스턴스 식별자 | 조회수(views) |
|---|---|---|
| 1 | 41823 | 1 |
| 2 | 58210 | 1 |
| 3 | 41823 | 2 |
| 4 | 58210 | 2 |
| 5 | 41823 | 3 |
(실제 값은 배포 환경·트래픽 상황에 따라 달라집니다. 위 표는 두 인스턴스가 번갈아 요청을 처리했을 때 나올 수 있는 예시입니다.)
4. 확인
- 20건의 응답 중 인스턴스 식별자가 2개 이상 서로 다르게 나타납니다.
- 같은 인스턴스 식별자를 가진 응답끼리는 조회수가 1, 2, 3처럼 순서대로 늘어납니다(그 인스턴스의
Map이 자기 몫만 세고 있다는 뜻). - 전체 20번의 요청을 하나의 조회수로 합산하면 20이 되어야 하지만, 실제로는 인스턴스별
Map이 따로 세고 있어 어떤 인스턴스에 물어보느냐에 따라 다른 값이 나옵니다. 화면에 조회수 하나를 보여줘야 한다면 이 값들 중 어느 것도 신뢰할 수 없습니다.
직접 해보기
seq 1 20을seq 1 5로 바꿔 요청 수를 줄여서 실행해 봅니다. 인스턴스 식별자가 하나로만 잡힌다면, 트래픽이 적어 인스턴스가 늘어나지 않은 것입니다.
확인 방법
요청 수가 적으면 Amplify가 인스턴스를 하나만 유지한 상태로 모든 요청을 처리할 수 있습니다. 인스턴스 격리 문제는 트래픽이 늘어나 인스턴스가 여러 개로 확장될 때만 드러나므로, 요청 수를 20~30개 이상으로 늘려 다시 시도합니다.
- 공지사항 ID를
1에서2로 바꿔 같은 반복 요청을 실행해 봅니다.1번 공지의 조회수와2번 공지의 조회수가 서로 다른 키로 완전히 독립적으로 늘어나는지 확인합니다.
확인 방법
lib/view-counts.js의 Map은 공지사항 ID를 키로 쓰므로, ID가 다르면 완전히 별개의 항목으로 취급됩니다. 다만 이 독립성은 “같은 인스턴스 안에서 ID별로 분리된다”는 뜻이지, “여러 인스턴스에 걸쳐 공유된다”는 뜻은 아닙니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 20번 내내 인스턴스 식별자가 똑같이 나옴 | 요청 수가 적어 인스턴스가 하나만 뜬 상태 | 요청 수를 늘리거나(seq 1 50), 동시 요청(&)으로 바꿔 트래픽을 몰아준다 |
로컬(npm run dev)에서 실행해 재현이 안 됨 | 로컬은 프로세스가 항상 하나뿐 | 반드시 배포된 Amplify URL을 대상으로 테스트한다 |
curl 요청이 401/403으로 실패함 | Amplify 앱에 접근 제한(비밀번호 보호 등)이 걸려 있음 | Amplify 콘솔 → 앱 설정 → 접근 제어 설정을 확인한다 |
| 조회수 합계가 20이 아니어서 버그로 오해함 | 인스턴스별 Map이 각자 따로 세고 있다는 사실을 놓침 | 이것이 바로 이번 편이 재현하려는 문제다. 13편에서 DynamoDB로 해결한다 |