티스토리 뷰

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.x
  • query-engine-linux-arm64-openssl-3.0.x
  • query-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 위치를 잘못 잡으면 생기는 문제도 같이 정리해볼 예정이다. (빌더/런너 환경이 달라지면서 엔진이 꼬이는 케이스)

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday