GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 04

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 4 장 GNOME 코어 라이브러리 사용하기

GNOME 코어 라이브러리 사용하기

GNOME 코어 라이브러리는 기본 유틸리티 클래스와 함수의 집합체다. 이는 간단한 일정 변환 함수부터 가상 파일시스템 접근 관리까지 많은 일을 다룬다. 코어 라이브러리가 없었다면 GNOME은 지금처럼 강력하지 않았을 것이다. 세상에는 이러한 강력한 기능이 없어 성공하지 못한 UI 라이브러리들이 많다. 그러한 기능들을 지원하기 위해 GNOME 코어 라이브러리를 사용하는 라이브러리가 GNOME 외에도 많다는 사실은 놀라운 일이 아니다.


GNOME 코어 라이브러리는 UI 애플리케이션을 지원하기 위한 non-UI 라이브러리인 GLib과 GIO로부터 구성된다. 해당 라이브러리들은 파일, 네트워크, 타이머, 운영체제 내 다른 중요한 측면들과 프로그램을 연결한다. 이러한 지식이 없다면 아름다운 프로그램은 만들 수 있을지언정 나머지 시스템과 상호작용을 할 수 없을지도 모른다.


이번 장에서 학습할 내용은 다음과 같다.

  • GLib 메인 루프와 기본 함수
  • GObject 시그널링 시스템과 프로퍼티
  • GIO 파일, 스트림, 네트워킹
  • GSettings 설정 시스템


그럼 시작해보자.


시작하기 전에

이번 장에는 Internet이나 로컬 네트워크로 접근을 요하는 연습문제가 몇 개 실려 있다. 프로그램을 실행하기 전에 먼저 Internet이나 로컬 네트워크로 양호하게 연결되어 있는지 확인하라. 제거가능(removable) 하드웨어와 탑재 가능(mountable) 파일시스템을 필요로 하는 연습문제도 실려있다.


이번 장에서는 Vala 연습문제에 대해 좀 더 다른 방식으로 시도해볼 것이다. 논의의 특성은 서로 무관하기 때문에 하나의 프로젝트에서 계속해서 파일을 수정하는 대신 Vala의 각 연습문제를 고유의 프로젝트에서 진행한다. 따라서 Vala 연습문제마다 새로운 프로젝트를 생성하고 그 프로젝트 내에서 작업할 것이다. 본 서적에 동봉된 소스 코드와 자신의 프로젝트를 쉽게 비교할 수 있도록 각 프로젝트의 이름을 명시하겠다. 앞 장과 마찬가지로 본문에 생성된 프로젝트는 Vala GTK+(간단한) 프로젝트다. 프로젝트 프로퍼티에서 GtkBuilder support for user interface 옵션을 체크해제하고 License 옵션에서 No licence 항목을 선택한다.


각 연습문제에서 JavaScript 코드는 Vala 코드를 따르고, 각 연습문제는 하나의 파일로 저장된다. JavaScript 코드의 기능은 정확히 같을 것이다. 따라서 Vala와 JavaScript 코드 중 하나를 이용하거나 둘 다 이용해도 좋다.


GLib 메인 루프

GLib는 메인 이벤트 루프로서 다양한 소스로부터 들어오는 이벤트를 처리한다. 이러한 이벤트 루프를 이용하면 이벤트를 포착하여 필요한 처리를 실행할 수 있다.


실행하기 - GLib 메인 루프 갖고 놀기

GLib 메인 루프를 소개하겠다.


1. core-mainloop라는 새로운 Vala 프로젝트를 생성하고 Main 클래스에 아래 코드를 이용하라.

using GLib;

public class Main : Object
{
    int counter = 0;

    bool printCounter() {
        stdout.printf("%d\n", counter++);
        return true;
    }

    public Main ()
    {
        Timeout.add(1000, printCounter);
    }

    static int main (string[] args)
    {
        Main main = new Main();
        var loop = new MainLoop();
        loop.run ();
        return 0;
    }
}


2. 그리고 아래는 JavaScript 코드에 해당하는 내용으로, 스크립트를 core-mainloop.js로 명명할 수 있다.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    init: function() {
        var counter = 0;
        this.printCounter = function() {
            Seed.printf("%d", counter++);
            return true;
        };
        GLib.timeout_add(0, 1000, this.printCounter);
    }
});
var main = new Main();
var context = GLib.main_context_default();
var loop = new GLib.MainLoop.c_new(context);
loop.run();


3. 프로그램을 실행하라. 프로그램이 카운터를 출력하고 계속 실행 중임을 확인할 수 있는가? 실행을 중지하는 유일한 방법은 Ctrl+C 키를 누르는 것이다.


무슨 일이 일어났는가?

이벤트의 유일한 소스인 timeout으로 GLib 메인 루프를 설정하였다.


먼저 counter 변수를 0으로 설정한다.

int counter = 0;


counter 변수의 값을 출력하기 위해 printCounter 라는 함수를 준비하고, 값을 출력한 직후 그 값을 증가시킨다. 이후 true를 리턴하여 카운터가 계속 계수하도록 한다.

bool printCounter() {
    stdout.printf("%d\n", counter++);
    return true;
}


생성자 내에서 1000 ms 간격으로 printCounter 함수를 가리키는 Timeout 객체를 생성한다. 즉, 1 초 간격으로 printCounter가 호출되고, printCounter가 true를 리턴하는 한 계속 반복하여 호출될 것이라는 의미다.

