> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# PostgreSQL 새 테이블 권한 오류: GRANT와 ALTER DEFAULT PRIVILEGES
- URL: https://mlog.me/postgresql-new-table-permission-denied-default-privileges/
- Published: 2026-10-26T00:20:41.000Z
- Updated: 2026-10-26T00:20:41.000Z
- Description: PostgreSQL에서 기존 테이블은 조회되는데 새 테이블만 permission denied가 나는 이유를 생성 역할·스키마·시퀀스 권한으로 나눠 점검합니다.
- Author: mLog
- Tags: 데이터베이스, PostgreSQL

기존 테이블은 조회되는데 방금 만든 테이블에서만 `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 구분](https://mlog.me/postgresql-hikaricp-pool-size-max-connections-budget/)을 먼저 참고해도 좋다.

## 1\. 계정 이름 대신 현재 역할과 권한을 확인한다

`appuser`처럼 애플리케이션용으로 보이는 이름도 관리자 권한을 가질 수 있다. Docker Official Image의 `POSTGRES_USER`는 빈 데이터 디렉터리를 처음 초기화할 때 슈퍼유저를 만든다. 이미 초기화된 데이터 볼륨에서 환경 변수만 바꾼다고 기존 역할이 바뀌지는 않는다. 이는 [공식 이미지 설명](https://hub.docker.com/%5F/postgres?ref=mlog.me)의 초기화 동작이며, 지금 운영 중인 계정의 상태는 직접 조회해야 한다.

```sql
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`은 이 실험에서 인증 없이 역할 전환만 확인하기 위한 설정이다.

```sql
-- 관리자 역할로 실행
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`이 필요할 수 있다.

```sql
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`은 그 객체의 권한을 받을 역할이다.

```sql
-- 관리자 또는 해당 기본 권한을 변경할 수 있는 역할로 실행
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`가 필요하다.

```sql
GRANT SELECT ON lab_app.gap_table TO lab_runtime;

```

이후 같은 역할로 조회하면 성공한다. 기존 객체는 `GRANT`, 이후 객체는 `ALTER DEFAULT PRIVILEGES`라는 구분이 여기서 드러난다.

별도 시험에서는 `lab_wrong_owner`의 기본 권한만 설정하고 `lab_owner`로 새 테이블을 만들어 보았다. 실행 역할의 조회는 여전히 거부됐다. `FOR ROLE`에는 권한을 받을 실행 계정이나 단순히 접속에 사용한 계정이 아니라, **실제로 객체를 생성하는 역할**이 들어가야 한다.

## 4\. 마이그레이션 계정이 다르면 같은 문제가 다시 생긴다

역할 멤버십으로 스키마에 테이블을 만들 수 있게 됐다고 해서, 그 역할의 기본 권한까지 따라오는 것은 아니다. 다음은 앞선 실험에 이어 실행한 별도 사례다.

```sql
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`에 설정한 기본 권한이 멤버 역할의 객체 생성에 자동으로 적용되지 않은 것이다.

실제 배포에서는 다음 중 프로젝트에 맞는 방식을 정한다.

1. 마이그레이션 세션이 허용된 소유 역할로 `SET ROLE`한 다음 객체를 생성한다.
2. 객체를 실제로 만드는 각 역할에 기본 권한을 설정한다.

어느 방식을 택하든 마이그레이션과 **같은 연결·같은 역할 전환 순서**로 시험 테이블을 만든 뒤 확인해야 한다. 평소 관리 도구에서 접속하는 계정만 확인해서는 부족하다. `SET ROLE`에 필요한 역할 전환 권한도 별도로 점검한다.

## 5\. 테이블 INSERT 권한이 있는데 SERIAL만 실패한다면

`SERIAL`을 사용한 테이블은 기본값을 채우기 위해 별도 시퀀스를 사용한다. 앞서 테이블의 기본 권한을 설정했어도, 시퀀스 권한까지 자동으로 부여되지는 않는다.

```sql
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`를 부여한 뒤 같은 형태의 삽입을 실행하자 성공했다.

```sql
GRANT USAGE ON SEQUENCE lab_app.serial_item_id_seq
TO lab_runtime;

```

앞으로 해당 역할이 만드는 시퀀스에도 필요하다면 기본 권한을 별도로 설정한다.

```sql
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\. 운영에 적용하기 전에 확인할 명령과 성공 기준

먼저 현재 생성 역할, 테이블 소유자, 실행 역할의 유효 권한을 확인한다. 아래 이름은 실제 스키마와 역할 이름으로 바꿔 사용한다.

```sql
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` 점검 시 사용할 명령이다.

```text
\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까지](https://mlog.me/vue-spring-boot-admin-sql-console-security/)에서 애플리케이션 쪽 통제를 함께 살펴볼 수 있다.

읽기·쓰기 연결을 나누는 구성도 DB 권한을 대신하지 않는다. 라우팅과 연결 설정은 [Spring Boot + PostgreSQL 읽기·쓰기 분리 완전 가이드: JPA·MyBatis 라우팅과 데이터 정합성](https://mlog.me/spring-boot-postgresql-read-write-splitting/)과 연결해 확인하면 된다.

## 검증 범위와 참고 문서

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](https://www.postgresql.org/docs/18/sql-alterdefaultprivileges.html?ref=mlog.me): 미래 객체, 생성 역할과 기본 권한.
- [PostgreSQL 18 — GRANT](https://www.postgresql.org/docs/18/sql-grant.html?ref=mlog.me): 객체별 권한과 시퀀스 권한.
- [PostgreSQL 18 — SET ROLE](https://www.postgresql.org/docs/18/sql-set-role.html?ref=mlog.me): 현재 역할의 전환과 권한 검사.
- [PostgreSQL 18 — Privileges](https://www.postgresql.org/docs/18/ddl-priv.html?ref=mlog.me): 스키마·테이블 접근과 소유권.
- [PostgreSQL 18 — Identity Columns](https://www.postgresql.org/docs/18/ddl-identity-columns.html?ref=mlog.me): IDENTITY 컬럼의 생성 동작.
- [Docker Official Image — postgres](https://hub.docker.com/%5F/postgres?ref=mlog.me): 초기 계정과 초기화 환경 변수.
- [PGlite 공식 문서](https://pglite.dev/docs/?ref=mlog.me): 이번 격리 시험에 사용한 PostgreSQL 엔진 환경.