공개 데이터베이스 서버 PostgreSQL(마지막 회)
-시스템 카탈로그와 SQL서버 벤치마크 이야기

한동훈 /리눅스코리아 대표 hoon@linuxkorea.co.kr

들어가면서

    이번 PostgreSQL 8 번째 기사가 연재의 마지막이 될 것 같다. 지금까지 한 내용은 전체 PostgreSQL의 기능 중에서 약 절반 정도 다룬 것 같은데, 실제 데이터베이스 구축 측면을 다루지 못했던 것 같고 아직 실을 내용도 많은데, 필자의 사정상 이 정도에서 마무리를 해야 할 것 같다. 나머지 몇 가지 하고 싶었던 이야기는 뒷부분에서 언급하겠다.

규칙(rule) 시스템

    규칙 시스템은 일종의 트리거와 비슷한 것이다. 하지만 PostgreSQL의 규칙 시스템은 제대로 되지 않는다. 가령, 규칙을 생성할 때, 실제 행동할 SQL 구문을 적어줄 때, instead를 적어주지 않으면 원래의 SQL 질의가 하고자 했던 일만 하고, 규칙으로 생성한 행동은 하지 않는다. instead를 적어두면 원래의 사용자 지시문은 수행하지 않고 instead를 적어두면 원래의 사용자 지시문은 수행하지 않고 instead 이후의 구문만 수행한다. 따라서 쓸 만한 것이 되지 않는다. 사실 PostgreSQL 매뉴얼에도 사용하지 말 것을 경고하고 있다. 앞으로 점차 트리거가 규칙 시스템을 대체할 것으로 보인다.

시스템 카탈로그는 데이터 사전

    PostgreSQL은 효율적인 데이터베이스 시스템을 관리하기 위해 다른 RDBMS와 마찬가지로 내부적인 카탈로그를 사용한다. 카탈로그는 다른 RDBMS에서 데이터 사전이라 불리기도 하며, 데이터베이스의 여러 가지 정보를 일목 정연하게 저장하고 있는 시스템의 테이블이다. PostgreSQL의 시스템 카탈로그는 클래스/타입 시스템 카탈로그와 상속, 규칙 시스템, 거대객체, 내부 용도의 카탈로그 등으로 크게 이루어져 있다.
    이중에서 사용자에게 의미가 있는 몇 가지 카탈로그를 살펴보자. 내용은 [표 1]과 같다.

    이름

    설명

    pg_class

    테이블, 시퀀스, 인덱스, 뷰의 객체에 대한 자세한 정보를 담고 있다.

    pg_database

    현재 시스템에 존재하는 데이터베이스에 대한 간단한 정보를 담고 있다.

    pg_group

    데이터베이스의 그룹별 사용자에 대한 정보를 담고 있다.

    pg_shadow

    유효한 사용자에 대한 정보를 담고 있다.

    pg_listener

    데이터베이스 상의 변화를 청취하고 있는 프로세스에 대한 정보를 담고 있다.

    그 외에도 여러 가지 카탈로그가 있지만 그다지 자주 사용되지 않는다. 위에서 언급한 카탈로그는 하나의 테이블이기 때문에 권한이 있다면 갱신할 수 있는 카탈로그도 있다. 하지만 postgres 수퍼 사용자나 일반 사용자가 갱신할 수 없는 카탈로그도 있다. 아울러 무작정 카탈로그를 갱신하다보면 시스템 데이터가 엉길 수도 있으므로 미리 잘 파악하고 적절히 조정한다면 쾌적한 데이터베이스 환경을 조성할 수 있다. 이제 하나씩 차례로 살펴보자. 카탈로그를 설명하는 각 항목에서 나오는 컬럼은 알아두면 좋은 것을 추렴한 것으로 컬럼을 모두 여기에서 서술하는 것은 아니다.

    ☞ PostgreSQL 6.3.2에서 자그마한 한글 문제

    6.3.1 버전이 나온지 얼마 되지 않아서 6.3.2 버전이 나왔다. 기능이 확충되었다기 보다 버그 수정 측면의 성격이 강하다. PostgreSQL 6.3 이상의 버전에서 자그마한 한글 사용 문제를 살펴보자.
    첫 번째 내용은 PostgreSQL을 컴파일하고 나서 psql에서 \d 명령으로 보니 한글 테이블 이름과 칼럼 이름이 깨어져 보인다는 것이다. 컴파일시 ./configure 파일에 로케일 관련 옵션을 사용하지 않아야 한다. 한글 사용은 MB=EUC_KR을 Makefile.custom 파일에 적어주는 것만으로 충분하다.
    두 번째 내용은 psql에서 \d 테이블 이름과 같이 사용했을 때, 테이블 이름에 해당하는 테이블이 있음에도 불구하고 출력되지 않는다는 것이다. 이 문제는 \d "한글 테이블 이름"과 같이 사용하면 해결된다. psql은 내부적으로 \d 다음의 테이블 명에서 ""가 나오면 그대로 사용하지만, 그냥 \d 테이블과 같이 인용 구문이 없으면 다중 바이트 환경을 고려하지 않고, 영문 대문자가 나오면 모두 소문자로 바꾸어 버리기 때문이다. 인용 부호를 사용하는 것이 정석이긴 하지만 불편하다고 생각되면 postgresql-6.3.2/src/bin/psql/psql.c에서 609에서 612줄을 한글일 경우 두 번째 바이트를 소문자로 변경하지 않도록 수정하면 된다.

pg_class - 관계(또는 객체)에 대한 종합적인 정보

    pg_class 카탈로그는 시스템에서 관계에 대한 종합적인 정보를 담고 있다. 즉, 사용자가 생성한 테이블, 뷰 등의 이름과 소유자는 누구이며, 튜플, 속성은 몇 개인지 등에 대한 내용을 다루고 있다. 이중에서 몇 가지 중요한 칼럼은 다음과 같다.

    이름

    타입

    설명

    relname

    NameData

    관계(또는 테이블)의 이름

    relowner

    oid

    관계의 소유자의 UNIX 시스템 ID

    relnatts

    int2

    컬럼의 개수

    일반 테이블에 대해 질의를 하듯이 pg_class에 대해서도 동일하게 할 수 있다. 테이블을 간단하게 만들고 데이터를 약간 입력한 다음 질의를 해보자.

    create table 회원 (이름 text, 전화번호 text, 주소 text, 나이 int);
    select relname, relowner, relnatts from pg_class where relname = '회원';

    relname|relowner|relnatts
    ------+------+------
    회원 | 500| 4

    회원 테이블의 소유자는 UNIX ID 500번이고 속성의 개수는 4개라는 것을 알 수 있다. 카탈로그에 대한 갱신은 현재까지의 버전에서는 약간 불안한 측면이 있다. 즉, 어떤 특정한 카탈로그에서 A라는 클래스를 삭제하면 이후에 A라는 이름의 클래스를 생성하지 못하는 경우가 가끔 있다. 다음과 같은 질의를 사용하면 처음에는 잘 되는 것 같지만 나중에 문제가 생긴다.

    delete from pg_class where relname = '회원';
    create table 회원 (이름 text, 전화번호 text, 주소 text, 나이 int);

    ERROR: cannot create 회원

    pg_class를 직접 갱신하면 pg_class의 내용만 갱신되고, 관련된 pg_attribute를 비롯한 여러 클래스들은 갱신되지 않아서 생기는 문제로 보인다.

pg_attribute - 컬럼에 대한 정보

    이름

    타입

    설명

    attname

    NameData

    컬럼 이름

    attlen

    int2

    컬럼의 바이트 길이, 가변 길이는 -1

    attnum

    int2

    컬럼 번호, 테이블에서 컬럼은 1부터 시작


    create table 회원2 (이름 text, 전화번호 text, 주소 text, 나이 int);
    select attname, attlen, attnum from pg_attribute where attname = '전화번호';

    attname|attlen|attnum
    ------+----+-----
    전화번호| -1| 2

    회원2 테이블에서 전화번호 컬럼은 테이블 내에서 두 번째에 위치하고, 바이트 길이는 가변이라는 것을 알 수 있다.

pg_database - 데이터베이스에 대한 정보

    시스템에 존재하는 데이터베이스에 대한 간단한 정보를 출력한다.

    이름

    타입

    설명

    datname

    NameData

    데이터베이스 이름

    datdba

    oid

    해당 데이터베이스 관리자의 UNIX 시스템 ID

    text

    datpath

    데이터베이스 디렉토리 $PGDATA를 기준


    select * from pg_database;

    datname|datdba|datpath
    -------+-----+-------

    template1| 501|template1
    test | 501|test
    mydb | 500|mydb

pg_group - 그룹별 사용자에 대한 정보

    이름

    타입

    설명

    groname

    NameData

    그룹 이름

    grosysid

    int2

    그룹의 UNIX 그룹 ID

    int2

    grolist[]

    그룹 회원들의 UNIX 사용자 ID의 목록


    UNIX 스타일의 파일 시스템에도 그룹별 사용자 개념이 적용되듯이, PsotgreSQL 데이터베이스 시스템에도 마찬가지이다. 일반적으로 데이터베이스에 다양한 사용자들이 접근할 수 있으므로, 사용자의 부서나 업무 성격에 따라 그룹을 나누어 주어, 권한 설정을 따로 할 수 있다. 즉, 권한을 부여(grant)할 때 그룹 ID가 요긴하게 사용된다. 여기서는 users라는 그룹을 만들어 몇몇 사용자를 등록하고, 회원2하는 테이블의 사용권을 이들 사용자에게 허용한다.

    insert into pg_group values ('users', 100, '{500, 501, 506, 510}');
    grant all on 회원2 to group users;

    500, 501 등의 숫자는 UNIX 사용자 ID이고, 100은 그룹 ID이다. 이들은 각각 /etc/passwd와 /etc/group의 관련 내용에 해당한다.

pg_shadow - 유효한 사용자와 그 허용 권한

    이름

    타입

    설명

    usename

    NameData

    사용자의 이름

    usesysid

    int2

    사용자의 UNIX 사용자 ID

    bool

    usecreatedb

    사용자가 데이터베이스를 만들 수 있는 가의 여부

    bool

    usetrace

    사용자가 추적 플래그를 설정할 수 있는 가의 여부

    bool

    usesuper

    사용자가 postgres 수퍼유저가 될 수 있는가의 여부

    bool

    usecatupd

    사용자가 카탈로그를 갱신할 수 있는가의 여부


    select * from pg_shadow where usename = 'hoon';

    usename|usesysid|usecreatedb|usetrace|usesuper|usecatupd|passwd|valuntil
    -------+------+---------+-------+------+--------+-----+-----
    hoon | 500|t |t |t|t | |

    hoon이라는 사용자는 UNIX 사용자 500번이고, 데이터베이스를 생성할 수 있는 권한이 있고, 추적 플래그를 설정할 수 있으며, 다른 사용자를 데이터베이스 사용자로 추가할 수 있는 수퍼 유저 권한도 있으며, 카탈로그를 생산할 수 있는 권한도 있다. 이것은 postgres 사용자가 adduser로 사용자를 초기 생성할 때 모두 yes로 했을 경우이다.

    update pg_shadow set usecatupd = 'f' where usename = 'hoon';
    UPDATE 1
    update pg_shadow set usecatupd = 't' where usename = 'hoon';
    ERROR: pg_shadow: Permission denied.

    카탈로그 갱신 권한을 다시 부여하려면, postgres 수퍼 사용자로 데이터베이스에 접속하여 해당 pg_shadow 컬럼을 갱신하면 된다.

pg_listener - 상황을 청취하고 있는 프로세스

    이름

    타입

    설명

    relname

    NameData

    동시성 통지를 희망하는 클래스의 이름

    listenerpid

    int4

    동시성 통지를 기다리고 있는, 응용 프로그램의 서버 프로세스 ID


    PostgreSQL 서버에는 동시성 통지와 청취라는 기능이 있다. 별달리 중요한 것은 아니고, 프로세스 사이의 동시성을 좀 더 확실히 하기 위해 지원하는 기능이다. 동일한 테이블 상에서 하나의 프로세스가 먼저 작업을 시작하기 전에 notify를 하고, 다음에 동일한 테이블을 접근하려는 프로세스가 listen을 하면, listen하는 프로세스는 해당 테이블에서 notify하는 다른 프로세스에게서 동시성 통지를 받게 된다. 이 때 listen하는 프로세스에 대한 정보가 pg_listener 카탈로그에 접수되는 것이다.

    select * from pg_listener;

    relname|listenerpid|notification
    ------+--------+--------
    회원2 | 17816| 0

    지금까지 카탈로그에 대한 정보를 살펴보았다. 이외에도 시스템 카탈로그가 여러 개가 있지만 실 용도는 높지 않고, PostgreSQL 시스템이 내부적으로 사용하는 경우가 많다. 될 수 있으면 정식 명령을 사용하도록 하고, 카탈로그에 대한 갱신은 꼭 필요한 경우에 부분적으로 사용하기 바란다. 앞에서도 설명했듯이, 카탈로그에 대한 부분적인 갱신은 전체 카탈로그의 유기적인 관계에 좋지 않은 영향을 미칠 수 있기 때문이다.

다른 RDBMS로의 데이터 이전

    데이터베이스를 사용하다보면 다른 데이터베이스 서버에 데이터를 이전하여야 할 경우가 가끔 생긴다. 기업환경에서 처음에는 소규모 데이터베이스를 구축하다가 데이터베이스가 대규모로 되어서 이전의 방식으로 관리하기가 곤란해지거나 다른 기어이 다른 데이터베이스를 사용하고 있어서 호환을 위해 서로 맞추어야 할 필요성이 있을 때, 데이터의 이전이 문제가 된다. 그리고 동일한 SQL 서버라고 하더라도 버전이 갱신됨에 따라 데이터의 형식이 바뀔 때도 있다. 어느 SQL 서버나 마찬가지이겠지만 기본적으로 데이터를 SQL 구문으로 축출할 수 있는 유틸리티를 제공하고 있다. PostgreSQL에서는 pg_dump가 그것이다. pg_dump는 PostgreSQL의 물리적인 데이터베이스에서 다양한 데이터를 아스키 형식의 질의(SQL문) 스크립트로 만들어 낸다. 따라서 이것을 동일한 PostgreSQL의 다른 버전으로 옮길 수가 있다. 다른 SQL 서버로 옮길 때에는 SQL 문법이 다를 수 있으므로 주의해야 한다. 이럴 때는 해당 스크립트를 적절히 편집하여 옮겨주어야 한다. pg_dump는 몇 가지 한계가 있다. 즉, 사용자 정의 규칙과 뷰는 이해를 하지 못해, 밖으로 축출할 수 없으며, 시스템 카탈로그에 저장되는 몇몇 인덱스도 마찬가지로 끄집어 낼 수 없다. 아울러 거대 객체(large objects)도 가능하지 않으므로 데이터를 이전할 때 주의해야 한다. 따라서 주로 이전 가능한 데이터를 테이블이라고 볼 수도 있다.
    동일한 PostgreSQL의 상위 버전으로는 사용자 정의 타입, 함수, 테이블, 인덱스, 전체 함수, 연산자 등도 이전할 수 있지만, 다른 SQL 서버로 데이터를 이전할 때에는 쌍방의 SQL 서버 확장 기능은 제대로 살릴 수 없으므로 주의해야 한다.
    결국, 타 SQL 서버로 차후에 데이터를 이전할 계획이라면, PostgreSQL의 확장 기능(사용자 정의 함수, 타입, 인덱스, 전체 함수, 연산자 등)을 비롯한, 호환되지 않는 SQL문은 미리부터 사용하지 않는 것이 좋다. SQL92, SQL3 등과 같은 다양한 표준들이 있으므로 대부분의 SQL 서버에서 지원하는 표준을 중심으로 사용하는 것이 차후의 데이터 이전을 위한 좋은 방법이다.