public Main ()
{
    Timeout.add(1000, printCounter);
}


main 함수에서 우리는 Main 클래스를 인스턴스화하고, MainLoop 객체를 생성하며, run을 호출한다. 이에 따라 프로그램을 수동으로 종료할 때까지 계속 실행될 것이다. 루프가 실행되면 루프로 전송된 이벤트를 수락한다. 앞에서 생성하였던 Timeout 객체가 그러한 이벤트를 생성한다. 타이머 간격이 만료될 때마다 메인 루프에게 그 사실을 알리면 메인 루프는 printCounter 함수를 호출한다.

static int main (string[] args)
{
    Main main = new Main();
    var loop = new MainLoop();
    loop.run ();
    return 0;
}


이제 JavaScript 코드를 살펴보자. 눈치 챈 사람이 있을지 모르지만 클래스 구조체가 앞 장에서 학습한 것과 약간 다르다. 여기서는 Seed Runtime의 클래스 구조를 사용했다.

GLib = imports.gi.GLib;
GObject = imports.gi.GObject;


여기서 GLib와 GObject를 가져온다. 다음으로 GObject를 기반으로 하는 Main이라는 클래스를 생성한다.


우리가 할 일은 이렇다. 아래 코드는 GType 을 Main이라 불리는 클래스로 상속(subclass)하고 객체 구조체를 인자로 전달함을 나타낸다.

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",


객체의 첫 번째 member는 parent로, 우리 클래스의 부모 클래스다. 해당 클래스에는 GObject.Object.type 을 할당하여 클래스가 앞서 가져온 GObject 모듈의 Object로부터 파생됨을 표시한다. 그리고 클래스를 Main이라고 명명한다. 이후 함수를 클래스 생성자이기도 한 init 함수에 넣는다. 클래스 member의 내용은 Vala 코드에서 살펴본 것과 비슷하며 꽤 간단하다.

    init: function() {
        var counter = 0;
        this.printCounter = function() {
            Seed.printf("%d", counter++);
            return true;
        };
        GLib.timeout_add(0, 1000, this.printCounter);
    }
});


그러면 Vala의 정적인 main 함수에 포함된 것과 동일한 코드가 된다. 여기서 Main 객체를 생성하고 GLib의 메인 루프를 생성한다.

var main = new Main();
var context = GLib.main_context_default();
var loop = new GLib.MainLoop.c_new(context);
loop.run();


시도해보기 - Timeout 중지하기

우리 프로그램은 끝이 없이 계수를 지속한다. 카운터가 10까지 도달하면 계수를 중지할 수 있는가?


Gnome3 light header.png
printCounter 리턴값을 이용하면 된다.
Gnome3 notice footer.png


아니면 카운터가 10에 도달하면 프로그램이 종료되도록 계수를 완전히 중지시킬 수 있는가?


Gnome3 light header.png
리턴값을 무시하고 코드를 재배열하며 어떻게서든 loop 객체를 Main 클래스로 전달하는 것도 가능하다. printCounter 함수에서 loop.quit()을 호출하면 10에 도달할 때마다 프로그램이 프로그램적으로 메인 루프를 깨도록 만들 수 있다.
Gnome3 notice footer.png


GObject 시그널

GObject는 우리가 소개할 수 있는 시그널링 메커니즘을 제공한다. 앞 장에서는 Vala 시그널링 시스템에 대해 논했다. 내부적으로는 사실상 GObject 시그널링 시스템을 사용하지만 언어 자체로 균일하게 통합되었음은 분명하다.


실행하기 - GObject 시그널 처리하기

JavaScript에서 이를 어떻게 실행하는지 살펴보자.


1. core-signals.js라고 불리는 새로운 스크립트를 생성하고 아래 코드로 채워라.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    signals: [
            {
                name: "alert",
                parameters: [GObject.TYPE_INT]
            }
        ],
    init: function(self) {
        var counter = 0;

        this.printCounter = function() {
            Seed.printf("%d", counter++);
            if (counter > 9) {
                self.signal.alert.emit(counter);
            }
            return true;
        };

        GLib.timeout_add(0, 1000, this.printCounter);
    }
});

var main = new Main();

var context = GLib.main_context_default();
var loop = new GLib.MainLoop.c_new(context);
main.signal.connect('alert', function(object, counter) {
    Seed.printf("Counter is %d, let's stop here", counter);
    loop.quit();
});
loop.run();


2. 스크립트를 실행하고 출력된 메시지를 확인하라.

0
1
2
3
4
5
6
7
8
9
Counter is 10, let's stop here


무슨 일이 일어났는가?

GObject 시그널링 시스템을 이용하면 객체가 발생시키는 알림을 구독할 수 있다. 단지 시그널을 수신하면 특정 액션을 실행하는 핸들러를 제공하기만 하면 된다. 여기서 우리는 이름과 매개변수가 있는 객체를 객체의 내용으로 넣음으로써 배열에서 시그널을 선언한다. 매개변수 타입은 GLib 시스템이 알고 있는 타입이다. 시그널에 어떤 매개변수도 포함하지 않은 경우 생략해도 좋다.

signals: [
        {
            name: "alert",
            parameters: [GObject.TYPE_INT]
        }
    ],
main.signal.connect('alert', function(object, counter) {
    Seed.printf("Counter is %d, let's stop here", counter);
    loop.quit();
});


