확장 (Extensions)
확장 (Extensions)
PostgreSQL을 쓰다 보면 어떤 기능을 직접 추가하고 싶을 때가 많아요. 그런데 유용한 확장 하나가 항상 SQL 객체 하나로 끝나지는 않아요. 예를 들어 새로운 데이터 타입 하나를 만든다면 그 타입을 다루는 함수도, 연산자도, 아마 인덱스 연산자 클래스까지 함께 필요하죠. 이렇게 서로 얽힌 객체들을 하나로 묶어서 관리하기가 편하도록 PostgreSQL이 제공하는 장치가 확장(extension) 이에요.
확장을 정의하려면 최소한 두 가지 파일이 필요해요. 첫째는 확장의 객체들을 만드는 SQL 명령이 담긴 스크립트 파일(script file), 둘째는 확장 자체의 몇 가지 기본 속성을 지정하는 제어 파일(control file) 이에요. 여기에 C 언어 코드가 포함된다면 그 코드를 컴파일한 공유 라이브러리 파일도 보통 함께 있게 되죠. 이 파일들이 준비되면, 단순한 CREATE EXTENSION 명령 하나로 그 객체들을 데이터베이스에 불러올 수 있어요.
왜 확장을 쓰는 걸까
확장을 쓰는 가장 큰 이유는, "느슨한(loose) 객체"들을 SQL 스크립트로 그냥 불러오는 것보다 PostgreSQL이 이 객체들이 한 덩어리라는 걸 알게 된다는 점이에요. 덕분에 생기는 이점이 여러 가지 있어요.
먼저 모든 객체를 DROP EXTENSION 명령 하나로 내릴 수 있어요. 별도의 "언인스톨" 스크립트를 관리할 필요가 없죠. 더 유용한 건 pg_dump의 동작이에요. pg_dump는 확장의 개별 멤버 객체를 덤프하지 않고, 대신 덤프에 CREATE EXTENSION 명령만 넣어요. 이 덕분에 이전 버전보다 더 많거나 다른 객체를 포함할 수도 있는 새 버전으로의 마이그레이션이 훨씬 쉬워져요. 다만 이런 덤프를 새 데이터베이스에 불러올 때는 그 확장의 제어 파일, 스크립트, 기타 파일들이 준비되어 있어야 한다는 점은 꼭 기억하세요.
확장에 포함된 개별 객체는 확장 전체를 내리지 않고는 따로 내릴 수 없어요. 또 확장 멤버 객체의 정의를 바꿀 수는 있지만(예를 들어 함수라면 CREATE OR REPLACE FUNCTION으로), 그렇게 바꾼 정의는 pg_dump가 덤프하지 않는다는 점을 명심하세요. 그런 변경은 보통 확장의 스크립트 파일에 같은 변경을 함께 가할 때에만 의미가 있어요. (다만 설정 데이터를 담는 테이블에는 특별한 규정이 있어요. 이 장의 확장 설정 테이블 절을 보세요.) 운영 환경에서는 확장 멤버 객체를 변경할 때 확장 업데이트 스크립트를 만드는 편이 일반적으로 더 낫습니다.
확장 스크립트는 GRANT와 REVOKE 문을 이용해 확장에 포함된 객체에 권한을 설정할 수 있어요. 각 객체에 설정된 최종 권한(있다면)은 pg_init_privs 시스템 카탈로그에 저장돼요. pg_dump를 쓸 때는 덤프에 CREATE EXTENSION 명령이 포함되고, 그 뒤에 덤프 시점의 권한 상태를 복원하기 위해 필요한 GRANT/REVOKE 문들이 함께 나와요.
참고로 PostgreSQL은 지금 확장 스크립트가 CREATE POLICY나 SECURITY LABEL 문을 실행하는 건 지원하지 않아요. 이런 것들은 확장을 만든 뒤에 설정해야 한답니다. 확장 객체에 붙은 모든 RLS 정책과 보안 레이블은 pg_dump가 만드는 덤프에 포함됩니다.
확장 메커니즘은 확장에 포함된 SQL 객체들의 정의를 조정하는 수정 스크립트를 패키징하는 기능도 제공해요. 예를 들어 확장 1.1 버전이 1.0에 비해 함수 하나를 추가하고 다른 함수 하나의 본문을 바꾼다면, 확장 작성자는 그 두 가지만 바꾸는 *업데이트 스크립트(update script)*를 제공할 수 있어요. 그런 다음 ALTER EXTENSION UPDATE 명령으로 이 변경을 적용하고, 특정 데이터베이스에 실제로 설치된 확장 버전을 추적할 수 있어요.
확장의 멤버가 될 수 있는 SQL 객체의 종류는 ALTER EXTENSION 문서에 나와 있어요. 특히 주의할 점은, 데이터베이스, 롤, 테이블스페이스처럼 데이터베이스 클러스터 전체에 걸친 객체들은 확장 멤버가 될 수 없어요. 확장은 하나의 데이터베이스 안에서만 알려지기 때문이에요. (확장 스크립트가 그런 객체를 만들지 못하게 막는 건 아니지만, 만들면 그 객체들은 확장의 일부로 추적되지 않아요.) 또 테이블은 확장 멤버가 될 수 있지만, 인덱스 같은 하위 객체는 직접적으로는 멤버로 간주되지 않아요. 스키마는 확장에 속할 수 있지만 그 반대는 성립하지 않아요. 즉 확장 자체는 스키마를 붙이지 않은 이름을 가지며 어떤 스키마 "안에" 존재하지 않아요. 다만 확장의 멤버 객체들은 객체 타입에 맞게 적절할 때 스키마에 속하게 되죠. 확장이 자기 멤버 객체들이 놓인 스키마를 소유하는 게 적절할 수도 있고 아닐 수도 있어요.
확장의 스크립트가 임시 객체(예: 임시 테이블)를 만들면, 그 객체들은 현재 세션이 끝날 때까지 확장 멤버로 취급되지만 세션이 끝나면 다른 임시 객체처럼 자동으로 삭제돼요. 이건 "확장 멤버 객체는 확장 전체를 내리지 않고는 내릴 수 없다"는 규칙의 예외예요.
확장 파일
CREATE EXTENSION 명령은 각 확장마다 제어 파일에 의존해요. 제어 파일은 확장과 같은 이름에 .control 접미사를 붙인 이름이어야 하고, 설치본의 SHAREDIR/extension 디렉터리에 있어야 해요. 또한 최소한 하나의 SQL 스크립트 파일이 있어야 하는데, 그 이름은 extension--version.sql 패턴을 따라요(예: 확장 foo의 버전 1.0이라면 foo--1.0.sql). 기본적으로 스크립트 파일도 SHAREDIR/extension 디렉터리에 두지만, 제어 파일에서 스크립트 파일의 다른 디렉터리를 지정할 수도 있어요.
확장 제어 파일의 추가 위치는 extension_control_path 파라미터로 설정할 수 있어요.
확장 제어 파일의 형식은 postgresql.conf와 같아요. 즉 parameter_name = value 꼴의 대입을 한 줄에 하나씩 나열하는 거예요. 빈 줄과 #으로 시작하는 주석은 허용돼요. 한 단어나 숫자가 아닌 값은 반드시 따옴표로 감싸야 해요.
제어 파일이 설정할 수 있는 파라미터는 다음과 같아요.
directory(string) — 확장의 SQL 스크립트 파일이 있는 디렉터리예요. 절대 경로가 아니라면 제어 파일이 있는 디렉터리를 기준으로 한 상대 경로가 돼요. 기본적으로 스크립트 파일은 제어 파일이 있는 같은 디렉터리에서 찾아요.default_version(string) — 확장의 기본 버전이에요(CREATE EXTENSION에서 버전을 지정하지 않으면 설치되는 버전). 생략할 수는 있지만, 그러면CREATE EXTENSION에VERSION옵션이 없을 때 설치가 실패하게 되니 보통은 생략하지 않는 게 좋아요.comment(string) — 확장에 대한 주석(아무 문자열)이에요. 주석은 확장을 처음 만들 때 적용되지만 업데이트 때는 적용되지 않아요(업데이트 때 적용하면 사용자가 추가한 주석을 덮어쓸 수 있기 때문). 대신 스크립트 파일에COMMENT명령을 써서 확장 주석을 설정할 수도 있어요.encoding(string) — 스크립트 파일이 사용하는 문자 집합 인코딩이에요. 스크립트 파일에 비-ASCII 문자가 포함되어 있다면 반드시 지정해야 해요. 그렇지 않으면 파일이 데이터베이스 인코딩으로 간주돼요.module_pathname(string) — 이 파라미터 값은 스크립트 파일의MODULE_PATHNAME이 나오는 곳마다 치환돼요. 설정하지 않으면 치환이 일어나지 않아요. 보통 이 값은 공유 라이브러리 이름으로 설정하고, C 언어 함수를 위한CREATE FUNCTION명령에서MODULE_PATHNAME을 써서 스크립트 파일이 공유 라이브러리 이름을 하드코딩하지 않게 하는 방식으로 사용해요.requires(string) — 이 확장이 의존하는 다른 확장들의 이름 목록이에요. 예:requires = 'foo, bar'. 이 확장을 설치하기 전에 그 확장들이 먼저 설치되어 있어야 해요.no_relocate(string) — 이 확장이 의존하면서,ALTER EXTENSION ... SET SCHEMA로 스키마를 바꾸는 것을 막아야 하는 확장들의 이름 목록이에요. 이 확장의 스크립트가 의존 확장의 스키마 이름을@extschema:name@문법으로 참조하는데 이름 변경을 추적할 수 없다면 필요해요.superuser(boolean) — 이 파라미터가true(기본값)면 슈퍼유저만 확장을 만들거나 새 버전으로 업데이트할 수 있어요(아래trusted도 함께 보세요).false로 설정하면 설치 스크립트나 업데이트 스크립트의 명령을 실행하는 데 필요한 권한만 있으면 돼요. 스크립트 명령 중 슈퍼유저 권한이 필요한 게 있다면 보통은true로 설정해야 해요. (그런 명령은 어차피 실패하겠지만, 미리 오류를 알려주는 게 사용자에게 더 친절하죠.)trusted(boolean) — 이 파라미터를true(기본값은 아님)로 설정하면superuser가true로 설정된 확장도 일부 비-슈퍼유저가 설치할 수 있게 돼요. 구체적으로, 현재 데이터베이스에CREATE권한이 있는 사람은 누구나 설치가 허용돼요.CREATE EXTENSION을 실행하는 사용자가 슈퍼유저는 아니지만 이 파라미터 덕에 설치가 허용된 경우, 설치 스크립트나 업데이트 스크립트는 호출한 사용자가 아니라 부트스트랩 슈퍼유저로 실행돼요.superuser가false면 이 파라미터는 무의미해요. 일반적으로 파일 시스템 접근처럼 평소엔 슈퍼유저에게만 허용되는 능력에 접근할 수 있게 될 수 있는 확장에는true로 설정하면 안 돼요. 또 확장을 trusted로 표시하려면 설치/업데이트 스크립트를 안전하게 작성하는 데 상당한 추가 노력이 필요해요. 이 장의 확장 보안 고려 사항 절을 보세요.relocatable(boolean) — 확장이 *이동 가능(relocatable)*하다는 건, 확장을 처음 만든 뒤에 포함된 객체들을 다른 스키마로 옮길 수 있다는 뜻이에요. 기본값은false, 즉 이동이 불가능해요. 자세한 내용은 확장의 이동 가능성 절을 보세요.schema(string) — 이 파라미터는 이동 불가능한 확장에서만 설정할 수 있어요. 확장이 정확히 그 이름의 스키마에만, 다른 스키마에는 절대 로드되지 않게 강제해요.schema파라미터는 확장을 처음 만들 때만 참고되며 업데이트 때는 참고되지 않아요. 자세한 내용은 확장의 이동 가능성 절을 보세요.
기본 제어 파일 extension.control 외에도, 확장은 extension--version.control 꼴의 이름을 가진 보조 제어 파일을 가질 수 있어요. 제공한다면 이것들은 스크립트 파일 디렉터리에 있어야 해요. 보조 제어 파일은 기본 제어 파일과 같은 형식을 따라요. 보조 제어 파일에 설정된 파라미터는 그 버전의 확장을 설치하거나 그 버전으로 업데이트할 때 기본 제어 파일을 덮어써요. 다만 directory와 default_version 파라미터는 보조 제어 파일에 설정할 수 없어요.
확장의 SQL 스크립트 파일에는 트랜잭션 제어 명령(BEGIN, COMMIT 등)과 트랜잭션 블록 안에서 실행할 수 없는 명령(예: VACUUM)을 제외한 모든 SQL 명령을 넣을 수 있어요. 스크립트 파일이 암묵적으로 트랜잭션 블록 안에서 실행되기 때문이에요.
확장의 SQL 스크립트 파일에는 \echo로 시작하는 줄도 넣을 수 있는데, 확장 메커니즘이 그런 줄을 무시해요(주석처럼 취급). 이 장치는 보통 스크립트 파일이 CREATE EXTENSION으로 로드되는 게 아니라 psql에 그대로 넘겨졌을 때 오류를 내는 데 사용돼요(이 장의 확장 예제의 예제 스크립트를 보세요). 그게 없다면 사용자가 실수로 확장 내용을 확장이 아니라 "느슨한" 객체들로 불러오게 될 수 있는데, 그 상태에서 복구하는 건 꽤 번거로워요.
확장 스크립트에 @extowner@라는 문자열이 있으면, 그 문자열은 CREATE EXTENSION이나 ALTER EXTENSION을 호출한 사용자의 (적절히 따옴표 처리된) 이름으로 치환돼요. 보통 이 기능은 trusted로 표시된 확장이 선택된 객체의 소유권을 부트스트랩 슈퍼유저가 아니라 호출한 사용자에게 부여하는 데 사용돼요. (다만 그렇게 하는 건 조심스러워야 해요. 예를 들어 C 언어 함수의 소유권을 비-슈퍼유저에게 주는 건 그 사용자에게 권한 상승 경로를 만들어 주는 셈이니까요.)
스크립트 파일은 지정된 인코딩이 허용하는 아무 문자나 담을 수 있는 반면, 제어 파일은 순수 ASCII만 담아야 해요. PostgreSQL이 제어 파일이 어떤 인코딩인지 알 방법이 없기 때문이에요. 실제로 이건 확장의 주석에 비-ASCII 문자를 쓰고 싶을 때만 문제가 돼요. 그런 경우 권장하는 방법은 제어 파일의 comment 파라미터를 쓰지 말고, 스크립트 파일 안에서 COMMENT ON EXTENSION으로 주석을 설정하는 거예요.
확장의 이동 가능성
사용자들은 확장에 포함된 객체를 확장 작성자가 생각한 것과 다른 스키마에 로드하고 싶어 하는 경우가 많아요. 이동 가능성(relocatability)은 세 가지 수준이 지원돼요.
- 완전히 이동 가능한 확장은 데이터베이스에 로드된 뒤에라도 언제든 다른 스키마로 옮길 수 있어요. 이건
ALTER EXTENSION SET SCHEMA명령으로 하며, 이 명령은 멤버 객체들을 모두 새 스키마로 자동으로 이름을 바꿔요. 보통 이는 확장이 객체들이 어떤 스키마에 있는지에 대한 내부 가정을 전혀 하지 않을 때만 가능해요. 또 확장의 객체들이 처음부터 모두 한 스키마 안에 있어야 해요(절차 언어처럼 어떤 스키마에도 속하지 않는 객체는 무시). 제어 파일에서relocatable = true로 설정하면 완전히 이동 가능한 확장으로 표시돼요. - 확장이 설치 중에는 이동 가능하지만 그 뒤로는 아닐 수도 있어요. 이건 보통 확장의 스크립트 파일이 대상 스키마를 명시적으로 참조해야 할 때, 예를 들어 SQL 함수의
search_path속성에 대상 스키마를 설정할 때 그런 경우예요. 그런 확장은 제어 파일에서relocatable = false로 설정하고, 스크립트 파일에서@extschema@를 써서 대상 스키마를 참조해요. 이 문자열이 나오는 모든 곳은 스크립트를 실행하기 전에 실제 대상 스키마의 이름(필요하면 큰따옴표로 감싼)으로 치환돼요. 사용자는CREATE EXTENSION의SCHEMA옵션으로 대상 스키마를 정할 수 있어요. - 확장이 이동을 전혀 지원하지 않는다면 제어 파일에서
relocatable = false로 설정하고,schema를 의도한 대상 스키마의 이름으로도 설정해요. 그러면 제어 파일에 명시된 것과 같은 스키마를 지정하는 경우가 아니면CREATE EXTENSION의SCHEMA옵션 사용이 막혀요. 이 선택은 보통 확장이@extschema@사용으로 대체할 수 없는 스키마 이름에 대한 내부 가정을 담고 있을 때 필요해요. 이 경우에도@extschema@치환 메커니즘은 쓸 수 있지만, 스키마 이름이 제어 파일에 의해 정해지므로 유용성은 제한적이에요.
어느 경우든 스크립트 파일은 search_path가 처음에 대상 스키마를 가리키도록 설정된 상태에서 실행돼요. 즉 CREATE EXTENSION은 본질적으로 이렇게 하는 것과 같아요.
SET LOCAL search_path TO @extschema@, pg_temp;
덕분에 스크립트 파일이 만드는 객체들이 대상 스키마로 들어가요. 스크립트 파일이 원하면 search_path를 바꿀 수 있지만, 그건 일반적으로 바람직하지 않아요. CREATE EXTENSION이 끝나면 search_path는 이전 설정으로 복원돼요.
대상 스키마는 제어 파일의 schema 파라미터가 주어졌다면 그 값으로, 아니라면 CREATE EXTENSION의 SCHEMA 옵션이 주어졌다면 그 값으로, 그것도 없다면 현재 기본 객체 생성 스키마(호출자의 search_path에서 첫 번째)로 결정돼요. 제어 파일의 schema 파라미터를 쓰면 대상 스키마가 없을 때 생성되지만, 다른 두 경우에는 이미 존재해야 해요.
제어 파일의 requires에 필수 확장들이 나열되어 있다면, 그 확장들의 대상 스키마가 새 확장의 대상 스키마 다음에 오도록 search_path의 초기 설정에 추가돼요. 그래서 새 확장의 스크립트 파일에서 그 객체들을 볼 수 있어요.
보안을 위해 pg_temp는 어떤 경우든 search_path 끝에 자동으로 추가돼요.
이동 불가능한 확장이 여러 스키마에 걸쳐 객체를 담을 수도 있지만, 보통은 외부에서 쓰려는 모든 객체를 확장의 대상 스키마로 여겨지는 하나의 스키마에 두는 게 좋아요. 그렇게 하면 의존 확장을 만들 때 search_path의 기본 설정과 잘 맞아요.
확장이 다른 확장에 속한 객체를 참조한다면, 그 참조는 스키마로 한정(schema-qualify)하는 걸 권장해요. 그러려면 확장의 스크립트 파일에 @extschema:name@을 쓰면 돼요. 여기서 name은 다른 확장의 이름이에요(반드시 이 확장의 requires 목록에 있어야 함). 이 문자열은 그 확장의 대상 스키마 이름(필요하면 큰따옴표로 감싼)으로 치환돼요. 이 표기법을 쓰면 확장 스크립트 파일에서 스키마 이름에 대한 하드코딩 가정을 피할 수 있지만, 사용하면 다른 확장의 스키마 이름이 이 확장의 설치된 객체에 박힐 수 있어요. (보통 @extschema:name@이 함수 본문이나 search_path 설정 같은 문자열 리터럴 안에서 쓰일 때 그렇게 돼요. 그 외의 경우 객체 참조는 파싱 과정에서 OID로 축소되어 이후 조회가 필요 없어요.) 다른 확장의 스키마 이름이 그렇게 박혔다면, 이 확장을 설치한 뒤 다른 확장이 이동되지 못하게 그 확장의 이름을 이 확장의 no_relocate 목록에 추가해야 해요.
확장 설정 테이블
일부 확장에는 설정 테이블이 포함돼요. 이런 테이블에는 사용자가 확장을 설치한 뒤 추가하거나 변경할 수 있는 데이터가 담겨요. 보통 테이블이 확장의 일부라면 그 테이블의 정의도 내용도 pg_dump가 덤프하지 않아요. 하지만 설정 테이블에는 그런 동작이 바람직하지 않아요. 사용자가 바꾼 데이터 변경이 덤프에 포함되지 않으면 덤프 후 복원 뒤에 확장이 다르게 동작할 수 있으니까요.
이 문제를 해결하려면, 확장의 스크립트 파일이 자신이 만든 테이블이나 시퀀스를 설정 릴레이션(configuration relation) 으로 표시할 수 있어요. 그러면 pg_dump가 그 테이블이나 시퀀스의 내용(정의는 아님)을 덤프에 포함하게 돼요. 그러려면 테이블이나 시퀀스를 만든 뒤 pg_extension_config_dump(regclass, text) 함수를 호출하면 돼요. 예를 들어:
CREATE TABLE my_config (key text, value text);
CREATE SEQUENCE my_config_seq;
SELECT pg_catalog.pg_extension_config_dump('my_config', '');
SELECT pg_catalog.pg_extension_config_dump('my_config_seq', '');
이런 방식으로 표시할 수 있는 테이블이나 시퀀스의 개수에는 제한이 없어요. serial이나 bigserial 컬럼과 연관된 시퀀스도 표시할 수 있어요.
pg_extension_config_dump의 두 번째 인자가 빈 문자열이면, 테이블의 전체 내용이 pg_dump로 덤프돼요. 이는 보통 테이블이 확장 스크립트가 만들 때 처음부터 비어 있을 때만 올바른 방법이에요. 테이블에 초기 데이터와 사용자 제공 데이터가 섞여 있다면 pg_extension_config_dump의 두 번째 인자가 덤프할 데이터를 고르는 WHERE 조건을 제공해요. 예를 들어 이렇게 할 수 있어요.
CREATE TABLE my_config (key text, value text, standard_entry boolean);
SELECT pg_catalog.pg_extension_config_dump('my_config', 'WHERE NOT standard_entry');
그리고 나서 standard_entry는 확장 스크립트가 만든 행에서만 true가 되도록 하면 돼요.
시퀀스의 경우 pg_extension_config_dump의 두 번째 인자는 아무 효과가 없어요.
사용자가 수정할 수 있는 초기 제공 행 같은 더 복잡한 상황은, 수정된 행이 올바르게 표시되도록 설정 테이블에 트리거를 만들어 처리할 수 있어요.
pg_extension_config_dump를 다시 호출하면 설정 테이블과 연관된 필터 조건을 바꿀 수 있어요. (보통 확장 업데이트 스크립트에서 유용해요.) 테이블을 더 이상 설정 테이블이 아니게 표시하는 유일한 방법은 ALTER EXTENSION ... DROP TABLE로 그 테이블을 확장에서 분리하는 거예요.
이 테이블들 사이의 외래 키 관계는 pg_dump가 테이블을 덤프하는 순서를 결정해요. 구체적으로 pg_dump는 참조하는 테이블보다 참조되는 테이블을 먼저 덤프하려고 해요. 외래 키 관계가 CREATE EXTENSION 시점(테이블에 데이터가 로드되기 전)에 설정되므로 순환 의존(원형 의존)은 지원되지 않아요. 순환 의존이 있으면 데이터는 여전히 덤프되지만, 덤프를 직접 복원할 수는 없고 사용자의 개입이 필요해요.
serial이나 bigserial 컬럼과 연관된 시퀀스는 그 상태를 덤프하려면 직접 표시해야 해요. 그 상위 릴레이션을 표시하는 것만으로는 충분하지 않아요.
확장 업데이트
확장 메커니즘의 한 장점은, 확장의 객체를 정의하는 SQL 명령의 업데이트를 편리하게 관리할 수 있는 방법을 제공한다는 거예요. 이는 확장의 설치 스크립트 각 릴리스 버전에 버전 이름이나 번호를 연결해서 이루어져요. 게다가 사용자들이 데이터베이스를 한 버전에서 다음 버전으로 동적으로 업데이트할 수 있게 하려면, 한 버전에서 다음 버전으로 가는 데 필요한 변경을 수행하는 업데이트 스크립트를 제공해야 해요. 업데이트 스크립트의 이름은 extension--old_version--target_version.sql 패턴을 따라요(예: foo--1.0--1.1.sql은 확장 foo의 버전 1.0을 버전 1.1로 바꾸는 명령을 담아요).
적절한 업데이트 스크립트가 있으면 ALTER EXTENSION UPDATE 명령으로 설치된 확장을 지정한 새 버전으로 업데이트할 수 있어요. 업데이트 스크립트는 CREATE EXTENSION이 설치 스크립트에게 제공하는 것과 같은 환경에서 실행돼요. 특히 search_path가 같은 방식으로 설정되고, 스크립트가 만든 새 객체는 자동으로 확장에 추가돼요. 또 스크립트가 확장 멤버 객체를 내리기로 한다면 그 객체들은 확장에서 자동으로 분리돼요.
확장에 보조 제어 파일이 있다면, 업데이트 스크립트에 사용되는 제어 파라미터는 그 스크립트의 대상(새) 버전과 연관된 것들이에요.
ALTER EXTENSION은 요청된 업데이트를 달성하기 위해 업데이트 스크립트 파일들의 시퀀스를 실행할 수 있어요. 예를 들어 foo--1.0--1.1.sql과 foo--1.1--2.0.sql만 있다면, 현재 1.0이 설치된 상태에서 2.0으로 업데이트를 요청하면 ALTER EXTENSION이 그 둘을 순서대로 적용해요.
PostgreSQL은 버전 이름의 속성에 대해 아무것도 가정하지 않아요. 예를 들어 1.1이 1.0 다음인지 알지 못해요. 그냥 사용 가능한 버전 이름을 맞춰보고 업데이트 스크립트를 가장 적게 적용하는 경로를 따라요. (버전 이름은 사실상 --나 앞뒤의 -를 포함하지 않는 아무 문자열이 될 수 있어요.)
때로는 "다운그레이드" 스크립트를 제공하는 게 유용해요. 예를 들어 foo--1.1--1.0.sql처럼 버전 1.1과 연관된 변경을 되돌리는 스크립트죠. 그렇게 한다면, 다운그레이드 스크립트가 더 짧은 경로를 만들면서 의도치 않게 적용될 가능성을 조심해야 해요. 위험한 경우는 여러 버전을 한 번에 건너뛰는 "빠른 경로(fast path)" 업데이트 스크립트가 있고 동시에 그 빠른 경로의 시작점으로 가는 다운그레이드 스크립트가 있는 경우예요. 한 버전씩 앞으로 가는 것보다 다운그레이드를 적용한 뒤 빠른 경로를 적용하는 게 더 적은 단계일 수 있어요. 다운그레이드 스크립트가 다시 만들 수 없는 객체를 내린다면 이는 바람직하지 않은 결과를 낳아요.
의도치 않은 업데이트 경로를 확인하려면 다음 명령을 쓰세요.
SELECT * FROM pg_extension_update_paths(' extension_name ');
이 명령은 지정한 확장에 대해 알려진 서로 다른 버전 이름의 각 쌍과, 출발 버전에서 대상 버전으로 가는 데 취해질 업데이트 경로 시퀀스를 보여줘요. 사용 가능한 업데이트 경로가 없으면 NULL이 돼요. 경로는 -- 구분자로 텍스트 형식으로 표시돼요. 배열 형식을 선호한다면 regexp_split_to_array(path,'--')를 쓸 수 있어요.
업데이트 스크립트를 이용한 확장 설치
한동안 유지돼 온 확장은 아마 여러 버전으로 존재하게 될 텐데, 그런 확장의 작성자는 업데이트 스크립트를 써야 해요. 예를 들어 foo 확장을 1.0, 1.1, 1.2 버전으로 릴리스했다면, 업데이트 스크립트 foo--1.0--1.1.sql과 foo--1.1--1.2.sql이 있어야 해요. PostgreSQL 10 이전에는 더 새로운 확장 버전을 직접 빌드하는 새 스크립트 파일 foo--1.1.sql과 foo--1.2.sql도 만들어야 했어요. 그렇지 않으면 새 버전을 직접 설치할 수 없고 1.0을 설치한 뒤 업데이트하는 방법으로만 설치할 수 있었거든요. 그건 번거롭고 중복되는 일이었는데, 이제는 불필요해졌어요. CREATE EXTENSION이 업데이트 체인을 자동으로 따라갈 수 있기 때문이에요. 예를 들어 스크립트 파일 foo--1.0.sql, foo--1.0--1.1.sql, foo--1.1--1.2.sql만 있다면 버전 1.2를 설치하라는 요청은 그 세 스크립트를 순서대로 실행해서 처리돼요. 그 과정은 처음에 1.0을 설치한 뒤 1.2로 업데이트한 것과 같아요. (ALTER EXTENSION UPDATE와 마찬가지로, 여러 경로가 가능하면 가장 짧은 것을 선호해요.) 확장의 스크립트 파일을 이런 방식으로 배치하면 작은 업데이트를 만들 때 필요한 유지보수 노력을 줄일 수 있어요.
이런 방식으로 유지보수되는 확장에 보조(버전별) 제어 파일을 쓴다면, 각 버전이 독립 실행형 설치 스크립트가 없어도 제어 파일이 필요하다는 점을 명심하세요. 그 제어 파일이 그 버전으로의 암묵적 업데이트가 어떻게 수행될지를 결정하니까요. 예를 들어 foo--1.0.control이 requires = 'bar'를 지정하는데 foo의 다른 제어 파일들은 그렇지 않다면, 1.0에서 다른 버전으로 업데이트할 때 bar에 대한 의존성이 빠지게 돼요.
확장 보안 고려 사항
널리 배포되는 확장은 자신이 놓인 데이터베이스에 대해 거의 가정하지 않는 게 좋아요. 그래서 확장이 제공하는 함수를 search-path 기반 공격에 의해 손상될 수 없는 안전한 방식으로 작성하는 게 적절해요.
superuser 속성이 true로 설정된 확장은 설치 및 업데이트 스크립트 안에서 수행하는 동작에 대한 보안 위험도 고려해야 해요. 악의적인 사용자가 트로이 목마 객체를 만들어 부주의하게 작성된 확장 스크립트의 이후 실행을 손상시키고, 그로 인해 슈퍼유저 권한을 획득하는 것은 그리 어렵지 않아요.
확장이 trusted로 표시되면 설치 스키마를 설치하는 사용자가 고를 수 있는데, 그 사용자는 슈퍼유저 권한을 얻기 위해 의도적으로 안전하지 않은 스키마를 쓸 수도 있어요. 따라서 trusted 확장은 보안 관점에서 극도로 노출되어 있고, 모든 스크립트 명령이 어떤 손상도 불가능하도록 신중히 검토되어야 해요.
함수를 안전하게 작성하는 방법에 대한 조언은 아래 확장 함수 보안 고려 사항 절에, 설치 스크립트를 안전하게 작성하는 방법에 대한 조언은 확장 스크립트 보안 고려 사항 절에 나와 있어요.
확장 함수 보안 고려 사항
확장이 제공하는 SQL 언어 및 PL 언어 함수는 실행될 때 search-path 기반 공격의 위험에 노출돼요. 이 함수들의 파싱이 생성 시점이 아니라 실행 시점에 일어나기 때문이에요.
CREATE FUNCTION 참조 페이지에는 SECURITY DEFINER 함수를 안전하게 작성하는 방법에 대한 조언이 있어요. 확장이 제공하는 어떤 함수에든 그 기법을 적용하는 게 좋은 습관이에요. 그 함수가 고권한 사용자에 의해 호출될 수 있으니까요.
search_path에 안전한 스키마만 담도록 설정할 수 없다면, 스키마를 한정하지 않은(unqualified) 각 이름이 악의적인 사용자가 정의한 객체로 해석될 수 있다고 가정하세요. search_path에 암묵적으로 의존하는 구성에 주의하세요. 예를 들어 IN과 CASE expression WHEN은 항상 search path를 사용해 연산자를 선택해요. 그 자리에는 OPERATOR(schema.=) ANY와 CASE WHEN expression을 사용하세요.
일반 목적의 확장은 보통 자신이 안전한 스키마에 설치됐다고 가정하면 안 돼요. 이는 자기 자신의 객체에 대한 스키마 한정 참조조차 완전히 위험이 없다는 뜻은 아니라는 걸 의미해요. 예를 들어 확장이 myschema.myfunc(bigint) 함수를 정의했다면, myschema.myfunc(42) 같은 호출이 적대적인 myschema.myfunc(integer) 함수에 가로채일 수 있어요. 함수와 연산자 파라미터의 데이터 타입이 선언된 인자 타입과 정확히 일치하도록, 필요하면 명시적 캐스트를 써서 주의하세요.
확장 스크립트 보안 고려 사항
확장 설치 또는 업데이트 스크립트는 스크립트가 실행될 때 발생하는 search-path 기반 공격을 방어하도록 작성되어야 해요. 스크립트의 어떤 객체 참조가 스크립트 작성자가 의도한 것이 아닌 다른 객체로 해석될 수 있다면, 즉시 또는 나중에 잘못 정의된 확장 객체가 사용될 때 손상이 발생할 수 있어요.
CREATE FUNCTION, CREATE OPERATOR CLASS 같은 DDL 명령은 일반적으로 안전하지만, 일반 목적 표현식을 구성 요소로 갖는 어떤 명령이라도 조심해야 해요. 예를 들어 CREATE VIEW는 물론 CREATE FUNCTION 안의 DEFAULT 표현식도 검토해야 해요.
때로 확장 스크립트는 일반 목적 SQL을 실행해야 할 수도 있어요. 예를 들어 DDL로는 불가능한 카탈로그 조정을 하려는 경우죠. 그런 명령은 안전한 search_path로 실행하는 데 주의하세요. CREATE/ALTER EXTENSION이 제공하는 경로가 안전하다고 믿지 마세요. 가장 좋은 방법은 search_path를 일시적으로 pg_catalog, pg_temp로 설정하고 확장의 설치 스키마에 대한 참조를 필요한 곳에 명시적으로 넣는 거예요. (이 방법은 뷰를 만들 때도 도움이 될 수 있어요.) 예제는 PostgreSQL 소스 코드 배포판의 contrib 모듈에서 찾을 수 있어요.
안전한 크로스-확장 참조는 보통 @extschema:name@ 문법으로 다른 확장의 객체 이름을 스키마 한정하는 것과 함께, 함수와 연산자의 인자 타입을 신중히 일치시키는 것이 필요해요.
확장 예제
여기 SQL만으로 된 확장의 완전한 예제가 있어요. 두 요소로 된 복합 타입으로, 슬롯 이름이 "k"와 "v"인 슬롯에 어떤 타입의 값이든 저장할 수 있어요. 텍스트가 아닌 값은 저장을 위해 자동으로 텍스트로 강제 변환(coerce)돼요.
스크립트 파일 pair--1.0.sql은 이렇게 생겼어요.
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION pair" to load this file. \quit
CREATE TYPE pair AS ( k text, v text );
CREATE FUNCTION pair(text, text) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1, $2)::@[email protected];';
CREATE OPERATOR ~> (LEFTARG = text, RIGHTARG = text, FUNCTION = pair);
-- "SET search_path" is easy to get right, but qualified names perform better.
CREATE FUNCTION lower(pair) RETURNS pair LANGUAGE SQL AS 'SELECT ROW(lower($1.k), lower($1.v))::@[email protected];'
SET search_path = pg_temp;
CREATE FUNCTION pair_concat(pair, pair) RETURNS pair LANGUAGE SQL AS 'SELECT ROW($1.k OPERATOR(pg_catalog.||) $2.k, $1.v OPERATOR(pg_catalog.||) $2.v)::@[email protected];';
제어 파일 pair.control은 이렇게 생겼어요.
# pair extension
comment = 'A key/value pair data type'
default_version = '1.0'
# cannot be relocatable because of use of @extschema@
relocatable = false
이 두 파일을 올바른 디렉터리에 설치하는 데 makefile이 꼭 필요한 건 아니지만, 이런 내용의 Makefile을 쓸 수는 있어요.
EXTENSION = pair
DATA = pair--1.0.sql
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
이 makefile은 PGXS에 의존하는데, PGXS에 대한 설명은 관련 문서에 나와 있어요. make install 명령이 pg_config가 알려주는 올바른 디렉터리에 제어 파일과 스크립트 파일을 설치해 줘요.
파일이 설치되면 CREATE EXTENSION 명령으로 그 객체들을 특정 데이터베이스에 불러오면 돼요.