PostgreSQL의 안정성은 어느 수준?

    가끔 PostgreSQL의 안정성에 대한 의문을 제기하는 분들을 보아왔다. 필자도 지금까지 PostgreSQL을 사용하면서 부분적으로 불안한 면이 있다는 것을 인정하지 않을 수 없다. 특히 가끔씩 psql에서 부적당한 질의를 서버에게 전달하였을 경우, 서버와의 접속이 끊어지는 경우가 있었다. 그리고, 수많은 사용자가 동시에 PostgreSQL 서버에 접속할 때 불안하다는 이야기를 어느 업체에 근무하는 분으로부터 이야기를 듣고 있다.
    사실 PostgreSQL은 사용자의 실수를 감지하고 보호할 만큼 충분히 고려되어 있지 않다. PostgreSQL 매뉴얼을 접해보면 그런 설명이 자주 나온다. 즉, '이러 이러한 기능이 있지만 아직 충분히 테스트되지 않은 것이니 문제가 생길 수 있다', '어떤 기능은 제공하지만, 이에 대한 에러는 SQL 서버에서 충분히 감지하지 못한다. 사용자가 주의해야 한다.'는 등의 이야기가 그것이다. 사용자가 올바로 사용하기만하면 PostgreSQL은 아주 멋지게 동작하지만, 언제나 사용자가 실수를 하지 않으리란 보장을 못한다. 이러한 점이 PostgreSQL의 단점이 아닐까 생각한다. 따라서 이것은 PostgreSQL의 개발이 현재 진행형이며 아직 완벽히 다듬어지지 않았다는 것을 뜻한다.

    MySQL을 제작한 TCX(http://www.tcx.se)사에서 자사의 홈페이지에 다양한 RDBMS들의 벤치마크 결과를 보여주고 있다. 이 회사에서 만든 '나를 미치게 해주세요'(crash-me)라는 안정성 테스트 프로그램이 있는데, crash-me 프로그램을 사용한, MySQL, Access97, Oracle, mSQL, EMPRESS, PostgreSQL, Solid 등의 데이터베이스 테스트에서 살아남은 것은 MySQL, Access97, Oracle 정도이며, PostgreSQL 6.3.1, 6.3.2를 비롯한 다수의 상용 RDBMS까지 테스트 도중에 다운되었다는 이야기가 있다. 필자에게 상당히 충격적인 사실로 받아들여지긴 했지만, 실망하기에는 아직 이르다고 본다. PostgreSQL의 경우에 버전의 갱신이 상당히 빠르게 진행되고 있고, 개발 팀에서도 이미 이러한 안정성의 문제를 감지하고 있을 것으로 보이며, 더욱이 RDBMS의 소스가 여러분 앞에 놓여 있으므로 용기와 실력만 있다면 직접 수정할 수도 있기 때문이다.

    공개 프로그램이 이 정도의 성능을 보여준다는 것은 놀라운 것이다. 그리고 대다수의 작은 작업에는 별 문제 없이 돌아가므로 안심해도 된다.

PostgreSQL과 다른 RDBMS들의 벤치마크

    속도면

    리눅스 환경의 RDBMS 사용자 사이에서 사실로 받아들여지는 소문이 몇 가지 있다. PostgreSQL은 엄청 느리고, mSQL과 MySQL 등은 기본적으로 데이터베이스 갱신 때마다 fsync 루틴을 호출하지 않기 때문이다. PostgreSQL에는 두 가지 모드가 있다. 처음 postmaster를 띄울 때, -o '-F' 옵션을 사용하지 않으면, 매번의 데이터 변화시 안정성을 위해 fsync 루틴을 호출하여 디스크에 변화된 내용을 모두 수록한다. 이 옵션을 사용하면 fsync 루틴을 호출하지 않아서, 결국 운영체제의 유연한 캐싱 능력을 충분히 활용하게 되므로, 전자에 비해 10배 이상의 속도 향상을 가져오게 된다. PostgreSQL이 느리다는 이야기는 fsync 루틴을 사용하는 '거북이' 모드를 초기 옵션으로 사용하는 사용자들에게서 나오는 말이다. fsync를 사용하지 않으면 다량의 복잡한 작업 시에 PostgreSQL은 mSQL보다 빨라진다. 말이 나온 김에 mSQL이 매우 빠르다는 이야기를 짚고 넘어가자. mSQL의 기본적인 생각은 꼭 필요한 기능을 지원하되, 속도에서 최고의 향상을 가지고 오자는 것은 거의 모든 면에서 최고의 속도를 자랑하고 있지만, 정렬(order by)과 결합(join)시에 치명적인 약점을 가지고 있는 것으로 나와있다. 10개의 컬럼을 정렬(order by)할 경우에 mSQL의 속도는 MySQL 속도보다 350배 느리고, 100개의 객체를 결합(join)할 때에는 10,000배 느리다는 것이다. 그 외에는 대체로 굉장히 빠르다고 벤치마크 결과는 이야기하고 있다. TCX사의 결과를 모두 신뢰할 필요는 없지만, 대체로 믿을 만한 것 같다. 종합적인 속도를 살펴보면, MySQL > PostgreSQL (Fast Mode) > Solid > PostgreSQL (Slow Mode) > mSQL의 순을 보여주고 있다. 물론, 테스트의 내용이 업무 환경에서 자주 사용하는 모델이 아닌 것들이 더러 있어 실제적인 속도는 개인과 기업의 일반적인 업무 패턴에 기반하여 실전에서 테스트되어야 한다고 본다. 과연 결합(join)을 100개씩이나 할 일이 있을까?

    기능면

    PostgreSQL은 일반적으로 mSQL, MySQL보다 뛰어난 기능을 많이 가지고 있다. SQL의 표준적인 면도 대략 이들보다 많이 지원하고 있다. 특히 MySQL 같은 경우에는 PostgreSQL에서 기본적으로 지원하는 뷰(view)와 트리거, union절, 트랜잭션 구문 등을 지원하지 않고 있다. 물론 PostgreSQL에서 지원하는 트리거는 구문 수준이 아니라 서버 수준의 프로그래밍 스타일의 문제이긴 하지만 말이다. 이들 기능은 데이터베이스 환경에서 자주 사용하는 것이다. MySQL이 속도가 빠르다고 나오는 것은 속도 문제에서 가장 민감한 영향을 받는 기능을 제거함으로써 이루어진 측면이 다분히 있다. 따라서 기능 대 속도면을 따지고 본다면 PostgreSQL이 MySQL에 비해 느리다고만 할 수는 없다. 그 외에 데이터베이스의 자원 제한 부분을 본다면 PostgreSQL이 조금 부족한 부분이 있다. 확장 기능으로 따진다면 사용자 정의 함수, 타입, 연산자, 전체함수 등을 복잡하게 지원하는 PostgreSQL을 따라올 RDBMS는 없는 것 같다.

나오는 말

    작년 11월부터 연재를 시작한 강좌를 이제 마무리할 때가 온 것 같다. 아직도 못다한 설명이 많지만, 아쉬운 점은 제대로 된 실전 데이터베이스 구축 예제를 보여주지 못한 것이다. 그 사이에 PostgreSQL 버전도 많이 갱신되었다. 오랜 시간 동안 PostgreSQL을 탐구하고 테스트 해보면서 느낀 점은 공개 RDBMS가 생각보다 상당히 많은 기능을 가지고 있다는 것을 느낀 것이다. 하지만 아직 현재 진행형이다. 부족한 부분들은 계속 수정되고 있고, 발전하고 있다. 수 메가에 이르는 방대한 소스가 여러분의 눈앞에 펼쳐져 있고, 마음대로 수정할 수 있는 자유가 있다. 외국의 유니 SQL 서버 소스를 엄청난 돈을 주고 사들이는 사람도 있는데, 어찌 보면 상당한 행복을 누릴 자유가 우리에게 있는지 모른다.

    그리고 얼마 전에 반가운 소식이 들려왔다. 개발 툴 회사로 유명한 볼랜드(현재는 인프라이즈로 바뀌었다)의 계열사인 인터페이스 소프트웨어(http://www.interbase.com) 사에서 자사의 전략 품목인 InterBase SQL Server를 리눅스 용으로 무료로 배포하고 있다는 것이다. 한눈에 봐도 공개 SQL 서버에서는 보기 힘든 강력한 기능과 친숙한 사용자 지원 기능이 돋보이는 것 같다.

    지금까지 설명한 PostgreSQL의 소스는 시간이 나면 필자의 홈페이지에 업로드 하도록 하겠다. 홈페이지가 자주 바뀌어서 애써 찾아온 독자 분들에게 미안한 마음이다. 리눅스월드 지면을 통해서 가끔 인사를 드릴 수 있을 것 같다.



, .

공개 데이터베이스 서버 PostgreSQL (7)
- SPI 와 트리거의 밀월여행

    한동훈 / 리눅스 코리아
    hoon@linuxkorea.co.kr
    http://www.linuxkorea.co.kr/~hoon

PostgreSQL 6.3.1 - 멀티바이트 환경의 기본 제공

    PostgreSQL 6.3.1의 새로운 버전이 발표될 때마다 기다려지는 바이지만, 이번만큼은 너무 정신없이 버전이 올라간 것이 아닐까 생각한다. 3월 1일, 6.3 버전이 나온지가 채 한 달도 되지 않아서 버그 패치버전이 또 다시 나왔다는 소식을 주변의 잘 아는 동료로부터 듣고, 회사 설립에 정신이 없는 필자에게 또 하나의 일거리가 된 느낌이었다.

    일단 새로운 버전이 나오면 설치한 다음 몇 가지 테스트를 하는 것이 습관처럼 되어 버렸다. 버전 6.3.1에서 달라진 내용 중 가장 중요한 사항은 그 동안 패치 파일로 별도로 제공되던 멀티바이트(한글, 일어, 중국어 등의 2바이트, 서양권 1바이트) 지원이 PostgreSQL 내에 자체 포함되었다는 것이다. 설치시에 자국의 사용언어를 위한 선택만 해주면 된다. PostgreSQL 소스 배포판을 푼 디렉토리를 중심으로 본다면, 한글 사용을 위한 멀티바이트 선택은 다음과같이 하면 된다.

      # echo MB=EUC_KR > src/Makefile.custom

SPI 와 트리거란 무엇인가?

    SPI는 서버 프로그래밍 인터페이스(Server Programming Interface)이다. psql은 postmaster 서버에 접속하여 질의를 처리하는 클라이언트 유틸리티라고 볼 수 있다.

    SPI는 SQL 질의를 사용자 정의 C함수 내부에서 실행할 수 있는 능력을 사용자에게 제공한다. 현재 버전의 PostgreSQL에서 서버측에 저장되는 프로시저와 트리거를 작성할 수 있는 유일한 방법이 SPI이다. 사실 SPI는 데이터베이스 서버 내의 분석기(Parser), 계획기(Planner), 최적화기(Optimizer), 실행기(Executor)에 간편하게 접군할 수 있는 인터페이스 함수의 집합일 뿐이다. SPI 에서는 또한 약간의 메모리 관리를 함께 수행한다.

먼저 SPI를 배우자! - SPI 인터페이스 함수

    SPI를 익히는 것은LIBPQ를 익힐 때와 유사하다. 다만, LIBPQ는 프론트엔드 인터페이스(또는 클라이언트의 서버 접속 인터페이스)이고,SPI 는 서버내에서의 프로그래밍 인터페이스라는 점에 유의하면 된다.SPI를 작성하는 언어는 단연 C이다. 작성하는 방법은 SPI제공 함수를 사용하여C함수로 작성한 다음, 이 함수를 PostgreSQL 내부에 등록하기 위해서 psql에서 함수 등록 질의를 사용하면 된다. 원하는 기능을 SPI함수와 트리거 데이터구조를 사용하여 C 함수로 작성한 다음,PSQL에서 함수 등록 절차를 거친다. 다음 트리거 생성 질의문을 사용하여 등록한다. 먼저, SPI 용으로 준비되어 있는 함수를 살펴보자. 이들 함수는 서버측에서 구동된다는 점을 빼면,LIBPQ와 외양이 유사하므로 익히기 쉬울 것이다.

    SPI_connect

    int SPI_connect(void) ;

    이 함수는 사용자의 프로시저를 SPI 관리자에 연결시킨다. SPI_connect는 Postgres 백엔드와 연결을 성립한다. 질의를 실행하기 위해서는 먼저 이 함수를 호출하여야 한다. SPI 함수 중에는 접속이 되지 않은 상태에서 호출할 수 있는 것이 더러 있다. 이미 접속이 되어있는 상태에서 SPI_connect를 다시 호출하면 SPI_ERROR_CONNECT를 반환한다. 연결이 성공적일 때에는, SPI_OK_CONNECT 가 반환된다. 내부적으로 이 함수는 질의 실행과 메모리 관리를 위한 내부적인 구조체를 최기화 한다.

    SPI_finish

    int SPI_finish(void) ;

    이 함수는 SPI 관리자와 사용자의 프로시저를 분리시킨다. 사용자는 SPI 관리자를 통해 작업을 마쳤다면 이 함수를 호출하여야 한다. 현재 접속이 되어있지 않은 상태에서 이 함수를 호출한다면, SPI_ERROR_UNCONNECTED를 반환한다. 그 외에는 SPI_OK_UNCONNECTED를 반환한다. 내부적으로는 SPI_connect에서 palloc를 통해 할당된 모든 메모리를 해제한다.

    SPI_exec

    int SPI_exec(char *query, int tcount) ;

    이 함수는 질의실행 계획을 수립하여, 질의를 수행한다. 서버의 분석기, 계획기, 최적화기가 이 함수의 계획수립시에 관여한다. char *query 는 질의 계획을 포함하는 문자열이고, int tcount 는 반환될 튜플의 최대 갯수이다. 질의가 성공적으로 수행되면 다음의 양수가 반환된다.

      SPI_OK_UTILITY : 유틸리티 질의(CREATE TABLE 등) 가 실행되었을 때
      SPI_OK_SELECT : SELECT 가 실행되었을 때
      SPI_OK_SELINTO : SELINTO ... INTO 가 실행되었을 때
      SPI_OK_INSERT : INSERT 가 실행되었을 때
      SPI_OK_DELETE : DELETE 가 실행되었을 때
      SPI_OK_UPDATE :UPDATE 가 실행되었을 때

    에러가 발생하면 다음 중 하나의 음수를 반환한다.

      SPI_ERROR_ARGUMENT :query 가 NULL이거나 tcount <0
      SPI_ERROR_UNCOMMECTED : 프로시저가 접속이 되지 않았을 때
      SPI_ERROR_COPY : COPY TO/FROM stdin가 실행될 때
      SPI_ERROR_CURSOR : DECLARE/CLOSE CURSOR, FETCH가 실행될 때
      SPI_ERROR_TRANSACTION : BEGIN/ABORT/END 가 실행될 때
      SPI_ERROR_OPUNKNOWN : 질의 타입이 불명확할 때

    SPI_exec는 연결된 프로시저에서 호출되어야 한다. tcount 가 0 이면, 질의 검색에서 해당하는 모든 튜플을 반환한다. tcount >0 이면, tcount 만큼만 반환한다. 예를 들어보자.

      SPI-exec ("insert into table select * from table", 5) ;

    위 함수는 적어도 5개의 튜플을 해당 테이블에 삽입할 것이다.

    주의 : 하나의 query 안에 여러개의 복합 질의를 전달할 수 있다.
    이 경우에 반환값은 마지막 질의가 실행된 결과이다.

    질의 수행결과로 실제 반환된 튜플의 갯수는 SPI_processed 전역 변수에 저장된다. SPI_OK_SELECT가 반환되고, SPI_processed 가 0보다 크다면, 전역 포인터 SPITupleTable *SPI_tuptable를 사용하여 선택된 튜플에 접근할 수 있다. SPI_finish 는 SPITupleTable에 할당된 메모리를 모두 해제하기 때문에 사용불가능 해진다는 점에 주의하자!
    SPI-exec 가 가장 중요하게 취급되므로 눈여겨 봐두는 것이 좋다.

    참고

    SPITupletable 구조체는 매우 중요하다. 왜냐하면 실제로 회수된 튜플에 접근할 수 있는 방법을 제공하고 있기 때문이다. 나중에 예제에서도 이 구조체 이야기가 나오므로 자세히 알아보도록 한다. SPITupletable에 대한 구조체 정의는 PostgreSQL이 설치된 디렉토리를 기준으로 include/executor/spi.h에 있다.

      typedef struct
      {
      uint32 alloced ; /* 할당된 값의 개수 */
      uint32 free ; /* 자유로운 값의 개수*/
      TupleDesc tupdesc ; /* 튜플 기술자*/
      HeadTuple * vals ; /* 실제 튜플을 담고 있는 포인터*/
      } SPITupleTable ;

    나중의 예제에 나오는 SPI_tuptable 은 SPITupleTable에 대한 포인터이다.

      SPITupleTable *SPI_tuptable ;

    로 미리 SPI관리자에서 정의되어 있다. 이 구조체는 SPI_tuptable ->vals

    SPI_prepare

    void *SPI_prepare(char *query, int nargs, argtypes) ;

    SPI_ prepare는 실행계획을 수립하여 반환한다. 하지만 질의를 실행하지는 않는다. query는 질의할 문자열이고, nargs는 매개인자(SQL함수에서 $1... $nargs와 같음)의 개수이며, 질의 안에 매개 인자가 없을 경우에 nargs는 0 이 될 수 있다. SPI_prepare에서 반환된 질의 계획은 현재 세션에서만 사용할 수 있다. SPI_finish를 사용하면 질의 계획에 할당된 메모리를 해제하기 때문에, 이후에도 사용하려면 SPI_saveplan을 사용하여 별도로 저장하여야 한다.

    SPI_prepare 함수는 잘 사용하지는 않지만, 특정한 경우에 유용할 수 있다. 가령 준비된 실행계획을 수행하는 것이 이따금 그냥 실행하는 것보다 빠를 수 있다. 같은 질의를 여러번 수행할 때가 좋은 예이다. SPI_prepare가 성공적으로 수행되면 널이 아닌 포인터가 반환되고, 실패하면 널이 반환된다. 이 함수의 수행 상태 결과는 SPI_result 전역 함수에 설정된다. 내용은 SPI_exec에서 반환하는 값과 동일하며, query가 NULL이거나 nargs < 0 이거나, nargs >0 && argtypes 가 NULL일 경우에는 SPI_ERROR_ARGUMENT가 세트된다는 점만 다르다.

    SPI_saveplan

    void *SPI_saveplan (void *plan) ;

    이 함수는 SPI_prepare에서 수립된 계획을 안전한 영역에 저장한다. 계획이 백업되는 영역은 SPI_finish 나 트랜잭션 관리자의 영향을 받지 않는 보호 구역이다. 이후의 SPI 버전에서는 수립된 계획을 시스템 카탈로그(데이터 사전)에 저장하거나 불러오는 것이 가능해질 것으로 보인다. 수립된 계획은 SPI_execp 함수를 사용하여 수행할 수 있다. 반환값을 실패시에는 NULL을 반환하고, 성공시에는 저장된 메모리 위치를 돌려준다. 전역 변수 SPI_result에 세트되는 값은 다음과 같다.

      SPI_ERROR_ARGUMENT :plan 이 NULL 일 경우
      SPI_ERROR_UNCONNECTED : 프로시저가 SPI 관리자와 연결되지 않았을 때

    주의 : 준비된 계획이 참조하는 테이블이나 함수 등의 객체중 하나가, 사용자의 실행 세션 동안에 사라진다면 SPI_execp 의 결과는 예측할 수 없게 될 것이다.

    SPI_execp

    int SPI_execp (void *plan, Datum *values, char *nulls, int tcount) ;

    이 함수는 준비된 계획이나 SPI_saveplan 이 반환한 계획을 실행한다. 함수의 인자중 plan은 실행할 계획이며, values는 실제 매개 인자의 값이며, nulls는 매개인자가 NULL을 가지는 것을 허용할 것인가를 설명하는 배열이다. nulls가 'n' 이면 NULL 이 허용되고, ' ' 이면 NULL 이 허용되지 않는다. tcount 는 계획이 실행된 결과로 돌려받을 튜플의 갯수를 지정한다. plan이 NULL 이거나 tcount <0 이면 SPI_ERROR_ARGUMENT를 돌려주고, values 가 NULL 이고 plan의 매개인자와 실제 매개인자인 values 가 맞지 않는다면 SPI_ERROR_ARGUMENT를 돌려준다. 그 외에는 SPI_exec와 동일하다. 그 외에 SPI_execp 의 결과에 따라 SPI_tuptable 은 반환된 실제 튜플의 값으로 설정되고, SPI_processed는 반환된 튜플의 갯수를 저장하게 된다. nulls가 NULL 이면 SPI_execp 는 실제 매개인자의 모든 값들이 NOT NULL이라고 가정한다.

    주의 : 준비된 계획이 참조하는 테이블이나 함수등의 객체 중 하나가, 사용자의 실행 세션동안에 사라진다면 SPI_execp의 결과는 예측할 수 없게 될 것이다.

    다음 함수부터는 실제로 튜플을 다루는데 사용하는 유틸리티이다.

    [index]의 형태로 사용하는데, 주로 회수된 튜플의 값을 얻기 위해 많이 사용된다. HeapTuple 은 하나의 튜플을 설명하는 자료형이다. 여기에서 또 중요한 구조체가 TupleDesc 이다. 이 구조체는 PostgreSQL 의 소스를 푼 디렉토리를 기준으로 src/include/access/tupdesc.h 에 정의되어 있다.

      typedef struct tupleDesc
      {
      /* 튜플에서 속성의 개수 */
      int natts ;
      /* attrs [N]은 속성 번호 N+1의 기술자에 대한 포인터이다. */
      AttributeTupleForm *attrs ;
      TupleConstr *constr ;
      } *TupleDesc ;

    TupleDesc 구조체는 SPI_getvalue 등의 함수에서 튜플 기술자의 인자로 사용되며, TupleDesc->natts 는 해당 튜플의 속성의 개수를 확인하는데 사용된다.

    SPI_copytuple

    HeapTuple SPI_copytuple (HeapTuple tuple) ;

    이 함수는 상위 실행기 수준에서튜플을 복사한다. 입력 값은 복사할 튜플이고 반환 값은 복사된 튜플이다. 입력 값인 tuple이 NULL일 경우 반환 값은 NULL이고, 그 외의 경우에는 NOT NULL 이다.

    SPI_modifytuple

    HeapTuple SPI_modifytuple (Relation rel, HeapTuple tuple, int nattrs,int *attnum,
    >> *values, chr *nulls) ;

    이 함수는 상위 실행기 수준에서 테이블의 튜플을 수정한다. rel과 tuple은 수정될 입력 튜플을 지정하고, nattrs 는 attnum에 포함되어 있는 속성번호의 개수이다. attnum은 수정할 속성번호로 이루어진 배열이며, values는 이전의 값을 대체할 새로운 값이며, nulls는 속성이 NULL 일 수 있는가를 설명한다. 반환값은 NULL이 되고, tuple 이 NULL 이 아니고 수정이 성공하였을 경우에는 NOT NULL 이 된다.

    이 함수의 자세한 결과는 전역 변수인 SPI_result 에 저장된다. rel 이 NULL 이거나 tuple이 NULL이거나 natts<=0 이거나 attnum 이 NULL이거나 values 가 NULL 이면 SPI_result 의 값은 SPI_ERROR_ARGUMENT 가 된다. attnum에서 유효하지 않은 속성 번호가 있을 경우(attnum이 0보다 작거나 튜플의 속성 개수보다 클 경우) 에는 SPI_ERROR_NOAT_TRIBUTE 로 세트된다. 애초에 전달된 튜플은 이 함수 수행이후에도 변화하지 않는다.

    SPI_fnumber

    int SPI_fnumber (TupleDesc tupdesc, char *fname) ;

    이 함수는 지정한 속성의 속성번호를 돌려준다. fname은 찾을 속성의 이름이고, tupdesc는 입력 튜플 기술자이다. 반환값은 해당 속성의 번호이다. fname 으로 지정한 속성이 없을 경우에는 SPI_ERROR_NOATTRIBUTE를 돌려주며, 속성번호는 1부터 시작한다.

    SPI_fname

    char *SPI_fname (TupleDesc tupdesc, int fnumber) ;

    이 함수는 지정한 속성의 속성 이름을 돌려준다. fnumber는 찾을 속성의 번호이고, tupdesc는 입력 튜플 기술자이다. 반환 값은 해당 속성의 이름이다. fnumber가 범위를 벗어나면 NULL이 반환되고,에러시에는 SPI_result전역 변수가 SPI_ERROR_NOATTRIBUTE 로 설정된다. 성공시에 반환되는 속성 이름은 새로운 메모리에 할당된 복사본이다.

    SPI_getvalue

    char *SPI_getvalue (HeapTuple tuple, TupleDesc tupdesc, int fnumber) ;

    이 함수는 지정한 속성의 값을 문자열로 반환한다. tuple 은 값을 얻어낼 입력 튜플이고, tupdesc는 입력 튜플의 기술자이다. fnumber는 속성번호이다. 반환 값은 성공적일 경우에는 속성 값이며, 그 외의 경우에는 NULL이다. 이 경우에 자세한 상태 값은 SPI_result 에 설정된다. fnumber 가 범위를 벗어나면 SPI_ERROR_NOATTRIBUTE 로 설정되며, 출력 함수가 유효하지 않다면 SPI_ERROR_NOOUTFUNC 로 설정된다. 성공시에 반환된 속성 값은 새로운 메모리에 할당된 복사본이다.

    SPI_getbinval

    Datum SPI_getbinval (HeapTuple tuple, TupleDesc tupdesc, int fnumber, bool *isnull) ;

    이 함수는 지정한 속성의 값을 이진값으로 돌려준다. tuple은 값을 얻어낼 입력 튜플이고, tupdesc는 입력 튜플의 기술자이다. fnumber는 속성 번호이다. 반환값은 바이너리 값으로, 반환 값을 위해서 새로운 공간을 할당하지 않는다는 점에 주의해야 한다. 만일 속성값이 NULL 일 경우에 isnull 이 1로 설정된다. fnumber 가 범위를 벗어나면 SPI_result 는 SPI_ERROR_NOATTRIBUTE로 설정된다.

    SPI_gettype

    OID *SPI_gettype (TupleDesc tupdesc, int fnumber) ;

    이 함수는 지정한 속성의 타입 이름의 복사본을 반환한다. tupdesc는 튜플 기술자이고, fnumber 는 속성 번호이다. 반환값은 번호로 지정한 속성의 타입 이름이다. 속성 번호가 알맞지 않을 경우에 SPI_result 는 SPI_ERROR_NOATTRIBUTE로 설정된다.

    SPI_gettypeid

    OID SPI_gettypeid (TupleDesc tupdesc, int fnumber) ;

    이 함수는 지정한 속성이 타입 아이디를 반환한다. 속성 번호가 알맞지 않을 경우에 SPI_result 값이 상기와 같이 설정된다.

    SPI_getrelname

    char *SPI_getrelname (Relation rel) ;

    이 함수는 지정한 테이블의 이름을 한부 복사해서 돌려준다. rel은 입력 테이블을 나타낸다.

    SPI_palloc

    void *SPI_palloc (Size size) ;

    이 함수는 사위 실행기 수준에서 메모리를 할당한다. size는 할당할 공간을 지정하는 8 진수이어야 한다. 물론 반환값은 새롭게 할당한 메모리의 주소이다.

    SPI_repalloc

    void *SPI_repalloc (void *pointer, Size size) ;

    이 함수는 상위 실행기 수준에서 메모리를 다시 할당한다. pointer는 이미 존재하는 메모리 주소이고, Size는 새롭게 할당할 메모리의 크기를 8 진수로 지정한 것이어야 한다. 반환 값은 당연히 이미 존재하는 메모리에 담긴 내용을 복사한 내용을 담고 있는 지정한 크기의 공간의 주소이다.

    SPI_pfree

    void SPI_pfree (void *pointer) ;

    이 함수는 상위 실행기 수준에서 메모리를 해제한다. pointer는 해제할 메모리 주소이다.

SPI에서 메모리 관리

    데이터베이스 서버는 메모리를 할당할 때 상호 문맥이 영향을 받지 않도록 독립적으로 할당한다. 즉, 하나의 문맥에서 메모리를 해제되는 것은 다름 문맥에 영향을 주지 않는다는 이야기다. 따라서 현재의 문맥이 아닌 다른 문맥에 대한 메모리 해제는 예측할 수 없는 결과를 야기할 수 있다. SPI 프로시저는 두 개의 메모리 문맥과 관련이 있다. 상위 실행기 메모리 문맥과 프로시저 메모리 문맥이 그것이다. 이러한 두 개의 메모리 문맥에서 변환은 SPI 관리자의 메모리 관리에 의해 이루어진다.

    SPI 관리자에 접속하기 이전은 상위 실행기 문맥에 놓여지게 되고, 모든 메모리 할당은 프로시저 그 자신에 의해 palloc/repalloc를 통하거나 SPI 유틸리티 함수를 통해서 이루어진다. SPI_connect 가 호출된 이후에는 프로시저 문맥에 놓여지게 되고, 이후의 모든 메모리 할당은 palloc/repalloc 또는 SPI 유틸리티 함수(SPI_copytuple, SPImodifytuple, SPI_palloc, SPI_repalloc 는 제외된다.)를 통해 이루어진다. SPI 관리자와 연결을 끊게 되면 현재의 문맥은 상위 실행기 문맥으로 복귀하게 되며, 프로시저 메모리 문맥에서 할당된 모든 메모리는 해제되고 더 이상 사용할 수 없게 된다. 상위 실행기로 어떠한 것을 되돌릴려면 상위 문맥에서 이를 위한 메모리를 할당해야 한다. SPI는 상위 실행기 문맥에서 할당된 메모리를 자동적으로 해제 해주지는 않는다. 하지만 SPI는 질의의 실행 도중에 할당된 메모리는 질의가 완료되면 자동으로 해제해준다.

데이터변화의 가시성 규칙

    PostgreSQL 에서의 가시성 규칙은상식적인 수준을 크게 벗어나지 않는다. 가시성 규칙은 프로시저 내부에서 질의가 처리될 때에 중요한 의미를 지닌다. 중요한 의미라기 보다는 작업하기에 따라서 사용자의 생각과는 다르게 작동할 수 있다는 것을 의미한다. 하지만 매우 규칙적이기 때문에 논리적으로 잘 추론하면 아무 이상이 없다.
    PostgreSQL에서 질의가 실행되는 도중에 질의 그 자체에 의해 이루어진 변화는 질의 검색기에 나타나지 않는다. 이러한 변화는 주로 SQL 함수나 SPI 함수, 또는 트리거에 의해 주로 이루어진다. 가령 예를 들어보면, "INSERT INTO a SELECT * FROM a" 질의에 의해 삽입된 튜플은 SELECT 검색시에 나타나지 않는다. 잠시 설명을 하자면, 이 질의는 자신의 테이블의 내용을 그대로 다시 복사한다. 물론 재귀는 하지 않으며, unique 인덱스 규칙이 있다면 이에 따른다. 질의 A의 실행 도중에 생성되는 변화는 질의 A 이후에 시작하는 질의에서는 나타난다. 하지만 질의 A가 실행되고 있는 도중에는 아무런 일도 없는 것처럼 보인다.

SPI의 예제

    이제 SPI 인터페이스 함수를 사용하여 테스트를 위한 함수를 만들어 보자. 잘 이해하려면 꼼꼼히 챙겨봐야 할 것이다. SPI 가 아주 유용하긴 하지만 관련되는 데이터 타입과 전역 변수, SPI 함수, SPI 상수들을 주의깊게 살펴봐야 할 것이며, 가시성 규칙에 따른 결과에도 주의를 기울여야 한다.

    #include <stdio.h>
    #include "executor/spi.h" /* SPI 함수를 위한 헤더 파일 */
    int execq (text *sql, int cnt) ;
    int execq (text *sql, int cnt) {
    int ret ;
    int proc = 0 ;
    SPI_connect () ; /* 먼저 SPI 관리자에 접속한다 */
    /* textout 내부함수는 text *를 char *로 변환한다. SPI에서 기본 문자열형은
    text 형으로, psql에서 입력하는 문자열은 text *로 서버에게 전달된다. */
    ret = SPI_exec (textout(sql), cnt) ; /* execq 함수에 전달되는 질의인 sql을
    실행한다. */
    proc = SPI_processed ; /* SPI_processed 에는 처리된 튜플의 갯수가
    저장된다. */
    /* SELECT 가 성공하였고, 반환된 튜플이 0개를 초과한다면 */
    if (ret == SPI_OK_SELECT && SPI_processed > 0) {
    /* SPI_tutable->tupdesc는 현재 튜플 리스트에 대한 기술자이다. */
    TupleDesc tupdesc = SPI_tuptable->tupdesc ;
    /* SPI_tuptable A>> tuptable로 백업한다. */
    SPITuleTable *tuptable = SPI_tuptable ;
    char buf[8192] ;
    int I ;
    for (ret = 0 ; ret < proc ; ret++) {
    /* 하나의 튜플데이터형에 회수된 튜플을 하나씩 차례대로 집어넣는다. */
    HeapTuple tuple = tuptable->vals[ret] ;
    /* 하나의 튜플에 대해 각각의 속성의 값을 하나씩 얻어낸다. */
    /* tupdesc->natts SPI_tuptable->tupdesc->natts 이며, 해당 튜플에서
    속성의 개수이다. */
    for (i = 1, buf[0] = 0 ; I <= tupdesc->natts ; i++)
    sprintf(buf + strlen(buf), "%s%s",
    SPI_getvalue(tuple, tupdesc, i),
    (i == tupdesc->natts) ? " " : "|") ;
    /* 더 이상의 속성이 없다면 공백을 출력하고,
    아니라면 필드 구분자로 |를 출력한다. */
    /* 로그 메시지를 출력한다. */
    elog(NOTICE, "EXECQ: %s", buf) ;
    }
    /* 접속을 종료한다. */
    SPI_finish() ;
    /* 실제로 처리된 튜플의 개수가 함수의 반환값이 된다. */
    return proc ;
    }

    위 소스 파일을 testspi.c로 저장한다. 일단 여기에서 개별적인 내용만 이해하고 넘어가자. 전체적인 줄기는 실제 사용 예를 보면서 이해하는 것이 좋다. 먼저 위의 함수를 공유 라이브러리로 컴파일을 해보자. 참고로 C 함수를 공유 라이브러리로 컴파일하기 위해서는 PostgreSQL 가 설치된 디렉토리의 헤더파일 뿐만이 아니라 원본 소스 디렉토리의 헤더파일까지 필요하다. 필자의 디렉토리와 여러분의 디렉토리가 다를 수 있으므로 헤더 파일 참조 디렉토리는 알아서 수정하자.

      $ gcc -c testspi.c -fPIC -I/usr/local/pgsql/include -I/usr/local/postgresql-6.3.1
      >> /src/include/
      $ gcc -shared -0 testspi.so testspi.0

    자주 이렇게 긴 명령 행을 타이핑 하는 것은 정신 건강에 해롭다. 간단히 스크립트를 만들어서 대체하도록 한다.

    # ! /bin/sh
    # name : funcom - Compiling Shared Object function for PostgreSQL
    CC=gcc
    PG_SRC_ROOT=/usr/local/postgersql-6.3.1/
    PG_INS_ROOT=/usr/local/pgsql/
    if [$# -le 0] ; then
    echo "usage : $0 <source>"
    exit
    fi
    PREFIX=${1%.*}
    $CC -c -o ${PREFIX}.o ${PREFIX}.c -fPIC -I${PG_INS_ROOT}/
    include -I${PG_SRC_ROOT}/src/include
    gcc -shared -o ${PREFIX}.so ${PREFIX}.o

    앞으로는 다음과 같이 간단하게 컴파일한다.

      $ ./funcom testspi

    이제 함수를 만들어보자. 이름은 execq로 한다.

      $ psql -c "create function execq (text, int4) returns int4 as ''pwd'/testspi.so'
      >> language 'c' ; " mydb

    execq 함수를 테스트하기 전에 execq 함수를 전체적인 안목에서 잠시 살펴보자. 첫 번째 if으로 인해 SELECT 시에만 execq 의 나머지 부분이 실행된다. 아울러 검색된 튜플이 0개를 초과할 때에만 실행된다는 것을 알 수 있다. execq 함수에서 반환되는 값은 SELECT 질의가 처리하는 튜플의 개수와 동일함을 알 수 있다. 그리고 for 루프안을 보면 매번 각각의 튜플마다 그 칼럼을 elog 함수로 출력함을 알 수 있다. 즉, a 라는 테이블에 2개의 튜플이 있다면 execq('select * from a', 0)을 실행하면 두 번의 상위 for 문이 수행되어 두 개의 알림글이 출력된다는 것을 짐작할 수 있다. 이제 SPI 함수로 작성한 execq함수를 테스트 해보자.

    mydb=> select execq('create table a (x int4)', 0) ;
    execq
    -----
    0
    (1 row)

    mydb=> insert into a values (execq('insert into a values (0)', 0) ;
    INSERT 167631 1
    mydb=> select execq('select * from a', 0) ;
    NOTICE : EXECQ : 0 <- execq가 삽입한 것임
    NOTICE : EXECQ : 1 <- execq가 반환하고 상위 INSERT가 삽입한 값

    execq
    -----
    2
    (1 row)

    mydb=> select execq('insert into a select x + 2 from a', 1) ;
    execq
    -----
    1
    (1 row)

    mydb=> select execq('select * from a', 10) ;
    NOTICE : EXECQ : 0
    NOTICE : EXECQ : 1
    NOTICE : EXECQ : 2 <- 0+2, 첫 번째 튜플만 삽입되었음

    execq
    -----
    3 <- 10 개가 튜플의 최대 횟수 제한 갯수인데, 3개가 실제 갯수임
    (1 row)

    mydb=> delete from a ;
    DELETE 3
    mydb=> insert into a values (execq('select * from a', 0) +1) ;
    INSERT 167712 1
    mydb=> select * from a ;
    x
    -
    1 <<< (0) +1
    (1 row)

    mydb=> insert into a values (execq('select * from a', 0) +1) ;
    NOTICE : EXECQ : 0
    INSERT 167713 1
    mydb=> select * from a ;
    x
    -
    1
    2 <- (1) +1
    (2 rows)

    -- 변화된 데이터의 가시성 규칙을 설명한다.

    mydb=> insert into a select execq('select * from a', 0) * x from a ;
    NOTICE : EXECQ : 1
    NOTICE : EXECQ : 2
    NOTICE : EXECQ : 1
    NOTICE : EXECQ : 2
    NOTICE : EXECQ : 2
    INSERT 0 2
    mydb=> select * from a ;
    x
    -
    1
    2
    2<<< 2개 튜플 * 1(첫번째 튜플의 x값)
    6 <<< 3개 튜플 (원래의 2개 + 방금 입력된 1개) * 2(두번째 튜플의 x값), 서로 다른 세션에서 execq() 함수는 변경된 내용을 볼 수 있다.
    (4 rows)

트리거! 그 이름만으로도 설레이는 마음

    PostgreSQL 의 현재버전(6.3.1) 에서는 다양한 인터페이스로 Perl 과 Tcl, Python, C 등을 지원한다. 하지만 아직 실제 프로시저 언어인 PL 에는 약점을 가지고 있다. 멀지 않은 세월에 제대로 된 PL 언어가 지원될 것으로 보인다. 뭐 하지만 C 함수를 호출하여 트리거의 역할을 수행하는 것은 현재로서도 가능하다. 그래도 아직 질의어(구문/절)차원에서는 아직 트리거 사건에 대한 처리는 지원하지 않고 있다. 트리거 사건시의 해당 튜플에 대해 INSERT, DELETE, UPDATE 시에 BEFORE 나 AFTER 구문을 지정하여 사용할 수 있다. C 함수 내부에서의 트리거는 주로 SPI 함수를 사용하여 처리된다. SPI 와 트리거를 땔래야 땔 수 없는 하나의 이유이다.

트리거의 생성

    트리거 사건이 일어나면, 트리거 관리자가 실행기에 의해 호출되어서 전역 구조체인 TriggerData *CurrentTriggerData를 초기화한다. 그리고 사건을 처리하기 위해 지정 트리거 함수를 호출한다. 트리거 함수는 트리거 함수를 등록하기 전에 만들어야 하며, 트리거 함수는 전달 매개 인자가 없으며, opaque를 반환하도록 해야 한다. 트리거의 생성 구문은 다음과 같다.

      CREATE TRIGGER <트리거 이름> <BEFORE|AFTER>
      <INSERT|DELETE|UPDATE>
      ON <> FOR EACH <ROW|STATEMENT>
      EXECUTE PROCEDURRE <프로시저 이름> (<함수인자>) ;

    트리거의 이름은 트리거를 제거할 때 빼고는 사용할 일이 없을 것이다. DROP TRIGGER 명령에서 트리거 이름이 인자로 사용된다.

    그 다음의 BEFORE, AFTER 는 사건의 발생시점을 지정한다. 즉, 사건이 발생한 이후, 또는 이전에 트리거 함수를 호출할 것인지를 지정한다. 다음의 INSERT/DELETE/UPDATE 구문은 어떤 사건이 함수를 트리거할 것인지를 지정한다. 여러 개의 사건을 OR 연산하여 함께 지정할 수 있다. 테이블 이름은 해당 사건이 어느 테이블에 적용될 것인지를 지정한다.

    다음의 FOR EACH 구문은 트리거가 테이블에 적용될 때, 각각의 로우마다 적용할 것인지, 아니면 전체 구문 단위로 적용할 것인지를 지정한다. 프로시저 이름은 호출할 C 함수의 이름이다. 함수인자는 CurrentTriggerData 구조체 내에 있는 함수로 전달된다. 함수에 매개 인자를 전달하는 목적은 같은 함수를 호출하는 서로 유사한 요청을 처리하는 다른 트리거를 허용하기 위해서 이다. 물론, 함수는 서로 다른 테이블을 트리거하는 데 사용된다. 이러한 함수는 '일반 트리거 함수'라고 부른다.

    트리거 함수는 자신을 호출한 실행기에게 HeapTuple을 반환할 수 있다. 만일 사건 이후에 (AFTER) 트리거가 수행되도록 트리거를 생성했다면, 실행기가 INSERT/DELETE/UPDATE를 수행한 이후에 트리거 함수를 호출하게 된다. 이 트리거 함수에서 사용자가 어떤 튜플(HeapTuple)을 반환한다고 해도 실행기 입장으로 볼 때는 자신은 일을 다 마친 이후이므로 해당 튜플을 무시한다. 하지만 BEFORE 트리거시에는 이야기가 달라진다. 먼저 실행기는 자신에게 맡겨진 중요한 작업(INSERT/DELETE/UPDATE)을 수행하기 전에 트리거함수를 먼저 호출한다. 사용자의 트리거 함수에서 적당한 튜플이 반환되면, 그것으로 자신의 작업을 수행한다. 즉, 사용자가 작성한 트리거 함수에서 무엇을 넘겨주느냐에 따라서 실행기가 작업하는 내용이 달라진다는 것이다.

    트리거 함수에서 NULL을 반환하면, 실행기는 반환받은 튜플이 NULL 이므로 아무일도 하지 않고 넘어간다. 즉, BEFORE 트리거에서 INSERT 시에 NULL을 돌려주도록 하였다면, 사용자가 해당 테이블에 INSERT 하더라도 실행기의 작업에서는 INSERT 되지 않는다는 것이다. 트리거의 작업에서는 INSERT를 할 수 있을 지는 몰라도. 만일 BEFORE 트리거에서 다른 튜플을 넘겨준다면(INSERT 와 UPDATE 시에만 가능하다) 원래의 튜플 대신에 반환한 튜플이 삽입될 것이다. 물론 UPDATE 시에는 갱신된 새로운 튜플이 될 것이다.

    PostgreSQL의 현재 버전에서는 트리거의 확장성에서 조금 부족한 듯한 부분이 있다. 즉, CREATE TRIGGER 시에 트리거의 초기화가 수행되지 않는다는 것이다. 또한, 동일한 테이블상에서 동일한 사건에 대해 하나 이상의 트리거를 정의할 시에, 트리거의 실행 순서는 예측할 수 없다는 것이다. 이러한 점들은 앞으로 보완될 것이다.

    트리거 함수가 SPI를 사용하여 SQL 질의를 실행하면, 이 질의는 또다시 트리거된다. 이러한 트리거는 '종속 트리거(cascading trigger)'라 부른다. 종속 횟수의 명시적인 제한은 없다.

    트리거가 INSERT 사건시에 수행되고, 동일한 테이블에 새로운 튜플이 삽입된다면, 이러한 트리거는 다시 트리거된다. 현재, 이러한 경우를 대비해 제공되는 동기화 기능은 아직 없지만, 조만간 제공될 것으로 보인다.

트리거 관리자와의 즐거운 대화

    위에서도 언급했듯이, 트리거 관리자에 의해 함수가 호출되면,
    TriggerData *CurrentTriggerData 구조체는 NOT NULL 이 되면서, 초기화된다. 처음에 CurrentTriggerData 가 NULL 인지 검사하는 것이 좋다.

    TriggerData 구조체는 src/include/commands/trigger.h에 정의되어 있다.

      typedef struct TriggerData
      {
      TriggerEvent tg_event ;
      Relation tg_relation ;
      HeapTuple tg_trigtuple ;
      HeapTuple tg_newtuple ;
      Trigger *tg_trigger ;
      } TriggerData

    각각의 멤버를 잠시 살펴보자.

    tg_event

    이 멤버는 트리거의 종류를 설명하는데 사용된다. 아래의 다양한 매크로를 사용하여 tg_event의 종류를 확인할 수 있다.

    TRIGGER_FIRED_BEFORE(event) 트리거가 사건의 BEFORE시에 수행된다면 TRUE반환
    TRIGGER_FIRED_AFTER(event) 트리거가 사건의 AFTER시에 수행된다면 TRUE반환
    TRIGGER_FIRED_FOR_ROW(event) 트리거가 매 로우마다 수행된다면 TRUE반환
    TRIGGER_FIRED_FOR_STATEMENT(event) 트리거가 매 구문마다 수행된다면 TRUE반환
    TRIGGER_FIRED_BY_INSERT(event) 트리거가 INSERT 시에 수행된다면 TRUE반환
    TRIGGER_FIRED_BY_DELETE(event)트리거가 DELETE 시에 수행된다면 TRUE반환
    TRIGGER_FIRED_BY_UPDATE(event)트리거가 UPDATE 시에 수행된다면 TRUE반환

    tg_relation

    이 멤버는 트리거되는 테이블을 설명하는 구조체에 대한 포인터이다. src/include/utils/rel.h에 자세한 내용이 정의되어 있다. 흥미로운 점은 tg_relation->rd_att(테이블 튜플의 기술자)와 tg_relation->rd_rel->relname(테이블의 이름. 이것은 char * 가 아니라 NameData 형이다. char * 의 복사본을 얻으려면, SPI_getrelname(tg_relation)을 사용하면 된다.)이다.

    tg_trigtuple

    이 멤버는 트리거되는 튜플을 가르키는 포인터이다. 이 튜플은 INSERT/DELETE/UPDATE 시에 각각 삽입/제거/갱신 되는 튜플이다.

    tg_newtuple

    이 멤버는 UPDATE 시에 튜플의 새로운 버전을 가리키는 포인터이다. 단, INSERT 나 DELETE 시에 이 값은 NULL이 된다.

    tg_trigger

    이 멤버는 src/include/utils/rel.h에정의되어 있는 트리거 구조체를 가리키는 포인터이다.

      typedef struct Trigger
      {
      char *tgname ;
      Oid tgfoid ;
      func_ptr tgfunc ;
      int16 tgtype ;
      int16 tgnargs ;
      int16 tgattr[8] ;
      char **tgargs ;
      } Trigger ;

    이 구조체에서 tgname 은 트리거의 이름이고, tgnargs 는 tgargs 에 있는 인자의 개수이며, tgargs 는 CREATE TRIGGER 구문에서 지정한 인자를 가리키는 포인터의 배열이다. 다른 멤버는 내부적인 목적으로만 사용된다.

트리거의 예제

    트리거에서의 데이터변화에 따른 가시성 규칙은 기본적으로 SPI 에서의 가시성 규칙과 동일하다. 덧붙여, BEFORE 트리거에서 삽입되는 튜플(tg_trigtuple)은 질의시에 나타나지 않는다. 이 튜플은 AFTER 트리거시에는 나타난다.

    이제 간단한 트리거의 예제를 살펴보자. 트리거의 대부분의 기능은 SPI를 통해서 실현된다. 아울러 트리거에서만 사용 되는 것은 위에서도 살펴본 트리거 관련 데이터 구조체들이다. 여기에 등장하는 trigf 함수는 트리거되는 테이블인 ttest 에 있는 튜플의 갯수를 출력하고, 만일 질의가 NULL을 x 에 삽입하려고 한다면 작동을 하지 않고 건너뛴다.(이 부분은 NOT NULL 규정과 똑같이 동작한다. 하지만 trigf 함수에서는 트랜잭션을 중단하지는 않는다.)

    #include <stdio.h>
    #include "executor/spi.h" /* SPI 에 필요한 헤더파일 */
    #include "commands/trigger.h" /* 트리거에 필요한 헤더파일 */
    HeapTuple trigf(void) ;
    HeapTuple
    trigf ()
    {
    TupleDesc tupdesc ; /* 튜플 기술자 */
    HeapTuple rettuple ; /* 하나의 튜플 저장고 */
    char *when ;
    bool checknull = false ;
    bool isnull ;
    int ret, I ;
    /* CurrentTriggerData가 NULL이라면 아직 초기화되지 않았다는것을 의미 */
    if (!CurrentTriggerData)
    elog(NOTICE, "trigf : triggers are not initialized") ;
    /* 실행기로 되돌릴 튜플을 지정한다. UPDATE 트리거시에는 새로운 튜플을 */
    /* 지정하고, INSERT/DELETE 경우에는 트리거되는 튜플을 지정한다. */
    if (TRIGGER_FIRED_BY_UPDATE(!CurrentTriggerData->tg_event))
    rettuple = CurrentTriggerData->tg_newtuple ;
    else
    rettuple = CurrentTriggerData->tg_trigtuple ;
    /* 만일, INSERT/DELETE 트리거이고, BEFORE 트리거이면 NULL이 삽입되지
    않도록 체크한다. */
    if (TRIGGER_FIRED_BY_DELETE(!CurrentTriggerData->tg_event)) &&
    TRIGGER_FIRED_BEFORE(!CurrentTriggerData->tg_event))
    checknull = true ;
    /* 더 이상 트리거 데이터 구조에 접근할 일이 없으므로, NULL로 지정한다. */
    CurrentTriggerData = NULL ;
    /* SPI 관리자로 접속을 성립한다. */
    if ((ret = SPI_connect()) < 0)
    elog(NOTICE, "trigf (fired %s) : SPI_connect returned %d", when, ret) ;
    /* 테이블에 있는 튜플의 갯수를 얻는다. */
    ret = SPI_exec("select count (*) from ttest", 0) ;
    if ( ret<0 )
    elog(NOTICE, "trigf (fired %s) : SPI_exec returned %d", when, ret) ;
    /* SPI_tuptable 에는 ttest 테이블의 튜플의 갯수가 첫 번째 튜플에 들어있다. */
    /* 이 갯수는 바로 위에서 실행한 "select count(*) from ttest" 의 결과이다. */
    I = SPI_ getbinval(SPI_tuptable->vals[0], SPI_tuptale->tupdesc, 1, &isnull) ;
    elog(NOTICE, "trigf (fired %s) : there are %d tuples in ttest", when, i) ;
    SPI_finish() ;
    /* 만일 튜플의 값이 NULL 인가를 체크해야 할 필요가 있다면, 첫 번째 컬럼의
    값을 검사해서*/
    /* 그 값이 NULL 이라면, 튜플의 값을 NULL로 하여 실행기에서 처리하지 않도록
    한다. */
    if (checknull)
    {
    I = SPI_getbinval(rettuple, tupdesc, 1, &isnull) ;
    if (isnull)
    }
    /* 튜플을 실행기로 반환한다. */
    return (rettuple) ;
    }

    이 프로그램을 testtrig.c 로 저장하고, 컴파일을 하도록 하자.

      $ ./funcom testtrig

    먼저, 트리거를 생성하기 이전에, 트리거가 의존하고 있는 테이블을 만들어 보자. 여기서는 동시에 트리거 함수를 등록한다.

      $ psql -c "create table ttest (x int4) ; create function trigf() returns opaque as
      >>''pwd'/testtrig.so' language 'c' ; " mydb

    아래는 트리거를 생성하고, 실제로 테스트하는 내용이다. 트리거를 생성할 때, INSERT/UPDATE/DELETE 모두를 BEFORE/AFTER 에 하나의 로우마다 트리거 되도록 하였다. 잘 따라해보고 왜 그렇게 되는지 위의 소스와 비교해서 유심히 살펴보도록 하자.

      mydb=>create trigger tbefore before insert or update or delete on ttest for each
      >> row execute procedure trigf () ;
      CREATE
      mydb=>create trigger tafter after insert or update or delete on ttest for each
      >> row execute procedure trigf () ;
      CREATE
      mydb=> insert into ttest values(null) ;
      NOTICE : trigf (fired before) : there are 0 tuples in ttest
      INSERT 0 0

      -- null을 INSERT 시도한 관계로 BEFORE 트리거에서 걸려서 실행기에게 null을 반환
      하여 삽입이 되지 않았다. 아울러 AFTER 트리거에서는 실행기가 반환 값을 무시
      된다. 따라서 아래에서 검색해 본 결과 삽입된 데이터는 없었다.

      mydb=> select * from ttest ;
      x
      -
      (0 row)

      mydb=> insert into ttest values (1) ;
      NOTICE : trigf (fired before) : there are 0 tuples in ttest
      NOTICE : trigf (fired after ) : there are
      1 tuples in ttest
      가시성 규칙에 따른 결과이다.
      INSERT 167793 1
      -- SELECT 에는 트리거가 적용되지 않는다.
      mydb=> select * from ttest ;
      x
      -
      1
      (1 row)

      mydb=> insert into ttest select x * 2 from ttest ;
      NOTICE : trigf (fired before) : there are 1tuples in ttest
      NOTICE : trigf (fired after ) : there are
      2 tuples in ttest
      가시성 규칙에 따른 결과이다.
      INSERT 167794 1
      mydb=> select * from ttest ;
      x
      -
      1
      2
      (2 rows)

      mydb=> update ttest set x = null where x = 2 ;
      NOTICE : trigf (fired before) : there are 2 tuples in ttest
      UPDATE 0
      mydb=> update ttest set x = 4 where x = 2 ;
      NOTICE : trigf (fired before) : there are 2 tuples in ttest
      NOTICE : trigf (fired after ) : there are 2 tuples in ttest
      UPDATE 1
      mydb=> select * from ttest ;
      x
      -
      1
      4
      (2 rows)

      mydb=> delete from ttest ;
      NOTICE : trigf (fired before) : there are 2 tuples in ttest
      NOTICE : trigf (fired after ) : there are 1 tuples in ttest
      NOTICE : trigf (fired before) : there are 1 tuples in ttest
      NOTICE : trigf (fired after ) : there are
      0 tuples in ttest
      가시성 규칙에 따른 결과
      delete 2
      mydb=> select * from ttest ;
      x
      -
      (0 row)

    위에서 트리거되는 상황을 살펴보자. 먼저 trigf를 생성할 때, INSERT/UPDATE/DELETE 에 대해 BEFORE/AFTER 에 모두 트리거를 걸었다. 물론 SELECT 사건시에는 트리거를 걸 수 없다. INSERT 시에 BEFORE 트리거에서는 아직 삽입을 하지 않아서 삽입되는 데이터가 나타나지 않는다. 물론 AFTER 트리거시에는 이미 액션이 이루어진 후이므로 데이터가 나타난다. 사용자가 null을 INSERT/UPDATE을 통해 입력하려고 하면, BEFORE 트리거에서 검사되어서 삽입되지 않는다. 그리고 하나의 로우가 처리될 때 마다 두 개의 AFTER/BEFORE 트리거가 실행됨을 상기하자. 가령, 마지막의 "delete from ttest" 이전에 현재 두 개의 로우가 테이블에 들어있다. DELETE 구문에 의해 첫 번째 로우를 제거하기 이전에(BEFORE) 트리거가 실행되어서 현재 튜플안에 있는 로우(튜플)의 갯수를 출력한다. 삭제가 이루어진 다음, AFTER 트리거가 실행되어서 1개의 로우가 있음을 알린다. 다음 로우를 삭제하기 위해서 DELETE 가 적용되면 또다시 위와 같이 BEFORE 트리거와 AFTER트리거가 작용한다.

나오면서

    지금까지 설명한 SPI 와 트리거는 저수준의 서버 프로그래밍 인터페이스였다.
    사실 PostgreSQL 이 저수준에서 대단한 확장 가능성을 가지고 있다는 것은 관심있는 사람이라면 잘 알 것이다. PostgreSQL 내부에 정의되어 있는 데이터 구조체까지도 접근할 수 있다는 점은 깊이있는 데이터베이스 시스템 공부에 많은 도움이 된다. 하지만 PostgreSQL 은 아직 PL(프로시저언어) 쪽에서는 약점을 가지고 있다. 이것은 조만간 PostgreSQL 개발팀에 의해 보완이 될 것으로 보인다. 트리거와 SPI 는 꼭 필요한 곳에 제대로만 사용한다면 아주 유용한 도구가 될 수 있을 것이다. 일반 SQL 구문을 주로 사용하는 이들에게는 그다지 유용할 것 같지는 않지만 ...

    PostgreSQL 은 트리거와 유사한 규칙(RULE) 시스템을 가지고 있다. 이 규칙 시스템을 사용하면 트리거의 역할을 SQL 구문 차원에서 수행할 수 있다. 중요한 테이블의 내용을 갱신이 있을 때마다 백업한다든지 하는 작업에 주로 사용될 것이다. 하지만 현재의 규칙 시스템은 대단히 불안하기 때문에 사용을 권장하지는 않는다. 다만 그다지 중요하지 않은 개인적인 작업에 규칙 시스템을 사용하는 것은 괜찮을지 모르지만 회사의 중요한 작업을 처리하는 곳에는 규칙 시스템 대신 트리거와 SPI를 사용하도록 하자.

    이번 호의 강좌는 이것으로 마친다.

, .

공개 데이터베이스 서버 PostgreSQL (6)
-ECPG로 Embedded SQL 프로그래밍하기

한동훈 /리눅스 코리아 대표 ddoch@hitel.kol.co.kr

PostgreSQL 버전 6.3 얼마나 달라졌나?

    3월 1일자로 PostgreSQL의 6.3 버전이 발표되었다. 사실 PostgreSQL 개발팀에서는 작년 연말 이전에 발표하겠다고 했었다. 6.2.1 버전 발표 이후로 상당한 시간이 흘러 6.3 버전이 발표된 만큼 공개 데이터베이스 서버 사용자들은 새로운 버전에 많은 기대를 걸고 있는 것 같다. 그 오랜 기간동안만큼 많은 것들이 추가되었고 달라졌다. PostgreSQL 6.0이 Postgres95의 장벽을 뛰어넘은 전 분야에서의 획기적인 버전이었다고 한다면, PostgreSQL 6.3은 6.0 이후에 가장 획기적인 변화를 가져왔다고 볼 수 있다.
    PostgreSQL 개발팀에서는 6.0 이후에 SQL92 표준을 지원하고 여러분은 이제 그 혜택을 마음껏 누릴 수 있게 되었다. 여러분은 이제 그 혜택을 마음껏 누릴 수 있게 되었다. PostgreSQL 사용자들에게는 이제 또 한번의 업그레이드 전쟁을 해야 할 뚜렷한 이유가 주어진 셈이다. 이제 6.3에서 달라진 기능을 살펴보자.

SUBSELECT의 지원

    오랫동안 PostgreSQL 사용자의 바램 중의 하나인 SUBSELECT를 마침내 지원한다. 보조 질의 중에 EXISTS, IN, ALL, ANY 예약어를 함께 사용할 수 있다.

PRIMARY KEY와 UNIQUE절 지원

    SQL92의 PRIMARY KEY와 UNIQUE절을 인덱스를 사용하여 구현하였다. FOREIGN KEY는 아직 구현되어있지 않다.

SQL 92의 여러 상수값 지원

    SQL92에는 상황에 따라 사용할 수 있는 여러 상수값들이 정의되어 있다. PostgreSQL 6.3 버전에서도 이러한 것들 중 여러 개를 지원한다.
    CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, CURRENT_USER를 비롯한 가변상수와 TRUE, FALSE를 비롯한 논리상수, IS TRUE, IS FALSE, IS NOT TRUE, IS NOT FALSE를 비롯한 조건절을 지원한다.

사용자 PROCEDURAL LANGUAGE 생성 지원

    사용자가 직접 함수에 사용할 언어를 작성할 수 있도록 해주는 유용한 기능이다. 일반 데이터베이스 사용자는 직접 사용할 기회가 없겠지만, 개발자라면 관심을 가질만 하다. 구문은 다음과 같다.

    create function plsample_call_handler () returns
    opaque as '/usr/local/pgsql/lib/plsample.so'
    language 'C';
    create procedural language 'plsample' handler
    plsample_call_handler lancompiler 'PL/Sample';
    language 'C';

새로운 PostgreSQL 프로시저 언어(PL)지원

    오라클에서 주로 사용하는 PL/SQL과 유사한 프로시저 언어이다. PL은 스크립트 언어이면서도 SQL의 기능을 강력하게 수행하는 프로시저 중심의 언어라고 볼 수 있다. PostgreSQL에서는 PL/TCL을 그 첫 번째로 제공한다.

자주 호출되는 함수의 빠른 속도 유지

    자주 사용되는 함수 중 몇 몇에 대해 즉각적인 처리를 함으로써 속도가 향상되었다.

락킹에서의 향상

    락킹은 데이터베이스 시스템에서 매우 중요한 위치를 차지한다. 교착상태라고 부르는 실제 deadlock을 감지함으로써 더 이상의 time out을 방지한다. 아울러 인덱스를 생성할 때에 공유 락(shard lock)을 사용한다. 그리고 사용자가 특정 데이터베이스의 테이블을 트랜잭션 구간 중에서 독점 락(exclusive lock)을 걸 수 있도록 허용하는 LOCK 명령을 사용할 수 있다.

UNIX 도메인 소켓 지원

    벡엔드와 프론트엔드 라이브러리에서 UNIX 도메인 소켓을 지원한다.

LIKE 와 ~, !~ 구문에 인덱스 사용

UNION, GROUP, DISTINCT의 선별적 지원

    SELECT 시에 UNION을 지원하고, INSERT 시에 UNION, GROUP, DISTINCT를 지원한다.

varchar의 디스크 저장형태 변경

    데이터형이 varchar인 데이터는 디스크에 저장될 때, 필요한 부분만 저장된다. 즉, varchar(100)이라고 해도 실제 데이터가 8바이트라면 그만큼만 저장된다.

.psqlrc 기동파일 지원

    psql을 사용할 시에 사용자가 여러 가지를 미리 편리하게 설정할 수 있도록, 홈디렉토리에 .psqlrc 파일을 만들 수 있도록 지원한다. $HOME/.psqlrc 파일에는 psql에서 수행할 수 있는 모든 명령이 올 수 있다. PostgreSQL에서 psql을 시작할 때마다 날짜형을 ISO로 맞추고, vacuum 명령을 수행한다면, $HOME/.psqlrc 파일에 다음과 같은 내용을 적어보자.

    vacuum;
    set DateStyle to 'ISO';

    다음은 psql을 가동할 때의 화면이다.

시간 여행(time travel) 기능 제거

    말 많던 시간 여행 기능이 드디어 제거되었다. 이 기능은 유용하긴 하지만, 데이터베이스 시스템의 속도를 저하시키는데 한몫을 해왔다. 오래 전에 지워진 데이터까지도 완전히 복구할 수 있도록 지원해 줌으로써 사용자에게 편리성을 제공해오기는 했지만, 그 때문에 데이터 처리 속도가 상당히 늦어졌기 때문이다. 이제 SQL 질의시에 데이터를 회수하는 데까지 걸리는 시간은 더욱 단축되었다. 하지만 시간 여행 기능은 트리거 기능을 사용하면 충분히 재생할 수 있으며, 실제로 6.2에서 시간 여행을 위한 일반적인 트리거 함수가 이미 추가되었다.

새로워진 문서들

    필자뿐만이 아니라 PostgreSQL에서 참조할 만한 문서가 너무 적다고 생각해온 여러분들에게는 정말 반가운 일이 아닐 수 없다. PostgreSQL 사이트(www.postgreSQL.org)에 몇 달 전부터 PostgreSQL 관련 문서화 프로젝트가 새롭게 추진되고 있다. 이와 함께 나타난 새로운 문서들은 PostgreSQL 매니아들에게 정말 갈증날 때의 오아시스가 아닐까 싶다. 6.3 버전에는 모두 postgres 매뉴얼과 관리자 매뉴얼, 프로그래머 매뉴얼, 튜토리얼과 사용자 매뉴얼이 분리된 채로 소스 배포본의 doc 디렉토리에 들어있다. 모두 뽑아본다면 두꺼운 책 한 권 분량을 방불케 할 것이다. 중복되는 내용이 더러 있다.

내장 언어 ECPG 지원

    ECPG는 PostgreSQL 6.3 이전까지만 해도 Linus Tolke(linus@epact.se)씨가 작성해서 PostgreSQL과는 별도로 지원하던 것이었는데, 6.3에 들어오면서 내부에 포함했다. ECPG는 일종의 C에서 사용하는 내장 SQL이다. 이번 호의 주제가 ECPG이므로 이후에서 충분히 설명하도록 하자.

그 외에 달라진 점

    이외에도 6.3 버전에서 달라진 것들이 많이 있다. PSQL에서 지원하는 기능도 확장되었으며, bin 디렉토리에 포함된 바이너리도 상당히 많아졌다. 아울러 중요한 점 한가지를 언급하자면 varchar()나 text() 대신에 기본적으로 char()를 쓰기를 권장한다. 왜냐하면 6.3에 들어와서 이 둘보다 char()가 상당히 속도가 빨라졌기 때문이다. 그리고 뷰에서도 뷰가 기반하고 있는 테이블과는 별도로 권한을 설정할 수 있게 되었다. 보안을 위해 GRANT, REVOKE 명령을 사용하여 조정하는 것이 좋다. 데이터베이스 접근을 제한하는 pg_hba.conf도 좀 더 강화되었다. 자세한 이야기는 man pg_hba.conf로 살펴보기 바란다. 그리고 pg_passwd 인증 데이터베이스가 UNIX 시스템의 /etc/passwd와는 분리가 되었다. 파이톤 인터페이스가 PyGreSQL 2.0으로 새로워졌으며, PostgreSQL에서 사용하는 형변환 연산자인 '::'는 상수가 아닌 값도 형변환 할 수 있게 되었다. 이외에도 시스템 카탈로그(데이터사전)도 몇 가지 변화되었으며, 여러 가지의 버그가 수정되었다. 자세한 내용은 소스 디렉토리의 HISTORY 파일을 참고하기 바란다.

PostgreSQL에서 멀티 바이트 사용하기

    PostgreSQL 6.3의 설치방법은 이전의 6.x 버전과 동일하다. PostgreSQL에서 다중 바이트를 처리할 수 있도록 해주는 패치를 만들고 있는 Tatsuo Ishiu씨는 이번에도 어김없이 6.3 버전을 위한 패치를 재빠르게 공급하고 있다. 다음 사이트에서 이러한 패치를 얻을 수 있다.

    ftp://ftp.sra.co.jp/pub/cmd/postgres/6.3/patches/6.3mbPL1.patch.gz

    PostgreSQL 6.3의 압축을 푼 곳에서 다음과 같이 패치하면 된다.

    #gzip -dc 6.3mbPL1.patch.gz | patch -p0

    이번 패치판은 예전의 일본어를 비롯한 2바이트권 사용자만을 위한 것이 아니라 그야말로 멀티바이트 사용자를 위한 것이다. 즉, 기존의 7비트에서 제대로 표현하기 힘든 독일어 등의 8비트 문자 사용자와 2바이트의 동양권 문자 사용자와 2바이트 이상의 UNICODE도 지원하고 있다. 이 패치를 적용하려면 src/Makefile.custom 파일에 예전의 JP=1 대신, MB=EUC_KR이라는 한 줄을 적어주면 된다. 사용할 수 있는 코드는 표와 같다.

    AIX

    Digital Unix (Alpha)

    FreeBSD

    FreeSCO

    HP-UX (PA-RISC)

    IRIX

    Linux (Alpha, m68k, x86)

    NetBSD (x86?)

    OpenStep/NextStep (x86)
    [xscanimage/xcam 테스트 안됨]

    OS/2

    Solaris (SPARC)

    SunOS (SPARC)


    사용하고자 하는 코드의 이름을 src/Makefile.custom에 적어주면 된다. 이들 패치를 적용시키면, 테이블명과 테이블 내의 컬럼명, 정규 표현식 검색에서 한글을 사용할 수 있다.

ECPG - C에서 사용하는 내장 SQL

    ECPG는 PostgreSQL의 Pro*C이다. 앞서 잠시 언급한 PL은 주로 SQL 측면에서 바라본 확장이지만 ECPG는 C언어에서 바라본 SQL 확장이다. 즉, ECPG는 C언어로 데이터베이스를 처리하는 데 편리함을 주기 위해서 개발된 전처리기이다. 사실 ECPG의 장점은 데이터베이스의 값을 C 변수로 쉽게 가져올 수 있다는 것이다. LIBPQ를 사용하는 응용 프로그램에서는 데이터베이스의 특정 테이블, 칼럼의 값을 얻으려면 여러 번 작업을 하여야 한다. 이러한 반복작업과 지루한 작업을 제거하여 더욱 손쉽게 데이터베이스 처리를 할 수 있도록 도와주는 도구가 바로 ECPG이다. ECPG는 다른 RDBMS의 내장 SQL로 쓰여진 프로그램을 PostgreSQL로 포팅하는 데에도 사용할 수 있다.

ECPG - 내장 SQL의 전처리기

    ECPG는 전처리기이다. 내장 SQL로 작성한 소스를 순수 C 소스로 변환하는 일을 한다.
    SQL을 충실하게 공부해본 경험이 있다면, 다음과 비슷한 코드를 본 기억이 있을 것이다.

    EXEC SQL BEGIN DECLARE SECTION;
    int i;
    EXEC SQL END DECLARE SECTION;

    EXEC SQL CREATE DATABASE testbase;
    EXEC SQL GRANT DBA TO mylove;

    EXEC SQL CREATE TABLE foo (
    bar int
    );

    여기에서 앞 부분의 선언 영역만 제외한다면 나머지 EXEC SQL 다음은 우리가 익히 보아왔던 SQL 질의문장이다. 이들 코드는 ECPG 전처리기에 의해 순수 C 소스로 변환된다. 마지막 구문이 순수 C 소스로 최종적으로 변환된다면, 다음과 비슷할 것이다.

    PQexec(conn, "CREATE TABLE foo ( bar int );");

    물론, EXEC SQL과 같은 내장 SQL 구문이 ECPG를 거치면 ECPGdo라는 중간 라이브러리 함수를 거치게 된다. P함수는 이 ECPG 라이브러리 루틴내부에서 호출된다. 위에서와 같은 내장 SQL은 직접 C 함수를 작성하는 것보다 쉽다. 그리고 더욱 직관적이어서 알아보기도 명확하다. 아울러 데이터베이스의 값을 C 변수로 넘기는 것은 아주 편리하게 디자인되어 있다. ECPG는 구문 분석기로 내장 SQL 구문을 읽어들여 정해진 규칙에 따라 C 소스를 만들어 내는 역할을 하는 것이다. ECPG에서 내장 SQL 구문을 어떻게 C 소스로 변환하는지 잠시 살펴보자.

    먼저, exec sql begin declare section;과 exec sql end declare section; 사이에 있는 모든 선언 구문은 그대로 출력된다. 이 둘 사이에 있는 변수는 ECPG 내부에서 인덱스 처리된 변수 목록에 들어간다. 그리하여 내장 SQL 구문에서 이들 변수가 사용되었을 시에 적절한 처리를 하게 되는 것이다. 각각 번역되는 예는 다음과 같다.

    VARCHAR var[180];
    --> struct varchar_var { int len; char arr[180];
    } var;
    exec sql include filename;
    --> #include <filename.h>

    exec sql connect 'database';
    --> ECPGconnect("database");
    exec sql open cursor;
    --> 무시
    exec sql commit;
    --> ECPGcommit(_LINE_);
    exec sql rollback;
    --> ECPGrollback(_LINE_);

    VARCHAR는 특별한 구조체 문장으로 번역되고, include 문장은 C의 #include 문장으로 변환된다. 그리고 open cursor 문장은 그냥 무시된다. 그 외의 몇 가지 내장 SQL문은 적절한 ECPG 함수로 변환된다. 함수 내에서 _LINE_ 인자는 디버깅을 위해 전달되는 것일 뿐이다. 추가적으로, exec sql include sqlca; 구문을 사용하여 sqlca.h 헤더파일을 포함하면, 다음과 같은 구조체가 포함된다.

    struct sqlca {
    int sqlcode;
    struct {
    int sqlerrml;
    char sqlerrmc[1000];
    } sqlerrm;
    } sqlca;

    이 구조체에서 sqlca.sqlcode는 에러 상황을 반영한다. 주어진 질의와 데이터베이스 정의가 일치하지 않는 것과 같은 치명적인 에러가 발생하였을 경우에 sqlca.sqlcode의 값은 음수이다. 그렇지 않고, 더 이상 회수할 데이터가 없는 것과 같은 보통의 에러가 발생하였을 경우에 sqlca.sqlcode의 값은 양수이다. 프로그래머가 응용 프로그램을 짤 때 다음과 같은 코드를 사용하게 될 것이다.

    exec sql whenever not found do set_not_found();
    exec sql whenever sqlerror sqlprint;

    첫 번째 루틴은 말 그대로 '데이터를 발견하지 못했을 경우에는 set_not_found() 함수를 실행하라'는 것이다. '데이터를 발견하지 못했을 경우'라는 것은 평범한 에러에 속하므로 sqlca.sqlcode가 0보다 큰 경우(양수)일 때이다. 두 번째 루틴은 'SQL에서 에러가 발생하였을 경우에는 에러 문장을 출력하라'는 것이다. 여기서 SQL 에러는 보통 치명적인 에러를 일컫는다. 따라서 sqlca.sqlcode가 0보다 작은 경우(음수)일 때이다. sqlca.sqlcode를 검사하는 시점은 매번의 사건이 발생한 직후이다. 따라서 위의 두 내장 SQL문은 출력되는 C 코드 내에서 다음과 같은 문장으로 이벤트가 발생한 이후마다 추가된다. 여기에서 SQLCODE는 ecpglib.h 파일에 sqlca.sqlcode로 정의되어 있다.

    if (SQLCODE > 0) set_not_found();
    if (SQLCODE < 0) sqlprint();

    내장 SQL을 C 소스로 변환하였다면, 이제 C 소스를 실행 가능한 바이너리로 컴파일해야 할 차례이다. C 소스를 컴파일하는 데에는 ECPG 라이브러리가 필요하다. libecpg.a, libecpg.so가 그것이다. 그리고 ECPG 라이브러리는 libpq를 사용한다. 따라서 순수 C 소스를 컴파일할 때에는 ECPG 라이브러리와 libpq를 순서대로 -lecg -lpq와 같이 링크시켜야 한다. ECPG 라이브러리는 정상적으로 설치하였다면 /usr/local/pgsql/lib에 있을 것이다. 이 라이브러리 루틴에는 두 개의 숨겨진 함수가 있다.

    ECPGdebug (int, FILE *stream)

    int가 0이 아니면 디버깅이 수행된다. 실제로 디버깅 정보를 수록하는 함수는 ECPGdo이다. 이 함수는 EXEC SQL COMMIT, EXEC SQL ROLLBACK, EXEC SQL CONNECT 이외의 모든 SQL 구문에서 호출된다. 호출된 ECPGdo 함수는 확장된 SQL 문자열과 입력 변수값이 삽입된 문자열을 기록하기 때문에, SQL 구문에서 에러를 검사하는데 매우 유용하다. LIBPQ가 제공하는 PQtrace 루틴보다는 훨씬 명확하고 실제 사용할 때에 많은 도움이 될 것이다.

    ECPGstatus()

    현재 데이터베이스에 접속되어 있으면 TRUE를 돌려주고, 아니면 FALSE를 돌려준다.

    testecpg.pgc

    /* testecpg.pgc - Embedded SQL ECPG의 테스트
    작성: PostgreSQL 배포본에 들어있는 testlibpq.c를 ECPG를 사용하여 재 작성함.
    내용: template 1 데이터베이스에 접속하여 pg_database 시스템 카탈로그의 내용을 출력함.
    날짜: 1998년 3월 10일
    작성자: 한동훈
    ddoch@hitel.kol.co.kr
    */
    #include <stdio.h>
    exec sql include sqlca;
    extern void ECPGdebug (int n, FILE *debug);
    exec sql whenever not found do set_not_found();
    exec sql whenever sqlerror sqlprint;
    static int not_found = 0;

    static void set_not_found() {
    not_found = 1;
    }

    int main() {
    exec sql begin declare section;
    struct ca_pg_database_struct {
    char datname[32];
    int datdba;
    char datpath[32];
    } ca_pg_database;
    exec sql end declare section;

    FILE *debug;

    if ((debug = fopen("/tmp/trace.out", "w")) != NULL) ECPGdebug(1, debug);

    exec sql connect 'template1';

    exec sql declare myportal cursor for
    select *from pg_database;

    exec sql open myportal;

    printf("%-20s%-20s%-20s\n\n",
    "데이터베이스 이름", "데이터베이스 관리자",
    "데이터베이스 경로");

    while (!not_found) {
    exec sql fetch myportal into : ca_pg_database;
    if (!not_found) {
    printf ("%-20s%-20d%-20s\n",
    ca_pg_database.datname, ca_pg_database.datdba,
    ca_pg_database.datpath);
    }

    exec sql close myportal;
    exec sql commit;

    if (debug != NULL)
    fclose(debug);
    return 0;
    }

ECPG의 설치와 기본 사용법

    ECPG를 설치하는 방법은 아주 간단하다. PostgreSQL 6.3 안에 있는 ECPG를 설치해보자. 만일 PostgreSQL이 6.3 버전 미만이라면 ECPG의 예전의 0.2 버전을 구해서 테스트 해볼 수 있긴 하지만 권하는 바는 아니다. PostgreSQL 6.3 버전에 포함된 ECPG는 1.0인데, 이 버전은 예전의 0.x 버전보다는 상당히 다듬어져서 비로소 쓸 만하게 되었다고 보기 때문이다. ECPG는 LIBPQ를 사용함으로 인해 PostgreSQL의 버전과 서로 맞아야 한다. 따라서 여기서는 PostgreSQL 6.3과 함께 ECPG를 설치하는 것으로 간주한다. PostgreSQL 6.3은 이미 설치하였다고 가정한다. postgres 계정으로 들어가서 ecpg 디렉토리로 이동해서 make install을 한다.

    $ cd postgresql-6.3/src/interfaces/ecpg
    $ make install

    ecpg 프로그램들은 /usr/local/pgsql/을 기준으로, 바이너리는 bin/에, 라이브러리는 lib/에, 헤더 파일은 include/에 설치될 것이다. ecpg에 공유 라이브러리가 포함되어 있으므로, 마지막으로 root로 들어가서 ldconfig를 수행하는 것을 잊지 말자.

    # ldconfig

    이제 ecpg를 실행시켜 보면 다음과 같은 메시지가 뜰 것이다.

    $ ecpg
    ecpg - the postgresql preprocessor, version: 1.0.0
    Usage: ecpg: [-v] [-d] [-o outout file name]
    file1 [file2]

    -v 옵션은 버전 정보를 보여주는 것이고, -d 옵션은 전처리시에 디버깅 정보를 보여준다. -o 옵션은 전처리되어서 출력될 파일의 이름을 지정해주는 것이고, 뒤의 file1...은 전처리할 내장 SQL 소스를 지정하는 것이다.

ECPG로 작성하는 예제 프로그램

    이제, ECPG로 간단한 예제 프로그램을 작성해보자. 지난 2월호에서 LIBPQ를 설명할 때 잠시 살펴본 적이 있는 testlibpq.c를 ECPG를 사용해서 동일하게 변환해 보도록 하자. testlibpq.c는 stc/test/examples/에 있다. 새롭게 재작성하는 프로그램은 testecpg.pgc라고 하겠다.
    이 프로그램의 컴파일은 다음과 같이 한다.

전처리 단계

    $ ecpg -o testecpg.c testecpg.pgc

컴파일 단계

    $ gcc -o testecpg testecpg.c -I/usr/local/pgsql/include -L/usr/local/pgsql/lib -lecpg -lpq

    일일이 이렇게 긴 명령을 입력하는 것은 번거로울 것이다. 간단하게 다음과 같은 스크립트를 사용하여 편리하게 컴파일하도록 하자.

    #!/bin/sh
    # name : ec
    CC=gcc
    CFLAGS="-lecpg -lpq -L/usr/local/pgsql/lib -I/usr/local/pgsql/include"
    ECPG_OPTIONS=
    if [ $# -le 0 ]; then
    echo "usage: $0 <ecpg source>"
    exit
    fi
    PREFIX=${1%.*}
    ecpg $ECPG_OPTIONS -o ${PREFIX}.c $1
    $cc -o $PREFIX ${PREFIX}.c $CFLAGS

    위의 파일을 ec로 만들고, chmod 명령으로 실행가능하게 만들어 놓자.

    $ chmod +x ec

    ecpg 소스를 전처리하고 컴파일하기 위해서는 그냥 ec filename으로 사용하면 된다.

    $ ./ec testecpg.pgc

    정상적으로 처리되면, 전처리된 소스는 testecpg.c로 만들어질 것이고, 실행가능한 바이너리는 testecpg로 만들어질 것이다. 컴파일된 testecpg를 실행한 결과는 다음과 같다.

    디버깅 정보를 담은 파일의 내용은 다음과 같다.

ECPG는 어디까지 와있는가?

    ECPG의 한계와 단점은 무엇인가? 조금 덩치가 큰 응용 프로그램을 ECPG로 작성하다보면 느끼겠지만 몇 가지 단점이 존재한다. ECPG 전처리기는 SQL 구문의 문법을 검사하지 못한다. 그리고 에러코드가 단순해서 좀 더 자세한 에러검사를 하기가 힘들며, 널(null)과 빈 문자열(empty)를 구분하지 못한다. 변수를 주고 받는 과정에서 편리하다고는 하지만 생각만큼 되지 않을 때가 있다. query라는 C 변수에 "select *from pg_database"라는 문자열이 포함되어 있다고 가정하자.

    exec sql :query;

    이 내장 SQL이 다음과 같이 확장되기를 기대한다면 에러가 발생한다.

    exec sql select *from pg_database;

    이게 ECPG 문제인지, 아니면 표준 Embedded SQL의 문제인지, 또는 필자의 희망사항인지는 알지 못한다. 그리고 특정 테이블의 컬럼수를 얻기에도 편리하지 않다. 해당 테이블에 대해 미리 알고 변수를 준비하고 있어야 작업을 할 수 있다.
    PostgreSQL 6.3에서 지원하는 Embedded SQL이 필자가 보기에는 어느 정도 쓸모가 있다고 본다. 그 이유는 예전의 ECPG 0.x 버전보다는 상당히 향상되었고, 프로그래머에게 편리함을 주고 있기 때문이다. 데이터베이스 프로그래밍을 잘만 기획한다면 ECPG로 libpq를 사용한 소스보다 절반 정도의 분량으로 한껏 편리하게 작업을 할 수 있을 것이다.
    PostgreSQL도 6.3 버전을 맞아 계속 발전하고 있는 것 같다. 언제 한번 환경이 갖추어지면 PostgreSQL과 전세계적으로 유명한 상용 RDBMS와의 벤치마크를 해보는 것도 유익할 것 같다. PostgreSQL 6.3과 함께 재미있는 리눅스 여행이 되길 바란다.

, .

공개 데이터베이스 서버 PostgreSQL (5)
Large Object 와 멀티미디어로 밤새기

한동훈 ddoch@hitel.kol.co.kr

1. Large Object란 무엇인가?

    Large Object는 우리말로 표현하면 거대 객체라고 할 수 있겠다. PostgreSQL에서는 기본 데이터 페이지가 8K(8192바이트)로 한정되어 있다.
    즉, 날씨라는 테이블이 있다고 한다면 그중 한 레코드의 도시, 날짜, 최저온도, 최고온도 등의 모든 필드의 길이(바이트단위)의 합이 8192를 넘을 수 없다는 것이다. 이 정도의 레코드 길이 제한은 웹 게시판 등의 소규모의 데이터를 다루기에는 충분할지 모르나, 멀티미디어 자료관리나 텍스트 게시판 등을 처리하기에는 역부족이다. 요즘 그림파일 하나만 해도 100K는 충분히 넘어가며 사운드 파일 같은 경우에는 1MB를 초과하는 경우도 많으며 동영상 같은 경우에는 더할 나위가 없을 것이다. 8K 정도면 텍스트 게시판에서 많아봐야 15페이지 정도밖에 되지 않는다. 이 정도로는 원하는 데이터나 자료를 관리하기에 부족함을 당연히 느낄 것이다. 거대객체는 PostgreSQL에서 이러한 8K 이상의 데이터를 다루기 위한 인터페이스이다.

    이제 데이터 저장측면에서 생각해보자. 우리가 다루기를 원하는 데이터가 8K 보다 크다고 하더라도 거대객체를 사용하지 않더라도 처리할 수 있는 경우가 있다. 가령 이미지를 다루는 경우를 생각해본다면, 이미지에 대한 각종 정보는 테이블에 저장하고 실제 이미지 파일은 파일 시스템에 파일로 그냥 저장할 수 있다. 이럴 경우에 사용자가 이미지 파일을 검색하기를 원한다면 테이블 정보에서 원하는 키워드로 검색을 한다.

    음, 이에 해당하는 파일 시스템상의 이미지 파일을 일반 UNIX 파일 처리 함수를 사용하여 읽어들일 수 있다. 물론, 원한다면 웹 브라우저 상에 그냥 출력할 수도 있다. 일반적으로 자주 사용하는 방법이, 이렇게 원하는 거대 데이터를 파일 시스템상에 저장하고 이에 대한 정보만 테이블에 저장하는 것이다. 이렇게만 사용한다면 거대객체를 굳이 사용할 필요는 없어진다. 그리고 이 경우에 이미지 파일은 엄밀히 따진다면 RDBMS의 외부에 있다고 볼 수 있다. 따라서 데이터베이스의 관리 측면에서 볼 때, 데이터베이스의 외부에 있음으로 인해 데이터의 무결성이 보장되지 않고, 검색 등의 작업에 있어서 데이터베이스의 효율성을 활용할 수 없다는 단점이 있다. 이러한 단점은 RDBMS의 사용에 있어서 치명적인 손해가 아닐 수 없다. 이로 인해 등장한 것이 거대객체이다.

    그림에서 알 수 있듯이 RDBMS는 내부 저장 시스템을 가지고 있다. 거대 데이터의 경우에는 파일 이미지는 데이터베이스의 내부 저장 메카니즘에 따라 저장시스템에 분리되어 효율적으로 저장되며, 이에 대한 정보는 테이블 내의 객체 식별자(oid)와 연결되어 유지하는 것이다. Oid는 PostgreSQL 서버가 데이터베이스 내의 모든 인스턴스에 자동적으로 부여하는 중복되지 않는 유일한 식별자이다. 거대객체 메카니즘을 사용할 때의 장점은 데이터의 무결성을 보장하고, 검색시에 데이터베이스의 효율성을 확보할 수 있다는 점 외에도 시간여행(time travel) 기능을 제공한다. UNIX 파일 시스템에서 파일이 지워지면 끝장이지만, PostgreSQL 데이터베이스 서버 내에서 지운 파일은 완전히 지워진게 아니라서 시간여행을 통해 고스란히 복구할 수 있다.

    PostgreSQL 서버의 거대객체의 최대 단점은 일반 UNIX 정규파일을 거대객체 내에 흡수하기 위하여 필요한 수입(import)과 수출(export)시에 많은 시간이 걸린다는 것이다. 수 메가 바이트 이상의 파일을 다룰 때, 수입은 한 번만 하겠지만 수출은 여러 번 하게 될 것이다. 사용자에게 온라인으로 데이터를 디스플레이해야 할 경우가 자주 발생할 것이기 때문이다. 이럴 경우에는 차라리 거대객체를 사용하지 않는 것이 좋다고 생각한다. 너무 자주 수출을 하게 되면 파일을 복사하는 것 이상의 자원을 잡아먹기 때문이다. 또 한가지는 누구나 느끼는 문제겠지만 좀 번거롭다는 것이다. 테이블 내에 필요한 모든 데이터를 저장하는 것이 아니라 객체 식별자만 저장하고 정작 데이터는 데이터베이스 내부의 별도의 저장시스템에 저장한다는 것도 그렇고, 수입(import)과 수출(export)이 필요하다는 점도 그렇다.

    ☞ 거대객체를 설명할 때 사용되는 import와 export를 대체할 적당한 우리말이 없는 것 같아,
    수입과 수출이라는 어감이 좋지 않은 말을 부득이 사용한다. 수입(import)은 파일 시스템
    상의 파일을 데이터베이스 서버에서 관리할 수 있도록 내부 저장 시스템에 잘게 쪼개어
    검색하기 좋도록 인덱스화하여 저장하는 것을 이야기한다. 수축(export)은 거꾸로
    데이터베이스 내부 저장 시스템에 저장되어 있는 거대객체를 외부 파일 시스템상에 출력
    하는 것을 이야기한다.

2. 멀티미디어 데이터와 검색

    굳이 멀티미디어뿐만이 아니라 8K를 넘어가는 문자, 숫자, 통계 데이터 등을 다루는 데에도 거대객체를 사용할 수 있음을 앞서 언급하였다. 그리고 이들 거대객체를 사용하는 데이터는 oid를 통해서 접근할 수 있다고 언급하였다. 멀티미디어든지 아니던지 이들 거대객체 데이터의 검색이나 수정은 모두 동일할 것이다. 거대객체에 대한 주변적인 정보는 기본 테이블에 저장할 것이지만 본격 거대객체 데이터는 테이블 내의 oid를 통해 접근될 것이기 때문이다. 따라서 이들 데이터를 검색하는 방법은 대체로 두 가지가 있다고 볼 수 있다. 단순히 주변적인 테이블 데이터를 검색하는 방법과 테이블 내의 oid 필드를 통하여 거대객체 데이터를 직접 검색하는 방법이다.

    거대객체 인터페이스를 사용하는 데이터의 유형이 여러 가지가 있겠지만 여기서는 논문을 관리하는 데이터베이스의 예를 들어보자(그 외의 멀티미디어 데이터도 마찬가지일 것이다).

    먼저, 논문이라는 테이블을 하나 만들 것이고, 필드는 대략 지은이, 날짜, 제목, 자료명, 크기, 형식, OID 정도가 될 것이다. 외부 논문 파일을 수입(import)하면서 테이블의 나머지 항목을 채우게 될 것이다. 물론 수입(import)시에 반환하는 oid를 테이블의 OID 필드에 저장함으로써 oid를 통해 거대객체에 접근할 수 있다. 이제 기본 골격은 다 만들어졌다고 가정하자(자세한 방법은 이후에 설명한다). 사용자가 검색을 원할 때에는 테이블 내의 어느 한가지 이상의 항목을 사용하겠지만, 편의를 위해 제목을 통해 검색하고 싶어한다고 가정한다. SELECT를 사용하여 논문 테이블의 제목 필드에서 원하는 레코드를 찾았다면, OID 필드를 통해 거대객체에 접근할 것이다. 이 부분에서 거대객체를 어떻게 다루느냐에 따라 두 가지 접근 방법이 있다고 할 수 있다.

    ▶ 논문이라는 거대객체에 대한 접근을 하지 않는 방법이다. 보통 이 경우가 많이 사용될 것
    이다. Oid를 통한 거대객체에 대한 직접적인 접근을 한다는 것은 이 경우에 있어서 논문을
    거대객체 인터페이스를 통해 열어서 검색하고 수정한다는 것을 의미한다. 텍스트 파일이나
    특정한 그래픽 파일일 경우에야 의미있는 일이겠지만 개별 바이트나 데이터 블록이 별
    의미없는 경우일 때에는 거대객체에 직접 접근한다는 것이 힘들 것이다. 논문 파일의 형식
    에도 여러 가지가 있을 것이다. Latex 파일 형식이나 ps 파일, 그럴리야 없겠지만 hwp
    파일, text 파일 형식도 있다고 가정해보자. 이 경우에 각각의 데이터 파일의 형식을 모르고
    서는 논문파일에 대한 직접적인 검색은 별 의미 없다고 볼 수 있다. 이렇게 직접적인 검색을
    원치 않을 경우에, 논문 파일을 수출(export)하여 파일 시스템상의 정규파일로 만들어서
    사용자가 내려받기를 할 수 있도록 만들 수 있을 것이다.

    ▶ 다른 하나의 방법은 논문(거대객체)에 직접적으로 접근하는 방법이다. 논문이 ps 파일
    이라면 ps 형태의 논문 중에서 특정한 부분을 찾아서 발췌할 수 있는 사용자 정의함수를
    만들어서 사용할 수 있다. 다른 하나의 예를 든다면 얼굴 사진을 저장하고 있는 얼굴사진
    이라는 거대객체 유형이 있다고 한다면, match라는 함수를 얼굴사진이라는 데이터에
    대해서 선언할 수 있다. match 함수는 사용자가 입력하는 사진에 가장 근접하는 얼굴사진
    을찾아내는 역할을 하도록 만들 수 있다는 것이다. 즉, 거대객체 내부에서 데이터 파일의
    직접적인 검색이나 편집등은 특정 작업에 대한 사용자 함수를 정의하여 사용하는 것이
    좋다. 문제는 이들 사용자 정의 함수를 작성하는 것이 해당 분야에 대한 약간의 전문지식을
    필요로 한다는 것이다. 가령 JPEG 형식의 그림을 다룬다면 JPEG 형식에 대해서 알아야
    한다. 이러한 사용자 정의 인터페이스를 스스로 만들 수 있다면, 검색뿐만이 아니라 수정
    까지도 가능하다. 이러한 일을 가능하게 해주는 것은 거대객체 인터페이스이다.
    거대객체 인터페이스는 UNIX의 저수준 파일 기술자를 다루는 것과 아주 유사하다.

3. 거대객체의 인터페이스

    거대객체에 대한 인터페이스는 주로 C언어로 작성된 함수이다. 거대객체와 관련된 모든 작업은 거대객체 인터페이스를 통해야 한다. PostgreSQL에서는 SQL에서 유용하게 사용할 수 있는 수입(import)과 수출(export) 함수를 지원한다. 이들 함수는 PostgreSQL 내부에 미리 등록이 되어 있어 바로 사용할 수 있다. 여기서는 먼저 PostgreSQL 내부에 등록되어 있는 수입(import)과 수출(export)에 관련된 함수를 살펴보도록 한다.

1) SQL 내부 등록 함수

    PostgreSQL 내부에 등록되어 있는 함수는 lo_export 이다. lo_import는 수입을 담당하고 lo_export는 수출을 담당한다. lo_import의 반환값은 수입을 한 거대객체의 oid(객체 식별자 타입)이고 lo_export의 반환값은 수행결과의 성공여부를 나타내는 참 거짓값이다. 이 두 함수를 사용하면 수입과 수출은 C함수를 사용하지 않고도 할 수 있으므로, 거대객체에 대한 직접적인 검색을 하지 않는 경우라면 거대객체의 C 인터페이스를 전혀 사용하지 않아도 거대객체를 기본적으로 다룰 수 있다.

    CREATE TABLE 사진 (
    이름 text,
    사진 oid
    );

    INSERT INTO 사진 (이름, 사진)
    VALUES ('내얼굴', lo_import('/etc/fstab'));

    SELECT lo_export (사진.사진, '/tmp/fstab') from 사진
    WHERE 이름 = '내얼굴';


    ☞ oid는 SQL에서 사용되는 데이터 타입이고, Oid는 C함수에서 사용되는 데이터 타입으로 서로 동일한 것이다.

2) C 인터페이스

    거대객체의 C 인터페이스에는 거대객체를 다루는 데 필요한 거의 모든 함수들이 포함되어 있다. 앞서 언급한 거대객체의 수입과 수출을 다루는 lo_import와 lo_export, 거대객체를 만들고(lo_creat), 열고(lo_open), 닫고(lo_close), 이동하고(lo_lseek), 쓰는(lo_write) 함수들이 있다. 거대객체를 다루는 기본적인 함수는 표준 UNIX의 저급 파일 기술자를 다루는 함수들과 그 이름의 앞부분에서 lo_만 빼면 동일하다. 여러분들은 이러한 lo_류의 함수를 사용하여 마치 UNIX 파일 시스템에서 파일을 만들고(creat), 열고(open), 이동하고(lseek), 쓰고(write), 닫는(close)것처럼 이러한 거대객체 인터페이스를 통해 거대객체에 대해서도 동일한 작업을 할 수 있다. UNIX 파일과 거대객체라는 대상만 다를 뿐이지 적용되는 개념과 실제 사용법은 동일하므로 거대객체라는 사실에 대해 너무 걱정하지 말기를 바란다. 그럼 거대객체의 C 인터페이스 함수를 하나씩 살펴보고, 이후에 이들 함수를 사용하는 간단한 예제를 보도록 하겠다.

    거대객체의 생성

    Oid lo_creat (PGconn *conn, int mode)

    이 함수는 거대객체를 생성하여 그 oid(객체 식별자 타입)를 돌려준다. UNIX의 creat 함수와 마찬가지로 그냥 빈 거대객체를 생성한다고 보아도 좋다. 실제로는 거의 쓰이지 않는다. 거대객체의 생성은 주로 수입(lo_import)을 통하여 만들어지기 때문이다.

    거대객체의 수입

    Oid lo_import (PGconn *conn, text *filename)

    앞서 설명한 거대객체를 수입(import)하는 함수이다. filename 이라 불리는 파일을 수입하여, 그 oid(객체 식별자 타입)를 반환한다. 파일 시스템상에 존재하는 외부 데이터 파일을 데이터베이스 내부에 가지고 올 때 사용된다.

    거대객체의 수출

    int lo_export (PGconn *conn, Oid large_object_id, text *filename)

    앞서 설명한 거대객체를 수출(export)하는 함수이다. oid(거대객체 식별자)로 large_object_id를 가지는 거대객체를 외부 파일 시스템상에 filename이라는 이름으로 끄집어 낸다. 되돌림 값은 수출의 성공여부를 나타내는 참 거짓값이다.

    거대객체를 열기

    int lo_open (PGconn *conn, Oid large_object_id, int mode, ...)

    large_object_id는 열려는 거대객체 식별자이다. mode는 읽기(INV_READ)나 쓰기(INV_WRITE), 또는 둘 다를 지정할 수 있다. 존재하지 않는 거대객체를 lo_open으로 열 수는 없다. lo_open의 반환값은 거대객체 기술자(UNIX의 저수준 파일 기술자와 동일시하기 바란다)로, 나중의 lo_read, lo_write, lo_write, lo_lseek, lo_tell, lo_close 함수에서 인자로 사용된다.

    거대객체에서 읽어오기

    int lo_read (PGconn *conn, int fd, char *buf, int len)

    거대객체 기술자 fd에서 len 길이만큼 읽어서 buf에 저장한다.

    거대객체에 쓰기

    int lo_write (PGconn *conn, int fd, char *buf, int len)

    이 함수는 buf에서 len 길이만큼의 바이트를 현재의 거대객체 기술자인 fd에 쓴다. 당연한 이야기겠지만 fd는 이전의 lo_open에서 반환된 값이어야 한다. lo_write의 반환값은 실제로 쓰여진 바이트 수이다. 만일 에러가 발생하였다면 음수를 돌려준다.

    거대객체 내부에서의 이동

    int lo_lseek (PGconn *conn, int fd, int offset, int whence)

    이 함수는 기술자 fd로 표현되는 거대객체 내부에서 현재의 읽기/쓰기 위치를 변경하는 데 사용된다. offset은 이동거리이다. whence는 UNIX의 lseek처럼 SEEK_SET, SEEK_CUR, SEEK_END를 사용할 수 있다.

    거대객체 내부에서의 위치 파악

    int lo_tell (PGconn *conn, int fd)

    현재의 거대객체의 기술자인 fd에서의 위치를 돌려준다.

    거대객체를 닫기

    int lo_close (PGconn *conn, int fd)

    이 함수는 거대객체 기술자 fd를 닫는 역할을 한다. lo_close가 성공적으로 수행되면 0을 돌려주고, 에러시에는 음수값을 반환한다.

    거대객체를 없애기

    int lo_unlink (PGconn *conn, Oid large_object_id)

    거대객체 식별자로 large_object_id 값을 가지는 거대객체를 내부 저장시스템에서 제거한다.

    지금까지 간단하게 거대객체 인터페이스를 살펴보았다. 이들 함수를 사용하기 위해서는 libpq/libpq-fs.h 헤더파일을 포함하여야 한다. 물론 libpq와 링크를 시키는 것이 필요하다.
    이제 이러한 인터페이스를 사용하여 어떻게 실전 어플리케이션을 작성할 것인지에 대해서 알아보자.

4. 예제 프로그램의 작성

1) 거대객체를 사용할 것인가 말 것인가

    거대객체를 사용하는 응용 프로그램을 어떻게 만들 것인가 하는 문제는 전체적인 설계를 어떻게 할 것인가 하는 문제다. 앞에서도 살펴보았지만 거대객체라고 해서 특별한 것은 없다.

    PostgreSQL 내부에서 8K 이하의 데이터는 효율적으로 다룰 수 있고, 그 이상의 데이터는 거대객체라는 인터페이스를 사용하는 것뿐이다. 그리고 거대객체에 대한 접근을 보장하기 위해 바로 위에서 설명한 인터페이스를 제공하는 것이다. 여러분들은 거대객체를 사용하는 어플리케이션을 만들고 싶어할 것이다.

    여러분이 원하는 데이터를 다루기 위한 데이터베이스를 구축하는데 거대객체가 필요할 수도 있다. 필자의 경우에도 실무 프로그래밍에서는 대부분 거대객체를 사용하지 않았었다. 이유는 단 한가지, 귀찮다는 것이었다. 일거리가 많아진다는 것은 프로그래머에게는 정말 귀찮은 일이다. 여러분이 사용하고자 하는 데이터가 단순히 덩치만 큰 데이터라면 거대객체를 사용하지 않아도 상관없다. 파일의 이름만 테이블에 저장해놓고 필요할 시에는 그 파일을 보여주면 된다.

    또한 수출(export)이 자주 발생할 것이라면 이 역시 거대객체를 사용하는 것이 단점이 된다. 수 메가나 되는 용량의 파일을 하루에 여러 수십 번, 아니 여러 수백 번씩 수출(export)을 하기 위해 복사를 하는 것은 어찌보면 대단히 불만스러울 수도 있다. 이런 경우라면 거대객체를 사용하지 않아도 된다는 것을 이야기하고 싶다. 하지만 여러분들의 데이터베이스가 무결성을 보장하는 것을 원하고, 대형의 멀티미디어 데이터를 데이터베이스 서버의 장악권에 확실히 두고 싶다면, 그리고 시간여행, 인덱스와 같은 데이터베이스 서버의 효율적인 기능을 활용하고 싶다면 거대객체를 사용하자. 아울러 여러분들의 멀티미디어 데이터에 대한 확실한 정보를 가지고 있다면, 혹은 이에 대한 검색, 수정 추가에 대한 사용자 정의 함수를 만들 수만 있다면, 거대객체 인터페이스는 여러분들의 과제를 해결하는데 최상의 솔루션을 제공할 것이다. 믿거나 말거나.

2) 간단한 예제 프로그램

    앞서 설명한 인터페이스 함수를 사용하여 간단한 예제를 만들어 보자. PostgreSQL 배포 패키지 안의 src/examples/ 디렉토리에 testlo.c라는 예제 프로그램이 있다. 다음에 나오는 예제 프로그램과 함께 살펴본다면 상당한 도움이 되리라 본다. 그리고 인터페이스 함수에 대한 설명을 즉석에서 보고자 한다면 man large_objects 명령으로 매뉴얼 페이지를 참고하는 것도 좋다. 매뉴얼 페이지에는 몇 개의 함수가 빠져있다. lo_tell이나 lo_unlink등이 그렇다. 본 예제프로그램에서는 거대객체 인터페이스 함수의 사용법을 보이기 위한 목적으로 작성한 것이다.
    예제 프로그램의 이름은 lo.c이다. 거대객체를 생성하고, 사용자에게서 입력을 받아서 거대객체 함수를 사용하여 몇 가지 테스트를 한 것이다. 예제 프로그램에서 진행순서는 다음과 같다.

    거대객체의 생성 - lo_creat
    거대객체를 열기 - lo_open
    사용자의 입력을 받아서 거대객체에 쓰기 - lo_write
    기술자 포인터를 마지막에 위치시켜서 거대객체의 크기를 구하기 - lo_lseek, lo_tell
    거대객체의 앞부분의 내용을 읽어서 출력하기 - lo_read
    거대객체의 기술자를 닫기 - lo_close
    거대객체를 export하기 - lo_export
    거대객체를 삭제하기 - lo_unlink

    〔 리스트 1 〕lo.c

    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>

    #include "libpq-fe.h"
    #include "libpq/libpq-fs.h"

    void main (int argc, char *argv[]) {
    char *database;
    PGconn *conn;
    PGresult *res;

    Oid large_object_id;
    int lo_fd, n, tmp;
    char buf[BUFSIZ];

    if (argc != 2) {
    fprintf(stderr, "사용법: %s database_name\n", argv[0]);
    exit(1);
    }

    database = argv[1];

    /* 데이터베이스에 접속한다. */
    conn = PQsetdb(NULL, NULL, NULL, NULL, database);

    if (PQstatus(conn) == CONNECTION_BAD) {
    fprintf(stderr, "데이터베이스 '%s'에
    접속 실패.\n", database);
    fprintf(stderr, "%s", PQerrorMessage(conn));
    PQfinish(conn);
    exit(1);
    }
    res = PQexec(conn, "begin"); /* 트랜잭션 블록의 시작 */
    PQclear(res);

    /* 빈 거대객체를 만들고, 열어서 데이터를 쓰고, export를 한 다음 지운다. */

    /* 거대객체를 생성해서 객체 식별자를 얻는다. */
    large_object_id = lo_creat(conn, INV_READ|INV_WRITE);
    if (large_object_id == 0) { /* 에러 */
    fprintf(stderr, "거대객체를 만들 수 없습니다.\n");
    }
    printf("거대객체의 식별자 값은 %d입니다.\n", large_object_id);

    /* 거대객체를 열어서 기술자를 얻는다. */
    lo_fd = lo_open(conn, large_object_id, INV_WRITE | INV_READ);
    if (lo_fd < 0) {
    fprintf(stderr, "거대객체를 열 수 없습니다.\n");
    }

    /* 사용자의 입력을 받아서 거대객체에 집어 넣는다. */
    printf("거대객체에 들어갈 내용을 입력해 주십시오.\n");
    printf("마치려면 CTRL-D를 입력하십시오.\n\n");
    while ((n = read(0, buf, BUFSIZ)) > 0) {
    tmp = lo_write(conn, lo_fd, buf, n);
    if (tmp < n) {
    fprintf(stderr, "Error while reading from stdin. \n");
    }
    }

    /* 거대객체의 크기를 구한다. */
    tmp = lo_lseek(conn, lo_fd, 0, SEEK_END);
    printf("입력한 내용은 총 %d 바이트입니다.\n\n", tmp);
    memset(buf, 0, BUFSIZ);

    /* 거대객체에서 일부를 읽어들인다. */
    lo_lseek(conn, lo_fd, 0, SEEK_SET);
    lo_read(conn, lo_fd, buf, 80);
    printf("거대객체에 저장된 내용 중 처음 80 바이트는 다음과 같습니다.\n\n");
    printf("%s\n\n", buf);
    lo_close(conn, lo_fd);

    /* 거대객체를 export한 다음, 삭제한다. */
    printf("입력한 거대객체를 /tmp/lo_%d 파일로 export한 다음 내부의" "
    거대객체를 삭제합니다.\n", getpid());
    memset(buf, 0, BUFSIZ);
    sprintf(buf, "/tmp/lo_%d", getpid());

    lo_export(conn, large_object_id, buf);
    tmp = lo_unlink(conn, large_object_id);
    if (tmp >= 0) {
    printf("삭제 성공.\n");
    }
    res = PQexec(conn, "end"); /* 트랜잭션 블록의 끝 */

    /* 정리하고 마친다. */
    PQclear(res);
    PQfinish(conn);
    exit(0);
    }

    컴파일은 다음과 같이 한다.

    $ gcc -o lo lo.c -I/usr/local/pgsql/include -L/usr/local/pgsql/lib -lpq -g -Wall;

    테스트 결과는 다음과 같다.

5. 논문 데이터베이스 구축 예제

    앞서 잠시 언급한 논문 데이터베이스를 하나 구축해보자. 처음에는 간단한 XPM 파일을 예제로 다루려고 했으나 좀 더 실용적인 논문 데이터베이스를 다루어 보도록 하겠다. 그리고 거대객체를 사용할 시에 거대객체에 대한 저급적인 접근은 피하도록 하겠다. 이유는 논문 데이터 파일의 형식에 대한 어떠한 제한을 두기에도 좀 그렇고, 저급적인 접근을 하려면 어느 정도 시간 동안의 연구가 필요하다고 보여지기 때문이다.

    논문 데이터베이스를 웹과 연동을 할 경우도 무시하지 못하는데, 이럴 경우에 논문 파일 형식에 대한 CGI 필터링이 필요할 지도 모른다. 가까운 예를 하나 들면 PS파일을 CGI 프로그램에서 HTML 파일로 변환하여 실시간으로 웹 브라우저 상에 보여주는 경우를 들 수 있다. (또 하나의 예로 특정 회사의 전유물이긴 하지만 HWP 형식의 파일을 실시간으로 웹 브라우저 상에 HTML로 출력해주는 HWP CGI 서버 등이 있다.) 웹 브라우저 상에서 곧바로 처리하지 못하는 파일 형식을 가지는 거대객체를 웹 상에 출력하려면 이러한 중간 CGI 인터페이스가 필요할 것이다.

    이러한 변환은 여러분의 몫이다. 여기서는 거대객체에 대해서는 import와 export만을 사용할 것이다. 나머지 검색은 논문 데이터에 대한 정보를 저장하는 테이블을 통해서 이루어질 것이다.

    먼저 데이터베이스에 필요한 테이블을 정의하여 보자. 데이터베이스는 mydb를 사용할 것이고, 테이블은 논문이라고 명명할 것이다. 논문 테이블은 다음과 같이 정의한다.

    CREATE TABLE 논문 (
    지은이 text,
    날짜 date,
    제목 text,
    자료명 text,
    크기 int4,
    형식 text,
    식별자 oid
    );

    이외에도 테이블에 논문에 대한 설명 필드나 분류코드 등이 들어갈 법하지만 간단하게 하기 위해서 생략하도록 한다. 이제 데이터를 입력해보자. psql의 \copy 명령을 사용하면 보통의 데이터는 쉽게 집어 넣을 수 있는데, lo_import 때문에 되지 않는 것 같다. 귀찮더라도 다음의 명령을 일일이 쳐주기 바란다. 아니면 파일에 담아서 psql의 \i 명령을 사용해도 좋다. 입력 인터페이스는 실제로는 웹을 통하는 것이 제일 간편하고 현대적인 방식일 것이다. 그 외에도 PostgreSQL을 지원하는 X용 프론트 엔드 프로그램을 사용하면 쉽게 할 수 있는 것도 있으니 찾아보기 바란다. lo_import 함수 안의 파일명은 미리 파일 시스템상에 아무 것이나 복사해서 만들어 두도록 하자.

    INSERT INTO 논문 VALUES ('미식가', '1980-02-20', '바퀴벌레 뒷다리에 대한 영양학적 분석', '바퀴벌레.html', 234500, 'HTML', lo_import('/tmp/바퀴벌레.ps'));

    INSERT INTO 논문 VALUES ('전일수', '1995-05-05', '객체지향 데이터 모델에서 클라스 계층 및 클라스 합성 계층의 집중화 기법', 'object_class.html', 101010, 'HTML', lo_import('/tmp/object_class.html'));

    INSERT INTO 논문 VALUES ('동석호', '1995-06-30', 'B형 간염 바이러스의 발현 및 증식에 있어서 X 유전자의 역할', 'xgene.html', 41098, 'HTML', lo_import('/tmp/xgene.html'));

    INSERT INTO 논문 VALUES ('장태성', '1995-09-25', '2단계 디스크 캐쉬 모형에 관한 연구', 'disk_cache.html', 141342, 'HTML', lo_import('/tmp/disk_cache.html'));

    INSERT INTO 논문 VALUES ('안이기', '1995-10-15', '비정상 박리에 관한 실험적 연구', 'tunnel.html', 874345, 'HTML', lo_import('/tmp/tunnel.html'));

    이제 테이블에 데이터가 삽입되었을 것이다. 데이터베이스의 사용자가 특정 인물이나 날짜기간, 제목 등에 대한 검색을 통해서 원하는 논문을 찾고 싶다고 하자. 실제로는 여기에 몇 가지 더 정보가 추가되고 온라인으로 논문 내용을 검색하도록 하는 것이 좋지만, 그럴 여력이 없으므로 간단한 SELECT만 해보도록 한다.

    눈으로 확인할 수 있는 것처럼, 거대객체를 외부 파일로 export하였다. 원한다면 사용자가 볼 수 있도록 하거나 내려받기를 하도록 하는 것도 생각해볼 수 있다.

    PHP/FI 와 거대객체

    얼마 전에 PHP/FI에서 거대객체를 다룰 수 있는 방법이 없느냐는 어느 분의 질문이 있었는데, 깊이 생각해보지 않아서 답변을 드리지 못했다. 결론은 PHP/FI에서 거대객체에 대한 인터페이스는 SQL에서 사용할 수 있는 lo_export와 lo_import 밖에 없다는 것이다. 이것만 사용해도 거대객체의 입출력은 할 수 있으므로 아쉬운 대로 사용할 수는 있다. 하지만 거대객체에 대한 저급적인 접근 방법을 PHP/FI에서 사용할 수 있는 방법은 없다. PHP/FI에서 사용자가 자신의 C언어 정의 함수를 사용할 수 있는 방법이 있긴 하지만, PostgreSQL의 C언어 거대객체 인터페이스를 PHP/FI에 가져다 붙인다는 것은 상당히 힘들다고 볼 수 있다. 결론적으로, PHP/FI에서도 SQL에서와 마찬가지로 멀티미디어 데이터를 데이터베이스 내부에 둘 수는 있다. 하지만 저급적인 접근은 할 수 없으므로 제한이 있을 수밖에 없다.

    지워진 거대객체의 복구

    어쩌다가 데이터가 지워지는 원치 않는 경우가 있을 수 있다. 트랜잭션이 살릴 수 없는 데이터도 시간여행을 통해서 라면 살릴 수 있다. 다만 테이블이나 데이터베이스의 삭제가 아닌 테이블 내의 데이터의 삭제이어야 하고, vacuum이 실행되기 전이어야 한다. 이럴 경우에는 다음과 같은 SQL문을 사용하여 원상 복구시킬 수 있다.

    insert into 논문 select *from 논문['epoch', 'now'];

6. 요약

    지금까지 간단하게 PostgreSQL의 거대객체 인터페이스에 관해 살펴보았다. 거대객체에 대한 개념과 거대객체를 사용할 때의 장단점, 거대객체 함수 인터페이스와 실제 사용 예를 살펴보았다. 그리고 상당히 초보적이긴 하지만 간단한 멀티미디어(라고 할 수도 없을 것같지만) 데이터를 거대객체에서 어떻게 다루는지에 대해서도 사례를 통해서 살펴보았다. 사실 이것은 아주 기초적인 것에 지나지 않는다. 실제로 이러한 데이터를 다루려면 웹 인터페이스를 사용하는 것이 현실적이고, 그렇다고 한다면 웹 상에서 멀티미디어 데이터를 출력할 수 있어야 한다. 그게 HTML이 되던 SGML이 되든 간에 데이터베이스를 사용하고자 하는 이에게 최대한의 내용을 보여주는 것이 중요하다고 볼 수 있다.

    처음에는 간단하나마 웹 상에서 보여줄 수 있는 자그마한 거대객체 어플리케이션을 작성해보려고 했지만, 짧은 시간 안에 이것저것 하려니 뜻대로 되지 않았다. 정말 리눅스에서는 할 것이 너무 많아 평생을 해도 다 못할 것 같다. :) 아, 이제 아쉬운 설 연휴도 PostgreSQL과 함께 끝나가고 있다.

    리눅스 월드에 연재되는 PostgreSQL 기사의 프로그램 소스와 지난 12월에 연재되었던 PHP/FI 프로그램 소스는 필자의 홈페이지(http://free.sense.co.kr/CGI_database/)와 하이텔 리눅스 동호회 자료실에 정기적으로 올려두고 있다. 소스가 필요하신 분은 참고하기 바란다.


, .