이후 시그널을 구독하고, counter 값을 출력하고 메인 루프를 깨는 클로저를 제공한다. 매개변수는 클로저의 두 번째 매개변수에서 정의됨을 주목하라. 첫 번째 매개변수는 객체 자체를 위해 예약(reserve)된다.


마지막으로 시그널의 이름을 호출함으로써 시그널을 발생시킨다. self는 init 함수에서 전달하는 Main 클래스다.

if (counter > 9) {
    self.signal.alert.emit(counter);
}


위를 호출하자마자 시그널은 메인 루프에서 처리되고 그것을 구독하는 객체로 전달될 것이다.


시도해보기 - Vala에서 작성하기

마지막에서 살펴보았듯이 앞의 코드와 비교할 때 시그널 선언, 시그널 발생, 구독은 Vala에서 더 수월하다. 앞의 코드를 Vala에서 쓴다면 어떤가?


GLib 프로퍼티

프로퍼티는 저장공간 시스템에서 키-값 쌍으로, GNOME 시스템의 모든 객체에 기반 클래스인 GObject의 모든 인스턴스에서 이용할 수 있다. 프로퍼티의 유용한 기능으로, 값이 변경되면 변경 내용을 구독할 수 있다는 점을 들 수 있겠다.


실행하기 - 프로퍼티 접근하기

프로퍼티 값이 변경되는지 감시할 뿐만 아니라 프로퍼티로/로부터 값을 설정하고 얻는 방법을 학습할 것이다.


1. core-properties.js라는 새로운 스크립트를 생성하고 아래 코드 내용으로 채워라.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    properties: [
        {
            name: 'counter',
            type: GObject.TYPE_INT,
            default_value: 0,
            minimum_value: 0,
            maximum_value: 1024,
            flags: (GObject.ParamFlags.CONSTRUCT
                | GObject.ParamFlags.READABLE
                | GObject.ParamFlags.WRITABLE),
        }
    ],
    init: function(self) {
        this.print_counter = function() {
            Seed.printf("%d", self.counter++);
            return true;
        }

        this.monitor_counter = function(obj, gobject, data) {
            Seed.print("Counter value has changed to " + obj.counter);
        }

        GLib.timeout_add(0, 1000, this.print_counter);
        self.signal.connect("notify::counter", this.monitor_counter);
    }
});

var main = new Main();
var context = GLib.main_context_default();
var loop = new GLib.MainLoop.c_new(context);
loop.run();


2. Vala에 해당하는 내용은 다음과 같다 (core-properties라는 새로운 프로젝트를 생성하고 아래 코드로 core_properties.vala를 채워라).

using GLib;

public class Main : Object
{
    public int counter {
        set construct;
        get;
        default = 0;
    }

    public bool print_counter() {
        stdout.printf("%d\n", counter ++);
        return true;
    }

    public void monitor_counter() {
        stdout.printf ("Counter value has changed to %d\n", counter);
    }

    public Main ()
    {
    }

    construct {
        Timeout.add(1000, print_counter);
        notify["counter"].connect ((obj)=> {
            monitor_counter ();
        });
    }

    static int main (string[] args)
    {
        Gtk.init (ref args);
        var app = new Main ();

        Gtk.main ();
        return 0;
    }
}


3. 위를 실행하고 출력되는 메시지를 확인하라. Ctrl+C 를 누르면 프로그램이 중지된다는 사실을 기억한다.

Counter value has changed to 0
Counter value has changed to 1
0
Counter value has changed to 2
1
Counter value has changed to 3
2
Counter value has changed to 4
3
Counter value has changed to 5
4
Counter value has changed to 6
5..


무슨 일이 일어났는가?

JavaScript 코드에서는 properties 배열 안에서 프로퍼티를 선언하고, 프로퍼티의 객체로 내용을 채워야 한다.


여기서 우리는 프로퍼티가 counter라는 이름을 갖고 있으며 정수 타입임을 설명한다. 기본값, 최소값, 최대값을 선언할 필요가 있다. 플래그 또한 필요로 한다. 플래그로부터 GObject.ParamFlags.CONSTRUCT를 확인할 수 있는데, 이는 프로퍼티가 생성 단계(construction phase)에서 초기화됨을 의미한다. 즉, 객체가 생성될 때 기본값이 설정된다는 뜻이다. 또 읽고 쓰기가 가능함을 확인할 수 있다.

properties: [
        {
            name: 'counter',
            type: GObject.TYPE_INT,
            default_value: 0,
            minimum_value: 0,
            maximum_value: 1024,
            flags: (GObject.ParamFlags.CONSTRUCT
                | GObject.ParamFlags.READABLE
                | GObject.ParamFlags.WRITABLE),
        }
    ]


아래 코드를 통해 우리는 변경되는 내용을 구독할 수 있다. 시그널링 시스템을 이용하며, 시그널의 이름은 notify:: 키워드 다음에 프로퍼티명을 이용해 생성된다. 그 다음으로 프로퍼티가 변경될 때마다 시그널 핸들러를 트리거할 것이다.

self.signal.connect("notify::counter", this.monitor_counter);


여기서 이제 프로퍼티의 값을 증가시킴으로써 값을 설정한다. 이 때 값을 수정하기 때문에 value monitor가 먼저 트리거된 후에 실제 값이 printf에 의해 출력됨을 주목한다.

this.print_counter = function() {
    Seed.printf("%d", self.counter++);
    return true;
}


