스키마
스키마 (Schemas)
한 데이터베이스 안에서 객체들을 논리적으로 묶어 관리하고 싶을 때가 있어요. 그때 쓰는 것이 스키마(schema)예요. PostgreSQL 스키마는 데이터베이스 안에 있으며 테이블을 비롯해 데이터 타입, 함수, 연산자 같은 이름 붙은 객체들을 담아요. 운영체제의 디렉터리와 비슷한데, 단지 스키마는 중첩이 안 된다는 차이가 있어요.
출처: 공식문서
전체 구조부터 보기
PostgreSQL 데이터베이스 클러스터(cluster)는 하나 이상의 이름 붙은 데이터베이스를 담아요. 롤(role)과 몇몇 다른 객체 타입은 클러스터 전체에 공유되죠. 클라이언트가 서버에 연결하면, 연결 요청에서 지정한 단 하나의 데이터베이스의 데이터에만 접근할 수 있어요. 클러스터 사용자가 반드시 클러스터의 모든 데이터베이스에 접근 권한을 갖는 건 아니라는 점도 기억하세요.
한 데이터베이스는 하나 이상의 스키마를, 스키마는 테이블을 담아요. 한 스키마 안에서는 같은 타입의 객체 두 개가 같은 이름을 가질 수 없어요. 게다가 테이블, 시퀀스, 인덱스, 뷰, 구체화된 뷰, 외부 테이블은 같은 네임스페이스를 공유해서, 예를 들어 같은 스키마 안에서는 인덱스와 테이블의 이름이 달라야 해요. 같은 객체 이름은 스키마가 다르면 충돌 없이 쓸 수 있어요. 예를 들어 schema1과 myschema 둘 다 mytable이라는 테이블을 가질 수 있죠.
데이터베이스와 달리 스키마는 엄격하게 분리되지 않아요. 사용자는 접속한 데이터베이스의 어떤 스키마 객체에도, 권한이 있다면 접근할 수 있어요.
스키마를 왜 쓰는가
- 많은 사용자가 서로 간섭하지 않고 하나의 데이터베이스를 쓰게 하려고.
- 데이터베이스 객체를 논리적 그룹으로 묶어 더 관리하기 쉽게 하려고.
- 서드파티 애플리케이션을 별도 스키마에 넣어 다른 객체 이름과 충돌하지 않게 하려고.
스키마 만들기 (CREATE SCHEMA)
스키마를 만들려면 CREATE SCHEMA 명령을 쓰고 원하는 이름을 지어요:
CREATE SCHEMA myschema;
스키마 안의 객체를 만들거나 접근하려면 스키마 이름과 테이블 이름을 점으로 구분한 **정규화된 이름(qualified name)**을 써요:
schema.table
이건 테이블 이름이 올 자리면 어디서든 동작해요. 테이블 수정 명령과 데이터 접근 명령 모두 포함해서요. 더 일반적인 문법인 database.schema.table도 쓸 수 있지만, 지금은 SQL 표준에 형식적으로 맞추기 위한 것일 뿐이에요. 데이터베이스 이름을 쓰려면 반드시 지금 연결된 데이터베이스와 같아야 해요.
그래서 새 스키마에 테이블을 만들려면:
CREATE TABLE myschema.mytable (
...
);
빈 스키마(안의 객체를 모두 지운 상태)를 지우려면:
DROP SCHEMA myschema;
안에 있는 모든 객체를 포함해 지우려면:
DROP SCHEMA myschema CASCADE;
종종 다른 사용자가 소유한 스키마를 만들고 싶을 때가 있어요(이게 사용자 활동을 잘 정의된 네임스페이스로 제한하는 방법 중 하나예요):
CREATE SCHEMA schema_name AUTHORIZATION user_name;
스키마 이름을 생략할 수도 있는데, 그러면 스키마 이름은 사용자 이름과 같아져요. pg_로 시작하는 스키마 이름은 시스템용으로 예약되어 있어 사용자가 만들 수 없어요.
public 스키마
앞 절에서 우리는 스키마 이름 없이 테이블을 만들었어요. 기본적으로 그런 테이블(및 다른 객체)은 public이라는 스키마에 자동으로 들어가요. 모든 새 데이터베이스가 그런 스키마를 포함하죠. 그래서 아래 두 명령은 동등해요:
CREATE TABLE products ( ... );
CREATE TABLE public.products ( ... );
스키마 검색 경로 (search_path)
정규화된 이름은 쓰기 번거롭고, 애플리케이션에 특정 스키마 이름을 하드코딩하지 않는 게 보통 좋아요. 그래서 테이블은 흔히 테이블 이름만으로 된 **비정규화 이름(unqualified name)**으로 참조돼요. 시스템은 **검색 경로(search path)**를 따라 어떤 테이블을 뜻하는지 결정해요. 검색 경로는 찾아볼 스키마 목록이에요. 검색 경로에서 처음 일치하는 테이블이 원하는 것으로 간주돼요. 검색 경로에 일치가 없으면, 데이터베이스의 다른 스키마에 일치하는 테이블 이름이 있어도 오류가 보고돼요.
서로 다른 스키마에 같은 이름의 객체를 만드는 능력은, 매번 정확히 같은 객체를 참조하는 쿼리를 쓰는 일을 복잡하게 만들어요. 또 사용자가 다른 사용자 쿼리의 동작을 악의적이든 실수든 바꿀 가능성도 열어요. 쿼리에서 비정규화 이름이 흔하고 PostgreSQL 내부에서도 쓰이기 때문에, 스키마를 search_path에 추가하는 것은 사실상 그 스키마에 CREATE 권한을 가진 모든 사용자를 신뢰하는 셈이에요.
검색 경로에서 첫 번째로 이름 붙은 스키마를 **현재 스키마(current schema)**라고 불러요. 첫 번째로 검색되는 곳일 뿐 아니라, CREATE TABLE에서 스키마 이름을 지정하지 않으면 새 테이블이 만들어지는 곳이기도 해요.
현재 검색 경로를 보려면:
SHOW search_path;
기본 설정에서는 이렇게 반환돼요:
search_path
--------------
"$user", public
첫 번째 요소는 현재 사용자와 같은 이름의 스키마가 검색된다는 뜻이에요. 그런 스키마가 없으면 항목은 무시돼요. 두 번째 요소는 앞서 본 public 스키마를 가리켜요.
우리 새 스키마를 경로에 넣으려면:
SET search_path TO myschema,public;
그러면 스키마 정규화 없이 테이블에 접근할 수 있어요:
DROP TABLE mytable;
또한 myschema가 경로의 첫 요소이므로 새 객체는 기본적으로 그 안에 만들어져요. SET search_path TO myschema;라고 쓰면 명시적 정규화 없이는 public 스키마에 더 이상 접근할 수 없어요. public 스키마는 기본적으로 존재한다는 것 외에 특별할 게 없고, 지울 수도 있어요.
검색 경로는 테이블 이름뿐 아니라 데이터 타입 이름, 함수 이름, 연산자 이름에도 똑같이 적용돼요. 표현식 안에서 정규화된 연산자 이름을 써야 한다면 특별한 조항이 있는데, 이렇게 써야 해요:
OPERATOR(schema.operator)
문법적 모호성을 피하기 위해 필요해요. 실무에서는 보통 연산자에 검색 경로를 의존해서 그런 지저분한 걸 쓰지 않아요.
스키마와 권한
기본적으로 사용자는 자신이 소유하지 않은 스키마의 어떤 객체에도 접근할 수 없어요. 허용하려면 스키마 소유자가 스키마에 USAGE 권한을 부여해야 해요. 기본적으로 모두가 public 스키마에 그 권한을 가져요. 스키마 객체를 사용하게 하려면 객체에 따라 추가 권한을 부여해야 할 수 있어요.
사용자가 남의 스키마에 객체를 만들게 허용할 수도 있는데, 그러려면 스키마의 CREATE 권한을 부여해야 해요. PostgreSQL 14 이하에서 업그레이드한 데이터베이스에서는 모두가 public 스키마에 그 권한을 가져요. 일부 사용 패턴은 그 권한을 회수하도록 요구하는데:
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
여기서 첫 번째 public은 스키마고, 두 번째 public은 "모든 사용자"를 뜻해요. 첫 번째는 식별자, 두 번째는 키워드라서 대소문자가 다른 거죠.
시스템 카탈로그 스키마 (pg_catalog)
public과 사용자 생성 스키마 말고도, 각 데이터베이스에는 pg_catalog 스키마가 있어요. 시스템 테이블과 모든 내장 데이터 타입, 함수, 연산자가 들어 있죠. pg_catalog는 항상 사실상 검색 경로의 일부예요. 경로에 명시적으로 이름이 없으면 경로의 스키마들을 검색하기 전에 암묵적으로 검색돼요. 이렇게 해서 내장 이름이 항상 찾을 수 있게 보장하죠. 하지만 선호에 따라 pg_catalog를 검색 경로 끝에 명시적으로 두면 사용자 정의 이름이 내장 이름을 덮도록 할 수도 있어요.
시스템 테이블 이름이 pg_로 시작하므로, 미래 버전에서 시스템 테이블과 같은 이름이 정의될 충돌을 피하려면 그런 이름은 피하는 게 좋아요. 시스템 테이블은 앞으로도 pg_ 접두사를 따르므로, 사용자가 pg_ 접두사를 피하는 한 사용자 테이블의 비정규화 이름과 충돌하지 않아요.
사용 패턴 (Usage Patterns)
스키마로 데이터를 여러 방식으로 구성할 수 있어요. **안전한 스키마 사용 패턴(secure schema usage pattern)**은 신뢰할 수 없는 사용자가 다른 사용자 쿼리의 동작을 바꾸지 못하게 막아요. 기본 설정으로 쉽게 지원되는 패턴이 몇 가지 있어요.
① 사용자 전용 스키마로 제한하기. 먼저 어떤 스키마에도 public CREATE 권한이 없게 하고, 임시가 아닌 객체를 만들어야 하는 각 사용자에 대해 그 사용자와 같은 이름의 스키마를 만들어요 (예: CREATE SCHEMA alice AUTHORIZATION alice). 기본 검색 경로가 $user로 시작하므로, 각 사용자가 별도 스키마를 가지면 기본적으로 자기 스키마에 접근해요. PostgreSQL 15 이상에서 기본 설정이 이 패턴을 지원해요. 이전 버전이나 이전 버전에서 업그레이드한 DB에서는 public 스키마의 public CREATE 권한을 제거(REVOKE CREATE ON SCHEMA public FROM PUBLIC)해야 해요.
② public 스키마를 기본 검색 경로에서 제거하기. postgresql.conf를 수정하거나 ALTER ROLE ALL SET search_path = "$user"를 실행해서요. 그다음 public 스키마에서 만들 권한을 부여해요. 이 경우 정규화된 이름만 public 스키마 객체를 선택해요. 정규화된 테이블 참조는 괜찮지만, public 스키마의 함수 호출은 안전하지 않거나 신뢰할 수 없어요. 함수나 확장을 public 스키마에 만들 거라면 첫 번째 패턴을 쓰세요.
③ 기본 검색 경로 유지 + public 스키마 만들 권한 부여. 모든 사용자가 public 스키마를 암묵적으로 접근해요. 스키마가 아예 없는 상황을 흉내 내서, 스키마 개념 없는 세상에서의 부드러운 전환을 주죠. 하지만 결코 안전한 패턴은 아니에요. 데이터베이스에 사용자가 하나뿐이거나 소수의 상호 신뢰하는 사용자만 있을 때만 받아들일 수 있어요.
어떤 패턴이든, 공유 애플리케이션(모두가 쓸 테이블, 서드파티가 제공하는 추가 함수 등)을 설치하려면 별도 스키마에 넣고, 다른 사용자가 접근하도록 적절한 권한을 부여하는 것을 잊지 마세요.
이식성 (Portability)
SQL 표준에는 같은 스키마의 객체가 다른 사용자가 소유한다는 개념이 없어요. 또 일부 구현은 소유자와 다른 이름의 스키마 생성을 허용하지 않아요. 사실 표준에 명시된 기본 스키마 지원만 구현한 DB에서는 스키마와 사용자 개념이 거의 동등해요. 그래서 많은 사용자가 정규화된 이름을 사실상 user_name.table_name으로 여겨요. 사용자마다 스키마를 만들어 주면 PostgreSQL도 사실상 그렇게 동작하죠.
또 SQL 표준에는 public 스키마 개념이 없어요. 표준에 최대로 맞추려면 public 스키마를 쓰지 말아야 해요. 어떤 SQL DB는 스키마를 아예 구현하지 않거나, (제한된) 크로스 데이터베이스 접근으로 네임스페이스를 지원할 수도 있어요. 그런 시스템과 작업해야 한다면 최대 이식성은 스키마를 아예 쓰지 않아야 얻을 수 있어요.