28P01은 서버가 연결을 받은 뒤 그 역할의 비밀번호를 거절할 때 나는 코드입니다. 역할 이름을 맞추고, 비밀번호의 특수문자는 그 조각만 퍼센트 인코딩합니다. 연결 거절, 이름 해석 실패, 풀 대기와 줄을 가릅니다. 비밀번호는 로그에 찍지 않습니다. PostgreSQL, Prisma, 프리뷰, 한국 1인 개발. 2026년 9월 문서.
서버가 비밀번호를 거절한 상태입니다. PostgreSQL 오류 표에서 28P01의 조건 이름은 invalid_password입니다. 클라이언트가 TCP로 붙어 인증을 시작했고, 서버가 그 역할에 대해 비밀번호가 맞지 않다고 닫은 자리입니다.
그래서 방화벽이나 꺼진 서버와 순서가 다릅니다. 호스트를 못 찾으면 DNS 오류가 먼저 나고, 포트에 아무도 없으면 연결 거절이 납니다. 풀에서 빈 연결을 못 받으면 프리즈마는 다른 코드로 대기 시간을 알립니다. 28P01 한 줄이 보이면 네트워크 칸은 이미 통과한 뒤입니다.
메시지에 찍힌 사용자 이름을 그대로 읽으세요. 기대한 역할이 아니면 비밀번호를 열 번 다시 쳐도 그 역할의 비밀과만 비교합니다. 역할이 맞으면 서버에 저장된 비밀과 클라이언트가 보낸 문자열이 다른 것입니다.
먼저 읽을 것 | password authentication failed for user 뒤의 따옴표 안이 실제 역할입니다. 대시보드에 적힌 사용자와 한 글자라도 다르면 비밀번호 재발급보다 그 이름을 먼저 맞춥니다.
28P01은 서버가 응답한 뒤 비밀번호 단계에서 거절했다는 뜻이다
접속 문자열이 비밀번호를 잘라 먹는 경우
URI의 마지막 @ 가 사용자 정보와 호스트를 가릅니다. 비밀번호 안에 @ 가 있으면 그 앞까지만 비밀번호로 보이고, 나머지는 호스트로 넘어갑니다. 그때는 엉뚱한 호스트라 28P01이 아니라 이름 해석 실패가 나기도 합니다. 호스트는 맞는데 로그인만 실패하면 #, ?, % 처럼 URI에서 특별한 글자가 비밀번호 뒤를 잘라 먹었는지 보세요.
libpq 문서는 접속 URI에 특별한 의미가 있는 기호가 있으면 퍼센트 인코딩을 하라고 합니다. # 는 프래그먼트 시작이라 그 뒤가 접속 정보에서 빠질 수 있고, ? 는 쿼리 시작으로 읽힙니다. % 는 인코딩의 시작 글자라 그대로 두면 다음 두 글자를 코드로 해석하려다 비밀번호가 바뀝니다.
키워드 형식은 이 구분이 없습니다. host= dbname= user= password= 를 공백으로 나열하면 @ 가 호스트 구분자가 아닙니다. 비밀번호에 공백이나 따옴표가 있으면 작은따옴표로 감싸고, 따옴표와 역슬래시는 역슬래시로 이스케이프합니다. 문서의 예는 host=localhost port=5432 dbname=mydb 형태입니다.
비밀번호만 인코딩해서 URI에 넣기
# 비밀번호 원문은 히스토리에 남기지 않는다. 예시는 가짜 값.
node -e "console.log(encodeURIComponent('p@ss#word'))"
# p%40ss%23word
# 키워드 형식은 @ 를 호스트 구분자로 쓰지 않는다
psql "host=db.example.com port=5432 dbname=app user=app"
퍼센트 인코딩으로 바꿀 글자
비밀번호 전체 URI를 한 번에 인코딩하면 스킴의 :// 까지 망가집니다. encodeURIComponent는 비밀번호 조각에만 씁니다. 아래는 접속 URI에서 자주 의미를 가지는 글자입니다. PostgreSQL 문서의 예는 등호를 %3D, 공백을 %20으로 바꿉니다.
글자
인코딩
그대로 두면
@
%40
호스트와 사용자를 가름
:
%3A
비밀번호와 포트 표기가 섞임
#
%23
프래그먼트로 뒤를 버릴 수 있음
?
%3F
쿼리 시작으로 읽힘
%
%25
다른 인코딩을 시작함
/
%2F
경로 구분자
공백
%20
토큰이 나뉨
프리즈마 접속 URI 안내도 같은 규칙을 따릅니다. 비밀번호 pe@ce 를 URI에 넣을 때는 pe%40ce 처럼 그 조각만 바꿉니다. 이미 인코딩된 값을 한 번 더 encodeURIComponent 하면 %40 이 %2540 이 되어 서버는 다른 비밀번호로 받습니다. 28P01이 인코딩 후에 생겼다면 이중 인코딩을 의심하세요.
역할, 데이터베이스, 비밀번호 파일
같은 비밀번호라도 역할이 다르면 실패합니다. 직접 접속의 사용자와 풀러 접속의 사용자가 문자 그대로 같은지 대시보드 연결 정보와 비교하세요. 앱 환경 변수의 사용자만 예전 프로젝트 것으로 남아 있으면 서버는 그 이름의 비밀번호가 틀렸다고 답합니다.
로컬에서 확인할 때는 비밀번호를 명령줄에 붙이지 않습니다. psql "host=... port=5432 dbname=app user=app" 처럼 넣고, 프롬프트에 비밀번호를 치면 히스토리에는 비밀이 남지 않습니다. 이 접속이 성공하면 앱이 읽는 환경 변수가 다른 값을 가진 것입니다. 실패하면 서버에 저장된 비밀과 지금 친 값이 다릅니다.
비밀번호 파일은 ~/.pgpass 이고 윈도우는 %APPDATA%\postgresql\pgpass.conf 입니다. libpq는 이 파일이 없어도 오류로 올리지 않고 그냥 지나갑니다. 파일 권한이 너무 열려 있으면 그 항목을 무시하는 동작이 있어, 파일은 있는데도 앱은 환경 변수의 다른 비밀번호를 씁니다. 호스트, 포트, 데이터베이스, 사용자 네 칸이 접속과 일치하는 줄만 적용됩니다.
pg_hba.conf에 맞는 줄이 없으면 문구가 다릅니다. no pg_hba.conf entry 는 비밀번호 불일치가 아니라 어떤 인증 규칙도 이 접속을 받지 않는다는 뜻입니다. 28P01만 이 글의 대상입니다. 규칙을 trust 로 풀어 오류를 지우는 방식은 그 주소의 모든 접속이 비밀번호 없이 들어옵니다. 규칙 방법은 서버 문서의 인증 절을 유지하세요.
역할 이름을 로그와 나란히 보기
# 앱이 실제로 보낸 사용자. 비밀번호는 출력하지 않는다
node -e "const u=new URL(process.env.DATABASE_URL); console.log(u.username, u.hostname, u.port)"
# 서버에 붙은 세션이 있을 때
# SELECT usename, datname, state FROM pg_stat_activity;
호스트와 포트가 맞아도 역할 이름이 다르면 같은 28P01이 난다
로컬은 되고 프리뷰만 거절될 때
로컬 .env 와 프리뷰 환경 변수는 다른 칸입니다. 로컬 파일이 맞는데 배포만 28P01이면, 프리뷰에 넣은 접속 문자열이 예전 비밀번호이거나 따옴표가 값에 포함된 경우입니다. 대시보드에서 값을 다시 저장할 때 앞뒤 공백이 들어가면 서버는 다른 비밀번호로 받습니다.
값을 로그로 찍지 마세요. 비교는 길이, 사용자 이름, 호스트, 포트면 충분합니다. 비밀번호가 바뀌었는지는 대시보드에서 다시 발급해 프리뷰 변수만 교체하고 함수를 한 번 재시작하면 됩니다. 이미 떠 있는 프로세스는 예전 환경 변수를 붙잡고 있습니다.
풀이 꽉 찬 오류와 섞지 마세요. sorry, too many clients already 는 자리 부족이고 인증 성공 이전의 다른 거절입니다. 준비된 문장 이름 충돌은 인증이 끝난 뒤의 풀러 설정입니다. 그 둘은 연결 풀 대기, 준비된 문장 글의 자리입니다. 호스트 이름 자체는 DNS ENOTFOUND, 포트가 닫힌 로컬 도커는 ECONNREFUSED입니다.
증상
어디가 막혔나
이 글에서 볼 것
28P01
인증 비밀번호
역할, 인코딩, 환경 변수
ENOTFOUND
호스트 이름
URI가 @ 로 쪼개졌는지
ECONNREFUSED
포트에 수신 없음
비밀번호 칸이 아님
too many clients
연결 자리 부족
max_connections
한국에서 혼자 프리뷰를 띄울 때는 로컬 직접 주소와 프리뷰 풀러 주소를 한 변수에 번갈아 넣다가 역할만 어긋나는 일이 많습니다. 변수 이름을 주소 종류별로 나누고, 앱이 읽는 이름이 어느 쪽인지 배포 로그의 호스트와 사용자만 대조하면 됩니다.
복사한 값에 #, @, ? 가 있으면 URI가 그 뒤를 비밀번호로 보지 않을 수 있습니다. 비밀번호 조각만 encodeURIComponent 로 바꾸거나, host= user= password= 키워드 형식으로 넣으세요. 전체를 한 번 더 인코딩하면 % 가 두 번 바뀌어 또 실패합니다.
로컬 psql은 되는데 앱만 28P01입니다.
앱이 읽는 환경 변수가 psql에 친 값과 다릅니다. 사용자 이름, 호스트, 포트만 출력해 비교하고 비밀번호는 찍지 마세요. 프리뷰라면 배포를 한 번 다시 해야 새 변수가 프로세스에 들어갑니다.
pg_hba를 trust로 바꾸면 해결인가요?
그 주소에 맞는 클라이언트는 비밀번호 없이 들어옵니다. 28P01을 지우는 방법이 아니라 인증을 끄는 설정입니다. 규칙이 없는 오류 문구는 이 코드와 다르니, 문구를 확인한 뒤 비밀번호와 역할만 맞추세요.
슈퍼베이스 풀러와 직접 주소의 사용자가 다릅니까?
대시보드가 보여주는 문자열을 주소 종류별로 그대로 써야 합니다. 직접 주소용 사용자를 풀러 포트에 넣으면 서버는 그 역할의 비밀번호가 틀렸다고 거절할 수 있습니다. 에러 따옴표 안과 화면의 사용자를 한 줄로 맞춰 보세요.
연결이 아예 안 되면 이 코드인가요?
아닙니다. 호스트를 못 찾거나 포트가 닫히면 28P01까지 가지 않습니다. 이 코드는 서버가 인증 답을 준 뒤에만 나옵니다. 타임아웃과 연결 거절은 각각의 글에서 포트와 DNS를 보면 됩니다.
.pgpass가 있는데 앱은 계속 실패합니다.
앱이 환경 변수로 비밀번호를 넘기면 그 값이 우선할 수 있습니다. 파일 권한이 열려 있으면 libpq가 그 줄을 무시하기도 합니다. 호스트, 포트, 데이터베이스, 사용자가 접속과 같은 줄인지 확인하고, 앱 변수와 파일이 서로 다른 비밀을 가리키지 않는지 보세요.
28P01은 서버가 그 역할의 비밀번호를 거절했다는 한 줄입니다. 메시지 속 사용자를 대시보드와 맞추고, 특수문자는 비밀번호 조각만 인코딩하거나 키워드 형식으로 넘기면 포트는 그대로 두고 로그인이 풀립니다. 관련 글: DNS, 준비된 문장, 로컬 포트 거절.