아래 코드는 값을 어떻게 읽는지를 보여준다.

this.monitor_counter = function(obj, gobject, data) {
    Seed.print("Counter value has changed to " + obj.counter);
}


JavaScript 코드와 반대로 Vala에서 프로퍼티의 선언은 매우 간단하다. 선언은 몇 가지가 추가된 일반적인 변수 선언과 비슷하다.

public int counter {
    set construct;
    get;
    default = 0;
}


하지만 최소값과 최대값을 설정하기 위한 메커니즘이 없다.


그 다음, 일반 변수를 읽고 쓰는 것과 마찬가지로 프로퍼티의 읽고 쓰기가 이루어짐을 확인할 수 있다. 클래스 외부에서는 member 변수를 참조하기 위해 일반적인 방법, 즉 객체명 다음에 마침표와 프로퍼티명을 붙여 이용할 수 있다.

public bool print_counter() {
    stdout.printf("%d\n", counter ++);
    return true;
}

public void monitor_counter() {
    stdout.printf ("Counter value has changed to %d\n", counter);
}


변경내용의 구독 또한 일반적인 시그널링 메커니즘을 사용하는데, 단, 시그널명 notify 다음에 사각 괄호에 프로퍼티명을 삽입하는 경우는 제외다.

notify["counter"].connect ((obj)=> {
    monitor_counter ();
}


코드에 이전에 보지 못한 새로운 내용이 눈에 띈다. 바로 construct 키워드다. 이것은 일반적인 생성자와 비슷한 객체를 생성하는 또 다른 방법이다. 이러한 생성 스타일은 실제 생성된 C 코드에서 GObject 생성이 실행되는 방법에 가깝다.


JavaScript와 Vala 코드의 이러한 차이에도 불구하고 둘 다 여느 클래스의 member와 똑같이 프로퍼티의 사용을 허용한다. 따라서 두 언어에서 main.counter로서 counter 프로퍼티로 접근이 가능하다 (객체명이 main이라고 가정하고).


팝 퀴즈 - 왜 0 값이 출력되는가

출력된 내용을 확인하면 다음과 같다.

Counter value has changed to 0


Q1. 카운터를 0으로 명시적으로 설정하지 않았다. 왜 이런 일이 발생했는가?

  1. 프로퍼티에 set construct 키워드가 정의되어 있기 때문이다.
  2. 0이 기본값이기 때문이다.


시도해보기 - 프로퍼티를 읽기 전용으로 만들기

프로퍼티가 읽기 전용이라면 더 이상 그 값을 설정할 수가 없다. 이제 counter 프로퍼티를 읽기 전용으로 만들어보자. 힌트: 프로퍼티 플래그를 활용하라.


설정 파일

많은 상황에서 우리는 프로그램이 행동하는 방식을 맞춤설정하기 위해 어떻게든 설정 파일에서 읽어와야 한다. 지금부터는 설정 파일을 이용해 GLib에서 가장 단순한 설정 메커니즘을 사용하는 방법을 학습할 것이다. 먼저 설정 파일이 있는데, 그 파일에는 애플리케이션의 이름과 버전이 포함되어 있어 프로그램 내에 어딘가에 출력할 수 있다고 가정해보자.


실행하기 - 설정 파일 읽기

우리가 할 일은 다음과 같다.


1. 설정 파일을 생성하고 core-keyfile.init라고 부르자. 그 내용은 다음과 같다.

[General]
name = "This is name"
version = 1


2. 새로운 Vala 프로젝트를 생성하고 core-keyfile로 명명하라. (src가 아니라) 프로젝트 디렉터리 안에 core-keyfile.ini 파일을 넣어라.


3. core_keyfile.vala가 아래와 같은 모습이 되도록 편집하라.

using GLib;

public class Main : Object
{
    KeyFile keyFile = null;
    public Main ()
    {
        keyFile = new KeyFile();
        keyFile.load_from_file("core-keyfile.ini", 0);
    }

    public int get_version()
    {
        return keyFile.get_integer("General", "version");
    }

    public string get_name()
    {
        return keyFile.get_string("General", "name");
    }

    static int main (string[] args)
    {
        var app = new Main ();
        stdout.printf("%s %d\n", app.get_name(), app.get_version());

        return 0;
    }
}


4. JavaScript 코드(core-keyfile.js라고 부르자)는 다음과 같은 모습이다 (.ini 파일은 스크립트와 동일한 디렉터리에 넣을 것을 기억하라).

#!/usr/bin/env seed

GLib = imports.gi.GLib;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    init: function(self) {

        this.get_name = function() {
            return this.keyFile.get_string("General", "name");
        }

        this.get_version = function() {
            return this.keyFile.get_integer("General", "version");
        }
        this.keyFile = new GLib.KeyFile.c_new();
        this.keyFile.load_from_file("core-keyfile.ini");
    }
});
var main = new Main();
Seed.printf("%s %d", main.get_name(), main.get_version());


5. 프로그램을 실행하고 출력된 내용을 확인하라.

"This is name" 1


무슨 일이 일어났는가?

우리가 사용 중인 설정 파일은 freedesktop.org의 Desktop Entry Specification 문서를 준수하는 키-값 쌍 구조체를 갖고 있다. GNOME 플랫폼에서는 이 구조체가 흔히 사용되는데, 런처에 의해 사용되는 .desktop 파일에서 활용되는 것이 보통이다. Windows를 사용하는 사람이라면 .ini 포맷과 비슷함을 발견할 것인데 이 포맷도 마찬가지로 설정에 사용된다.


