PostgreSQL 새 테이블 권한 오류: GRANT와 ALTER DEFAULT PRIVILEGES
기존 테이블은 조회되는데 방금 만든 테이블에서만 permission denied for table이 발생한다면, 테이블을 만든 역할과 기본 권한부터 확인해 보자.
GRANT ... ON ALL TABLES IN SCHEMA는 실행할 때 존재하는 테이블에 적용된다. 이후에 생기는 테이블까지 자동으로 따라가지 않는다. 새 객체에는 그 객체를 생성한 역할의 ALTER DEFAULT PRIVILEGES 설정이 적용된다. 따라서 기존 객체의 권한 보정과 앞으로 만들 객체의 기본 권한 설정은 따로 해야 한다.
이번 글에서는 이 차이를 별도 PostgreSQL 엔진에서 재현하고, SERIAL 시퀀스 권한과 마이그레이션 역할까지 확인한다.
시작은 “접속한 계정이 관리자일까?”라는 질문이었다
2026년 10월 14일 Oracle Cloud E2에 PostgreSQL을 설치할 때, postgres:18.6-bookworm 컨테이너가 실행되고 appuser로 appdb에 접속해 읽기·쓰기가 되는 것까지 확인했다. 그다음 남은 질문은 “이 계정이 관리자 계정인가?”, “다른 계정은 어떻게 추가하나?”였다.
여기까지가 당시 확인한 운영 기록이다. 새 테이블 권한 오류가 실제 운영 서버에서 발생했다거나, 운영 계정을 분리해 해결했다는 기록은 없다. 아래 오류와 성공 결과는 이 글을 위해 만든 격리된 시험 데이터베이스에서 얻었다.
접속 계정의 권한과 접속 개수는 다른 설정이다. 연결 수를 함께 정리하고 있다면 PostgreSQL 연결 풀 20개는 어디서 정할까: HikariCP와 max_connections 구분을 먼저 참고해도 좋다.
1. 계정 이름 대신 현재 역할과 권한을 확인한다
appuser처럼 애플리케이션용으로 보이는 이름도 관리자 권한을 가질 수 있다. Docker Official Image의 POSTGRES_USER는 빈 데이터 디렉터리를 처음 초기화할 때 슈퍼유저를 만든다. 이미 초기화된 데이터 볼륨에서 환경 변수만 바꾼다고 기존 역할이 바뀌지는 않는다. 이는 공식 이미지 설명의 초기화 동작이며, 지금 운영 중인 계정의 상태는 직접 조회해야 한다.
SELECT version(), current_database(),
session_user, current_user;
SELECT rolname, rolcanlogin, rolsuper,
rolcreatedb, rolcreaterole, rolinherit
FROM pg_roles
WHERE rolname IN (session_user, current_user);
session_user는 세션의 원래 사용자, current_user는 현재 권한 검사에 사용하는 역할이다. SET ROLE을 사용했다면 서로 다를 수 있다. 슈퍼유저로 권한을 시험하면 일반 실행 계정의 오류가 가려질 수 있으므로, 실제 애플리케이션 역할로 확인한다.
권한은 다음 순서로 나눠 보면 편하다.
- 데이터베이스
CONNECT: 해당 데이터베이스에 접속할 수 있는가. - 스키마
USAGE: 스키마 안의 객체를 참조할 수 있는가. - 테이블
SELECT,INSERT등: 그 객체에서 필요한 작업을 할 수 있는가. - 시퀀스
USAGE등: 번호 생성에 사용하는 별도 시퀀스를 사용할 수 있는가. - 소유권: 테이블 구조를 바꾸거나 삭제할 수 있는가.
USAGE ON SCHEMA만으로 테이블 조회 권한이 생기지는 않는다. 반대로 테이블 권한이 있어도 스키마 접근 권한이 빠지면 접근할 수 없다. 역할 멤버십과 PUBLIC에서 받은 권한도 있으므로, 오류가 없다고 직접 부여한 권한만 작동한다고 단정하지 않는다.
2. 기존 테이블 GRANT가 새 테이블에는 적용되지 않는 상황
시험 환경: 2026년 10월 26일, PGlite 0.5.11이 제공하는 PostgreSQL 18.6 엔진, 메모리 내 데이터베이스. 관리자 세션에서 SET ROLE로 비슈퍼유저 역할의 SQL 권한을 확인했다. Docker 컨테이너나 실제 서버에 접속한 실험은 아니며, 네트워크 인증·pg_hba.conf·별도 로그인 연결은 검증하지 않았다.
다음 이름은 모두 시험용이다. 운영 데이터베이스에 그대로 실행하지 말고 비어 있는 시험 데이터베이스에서 사용한다. NOLOGIN은 이 실험에서 인증 없이 역할 전환만 확인하기 위한 설정이다.
-- 관리자 역할로 실행
CREATE ROLE lab_owner NOLOGIN NOSUPERUSER;
CREATE ROLE lab_runtime NOLOGIN NOSUPERUSER;
CREATE SCHEMA lab_app AUTHORIZATION lab_owner;
GRANT USAGE ON SCHEMA lab_app TO lab_runtime;
-- 객체를 만드는 역할로 전환
SET ROLE lab_owner;
CREATE TABLE lab_app.old_table (
id integer PRIMARY KEY,
note text
);
INSERT INTO lab_app.old_table VALUES (1, 'existing');
GRANT SELECT ON ALL TABLES IN SCHEMA lab_app
TO lab_runtime;
-- GRANT가 끝난 다음에 새 테이블 생성
CREATE TABLE lab_app.gap_table (
id integer PRIMARY KEY,
note text
);
RESET ROLE;
이제 실행 역할로 각각 조회한다. 아래 두 문장은 한 트랜잭션으로 묶지 않고 개별 실행한다. 오류가 난 트랜잭션에서는 먼저 ROLLBACK이 필요할 수 있다.
SET ROLE lab_runtime;
SELECT * FROM lab_app.old_table;
-- 성공: id=1, note=existing
SELECT * FROM lab_app.gap_table;
-- SQLSTATE 42501
-- permission denied for table gap_table
RESET ROLE;
두 테이블은 같은 스키마에 있고 생성 역할도 같다. 차이는 권한을 부여한 시점이다. ALL TABLES는 그때 존재하던 객체를 대상으로 하므로, 이후 생성한 gap_table에는 조회 권한이 없다.
3. 미래 객체는 생성 역할을 지정해 기본 권한을 설정한다
이 실험에서 테이블을 생성하는 역할은 lab_owner다. 따라서 FOR ROLE lab_owner를 대상으로 설정한다. TO lab_runtime은 그 객체의 권한을 받을 역할이다.
-- 관리자 또는 해당 기본 권한을 변경할 수 있는 역할로 실행
ALTER DEFAULT PRIVILEGES
FOR ROLE lab_owner
IN SCHEMA lab_app
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES
TO lab_runtime;
SET ROLE lab_owner;
CREATE TABLE lab_app.future_table (
id integer PRIMARY KEY,
note text
);
RESET ROLE;
SET ROLE lab_runtime;
INSERT INTO lab_app.future_table
VALUES (1, 'created after defaults')
RETURNING *;
SELECT * FROM lab_app.future_table;
RESET ROLE;
새로 생성한 future_table에서는 삽입과 조회가 모두 성공했다. 이 예시는 읽기·쓰기가 필요한 실행 역할을 가정한다. 읽기 전용 계정에는 SELECT처럼 필요한 권한만 부여한다.
반면 앞서 만든 gap_table은 기본 권한을 설정한 뒤에도 계속 42501을 반환했다. 이미 존재하는 객체에는 별도의 GRANT가 필요하다.
GRANT SELECT ON lab_app.gap_table TO lab_runtime;
이후 같은 역할로 조회하면 성공한다. 기존 객체는 GRANT, 이후 객체는 ALTER DEFAULT PRIVILEGES라는 구분이 여기서 드러난다.
별도 시험에서는 lab_wrong_owner의 기본 권한만 설정하고 lab_owner로 새 테이블을 만들어 보았다. 실행 역할의 조회는 여전히 거부됐다. FOR ROLE에는 권한을 받을 실행 계정이나 단순히 접속에 사용한 계정이 아니라, 실제로 객체를 생성하는 역할이 들어가야 한다.
4. 마이그레이션 계정이 다르면 같은 문제가 다시 생긴다
역할 멤버십으로 스키마에 테이블을 만들 수 있게 됐다고 해서, 그 역할의 기본 권한까지 따라오는 것은 아니다. 다음은 앞선 실험에 이어 실행한 별도 사례다.
CREATE ROLE lab_migrator NOLOGIN NOSUPERUSER INHERIT;
GRANT lab_owner TO lab_migrator;
SET ROLE lab_migrator;
SELECT session_user, current_user;
CREATE TABLE lab_app.by_migrator (id integer);
RESET ROLE;
SELECT pg_get_userbyid(relowner) AS owner
FROM pg_class
WHERE oid = 'lab_app.by_migrator'::regclass;
-- lab_migrator
lab_migrator는 상속한 권한으로 테이블을 만들 수 있었지만, 생성 시점의 current_user는 lab_migrator였다. 테이블 소유자도 lab_migrator였고, lab_runtime의 조회는 거부됐다. lab_owner에 설정한 기본 권한이 멤버 역할의 객체 생성에 자동으로 적용되지 않은 것이다.
실제 배포에서는 다음 중 프로젝트에 맞는 방식을 정한다.
- 마이그레이션 세션이 허용된 소유 역할로
SET ROLE한 다음 객체를 생성한다. - 객체를 실제로 만드는 각 역할에 기본 권한을 설정한다.
어느 방식을 택하든 마이그레이션과 같은 연결·같은 역할 전환 순서로 시험 테이블을 만든 뒤 확인해야 한다. 평소 관리 도구에서 접속하는 계정만 확인해서는 부족하다. SET ROLE에 필요한 역할 전환 권한도 별도로 점검한다.
5. 테이블 INSERT 권한이 있는데 SERIAL만 실패한다면
SERIAL을 사용한 테이블은 기본값을 채우기 위해 별도 시퀀스를 사용한다. 앞서 테이블의 기본 권한을 설정했어도, 시퀀스 권한까지 자동으로 부여되지는 않는다.
SET ROLE lab_owner;
CREATE TABLE lab_app.serial_item (
id serial PRIMARY KEY,
note text
);
RESET ROLE;
SET ROLE lab_runtime;
INSERT INTO lab_app.serial_item(note)
VALUES ('first') RETURNING id;
-- SQLSTATE 42501
-- permission denied for sequence serial_item_id_seq
RESET ROLE;
시험에서는 테이블 INSERT 권한이 있는데도 시퀀스 접근에서 실패했다. 필요한 기존 시퀀스에 USAGE를 부여한 뒤 같은 형태의 삽입을 실행하자 성공했다.
GRANT USAGE ON SEQUENCE lab_app.serial_item_id_seq
TO lab_runtime;
앞으로 해당 역할이 만드는 시퀀스에도 필요하다면 기본 권한을 별도로 설정한다.
ALTER DEFAULT PRIVILEGES
FOR ROLE lab_owner
IN SCHEMA lab_app
GRANT USAGE ON SEQUENCES
TO lab_runtime;
이 설정 뒤에 lab_owner로 만든 새 SERIAL 테이블에서는 실행 역할의 삽입이 성공했다. 기존 시퀀스는 이 설정의 소급 적용 대상이 아니다. 또한 번호 생성만 필요하다면 무작정 ALL PRIVILEGES를 부여하기보다 필요한 시퀀스 동작에 맞춰 권한을 정한다.
IDENTITY를 SERIAL과 똑같이 단정하지 않는다
이번 시험에서는 시퀀스 기본 권한을 제거한 뒤 GENERATED ALWAYS AS IDENTITY 컬럼을 가진 테이블을 별도로 만들었다. 실행 역할에 해당 시퀀스의 USAGE가 없는 상태에서도, ID 컬럼을 생략한 일반 INSERT ... RETURNING id는 성공했다. 같은 역할이 그 시퀀스를 대상으로 nextval()을 직접 호출하면 42501로 거부됐다.
따라서 “자동 증가 컬럼이면 항상 시퀀스 USAGE가 필요하다”는 식으로 일반화하지 않는다. SERIAL인지 IDENTITY인지, 애플리케이션이 기본값에 맡기는지 직접 nextval()을 호출하는지 구분한다. 이 결과의 검증 범위는 위에 적은 PostgreSQL 18.6 시험 환경이다.
6. 운영에 적용하기 전에 확인할 명령과 성공 기준
먼저 현재 생성 역할, 테이블 소유자, 실행 역할의 유효 권한을 확인한다. 아래 이름은 실제 스키마와 역할 이름으로 바꿔 사용한다.
SELECT current_database(), session_user, current_user;
SELECT n.nspname AS schema_name,
c.relname AS object_name,
c.relkind,
pg_get_userbyid(c.relowner) AS owner
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'lab_app'
AND c.relkind IN ('r', 'p', 'S')
ORDER BY c.relname;
SELECT
has_schema_privilege(
'lab_runtime', 'lab_app', 'USAGE'
) AS schema_usage,
has_table_privilege(
'lab_runtime', 'lab_app.future_table', 'SELECT'
) AS table_select;
psql에서는 다음 메타 명령으로 객체 권한과 기본 권한을 펼쳐 볼 수 있다. 이는 PGlite API로 실행한 SQL이 아니라, 실제 psql 점검 시 사용할 명령이다.
\dp lab_app.*
\ddp
기존 객체가 여러 개라면 GRANT ... ON ALL TABLES IN SCHEMA나 ALL SEQUENCES로 보정할 수 있다. 다만 먼저 그 스키마의 모든 객체가 해당 실행 계정의 접근 대상인지 확인한다. 공용 스키마에서 편의상 전체 권한을 주기보다, 필요한 테이블·시퀀스를 명시하는 편이 권한 범위를 판단하기 쉽다.
최종 판정은 조회 성공 한 번으로 끝내지 않는다.
- 기존 객체: 실행 계정이 필요한 조회·삽입을 수행할 수 있다.
- 새 객체: 실제 마이그레이션 역할로 만든 새 테이블도 같은 작업이 가능하다.
- 시퀀스: 실제 INSERT 형태로 번호 생성이 된다. 직접 호출하는 시퀀스 함수가 있다면 따로 확인한다.
- 제한: 실행 계정이 필요 없는 테이블 생성·구조 변경·삭제를 할 수 없다.
- 접속: 실제 애플리케이션 연결로 인증, 데이터베이스
CONNECT, 스키마 접근을 확인한다.
이번 격리 실험에서는 lab_runtime의 테이블 생성, ALTER TABLE, DROP TABLE이 모두 42501로 거부됐다. 앞의 DML 권한을 부여했다고 테이블 소유권까지 넘어간 것은 아니다. 실제 계정에서는 다른 역할 멤버십이나 기존 권한 때문에 결과가 달라질 수 있다.
관리 화면에서 SQL을 실행하게 한다면 계정 분리 외에도 허용 쿼리와 감사 기록이 필요하다. Vue 3·Spring Boot 운영 SQL 콘솔 실전 설계: CRUD에서 보안·감사·Excel까지에서 애플리케이션 쪽 통제를 함께 살펴볼 수 있다.
읽기·쓰기 연결을 나누는 구성도 DB 권한을 대신하지 않는다. 라우팅과 연결 설정은 Spring Boot + PostgreSQL 읽기·쓰기 분리 완전 가이드: JPA·MyBatis 라우팅과 데이터 정합성과 연결해 확인하면 된다.
검증 범위와 참고 문서
2026년 10월 26일 실행한 격리 실험에서는 기존·신규 객체 접근, 잘못 지정한 생성 역할, 기본 권한의 비소급 적용, 역할 멤버십, SERIAL·IDENTITY, DDL 거부를 포함한 18개 확인 항목이 예상한 성공 또는 거부 결과와 일치했다. 의도한 권한 오류도 정상적인 검증 결과에 포함된다.
시험 엔진이 보고한 버전은 PostgreSQL 18.6 (PGlite 0.5.11) on wasm32-unknown-emscripten이다. 실제 Oracle Cloud 서버의 현재 버전·권한·배포 계정 상태를 확인한 결과로 읽어서는 안 된다. 운영 서버 적용, 실제 로그인 연결, 동시 접속과 성능 측정은 수행하지 않았다.
아래 공식 문서는 2026년 10월 26일 확인했다.
- PostgreSQL 18 — ALTER DEFAULT PRIVILEGES: 미래 객체, 생성 역할과 기본 권한.
- PostgreSQL 18 — GRANT: 객체별 권한과 시퀀스 권한.
- PostgreSQL 18 — SET ROLE: 현재 역할의 전환과 권한 검사.
- PostgreSQL 18 — Privileges: 스키마·테이블 접근과 소유권.
- PostgreSQL 18 — Identity Columns: IDENTITY 컬럼의 생성 동작.
- Docker Official Image — postgres: 초기 계정과 초기화 환경 변수.
- PGlite 공식 문서: 이번 격리 시험에 사용한 PostgreSQL 엔진 환경.