Prisma Query Engine과 Alpine 환경에서 발생한 DB 조회 Hang 이슈 분석
이번 글은 내가 겪은 “배포 서버에서 Prisma DB 조회가 이유 없이 멈추는(hang) 문제”를 스터디용으로 정리한 기록이다. 결론부터 말하면:
- Cursor(혹은 템플릿)가
node:alpine기반 Dockerfile을 만들어줬고 - Prisma는 내부적으로 네이티브 엔진(실행 파일)을 쓰기 때문에
- Alpine(musl) + Prisma 엔진 + 배포 환경(특히 ARM64/AWS) 조합에서 멈춤이 터질 수 있었다
중요한 건, 이 문제가 “코드 로직 버그”라기보다 런타임 환경 호환성 문제라는 점이다. 그래서 에러 로그가 뚜렷하지 않고, 로컬에서는 잘 돌아가다가 배포에서만 터지기도 한다.
1. Alpine이 뭐야?
Alpine Linux는 Docker에서 자주 쓰는 초경량 리눅스 배포판이다. 이미지가 작아서 빌드/푸시/풀 속도가 빠르다.
- 장점: 이미지 용량이 작다 (배포 파이프라인이 빨라짐)
- 특징: 일반적인 리눅스 서버들과 달리 glibc가 아니라 musl libc를 쓴다
Alpine은 “가볍게” 만들기 위해 리눅스의 기본 구성(특히 C 런타임)을 다른 걸 쓰고,
그게 네이티브(바이너리) 의존성이 있는 라이브러리에서는 호환성 이슈로 이어질 수 있다.
여기서 말하는 “호환성”은 단순히 “실행되냐/안되냐”가 아니라,
실행은 되는데 특정 상황에서 멈춘다 같은 애매한 장애로 나타날 수 있다는 점이 문제다.
2. Prisma는 “그냥 JS 라이브러리”가 아니다
Prisma를 쓰면 코드에서는 이렇게 보인다.
await prisma.user.findUnique({ where: { id: 1 } })
그래서 “Prisma도 그냥 npm 패키지(자바스크립트)겠지?” 싶은데, 실제 구조는 다르다. Prisma는 내부적으로 Query Engine이라는 별도 프로그램을 함께 사용한다.
Prisma 내부 흐름(핵심)
Next.js/Node 코드
→ Prisma Client (JS)
→ Prisma Query Engine (Rust로 만든 네이티브 실행 파일)
→ DB (PostgreSQL 등)
여기서 엔진(Engine)은 말 그대로 리눅스 실행 파일(binary)이다. 즉 Prisma는 “순수 JS 라이브러리”가 아니라, JS + 네이티브 바이너리가 결합된 구조라고 이해해야 한다.
그리고 이 엔진은 OS/CPU/OpenSSL 조합에 따라 별도 빌드된 파일을 사용한다. 그래서 Prisma에는 “binaryTargets” 같은 설정이 존재한다.
3. 왜 Alpine에서 문제가 생겼나?
핵심은 libc 차이 + 네이티브 바이너리 호환성이다. Prisma Query Engine은 “내 OS 환경에 맞는 바이너리”를 골라서 쓰는데, Alpine은 일반적인 서버 계열과 기본 전제가 다르다.
3-1. musl vs glibc
- Alpine: musl libc
- Debian/Ubuntu 계열: glibc
Prisma 엔진은 아래 조건에 따라 다른 바이너리를 사용한다.
- OS 종류 (linux 등)
- libc 종류 (glibc vs musl)
- CPU 아키텍처 (x86_64 vs arm64)
- OpenSSL 버전 (3.0.x 등)
3-2. 왜 “에러”가 아니라 “멈춤(hang)”이었나?
이런 계열의 문제는 종종 아래처럼 나타난다:
- 쿼리 호출(
findUnique등)을 했는데 응답이 안 옴 - 프로세스가 죽지 않아서 crash 로그가 없음
- 결국 타임아웃까지 대기
- 로컬에서는 재현이 안 되고, 배포 환경에서만 재현되기도 함
즉, “Prisma 코드가 잘못됐다”라기보다,
“Node ↔ Prisma 엔진 ↔ DB 중간의 엔진 통신/호환이 깨져서 멈춘 것”에 가깝다.
4. Cursor가 왜 초반에 못 잡았나?
Cursor(또는 템플릿/AI)가 초반 셋업에서 잘 해주는 건 보통:
- 로컬에서 바로 실행 가능
- 의존성 설치, 기본 Dockerfile, 기본 compose 구성
- 이미지 작게(Alpine 선호) + 빠르게 시작
하지만 이번 이슈는 정적 분석(코드만 보기)로 잡기 어려운 성격이다. 왜냐면 원인이 “런타임 환경”에 걸려 있기 때문이다.
특히 배포가 다음 조건 중 하나를 포함하면 위험도가 올라간다:
- AWS Graviton 등 ARM64 환경
- OpenSSL 버전/빌드 체인 차이
- 멀티 스테이지 Docker 빌드에서 generate/run 환경이 어긋남
- Alpine(musl) + Prisma 엔진 조합
그래서 “처음엔 되는 것처럼 보이다가” 운영에서 터지기 쉽다.
5. 내가 겪은 증상 요약
- 배포된 앱에서 Prisma DB 조회 시 멈춤
- 예:
prisma.findUnique()호출 후 응답 없음 - 에러 로그 거의 없음
- 타임아웃까지 대기
- 환경: Docker 이미지
node:20-alpine3.18, AWS(또는 ARM64) 배포
6. 해결 방법: Debian(glibc)로 통일
방향은 단순했다.
Alpine → Debian 기반으로 변경하고,
Prisma도 Debian/glibc용 엔진 타깃으로 맞춘다.
6-1. Prisma 설정 변경 (schema.prisma)
변경 전
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
}
변경 후
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-arm64-openssl-3.0.x"]
}
- musl 타깃 제거
debian-openssl-3.0.x사용- ARM64 배포 고려 시
linux-arm64-openssl-3.0.x포함
6-2. Docker 베이스 이미지 변경
변경 전
FROM node:20-alpine3.18
변경 후
FROM node:20-slim
그리고 패키지 설치도 Alpine의 apk에서 Debian의 apt-get으로 변경했다.
(Builder에는 build tool들, Runner에는 openssl 등 최소만 설치)
7. 결과
- 재빌드/재배포 후 DB 조회가 멈추지 않음
findUnique등 쿼리 정상 응답
핵심은 결국:
Docker는 glibc 기반(Debian), Prisma는 그에 맞는 엔진 타깃으로 통일
8. (추가) Prisma 엔진 타깃/설치 상태 확인법
이 이슈를 빨리 잡으려면 “내가 지금 어떤 엔진을 쓰는지”를 확인할 수 있어야 한다. 아래는 로컬/컨테이너/CI 어디서든 바로 확인 가능한 체크리스트다.
8-1. 현재 Prisma/엔진 버전 확인
npx prisma -v
여기서 보통: Prisma 버전, Engine 버전 등의 힌트를 얻을 수 있다.
8-2. Prisma Client/엔진 생성(generate) 확인
npx prisma generate
Docker/CI에서 generate가 누락되면 런타임에서 엔진을 못 찾거나, 환경에 따라 실행 시점에 다운로드/실패 같은 예기치 못한 문제가 날 수 있다.
8-3. node_modules 안에서 엔진 파일 직접 확인
ls -al node_modules/.prisma/client
여기에서 파일명을 보고 내가 의도한 타깃인지 확인한다. 예시(환경에 따라 이름이 조금 달라질 수 있음):
query-engine-debian-openssl-3.0.xquery-engine-linux-arm64-openssl-3.0.xquery-engine-linux-musl-openssl-3.0.x(보이면 Alpine 계열)
8-4. 컨테이너 안에서 OS/아키텍처 확인
컨테이너 셸 진입:
docker run --rm -it <IMAGE_NAME> sh
OS 확인:
cat /etc/os-release
CPU 아키텍처 확인:
node -p "process.arch"
엔진 파일 확인:
ls -al node_modules/.prisma/client
8-5. “멈춤” 여부를 최소 쿼리로 빠르게 확인하는 스모크 테스트
node -e "const {PrismaClient}=require('@prisma/client'); const p=new PrismaClient(); p.user.findFirst().then(r=>{console.log('OK',r); process.exit(0)}).catch(e=>{console.error(e); process.exit(1)});"
이 테스트가 응답 없이 멈추면, 엔진/런타임 호환성 문제를 강하게 의심할 수 있다. 배포 전 CI의 smoke test로도 활용 가능하다.
8-6. binaryTargets 바꿨으면 “깨끗한 재설치/재생성”이 필요할 수 있음
rm -rf node_modules
rm -rf node_modules/.prisma
npm install
npx prisma generate
예전 엔진이 남아 있으면 “바꿨는데도 왜 그대로지?” 같은 혼란이 생길 수 있어서, 설정 변경 후에는 이렇게 깨끗하게 다시 생성하는 게 안전하다.
9. 실무 결론
Prisma는 단순 JS 라이브러리가 아니라 Rust로 만든 네이티브 Query Engine을 사용하는 구조다. 그래서 아래 요소가 달라지면 문제가 생길 수 있다.
- OS libc (musl vs glibc)
- OpenSSL 버전
- CPU 아키텍처 (x64 vs arm64)
- Docker 베이스 이미지
안전한 기본값(Prisma 쓰는 팀에서 흔히 선택하는 쪽):
- Docker:
node:20-slim(Debian/glibc) - Prisma:
debian-openssl-3.0.x+ (필요 시)linux-arm64-openssl-3.0.x
다음에는 Docker multi-stage에서 prisma generate 위치를 잘못 잡으면 생기는 문제도 같이 정리해볼 예정이다. (빌더/런너 환경이 달라지면서 엔진이 꼬이는 케이스)