GLib는 이러한 타입의 설정 파일에 접근하기 위해 KeyFile 클래스를 제공한다. 생성자에 다음과 같은 코드 조각이 발견될 것이다.

keyFile = new KeyFile();
keyFile.load_from_file("core-keyfile.ini", 0);


이는 KeyFile의 객체를 초기화하고 core-keyfile.ini 파일을 객체로 로딩한다.


core-keyfile.ini 파일을 잠시 살펴보면, 한 쌍의 사각 괄호 안에 섹션이 쓰여 있다.

[General]


그 다음에 따라오는 모든 엔트리는 섹션명을 명시함으로써 접근 가능하다. 여기서는 두 가지 메서드, get_version()과 get_name()을 제공하는데, 이는 설정 파일에서 name과 version 엔트리의 값을 얻는 데에 사용되는 단축키에 해당한다.

public int get_version()
{
    return keyFile.get_integer("General", "version");
}

public string get_name()
{
    return keyFile.get_string("General", "name");
}


메서드 내부를 보면 version 엔트리에서 정수값을 얻고 name 엔트리에서 문자열 값을 얻을 뿐이다. General 섹션으로부터 엔트리를 얻음을 확인할 수도 있다. 이러한 메서드 내에서는 값을 즉시 리턴한다.


아래 코드에서 보여주듯이 우리는 메서드로부터 값을 소모하고 출력한다.

stdout.printf("%s %d\n", app.get_name(), app.get_version());


꽤 쉽지 않은가? JavaScript 코드는 쉽고 간단하기 때문에 더 이상 설명할 필요가 없다.


시도해보기 - 다중 섹션 설정


설정 파일 내부에 더 많은 섹션을 추가하고 값으로 접근해보자. 가령 license_file과 customer_id를 엔트리로 갖고 있는 License라는 특정 섹션이 있다고 치자. 후에 이 정보를 이용해 고객이 소프트웨어를 이용할 권리를 갖고 있는지 확인한다고 가정하자.


GIO, 입/출력 라이브러리

실제 세계에서 우리 프로그램은 로컬로 저장되든 원격으로 저장되든 파일로 접근할 수 있어야 한다. 읽어야 할 파일의 집합이 있다고 가정해보자. 파일은 로컬로, 그리고 원격으로 퍼져있다. GIO는 추상적인 방식으로 파일과 상호작용하도록 API를 제공하기 때문에 이러한 파일을 쉽게 조작하도록 해준다.

실행하기 - 파일 접근하기


어떻게 작용하는지 살펴보자.


1. core-files.js라는 새로운 스크립트를 생성하고 아래의 행으로 채워라.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
Gio = imports.gi.Gio;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    init: function(self) {
        this.start = function() {
            var file = null;
            var files = ["http://en.wikipedia.org/wiki/Text_file", "core-files.js"];
            for (var i = 0; i < files.length; i++) {
                if (files[i].match(/^http:/)) {
                    file = Gio.file_new_for_uri(files[i]);
                } else {
                    file = Gio.file_new_for_path(files[i]);
                }

                var stream = file.read();
                var data_stream = new Gio.DataInputStream.c_new(stream);
                    var data = data_stream.read_until("", 0);

                    Seed.print(data)
            }
        }
    }
});

var main = new Main();
main.start();


2. 아니면 core-files라는 Vala 프로젝트를 생성할 수 있다. src/core_files.vala를 아래의 코드로 채워라.

using GLib;

public class Main : Object
{
    public Main ()
    {
    }

    public void start ()
    {
        File file = null;
        string[] files = {"http://en.wikipedia.org/wiki/Text_file", "src/core_files.vala"};

        for (var i = 0; i < files.length; i++) {
            if (files[i].has_prefix("http:")) {
                file = File.new_for_uri(files[i]);
            } else {
                file = File.new_for_path(files[i]);
            }

            var stream = file.read();
            var data_stream = new DataInputStream(stream);
            size_t data_read;
                var data = data_stream.read_until("", out data_read);
                stdout.printf(data);
        }
    }

    static int main (string[] args)
    {
        var app = new Main ();
        app.start();
        return 0;
    }
}


3. 프로그램을 실행하고, Internet에서 Wikipedia 페이지와 함께 로컬 디렉터리에서 프로그램의 소스 코드를 가져옴을 주목하라.

GNOME3 Chapter04 01.png


무슨 일이 일어났는가?

GIO는 강력한 가상 파일시스템 API 세트를 제공한다. 인터페이스 세트를 제공하여 특정 구현에 의해 확장되는 기반의 역할을 한다. 예를 들어, 여기서는 파일에 대한 함수를 정의하는 GFile 인터페이스를 사용한다. GFile API는 파일이 어디에 위치하는지, 파일이 어떻게 읽히는지 또는 다른 세부 사항은 알려주지 않는다. 그저 함수만 제공하며 그 뿐이다. 애플리케이션 개발자에게 주어지는 구체적인 구현이 사실상 모든 일을 수행한다. 이것이 무슨 뜻인지 살펴보자.


아래 코드에서는 files 배열로부터 파일 위치를 얻는다. 그리고 위치에 HTTP 프로토콜 식별자가 있는지를 확인하며, 식별자가 발견되면 file_new_for_uri를 이용해 GFile 객체를 생성하고, 식별자가 없으면 file_new_for_path를 이용한다. 로컬 파일에도 물론 file_new_for_uri를 사용할 수 있지만 파일명 뒤에 file::// 프로토콜 식별자를 붙여야 한다.

if (files[i].match(/^http:/)) {
    file = Gio.file_new_for_uri(files[i]);
} else {
    file = Gio.file_new_for_path(files[i]);
}


이는 원격 파일의 처리와 로컬 파일의 처리의 유일한 차이점이다. 이후부터는 GIO와 동일한 함수를 이용해 로컬 드라이브 또는 웹 서버로부터 파일로 접근할 수 있다.

var stream = file.read();
var data_stream = new Gio.DataInputStream.c_new(stream);
var data = data_stream.read_until("", 0);


여기서 GFileInputStream 객체를 얻기 위해 read 함수를 이용한다. API는 파일이 어디 있든 동일한 함수를 제공함을 명심한다.


결과가 되는 객체는 스트림이다. 스트림은 한 곳에서 다른 곳으로 흐르는 데이터의 시퀀스다. 스트림은 객체로 전달되어 다른 스트림이 되도록 변형하거나 소모할 수 있다.


본문에 소개된 예제의 경우 처음엔 file.read 함수로부터 스트림을 얻는다. 데이터를 쉽게 읽도록 이 스트림을 GDataInputStream으로 전송한다. 새로운 스트림을 이용해 어떤 내용도 찾지 않을 때까지, 즉 파일 끝에 도달할 때까지 데이터를 읽을 것을 GIO에게 요청한다. 이후 데이터를 화면으로 출력한다.


GIO를 이용한 네트워크 접근

GIO는 네트워크로 접근하는 데에 적절한 함수를 제공한다. 이번 절에서는 소켓 클라이언트와 서버 프로그램을 생성하는 방법을 학습할 것이다. 한 곳에서 다른 곳으로 데이터를 전송할 수 있는 간단한 채팅(chat) 프로그램을 만든다고 가정해보자.


실행하기 - 네트워크 접근하기


간략하게 JavaScript로만 실행하지만, 본 서적에 첨부된 코어 서버와 코어-클라이언트 프로젝트에서 Vala 프로그램을 살펴볼 수 있다. 그렇다면 네트워크로 접근하기 위해 어떤 단계들이 필요한지 살펴보도록 하자.


1. core-server.js라는 새로운 스크립트를 생성하여 아래의 내용으로 채워라.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
Gio = imports.gi.Gio;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    init: function(self) {
        this.process = function(connection) {
            var input = new Gio.DataInputStream.c_new (connection.get_input_stream());
            var data = input.read_upto("\n", 1);
            Seed.print("data from client: " + data);
            var output = new Gio.DataOutputStream.c_new (connection.get_output_stream());
            output.put_string(data.toUpperCase());
            output.put_string("\n");
            connection.get_output_stream().flush();
        }

        this.start = function() {
            var service = new Gio.SocketService();
            service.add_inet_port(9000, null);
            service.start();
            while (1) {
                var connection = service.accept(null);
                this.process(connection);
            }
        }
    }
});

var main = new Main();
main.start();


2. 스크립트를 실행하라. Ctrl+C를 직접 누를 때까지 프로그램은 실행될 것이다.


3. 다음으로 core-client.js라는 또 다른 스크립트를 생성하되 코드는 아래와 같다.

#!/usr/bin/env seed

GLib = imports.gi.GLib;
Gio = imports.gi.Gio;
GObject = imports.gi.GObject;

Main = new GType({
    parent: GObject.Object.type,
    name: "Main",
    init: function(self) {

        this.start = function() {
            var address = new Gio.InetAddress.from_string("127.0.0.1");
            var socket = new Gio.InetSocketAddress({address: address, port: 9000});
            var client = new Gio.SocketClient ();
                var conn = client.connect (socket);

                Seed.printf("Connected to server");

            var output = conn.get_output_stream();
            var output_stream = new Gio.DataOutputStream.c_new(output);

                var message = "Hello\n";
            output_stream.put_string(message);
            output.flush();

            var input = conn.get_input_stream();
            var input_stream = new Gio.DataInputStream.c_new(input);
            var data = input_stream.read_upto("\n", 1);
            Seed.printf("Data from server: " + data);
        }
    }
});

var main = new Main();
main.start();


4. 프로그램을 실행하고, 서버와 클라이언트 프로그램의 출력 결과를 확인하라. 서로 대화를 할 수 있게 되었다!

GNOME3 Chapter04 02.png


무슨 일이 일어났는가?

GIO는 사용법이 매우 쉬운 저수준을 비롯해 고수준 네트워킹 API까지 제공한다. 서버를 먼저 살펴보자. 포트 번호 9000에 서비스를 연다. 이는 임의의 숫자로 원하는 숫자를 사용할 수도 있으나 몇 가지 수는 제한된다.

var service = new Gio.SocketService();
service.add_inet_port(9000, null);
service.start();


개발자의 포트 번호와 동일한 번호로 서비스가 실행 중이라면 또 다른 서비스를 실행할 수 없다. 1024 보다 적은 포트 번호를 사용하길 원한다면 프로그램을 루트로 실행해야 한다. 그리고 서비스가 들어오는 연결을 수락하면 호출되는 무한 루프로 들어간다. 여기서는 연결을 처리하기 위해 우리 프로세스 함수만 호출하면 끝이다.

while (1) {
    var connection = service.accept(null);
    this.process(connection);
}


서버의 기본 활동은 이처럼 쉽게 정의된다. 하지만 처리의 세부적인 내용은 또 다르다.


이후 연결에서 들어오는 입력 스트림을 바탕으로 GDataInputStream 객체를 생성한다. 그리고 행 문자의 끝, 즉 \n 문자를 찾을 때까지 데이터를 읽는다. 이것은 하나의 문자이므로 1을 넣어야 한다. 이후 들어오는 데이터를 출력한다.

var input = new Gio.DataInputStream.c_new (connection.get_input_stream());
var data = input.read_upto("\n", 1);
Seed.print("data from client: " + data);


재미를 위해 클라이언트로 무언가 리턴하고자 한다. 연결 객체로부터 들어오는 GDataOutputStream 클래스의 객체를 생성한다. 클라이언트로부터 들어오는 데이터를 모두 대문자로 변경하고, 다시 스트림을 통해 전송한다. 마침내 파이프를 제거(flushing down the pipe)함으로써 모든 것이 전송되도록 확보한다. 서버측은 이게 모두다.

var output = new Gio.DataOutputStream.c_new (connection.get_output_stream());
output.put_string(data.toUpperCase());
output.put_string("\n");
connection.get_output_stream().flush();


클라이언트측에서는 먼저 GInetAddress의 객체를 만든다. 이후 객체는 GInetSocketAddress로 입력되어 본인이 연결하길 원하는 주소의 포트를 정의할 수 있다.

var address = new Gio.InetAddress.from_string("127.0.0.1");
var socket = new Gio.InetSocketAddress({address: address, port: 9000});


다음으로 GSocketClient에서 socket 객체를 SocketClient와 연결한다. 이후 모든 것이 괜찮아지면 서버로 연결이 구축된다.

var client = new Gio.SocketClient ();
var conn = client.connect (socket);


클라이언트측에서는 대체적으로 서버측과 반대로 프로세스가 발생한다. 여기서는 연결 객체로부터 들어오는 스크림을 바탕으로 GDataOutputStream을 먼저 생성한다. 그 다음 이곳으로 메시지를 전송하기만 하면 된다. 파이프라인에 남은 데이터가 모두 제거되도록 flush 기능도 원할 것이다.

var output = conn.get_output_stream();
var output_stream = new Gio.DataOutputStream.c_new(output);

var message = "Hello\n";
output_stream.put_string(message);
output.flush();


서버로부터 무언가 얻을 것으로 예상하므로 입력 스트림 객체를 생성한다. 새로운 행을 찾을 때까지 읽어온 다음에 데이터를 출력한다.

var input = conn.get_input_stream();
var input_stream = new Gio.DataInputStream.c_new(input);
var data = input_stream.read_upto("\n", 1);
Seed.printf("Data from server: " + data);


시도해보기 - 에코 서버 만들기

에코 서버(echo server)는 그곳으로 전송된 모든 것을 어떠한 수정도 거치지 않고 본래 상태대로 리턴하는 서비스다. 가령, "Hello"라고 전송하면 서버는 "Hello"라고 전송한다. 때로는 두 개의 호스트 사이에 연결이 제대로 되는지 확인 시 사용된다. 서버 프로그램을 에코 서버로 수정하는 건 어떨까? 무한 루프로 넣을 수도 있지만 "quit"을 입력하면 서버의 연결은 해제된다.


GSettings 이해하기

앞서 우리는 애플리케이션 설정을 읽기 위해 GLib 설정 파서(configuration parser)를 이용했다. 이제 GSettings를 이용해 좀 더 고급의 설정 시스템을 사용해보도록 하겠다. 이를 이용하면 시스템을 이용하는 모든 애플리케이션을 비롯해 GNOME 플랫폼에 걸쳐 설정(configurations)으로 접근할 수 있다.


실행하기 - GSettings 학습하기

dconf-editor 툴이 시각화한 GSettings 설정 시스템이 어떤 모습인지 살펴보자.

  1. Terminal을 시작한다.
  2. Terminal에서 dconf-editor를 실행한다.
  3. 애플리케이션 왼쪽에 org 트리를 탐색하고, gnome→desktop→background 를 선택한다.
GNOME3 Chapter04 03.png


무슨 일이 일어났는가?

GSettings는 GNOME3에서 처음으로 도입되었다. 이전에는 GConf를 이용해 설정이 처리되었다. GNOME3에서는 포함된 모든 GNOME 애플리케이션이 GSettings를 사용하도록 이동되었다. 키-값 쌍을 이용해 설정을 GConf와 GSettings로 저장한다는 개념은 똑같이 남아 있다. 하지만 GSettings는 많은 측면에서 향상되었는데, 스키마를 메타데이터로 강요함으로써 사용이 좀 더 제한된다는 점도 포함된다. GConf를 이용하면 시스템에 자유롭게 값을 저장하고 그로부터 읽어올 수 있다.


GSettings는 사실상 유일한 최상위 수준 레이어다. 그 아래에는 dconf라고 불리는 저수준의 시스템이 있는데, 값을 실제로 저장하고 읽는 일을 처리한다. 여기서 논하는 툴은 키와 값을 상속구조로 표시하므로 값을 훑어보고 읽을 수 있으며 심지어 새 값을 쓸 수도 있다 (물론 스키마가 새 값을 쓰는 것이 가능하다고 말할 경우).


스크린샷을 보면 org.gnome.desktop.background 에 다수의 엔트리가 있으며, 그 중 하나는 데스크톱 배경 이미지의 URI를 포함하는 picture-uri임을 확인할 수 있다.


GSettings API

이 서적에서는 API가 다른 관리 툴에 비해 흥미로운 툴에 해당한다. GSettings를 시각적으로 확인했다면 API를 통해 GSettings로 접근할 때가 되었다.


실행하기 - 프로그램적으로 GSettings에 접근하기

GNOME 데스크톱의 배경 이미지를 설정하기 위한 툴을 생성한다고 가정해보자. 다음과 같이 실행한다.


1. core-settings라는 새로운 Vala 프로젝트를 생성하고, core_settings.vala를 아래와 같이 수정하라.

using GLib;

public class Main : Object
{
    Settings settings = null;
    public Main ()
    {
        settings = new Settings("org.gnome.desktop.background");
    }

    public string get_bg()
    {
        if (settings == null) {
            return null;
        }

        return settings.get_string("picture-uri");
    }

    public void set_bg(string new_file)
    {
        if (settings == null) {
            return;
        }
        if (settings.set_string ("picture-uri", new_file)) {
            Settings.sync ();
        }
    }

    static int main (string[] args)
    {
        var app = new Main ();
        stdout.printf("%s\n", app.get_bg());
        app.set_bg ("file:///usr/share/backgrounds/gnome/Wood.jpg");
        return 0;
    }
}


2. JavaScript 코드는 꽤 간단하므로 아래에 일부만 소개하여 Vala 코드에서 어떤 적응(adaptation)을 필요로 하는지만 확인하도록 하겠다.

init: function(self) {
    this.settings = null;

    this.get_bg = function() {
        if (this.settings == null)
            return null;

        return this.settings.get_string("picture-uri");
    }

    this.set_bg = function(new_file) {
        if (this.settings == null)
            return;

        if (this.settings.set_string("picture-uri", new_file)) {
            Gio.Settings.sync();
        }
    }

    this.settings = new Gio.Settings({schema: 'org.gnome.desktop.background'});
}


3. 위를 실행하고 현재 데스크톱 배경 이미지에 변경된 내용을 확인하라. 현재 데스크톱 배경은 코드에 명시된 파일로 변경될 것이다.


무슨 일이 일어났는가?

이 연습문제에서는 데스크톱 소유로 이미 설치된 스키마, org.gnome.desktop.background를 사용하기 때문에 설정으로 접근하기 위해서는 API를 사용하면 된다. 자세한 내용을 살펴보자.


먼저 스키마 이름인 org.gnome.desktop.background를 명시함으로써 GSettings로 연결을 시작하면 GSettings 객체가 리턴된다.

settings = new Settings("org.gnome.desktop.background");


이후 초기화가 실패할 때를 대비해 간단한 안전망(safety net)을 넣는다. 실제 세계에서는 간단한 리턴 대신 재초기화를 실행할 수 있다.

if (settings == null) {
    return null;
}


이후 picture-uri 키 아래에서 string 타입의 값을 얻어 원하는 대로 소모할(consume) 수 있다.

return settings.get_string("picture-uri");


마지막으로 똑같은 키를 이용해 값을 설정한다. 설정이 성공하면 sync 함수를 호출함으로써 GSettings에게 그 값을 디스크로 저장하도록 요청한다. 쉽지 않은가?

if (settings.set_string ("picture-uri", new_file)) {
    Settings.sync ();
}


요약

이번 장에서는 GNOME 코어 라이브러리에 대해 많은 내용을 학습하였다. 라이브러리의 모든 내용을 다루진 않았지만 GNOME 애플리케이션을 빌드하는 데에 필요한 기본적이고 중요한 내용은 모두 살펴보았다.


이제 GLib는 다양한 소스로부터 이벤트를 처리하는 메인 루프를 제공한다는 사실을 알게 되었다. 그리고 GObject 프로퍼티와 시그널링 시스템에 관해 논했다. 또 timeout을 이용해 메인 루프가 처리하는 이벤트를 살펴보고, 프로퍼티의 값이 변경될 때 시그널도 살펴보았다. 프로그래밍 언어와 관련해, Vala는 GNOME과 좀 더 통합되고, JavaScript는 GObject 프로퍼티나 시그널을 사용하는 데에 더 많은 코드를 필요로 함을 알아냈다.


파일을 로컬과 원격으로 접근하는 실습도 해보고, GIO가 제공하는 API는 파일이 어디에 있든 접근하는 방식을 추상화하기 때문에 사용법이 매우 쉽다는 사실도 알아냈다.


GIO를 이용해 간단한 클라이언트 및 서버 채팅 프로그램을 만드는 연습도 실행했으며, 그러한 흥미로운 프로그램을 생성하기 위해서는 JavaScript와 Vala 모두 최소한의 코드량만 필요로 한다는 사실을 알아냈다.


마지막으로, GSettings에 관해 논하고 이를 이용해 GNOME 데스크톱 배경 이미지를 읽고 쓰는 연습을 했다.


GNOME 애플리케이션의 기본을 학습했으니 다음 장에서는 그래픽 프로그램의 기본 내용을 배워보겠다.


Notes