GNOME3ApplicationDevelopmentBeginnersGuide:Chapter 03
- 제 3 장 프로그래밍 언어
프로그래밍 언어
GNOME3가 생기기 오래 전에는 C가 GNOME 애플리케이션을 생성하는 첫 프로그래밍 언어였는데, 이후로 C++, C#, Python, 그 외의 언어들이 생겨났다. GNOME이 버전 3에 가까이 발전하자 Vala와 JavaScript는 더욱 더 유명해지고 중요한 부분이 되었다. JavaScript는 오랜 시간 사용되었고 사람들도 이에 익숙하다. Vala는 비교적 새로운 언어에 해당하지만 이것으로 작성한 프로그램은 속도가 빠르고, Java나 C#에서 취하는 구문과 비슷한 구문을 가진다는 이유로 GNOME 애플리케이션 개발자들 사이에서 유명세를 얻고 있다.
이번 장에서는 두 개의 프로그래밍 언어를 논하겠다. 먼저 JavaScript와 Vala의 기본 내용을 빠르게 살펴보고 이 언어들을 이용해 애플리케이션을 만들기에 충분한 지식을 습득하도록 하자.
본 장에서 학습할 내용은 다음과 같다.
- JavaScript에서 데이터 유형 다루기
- JavaScript에서 반복 제어하기
- JavaScript에서 기본적인 객체 지향 프로그래밍
- JavaScript 객체 구성하기
- JavaScript 프로토타입 사용하기
- JavaScript 프로그램 모듈화(modularize)하기
- Vala member 접근 명시자
- Vala에서 기본 데이터 유형
- Gee 컬렉션 라이브러리
이제 Seed를 이용해 JavaScript 구현을 먼저 시작해보자.
JavaScript를 이용한 GNOME 프로그래밍
사실 GNOME에는 두 가지 상충되는 JavaScript 구현이 있는데, 둘의 차이는 사용되는 엔진에 있다. 첫 번째 구현은 Gjs로, Mozilla가 생성한 JavaScript 엔진인 Spidermonkey를 기반으로 한다. 두 번째는 우리가 살펴볼 Seed다. 이는 WebKit의 JavaScript 코어 엔진을 기반으로 한다. Seed를 선택한 이유는 공식적으로 GNOME3에서 사용되기 때문이다.
실행하기 - Seed에 인사하기
이제 Seed가 어떻게 작동하는지 살펴볼 때가 되었다.
- GNOME Shell의 Activities 에 위치한 Terminal 메뉴 혹은 Applications | Accessories | Terminal 으로부터 terminal을 실행하라.
- Terminal 콘솔에 seed를 입력하여 Seed를 실행하라.
$ seed
- Seed 프롬프트를 입력하고 있다.
>
- 아래 코드를 입력하고 리턴 키를 입력한다.
print("Hello, world")
- 텍스트가 출력된다.
Hello, world
무슨 일이 일어났는가?
Seed는 인터프리터다. 이렇게 Seed를 실행할 때는 Seed 상호작용 모드를 입력하고 있으므로 우리가 입력한 코드의 결과를 상호작용적으로 제공함을 의미한다. 이 모드에서는 해당 셸에 유효한 JavaScript 코드는 무엇이든 입력할 수 있다. 하지만 JavaScript 프로그래밍에 익숙한 사람들은 웹 애플리케이션을 위한 코딩 시 이와 같은 코드를 입력할 수 없다. 가령 아래의 예제 코드는 입력할 수 없을 것이다.
console.log("Hello, world")
그 이유는 Seed가 document 또는 console과 같은 객체들을 제공하지 않기 때문이다. 따라서 필요한 객체를 가져오지 않는 한 기본적인 JavaScript만 허용된다.
시도해보기 - 좀 더 많은 JavaScript 시도하기
말이 나온 김에 덧셈, 뺄셈, 변수 할당과 같은 JavaScript 코드를 셸에 입력해보자. 아래를 예로 들어보겠다.
var a=1
var b=2
b+a
a-b
리스트는 계속된다. 이 리스트는 셸의 기본 개념을 이해하기 위해 제시하였다. JavaScript의 경험이 있지만 기술이 아직 미숙하다면 시작 단계로 이용해도 좋다. 아니면 좀 더 현명하게 이것을 계산기로 사용하는 방도를 고려해도 좋다.
끝내지 않은 행을 준다고 가정하자.
var c=
프롬프트가 아래와 같이 변하는 것을 볼 것이다.
. .
이는 우리가 프롬프트를 끝낼 필요가 있으며, 그렇지 않으면 Seed가 구문 오류 메시지를 방출(spew)할 것이란 뜻이다.
이것이 끝나면 Ctrl+C 키 조합을 눌러 꺼도 되는데, 이럴 경우 시스템의 terminal 콘솔로 돌아갈 것이다.
실행하기 - Seed로 프로그램 실행하기
상호작용 모드는 실제 애플리케이션에서는 이용할 법한 접근법이 아니다. 지금부터는 코드를 파일에 넣은 후에 실행하고자 한다. 준비 되었는가?
- 1. Anjuta를 작동시켜라.
- 2. File l New를 통해 새로운 파일을 생성하라.
- 3. 아래 코드 조각을 이용해 에디터를 채워라.
#!/usr/bin/env seed print("Hello, world")
- 4. 이 파일을 hello-world.js로 저장하라. 이를 위한 새 디렉터리를 생성하고(예: hello-worldjs) 그 안에 해당 파일을 넣어라.
Create folder 버튼을 클릭하고 hello-worldjs를 입력한 후 새로 생성된 폴더를 클릭하라. |
- 5. Run 메뉴를 클릭하고 Execute를 선택하라. 작은 대화상자가 표시되면 Program 필드는 /usr/bin/seed로 채우고 Arguments 필드는 hello-world.js로 채운다. Run in terminal 옵션이 체크되어 있도록 하라.
무슨 일이 일어났는가?
이러한 프로그램 호출 방법을 스크립팅이라고 부른다. 해당 접근법을 이용하면 파일 자체가 직접 Seed에 의해 로딩되고 실행된다. 이것은 Bash, Perl, Python과 같은 다른 스크립트와 비슷한 방법을 이용한다. 첫 번째 행에서 스크립트의 인터프리터로 사용된 프로그램을 표시하기 위해 해시 뱅(#!) 기호를 사용함을 확인할 수 있을 것이다. 우리는 /usr/bin/seed를 직접 넣는 대신 /usr/bin/env 다음에 seed를 사용했다. 그 이유는 seed의 위치를 엄격하게 고정하고 싶지 않기 때문이다. env를 이용해 시스템은 Seed의 정확한 위치를 찾도록 시스템 경로를 받아들일 것이다. 예를 들어, /usr/bin 대신 /usr/local/bin에 seed가 있으면 프로그램은 여전히 작동할 것이란 뜻이다.
그렇다면 hello-world.js를 직접 입력하지 않고 여전히 Run 대화상자에 /usr/bin/seed를 넣어야 하는 이유가 궁금할 것이다. 그것은 스크립트의 실행 가능 프로퍼티를 설정하지 않았기 때문이다. Linux 관리 기술을 발휘하여 hello-world.js 를 위치시킨 디렉터리로 가서 terminal에서 아래 명령을 호출해보자.
chmod +x hello-world.js
그 다음 Run 대화상자의 Program 필드에 직접 hello-world.js를 넣어보자. Run 메뉴와 Execute메뉴를 접근할 때 더 이상 이 대화상자가 보이지 않을 것이다. Anjuta는 우리가 이미 프로그램 인자를 설정하였고 실행할 준비가 되었다고 생각하기 때문이다. 이를 다시 변경하고 싶다면 Run 메뉴로 돌아가 Program Parameters... 메뉴를 선택할 수 있다. 이 방법이 아니라면 아래와 같이 시스템 콘솔에서 직접 스크립트를 실행할 수도 있다.
./hello-world.js
느슨한 타입의 언어
JavaScript는 느슨한 타입의 프로그래밍 언어로 알려져 있다. 즉, 변수가 숫자든, 문자열이든, 배열이든 그 타입을 선언하지 않고도 사용할 수 있다는 뜻이다. 단순히 변수를 선언하기 위해 var 지시어를 이용하면(혹은 이용하지 않으면) 된다. 어떻게 작동하는지는 곧 살펴볼 것이다.
실행하기 - 데이터 유형 다루기
이제 JavaScript에서 기본적인 데이터 유형을 논하고 어떻게 상호작용할 수 있는지 살펴보자. 이것이 끝나면 필요에 따라 사용할 유형을 선택할 수 있을 것이다.
- hello-world-data-types.js라고 불리는 새로운 파일을 생성하여 아래 내용으로 채운다.
#!/usr/bin/env seed print("Hello world") var number = 1; print(number); number = number + 0.5; print(number); print(number.length); number = number + " is a number? no, it is now a string"; print(number.length); print(number); number = (number.length == 0) print(number); number = undefined print(number);
- 파일을 실행하라.
- number 변수가 출력되므로, 다음과 같다:
Hello world 1 1.5 [undefined] 1.5 is a number? no, it is now a string 39 0 [undefined]
무슨 일이 일어났는가?
두 가지 흥미로운 점이 눈에 띌 것이다. 첫째, JavaScript는 데이터 유형 간 효율적으로 이용할 수 있다. 새로운 값을 할당하면 하나의 변수 타입에서 다른 타입으로 변경이 가능하다. 두 번째는 지금까지 말했던 바와 같이 변수의 타입을 선언할 필요가 없다는 것이다. 이 코드에는 초기에 값을 1로 설정한 number 변수가 있다.
var number = 1;
print(number);
이제 number 변수는 일반 정수에 불과하다.
number = number + 0.5;
print(number);
그리고 0.5를 더하여 부동 소수점 데이터 유형으로 만든다. JavaScript는 문제 없이 이를 받아들이므로 이제 1.5라는 값을 가진다.
print(number.length);
이제 숫자의 .length 프로퍼티로 접근을 시도한다. 이 시점에서 number 변수의 타입은 숫자이고, 어떤 길이도 갖고 있지 않기 때문에 number.length 값은 [undefined]가 된다.
이제 JavaScript에는 알려지지 않은 값 개념이 있음을 확인할 수 있는데, 이는 [undefined]로 설명된다. 변수가 정의되지 않으면 그 안에 어떤 것으로도 접근할 수 없고, JavaScript는 변수를 오류로 생각하게 될 것이다.
number = number + " is a number? no, it is now a string";
print(number);
이제 number를 문자열과 결합하여 전체를 효과적으로 문자열로 만들었다. 이제 number.length가 인터프리터에 의해 정의되고 39의 값을 가지며 그 안에 39개의 문자가 있음을 보여준다.
number = (number.length == 0)
print(number);
여기서 우리는 표현식으로부터 온 Boolean 값을 number로 할당한다 (number.length==0). number.length는 0이 아니므로 표현식은 false를 리턴하고, 0으로 출력된다. 이것이 true라면 1로 출력될 것이다.
number = undefined
print(number);
이제 숫자를 undefined로 설정했는데, 이는 거꾸로된 단어(reserved word)이므로 앞에서 보인 바와 같이 설정할 수 있겠다.
재밌지 않은가?
팝 퀴즈 - 이제 값은 무엇인가?
Q1. 코드의 끝마다 모든 데이터 타입의 할당이 실행되고 난 후 number.length의 값은 무엇인가? 아래에서 선택하라 (문자열로 할당한 직후에는 39의 값을 가졌음을 기억하라).
- 숫자를 undefined으로 설정했으므로 0이 값이다.
- 숫자를 undefined으로 설정했으므로 undefined가 값이다.
- 정의되지 않은 값으로부터 .length로 접근을 시도하므로 JavaScript는 오류라고 생각할 것이다.
반복 제어하기
프로그래밍 시 거의 대부분의 경우 특정 코드 부분을 반복적으로 실행해야 한다. 이는 코드 내에 반복 제어(루프 또는 반복 제어로 알려짐)를 포함시킴으로써 실행한다. JavaScript에서는 꽤 쉽다.
실행하기 - 반복 제어하기
반복을 제어하려면 아래 단계를 실행한다.
- hello-world-iteration.js라는 새로운 파일을 생성하여 아래 코드를 넣어라.
#!/usr/bin/env seed print("Hello, world") for (i = 0; i < 10; i ++) { print("Iteration number #" + i); }
- 파일을 실행하라.
- 텍스트가 10회 출력됨을 확인할 수 있다.
무슨 일이 일어났는가?
코드에서 우리는 JavaScript에게 for 루프를 이용해 10 회 반복할 것을 알린다. i의 값을 초기에 설정하기 때문에 (코드에서는 i=0) JavaScript는 1이 아니라 0부터 인덱스를 시작함을 알 수 있다. 각 반복에서 1을 i로 더한다(for 루프에서 i++ 표현식을 보면 "i 값을 1씩 증가시켜라"는 의미다). i 값이 한계, 즉 10을 깨면(break the constraint) 루프는 즉시 멈춘다. 루프의 끝에는 i 값이 10이다. 10은 10보다 적은 수가 아니므로 (코드에서는 i<10를 넣었다) 루프를 깬다. 따라서 텍스트는 0부터 10까지가 아니라 0부터 9까지 표시한다.
시도해보기 - 내려세기
이제 올려세기가 끝났다. 그렇다면 내려세기는 어떨까?
실행하기 - 배열 조작하기
배열은 동일한 타입으로 된 다수의 항목을 보유할 수 있는 박스의 집합체라고 가정해보자. 그리고 그러한 박스를 채워보자.
- hello-world-array.js라는 새로운 스크립트를 생성하고 아래와 같이 채워라.
#!/usr/bin/env seed print("Hello world") var boxes = [] for (i = 0; i < 10; i ++) { boxes[i] = i * 2; } for (i = 0; i < boxes.length; i ++) { print("Box content #" + i + " is " + boxes[i]) }
- 스크립트를 실행하라.
- 박스 번호와 내용을 명시하는 텍스트가 보일 것이다.
무슨 일이 일어났는가?
가장 먼저 할 일은 boxes를 배열로 선언하는 일이다.
var boxes = []
배열의 크기는 설정하지 않았음을 기억하라. 그저 그것이 배열이라는 것만 밝히고 JavaScript가 알아서 하도록 내버려 두었을 뿐이다. 이는 우리가 배열의 내용을 수정할 때마다 언제든 배열이 줄어들거나 늘어날 수 있기 때문이다. 박스의 내용을 채우도록 한다.
for (i = 0; i < 10; i ++) {
boxes[i] = i * 2;
}
그리고 각 박스에 값을 배열의 인덱스에 2를 곱한 값으로 설정한다. C 프로그래밍 언어에서와 같이 무언가를 할당하지 않고 인덱스 i에 내용을 직접 설정했다. 이후 배열의 내용을 출력하였다.
for (i = 0; i < boxes.length; i ++) {
print("Box content #" + i + " is " + boxes[i])
}
배열의 길이는 객체 내의 length 변수로부터 얻을 수 있다. 이번 경우 boxes.length로부터 길이를 얻을 수 있다. 따라서 배열을 채울 때마다 길이가 자동으로 조정된다.
시도해보기 - 원하는 내용으로 채워보기
앞 절에서는 박스를 숫자로 채우는 작업을 시도했는데, 다른 데이터 유형은 어떨까? 박스를 문자열, 심지어 문자열과 정수를 섞어서 채워보도록 한다. 결과가 놀라운가?
JavaScript를 이용한 객체 지향 프로그래밍[OOP]
객체 지향 프로그래밍에 이미 익숙하다면 JavaScript에서 OOP는 무언가 제한적이고, 일반적인 OOP 실제를 따르지 않다는 사실에 준비되어야 한다. 이는 언어 자체가 전체적인 OOP 언어가 아니기 때문이다. 따라서 우리는 JavaScript 가 가진 한계 내에서 OOP 개념을 조정하고자 노력 중이다.
실행하기 - JavaScript 객체 이용하기
이제 가장 중요한 부분, JavaScript 객체를 맛볼 때가 되었다. 객체는 이번 책에서 광범위하게 사용할 예정이다. 먼저 간단한 객체를 하나 소개하겠다.
- hello-world-object.js라는 새 스크립트를 생성하고 아래 코드로 채워라.
#!/usr/bin/env seed print("Hello world") var book = {}; print(book); print(book.isbn); book.isbn = "xxxx-1234-1234"; book.title = "A somewhat interesting book" print(book); print(book.isbn); print(book.title);
- 스크립트를 실행하라.
- 출력된 값을 확인하라.
Hello world [object Object] [undefined] [object Object] xxxx-1234-1234 A somewhat interesting book
무슨 일이 일어났는가?
여느 데이터 유형과 마찬가지로 변수를 쉽게 객체로 할당할 수 있다.
var book = {};
이 행에서 우리는 book을 빈 객체로 정의한다. 여기서 객체는 중괄호로 초기화된다. 이 객체는 가장 단순한 형태에 속한다.
print(book);
이는 객체가 Object 타입임을 알리는 [object Object]를 출력한다.
print(book.isbn);
여기서 객체의 .isbn 프로퍼티로 접근을 시도하지만 비어 있기 때문에 처음엔 [undefined]가 된다.
book.isbn = "xxxx-1234-1234";
book.title = "A somewhat interesting book"
여기서 객체의 프로퍼티에 다른 값을 할당한다. 이 시점에서는 객체 안에 어떤 것이든 넣어도 좋으며, 객체를 사용하기 전에 초기화하거나 선언하지 않아도 된다.
print(book);
이때 다시 book 변수를 출력하는데, 여전히 Object 타입의 객체라고 말하고 있다.
print(book.isbn);
print(book.title);
하지만 값이 할당되고 출력이 가능해지므로 상황은 변한 셈이다.
권력엔 책임이 따른다
아마도 눈치를 이미 챘겠지만 JavaScript에서는 데이터 유형과 관련해 어떤 행위든 마음대로 할 수 있다. 하지만 이렇게 힘이 많을 때에는 코드의 정확도를 철저하게 검사해야 한다. JavaScript를 이용해 엉성하게 프로그래밍을 할 경우 코드가 증가하면서 오류를 추적하는 일이 더 힘들어지기 때문에 끔찍하게 변한다. JavaScript는 대·소문자에 민감하기 때문에 오류를 추적하기가 더 힘들어질 것이다. 코드 어딘가에 아래와 같은 행을 설정했다고 가정하자.
book.authorFirstName = "Random Joe"
그리고 코드의 다른 부분에서 이 행을 이용해 변수를 수정해보자.
book.authorFirstname = "Another Joe"
우리는 객체 안에 원하는 것을 마음대로 설정할 수 있기 때문에 JavaScript는 별다른 불평을 하지 않을 것이다. 하지만 앞에서와 같이 오식의 실수를 하지 않도록 코드를 두 번, 아니, 세 번 검사해야 할 책임이 있다. 그건 그렇고 지금 이야기 중인 버그는 발견했는가?
시도해보기 - 객체를 채우는 또 다른 방법
앞의 코드 일부를 아래와 같은 모습으로 수정하고 어떤 일이 일어나는지 살펴보라.
var book = {
isbn:"xxxx-1234-1234",
title:"A somewhat interesting book"
}
이제 우리는 다른 방식으로 ISBN과 제목을 객체 내에서 정의하고 있다. 여기서는 등호 대신 콜론을 이용하고, 정의부 사이에 콤마를 넣었다. 이러한 표기법을 JSON(JavaScript 객체 표기법)이라 부른다.
객체 구성하기
가장 단순한 형태의 JavaScript 객체를 사용했으니 좀 더 복잡한 객체를 사용해보자. 이는 JavaScript를 이용한 객체 지향 프로그래밍의 모험이 되겠다.
실행하기 - 생성자 갖고 놀기
객체의 생성을 이야기할 때는 생성자라는 특수 함수를 호출함을 의미한다. 그 방법을 살펴보자.
- hello-world-constructor.js라는 새 파일을 생성하고 아래 코드로 채워라.
#!/usr/bin/env seed print("Hello world") var Book = function(isbn, title) { this.isbn = isbn; this.title = title; } book = new Book("1234", "A good title"); print(book.isbn); print(book.title);
- 파일을 실행하라.
- 출력된 값을 확인하라.
Hello world 1234 A good title
무슨 일이 일어났는가?
앞의 코드와 사실상 비슷하지만, 먼저 클래스로 정의한 다음 객체로 인스턴스화한다는 점에서 차이가 있다.
var Book = function(isbn, title) {
this.isbn = isbn;
this.title = title;
}
이는 Book 클래스의 생성자다. 그 안에서 우리는 생성자 함수 내에 인자로 전달된 isbn 변수를 .isbn 프로퍼티에 할당한다. .title 프로퍼티에도 같은 일이 발생한다.
book = new Book("1234", "A good title");
여기서 제공된 인자로 Book 클래스를 인스턴스화함으로써 book(모두 소문자다!)이라는 새로운 변수를 생성한다.
print(book.isbn);
print(book.title);
이제 .isbn과 .title 의 값이 출력되는 것을 볼 수 있다.
클래스와 객체
클래스는 앞에 소개한 코드에 포함된 var Book = function(..) {...} 처럼 하나의 정의에 불과하며, 후에 new 연산자를 이용해 실제로 인스턴스화하기 전에는 객체가 아니다. 클래스가 객체가 되면 우리는 이것을 클래스의 인스턴스라고 부른다. 앞서 우리는 클래스 정의 없이 중괄호만 이용해 다른 방식으로 인스턴스화를 실행하였다.
관례상 주로 대문자와 소문자가 섞인 CamelCase를 이용해 클래스를 명명하는데, 첫 번째 단어의 첫 문자는 대문자로 시작한다 (예: Book). 반대로 객체 인스턴스나 변수의 경우 첫 단어의 첫 문자에 소문자를 이용한다 (예: book).
팝 퀴즈 - 차이점이 눈에 띄는가?
아래 코드를 살펴보자.
var Circle = function(radiusInPixel) {
this.radius = radiusInPixel
}
var circle = new Circle(100);
Q1. circle은 무엇이고 Circle은 무엇인가? 아래에서 무엇이 올바른 문(statement)인가?
- Circle은 정의를 갖고 있기 때문에 클래스이고, circle은 Circle 클래스로부터 인스턴스화된 객체다.
- circle은 정의를 갖고 있기 때문에 객체이고, Circle은 circle 객체의 인스턴스다.
프로토타입 이용하기
OOP에서는 객체에 부착된 함수나 메서드를 가질 수 있다. 즉, 함수가 메모리에서 특정 객체에 한정된다는 말이다. 하나의 객체에서 어떤 함수를 호출하면 동일한 함수를 가진 동일한 타입의 다른 객체를 간섭하지 않는다는 의미다. JavaScript에서는 이러한 기능을 획득하기 위해 프로토타입을 이용한다.
실행하기 - 프로토타입 추가하기
이제 몇 가지 메서드를 클래스로 추가해보자. 여기서는 prototype 객체를 이용해 메서드를 정의해보겠다.
- hello-world-prototype.js라고 불리는 새로운 스크립트를 생성하고 아래 내용으로 채워라.
#!/usr/bin/env seed print("Hello world") var Book = function(isbn, title) { this.isbn = isbn; this.title = title; } Book.prototype = { printTitle: function(){ print("Title is " + this.title); }, printISBN: function() { print("ISBN is " + this.isbn); } } var book = new Book("1234", "A good title"); book.printTitle(); book.printISBN();
- 스크립트를 실행하라.
- 출력된 값을 확인하라.
Hello world Title is A good title ISBN is 1234
무슨 일이 일어났는가?
JavaScript 객체에서 prototype은 클래스나 객체 내의 모든 프로퍼티와 메서드를 보유하는 특수 객체다. 따라서 지금은 프로토타입을 우리만의 메서드로 채우고자 한다.
var Book = function(isbn, title) {
this.isbn = isbn;
this.title = title;
}
이 코드에는 앞에서와 동일한 생성자가 있다.
Book.prototype = {
이후 프로토타입의 선언을 시작하고, 고유의 메서드 선언으로 채울 준비를 한다.
printTitle: function(){
print("Title is " + this.title);
},
여기서 함수 본체에서 설명하듯이 첫 번째 메서드를 넣는다.
printISBN: function() {
print("ISBN is " + this.isbn);
}
메서드를 정의하기 위해 등부호 대신 콜론을 이용하고, 메서드 끝에는 콤마를 추가할 것이므로 이 행 다음에 다른 메서드나 member의 선언이 따라올 것임을 의미한다. 앞의 코드에서는 다른 방식으로 book 객체를 정의한 바 있는데, 기억하는가? |
이제 다음 메서드가 따라온다. 여기서는 콤마를 넣지 않고 정의를 끝마친다.
var book = new Book("1234", "A good title");
그 다음에 명시된 인자와 함께 Book 객체를 생성함으로써 book 변수를 선언한다.
book.printTitle();
book.printISBN();
마지막으로 메서드를 호출하여 사용한다 (메서드명 다음에 괄호를 주목하라).
시도해보기 - 더 많은 메서드 추가하기
메서드를 좀 더 추가해보는 건 어떨까? 가령 다음과 같은 메서드가 필요하다고 가정하자.
- isbn을 리턴하는 getISBN()
- 책 제목을 리턴하는 getTitle()
콜론과 콤마를 잊지 말라! |
실행하기 - 객체의 프로토타입 수정하기
앞서 언급했듯이 클래스가 아니라 객체의 프로토타입에 직접 무언가를 넣을 수도 있다. 이것은 일상생활에서 흔히 하는 일은 아니지만 알아두면 후에 용이하게 작용할 것이기에 학습하고자 한다. 우리 프로토타입에 정의된 함수를 런타임 시 다른 함수로 대체하길 원한다고 가정해보자!
- hello-world-proto.js라는 새로운 스크립트를 생성하고 아래 내용으로 채워라.
#!/usr/bin/env seed print("Hello world") var Book = function(isbn, title) { this.isbn = isbn; this.title = title; } Book.prototype = { printTitle: function(){ print("Title is " + this.title); }, printISBN: function() { print("ISBN is " + this.isbn); } } var book = new Book("1234", "A good title"); book.printTitle(); book.printISBN(); book.__proto__ = { author: "Joe Random", printAuthor: function() { print("Author is " + this.author); } } book.printAuthor(); var anotherBook = new Book("4567", "A more better title"); anotherBook.printTitle(); anotherBook.printISBN(); anotherBook.printAuthor(); // this is invalid
- 스크립트를 실행하라.
- 첫 번째 책의 저자를 출력하지만 두 번째 책의 출력은 실패함을 주목하라.
Hello world Title is A good title ISBN is 1234 Author is Joe Random Title is A more better title ISBN is 4567 ** (seed:4911): CRITICAL **: Line 39 in hello-world.js: TypeError 'undefined' is not a function (evaluating 'anotherBook. printAuthor()')
무슨 일이 일어났는가?
런타임 시 프로토타입을 수정하기 위해서는 알아야 할 작은 비밀이 하나 있다. 프로토타입은 더 이상 prototype 프로퍼티가 아니라 __proto__를 이용해야 접근할 수 있다는 점이다. 아래 행에서는 book 객체를 인스턴스화한다.
var book = new Book("1234", "A good title");
그리고 __proto__를 이용해 접근하는 프로토타입 안에 두 개의 프로퍼티를 추가한다.
book.__proto__ = {
author: "Joe Random",
printAuthor: function() {
print("Author is " + this.author);
}
}
바로 사용해보자.
book.printAuthor();
하지만 다른 인스턴스에서는 이를 실행할 수 없었다. 이유를 알겠는가? 그렇다, 우리는 book 객체만 수정했기 때문에 anotherBook 객체엔 영향을 미치지 않기 때문이다.
var anotherBook = new Book("4567", "A more better title");
anotherBook.printAuthor(); // this is invalid
팝 퀴즈 - 전역적으로 만드는 방법은?
Q1. Book 클래스로부터 생성된 모든 객체에 printAuthor 메서드를 추가하는 최선의 방법은 무엇인가?
- 생성된 모든 객체에서 __proto__에 printAuthor를 추가하면 모든 객체에서 이용 가능한 함수를 갖게 될 것이다.
- Book 클래스 프로토타입에서 printAuthor를 추가하면 Book으로부터 생성된 모든 객체는 함수를 갖게 될 것이다.
시도해보기 - 구현부의 세부 내용 변경하기
anotherBook 객체는 특별한 책을 선언하는 데에만 사용하길 원한다고 가정해보자. 너무 특별하여서 printTitle 함수에 <book-title>을 책 제목으로 하고 <book-title> is a really good title 이라는 내용을 출력하길 원한다고 치자.
anotherBook 객체 내의 __proto__ 에서 함수를 재정의하기만 하면 된다. |
모듈화
커다란 프로젝트를 구현하여 단일 스크립트 안에 넣었다고 상상해보자. 디버깅이 엄청나게 까다로워지기 때문에 머지않아 끔찍한 일이 될 것이다. 따라서 코드가 더 커지기 전에 논해보도록 하자.
실행하기 - 프로그램 모듈화하기
이제 우리 소프트웨어를 모듈화할 것이다.
- hello-world-module.js 라는 새로운 파일을 생성하여 아래 내용으로 채우자.
#!/usr/bin/env seed print("Hello world") var BookModule = imports.book var book = new BookModule.Book("1234", "A good title"); book.printTitle(); book.printISBN();
- book.js이라는 또 다른 새 스크립트를 생성하고 아래 내용으로 채워라.
var Book = function(isbn, title) { this.isbn = isbn; this.title = title; } Book.prototype = { printTitle: function(){ print("Title is " + this.title); }, printISBN: function() { print("ISBN is " + this.isbn); } }
- (book.js 가 아니라) hello-world-module.js 를 실행하라.
- 출력 내용을 확인하라.
무슨 일이 일어났는가?
출력된 결과를 살펴보면 앞의 코드와 정확히 똑같음을 볼 수 있다. 하지만 여기서는 코드를 두 개의 파일로 나누겠다.
var BookModule = imports.book
var book = new BookModule.Book("1234", "A good title");
여기서 imports 명령을 이용한 book의 결과와 함께 BookModule 변수에 부착할 것을 Seed로 요청한다. 이 때는 현재 우리 디렉터리에 book.js가 있을 것으로 기대한다. 그래야만 book.js 내 모든 객체들을 BookModule 변수로부터 접근할 수 있다. 따라서 우리는 앞의 행을 이용해 book 객체를 구성한다.
뿐만 아니라 book.js에 더 이상 해쉬뱅(hashbang) 행이 없음을 주목하라. book.js 가 아니라 hello-world-module.js 를 진입점으로 사용하기 때문에 해쉬뱅은 필요하지 않은 것이다.
이 접근법을 이용하면 파일에 객체를 배치시켜 필요할 때마다 가져올 수 있다. 메모리 사용이 효율적으로 변할 뿐 아니라 코드 구조체를 깨끗하게 유지하기도 한다.
이로써 GNOME 애플리케이션 개발 프로그래밍 언어로서 JavaScript에 관한 간략한 소개는 끝이 난다. 이제 Vala로 넘어가 보자.
Vala 알아가기
Vala는 JavaScript에 비해 새로 생긴 언어로, 그러한 개념이 생긴 이후부터 GNOME 개발에서 유일하게 사용하는 언어에 해당한다. 이것은 꽤 흥미로운 개념을 바탕으로 하는데, 바로 프로그래머들이 C#와 Java와 닮은 구문에 노출되지만 그 바닥에서는 코드가 순수 C 언어로 번역되어 바이너리로 컴파일될 것이다.
이 덕분에 GNOME 프로그래밍에 좀 더 쉽게 접근할 수 있는데, C를 이용한 GNOME 애플리케이션의 개발은 초보자들이 이해하기엔 꽤 까다롭기 때문이다. 수많은 표준(boilerplate) 코드 조각을 복사하여 자신의 소스 코드로 복사한 다음 지침서에 따라 수정해야 하는 작업이 요구되기 때문이다. Vala에선 이러한 단계가 완전히 숨겨진다.
JavaScript에서 시도했던 모험과 비슷하게 어떠한 그래픽 요소도 구현하지 않고 Vala 언어의 기본을 학습하고자 한다. Vala는 모든 특성을 갖춘(full-blown) 프로그래밍 언어로, Vala를 학습하는 내내 OOP 개념을 사용할 것이다.
이제 실험으로 사용하게 될 프로젝트를 준비해보자. 제 2장, 무기 준비하기에서 소개한 단계를 기억하는가? 좋다! 약간 내용을 변경하여 단계를 실행해보자. hello-vala를 프로젝트명으로 사용하겠다.
위의 스크린샷을 보면 Project options에서 No license를 선택해 다음으로 실행할 수정내용을 최소화하였다. Vala의 핵심을 이해하도록 간단한 텍스트 기반의 애플리케이션을 실행하길 원하므로 Use GtkBuilder for user interface 옵션을 체크 해제한다.
실행하기 - 프로그램의 진입점
애플리케이션을 처음부터 만드는 것이 무엇인지 이해할 수 있도록 생성된 코드를 모두 우리만의 코드로 대체할 것이다.
- 생성된 hello_vala.vala 파일을 편집하고 아래 내용으로 채워라.
using GLib; public class Main : Object { public Main () { } static int main (string[] args) { stdout.printf ("Hello, world\n"); return 0; } }
- Run 메뉴를 클릭하고 Execute를 선택하라.
- 출력된 텍스트를 확인하라.
Hello, world
무슨 일이 일어났는가?
Book 클래스를 살펴보는 것으로 시작한다.
using GLib;
이 행은 우리가 GLib 네임스페이스를 사용 중이라고 말한다.
public class Main : Object
이는 Main 클래스의 정의다. GLib.Object 클래스로부터 파생되었음을 표시한다. 첫 행에서 이미 GLib 네임스페이스를 사용하고 있음을 명시했기 때문에 GLib.Object라는 풀네임이 아니라 Object만 쓴다.
public Main ()
{
}
앞의 구조체는 클래스의 생성자다. 이제 빈 구조체가 생겼다.
static int main (string[] args)
{
stdout.printf ("Hello, world\n");
return 0;
}
}
이것이 우리 프로그램의 진입점이다. 정적으로 선언되면 main 함수는 애플리케이션에서 실행될 첫 번째 함수로 간주될 것이다. 이 함수가 없이는 애플리케이션을 실행할 수가 없다.
또 한 가지, 정적인 main 함수는 하나만 존재해야 하는데, 그렇지 않으면 프로그램이 컴파일하지 않을 것이기 때문이다.
시도해보기 - 생성된 C 코드 살펴보기
이제 생성된 C 코드를 src/ 디렉터리에서 이용할 수 있을 것이다. Files dock를 이용해 파일시스템을 탐색하여 hello_vala.c를 찾아라. 이를 열고 Vala가 어떻게 Vala 코드를 C 코드로 변형하는지 확인하라.
C 코드를 수정할 수도 있지만 Vala 코드를 변경할 때마다 변경된 내용을 덮어쓸 것이며, C 코드가 재생성될 것이다.
Member 접근 명시자
Vala는 member 접근 명시자 집합을 정의하는데, 이를 이용하면 다른 클래스 또는 그로부터 상속된 클래스로부터 member로 접근할 수 있을 것인지 정의할 수 있다. 이러한 용어는 사용하기 쉽고 깔끔한 애플리케이션 프로그래밍 인터페이스(API) 집합을 만들 수 있는 방법을 제공한다.
실행하기 - member 접근성 정의하기
클래스 member로의 접근성을 어떻게 명시하는지 살펴보자.
- 새로운 파일을 생성하고 src/ 디렉터리에 book.vala로 저장하라. 파일을 아래 내용으로 채워라.
using GLib; public class Book : Object { private string title; private string isbn; public Book(string isbn, string title) { this.isbn = isbn; this.title = title; } public void printISBN() { stdout.printf("%s\n", isbn); } public void printTitle() { stdout.printf("%s\n", title); } }
- 이를 프로젝트로 추가할 필요가 있다. Project 메뉴를 클릭하고 Add Source File...을 선택하라.
- 다음 대화상자에서 Target 옵션을 클릭하고 src/ 안에서 hello_vala를 찾은 다음 그 아래의 파일 선택 박스에서 book.vala를 선택하라.
- hello_vala.vala의 main 함수가 아래와 같은 모습이 되도록 수정하라.
using GLib; public class Main : Object { public Main () { var book = new Book("1234", "A new book"); book.printISBN (); } static int main (string[] args) { stdout.printf ("Hello, world\n"); var main = new Main(); return 0; } }
- 파일을 실행하라.
- 프로그램을 빌드할 수 없음을 주목하라.
무슨 일이 일어났는가?
오류 메시지를 확인하면 Book.printISBN로 호출하려는 접근을 모두 거부함을 볼 수 있다 (이러한 점 표기법은 Book 클래스로부터 printISBN member로 읽는다).
var book = new Book("1234", "A new book");
book.printISBN ();
Main 클래스 생성자의 내용은 다음과 같다. 여기서 우리는 Book을 book 변수로 인스턴스화하고 printISBN을 호출한다.
void printISBN() {
stdout.printf(isbn);
}
하지만 Book 클래스의 내용은 위와 같다. 별 문제가 없어 보이지만 클래스 함수로부터 이 함수로 접근할 수 없도록 만드는 데 중요한 내용이 빠진 것으로 밝혀졌다.
접근 명시자
Vala가 인식하는 접근 명시자의 리스트는 다음과 같다.
- private: 클래스 또는 구조체 내로 접근이 제한된다.
- public: 접근이 제한되지 않는다.
- protected: 클래스 내로 그리고 그 클래스로부터 상속된 클래스 내로 접근이 제한된다.
- internal: 패키지 내부의 클래스 내로 접근이 제한된다.
어떤 것도 명시하지 않으면 접근은 기본적으로 private으로 설정된다. 이 때문에 프로그램을 빌드할 수 없는 것이다.
팝 퀴즈 - 이를 어떻게 고칠까?
앞서 언급하였듯 printISBN 함수 앞에 어떤 명시자도 넣지 않기 때문에 private한 것으로 간주된다. 이러한 상황은 printISBN 함수 안에 올바른 접근 명시자를 넣음으로써 고칠 수 있다.
Q1. 아래 옵션 중 어떤 명시자가 올바르다고 생각하는가?
- public. Book 클래스 밖에 있는 Main 클래스로부터 접근하길 원하기 때문이다.
- 없다. 그저 Main 생성자에서 printISBN을 호출하는 방법을 수정하길 원할 뿐이다.
기본 데이터 유형
이제 나아가 이용 가능한 기본 데이터 유형을 학습할 차례이므로 문자열, 숫자, Boolean과 상호작용하는 방법을 살펴볼 것이다.
- bookstore.vala라는 새로운 파일을 생성하고 src/ 에 넣어라. 파일을 아래 내용으로 채워라.
using GLib; public class BookStore { private Book book; private double price = 0.0; private int stock = 0; public BookStore (Book book, double price, int stock) { this.book = book; this.price = price; this.stock = stock; } public int getStock() { return stock; } public void removeStock(int amount) { stock = stock - amount; } public void addStock(int amount) { stock = stock + amount; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public bool isAvailable() { return (stock > 0); } }
- 이 파일을 프로젝트로 추가하라.
- 우리 Main 클래스를 아래와 같은 모습이 되도록 수정하라.
using GLib; public class Main : Object { public Main () { var book = new Book("1234", "A new book"); book.printISBN (); var store = new BookStore(book, 4.2, 10); stdout.printf ("Initial stock is %d\n", store.getStock()); stdout.printf ("Initial price is $ %f\n", store.getPrice()); store.removeStock(4); store.setPrice(5.0); stdout.printf ("Stock is %d\n", store.getStock()); stdout.printf ("and price is now $ %f\n", store.getPrice()); var status = "still available"; if (store.isAvailable() == false) { status = "not available"; } stdout.printf ("And the book is %s\n", status); } static int main (string[] args) { stdout.printf ("Hello, world\n"); var main = new Main(); return 0; } }
- 파일을 실행하라.
- 데이터가 어떻게 조작되고 출력되는지 확인하라.
Hello, world 1234 Initial stock is 10 Initial price is $ 4.200000 Stock is 6 and price is now $ 5.000000 And the book is still available
무슨 일이 일어났는가?
호출하는 코드, Main 생성자부터 분석을 시작해보자.
var store = new BookStore(book, 4.2, 10);
BookStore 클래스로부터 새로운 store 객체를 인스턴스화한다. book 객체, 부동 소수점수, 정수로 객체를 초기화한다.
public BookStore (Book book, double price, int stock) {
앞에서와 같이 BookStore 생성자에서 인자 리스트에 데이터 유형을 명시해야 한다. 이후 Book 객체, double 정밀도의 숫자, 정수를 수락하길 원한다고 말해야 한다.
this.book = book;
this.price = price;
this.stock = stock;
다음으로 book, price, stock이라는 private member들에게 인자를 할당한다. 여기서 인자로부터 book member를 private member로부터 book member로 할당하고 싶다는 것을 표기하기 위해 this. 를 이용한다. 인자 변수를 다른 이름, 가령 bookObject로 명명한다면 this를 생략해도 좋은데, 더 이상 이름이 모호하지 않아서 bookObject가 우리의 member가 아니라 인자 리스트로부터 비롯됨을 알기 때문이다. price와 stock에도 똑같은 일이 일어난다.
stdout.printf ("Initial stock is %d\n", store.getStock());
이것이 바로 printf 를 이용해 정수를 출력하는 방식이다. %d는 정수에 대한 플레이스홀더로 사용한다.
stdout.printf ("Initial price is $ %f\n", store.getPrice());
그리고 이것은 printf를 이용해 실수를 출력하는 방식이다. 실수에 대한 플레이스홀더로 %f를 사용한다.
store.removeStock(4);
이후 스톡에서 4개의 책을 제거한다. 내부적으로 이는 아래와 같이 BookStore.removeStock에서 정의된다.
stock = stock - amount;
단순히 정수라는 이유로 수학 표현식을 이용해 뺄셈을 실행한다.
var status = "still available";
if (store.isAvailable() == false) {
status = "not available";
}
다음으로 Boolean 표현식 평가가 있다. 값이 false라면 status의 값을 변경한다. status에 대한 타입이 string일 경우 값을 그저 할당하면 된다.
stdout.printf ("And the book is %s\n", status);
마지막으로 우리 문자열 값을 printf에 넣기 위해 %s를 플레이스홀더로 사용한다.
Gee, 이게 뭘까?
Gee는 Vala로 쓴 컬렉션 라이브러리다. 컬렉션의 기본 타입으로는 리스트, 세트, 맵이 있다. 이들은 배열과 비슷하지만 강력한 기능들을 더 많이 갖고 있다.
실행하기 - Gee 라이브러리 추가하기
Gee를 좀 더 가까이 살펴보도록 하자. 하지만 먼저 이것을 프로젝트에 추가해보자.
- Project 메뉴를 클릭하고 Add Library...를 선택하라.
- Select the target for the library의 src/ 에서 hello_vala를 찾아라.
- New library... 버튼을 클릭하라.
- 리스트에서 gee를 찾아 대화상자 하단에 Module 옵션에서 체크한 다음 HELLO_VALA를 찾아라. 이는 Gee를 C 컴파일 단계로 추가함을 의미한다. 기본적으로 이 단계는 Gee를 빌드 시스템으로 추가하도록 configure.ac 파일을 수정하는 것이다.
- 이후 Files dock의 /src 디렉터리에서 Makefile.am 을 찾아 열어라. hello_vala_VALAFLAGS stanza를 찾아 아래와 같은 내용으로 수정하라.
hello_vala_VALAFLAGS = \ --pkg gtk+-3.0 \ --pkg gee-1.0
- Makefile.am 파일을 저장하고 닫아라. 이는 Gee를 Vala 컴파일 단계로 추가한다는 의미다.
- Build 를 클릭하고 Clean Project를 선택하라. 빌드 시스템이 준비한 모든 스크립트와 생성된 코드로부터 프로젝트를 삭제하여(clean) Makefile.am 과 configure.ac 에서 변경한 내용을 모두 정리할 수 있을 것이다.
- 앞의 코드를 다시 실행해보라. 더 이상 오류가 발생하지 않을 것이다.
무슨 일이 일어났는가?
Gee를 프로젝트로 추가하였다. Vala에 대한 Anjuta의 지원은 아직 완벽하지 않으므로, 라이브러리를 프로젝트로 추가하기 위해서는 C 컴파일용과 Vala 컴파일용으로 두 가지 액션(방금 보인 바와 같이)을 취해야 한다. 두 단계를 거치지 않으면 Vala가 Gee 네임스페이스를 인식할 수 없을 뿐 아니라 C 컴파일러가 Gee 헤더와 라이브러리를 찾을 수 없을 것이기 때문에 프로그램을 빌드할 수가 없다.
실행하기 - Gee 실행하기
프로젝트에 Gee를 실행하고 나면 Gee가 제공하는 기능을 빠르게 확인해보자. 그 중에서 간단한 배열 리스트부터 시작해보자.
- book.vala가 아래와 같은 모습이 되도록 수정하라.
using GLib; using Gee; public class Book : Object { private string title; private string isbn; private ArrayList<string> authors; public Book(string isbn, string title) { this.isbn = isbn; this.title = title; authors = new ArrayList<string>(); } public void addAuthor(string author) { authors.add(author); } public void printISBN() { stdout.printf("%s\n", isbn); } public void printTitle() { stdout.printf("%s\n", title); } public void printAuthors() { foreach (var author in authors) { stdout.printf("Author name: %s\n", author); } } }
- Main 클래스 생성자가 아래의 내용을 포함하도록 수정하라.
var book = new Book("1234", "A new book"); book.printISBN (); book.addAuthor("Joe Random"); book.addAuthor("Joe Random Jr."); book.printAuthors();
- 위를 실행하라.
- 책의 모든 저자를 출력하는지 확인하라.
Hello, world 1234 Author name: Joe Random Author name: Joe Random Jr. Initial stock is 10 Initial price is $ 4.200000 Stock is 6 and price is now $ 5.000000 And the book is still available
무슨 일이 일어났는가?
Gee가 제공하는 많은 컬렉션 데이터 구조체들 중 하나인 배열 리스트를 활용하고자 한다.
using Gee;
Gee를 사용하기 위해서는 먼저 우리가 Gee 네임스페이스를 사용하고 있음을 선언한다.
이는 사실 생략할 수도 있지만 모든 Gee 클래스의 앞에는 Gee. 라는 접두사를 항상 추가해야 한다. |
이제 Book 클래스에서 member 선언을 살펴보자.
public class Book : Object {
private string title;
private string isbn;
private ArrayList<string> authors;
꺾쇠 괄호로 된 구조체는 일반적(generics) 프로그래밍이라 부른다. 이는 데이터 구조체에 포함된 데이터(지금 맥락에서는 ArrayList)는 일반적이란 의미다. 정수 타입의 배열이 있다면 ArrayList<int> 라는 식으로 넣을 것이다. 따라서 이러한 특정 행에 우리는 string 타입의 내용을 가진 ArrayList를 갖고 있으며, authors라는 이름으로 된 리스트를 호출한다. 생성자 내에서 우리는 아래의 구문을 이용해 배열 리스트를 초기화해야 한다.
public Book(string isbn, string title) {
this.isbn = isbn;
this.title = title;
authors = new ArrayList<string>();
}
이는 string 타입으로 된 내용을 가진 ArrayList 객체를 할당할 필요가 있다는 의미다. 선언만으로는 충분하지 않음을 주목한다. 이 사실을 잊어버리면 프로그램은 충돌할 것이다.
public void addAuthor(string author) {
authors.add(author);
}
여기서 우리는 ArrayList 클래스가 제공하는 add 함수를 이용한다. 이름에서 알 수 있듯이 데이터를 배열 리스트로 추가할 것인데, 문자열 내용으로 선언하고 초기화하기 때문에 string만 수락할 수 있음을 주목한다.
public void printAuthors() {
foreach (var author in authors) {
stdout.printf("Author name: %s\n", author);
}
}
}
이제 배열 리스트의 내용을 반복한다. 각 반복마다 얻은 값을 author 변수로 할당하는 동안 반복하도록 foreach 명령을 이용한다. var author in authors 표현식을 이용함을 주목하라. author 변수를 string으로 명시하지는 않지만 대신 var 키워드를 이용해 자동 변수 생성을 이용한다. 이 행에서 authors 변수의 내용에 따라 var에 타입이 할당될 것이다. authors 내용 타입은 string이기 때문에 var 키워드에 바인딩된 author 변수는 string이 될 것이다. 이러한 유형의 생성은 컬렉션 또는 데이터 구조체에 저장된 데이터 유형에 따라 클래스가 어떤 데이터 유형이든 처리할 수 있도록 일반화할 때 매우 유용하다.
선언 중에 member 초기화하기
앞의 코드에서 생성자 내에 배열 리스트를 초기화한다. 아니면 생성자 내에서 초기화하지 않고 선언 영역에서 선언하는 동안 초기화하는 대안적인 방법이 있다. 이는 아래와 같이 실행하면 된다.
private ArrayList<string> authors = new ArrayList<string>();
코드가 증가하고 하나 이상의 생성자를 갖게 되면 모든 초기화 코드를 모든 생성자로 복사해야 하기 때문에 생성자 내에서 초기화하는 방법보다 위의 대안책이 더 낫다.
실행하기 - 시그널 기다리기
Vala는 시그널을 발생시키고 기다리기 위한 구조체를 갖고 있는데, 이는 코드에 무언가가 발생하면 정보를 구독(subscribe to)하는 메커니즘에 해당한다. 우리는 시그널로 어떤 액션을 실행하는 함수를 연결함으로써 시그널을 구독할 수 있다. 어떻게 작동하는지 살펴보자.
- bookstore.vala 파일을 수정하고 두 개의 선언을 추가하라.
public class BookStore { ... public signal void stockAlert(); public signal void priceAlert();
- bookstore.vala의 removeStock과 setPrice 함수가 다음과 같은 모습이 되도록 수정하라.
public void removeStock(int amount) { stock = stock - amount; if (stock < 5) { stockAlert(); } } public void setPrice(double price) { this.price = price; if (price < 1) { priceAlert(); } }
- Main 구조체가 아래와 같은 모습이 되도록 수정하라.
public Main () { var book = new Book("1234", "A new book"); book.printISBN (); book.addAuthor("Joe Random"); book.addAuthor("Joe Random Jr."); book.printAuthors(); var store = new BookStore(book, 4.2, 10); store.stockAlert.connect(() => { stdout.printf ("Uh oh, we are going to run out stock soon!\n"); }); store.priceAlert.connect(() => { stdout.printf ("Uh oh, price is too low\n"); }); stdout.printf ("Initial stock is %d\n", store.getStock()); stdout.printf ("Initial price is $ %f\n", store.getPrice()); store.removeStock(4); store.setPrice(5.0); stdout.printf ("Stock is %d\n", store.getStock()); stdout.printf ("and price is now $ %f\n", store.getPrice()); store.removeStock(4); var status = "still available"; if (store.isAvailable() == false) { status = "not available"; } stdout.printf ("And the book is %s\n", status); store.setPrice(0.2);
- 위를 실행하라.
- 출력된 메시지를 확인하라.
Hello, world 1234 Author name: Joe Random Author name: Joe Random Jr. Initial stock is 10 Initial price is $ 4.200000 Stock is 6 and price is now $ 5.000000 Uh oh, we are going to run out stock soon! And the book is still available Uh oh, price is too low
무슨 일이 일어났는가?
스톡이 실행되고 있으며 가격이 너무 낮다고 출력된 경고 메시지는 BookStore 클래스가 출력하는 것이 아니라 Main 클래스에 의해 출력된다. 이는 Main 클래스가 시그널을 구독하는 시나리오를 가정하며, Main 클래스가 시그널로부터 정보를 수신하면 무언가 조치를 취할 것이다.
public signal void stockAlert();
public signal void priceAlert();
먼저 시그널을 발행하려는 클래스에서 시그널을 정의해야 한다. BookStore에서 우리는 두 개의 시그널을 선언한다. signal 키워드로 된 메서드 시그니처만 선언함을 주목한다. 따라서 함수의 본체는 선언하지 않는다. 이러한 시그널을 구독하는 객체는 발생된 시그널을 처리하도록 함수를 제공한다는 사실은 매우 중요하다.
if (stock < 5) {
stockAlert();
}
...
if (price < 1) {
priceAlert();
}
두 개의 코드 조각은 시그널을 어떻게 발생시키는지 보여준다. stock이 5보다 적으면 stockAlert 시그널을, price가 1보다 적으면 priceAlert 시그널을 발생시킨다. BookStore 클래스는 다음에 발생하는 일은 신경 쓰지 않고 시그널에 대해 알릴(announce) 뿐이다.
store.stockAlert.connect(() => {
stdout.printf ("Uh oh, we are going to run out stock soon!\n");
});
store.priceAlert.connect(() => {
stdout.printf ("Uh oh, price is too low\n");
});
여기서는 Main 클래스 구조체가 스스로를 두 개의 시그널로 연결한다. => 연산자를 이용하면 함수 본체를 제공하기 위한 구조체를 확인할 수 있다. 이러한 구조체를 클로저(closure) 또는 익명 함수(anonymous function)라고 부른다. 해당 함수의 매개변수는 => 앞에 정의되므로, 현재 맥락이라면 어떤 매개변수도 제공되지 않았음을 가리킬 것이다. 이러한 사실은 빈 괄호 내용으로 알 수 있다.
함수 본체 내에서 우리는 store 객체가 시그널을 발생시키면 무슨 일이 일어나야 하는지를 선언한다. 본문에서는 일부 경고 텍스트만 출력한다. 실제로는 네트워크의 연결을 해제하고 이미지를 표시하는 행위를 비롯해 원하는 액션을 모두 실행할 수 있다.
store.removeStock(4);
...
store.setPrice(0.2);
이제 실제 시그널이 발생하고 텍스트가 출력된다.
시도해보기 - 매개변수를 시그널에 넣기
매개변수를 시그널에 넣을 수도 있다. 그저 원하는 매개변수를 시그널 선언에 넣으면 된다. 그리고 시그널로 연결할 때 => 연산자 앞에 매개변수를 넣는다. 그렇다면 priceAlert 시그널이 하나의 매개변수, 즉 책의 가격을 매개변수로 갖도록 수정하는 건 어떨까?
요약
애플리케이션을 생성하고 곧바로 Seed와 Vala를 실행하는 작업은 쉬우면서도 빠르다. 그렇다면 이 책에서 둘 다 학습하고 사용하는 이유는 무얼까?
JavaScript는 해석형(interpreted) 언어로, 프로그램의 내용물을 확인하고 재컴파일이 필요 없이 직접 수정이 가능하다. 반면 Vala는 컴파일러형 언어에 해당한다. 소스 코드를 수정하기 위해서는 그곳에 접근할 필요가 있다. GNOME 플랫폼에 상업용 소프트웨어를 만들고자 한다면 Vala가 더 나은 선택이다.
Seed에서 JavaScript로 프로그램을 만들기란 꽤 쉬우며 Anjuta에서 프로젝트 관리를 필요로 하지 않는 반면 Vala에서는 수동으로 종속성을 처리해야 한다. 향후 Anjuta 버전에서는 이 문제가 해결되길 바란다.
이제 JavaScript와 Vala 코드의 기본적인 구조체, 즉 기본 데이터 유형을 조작하는 것부터 객체 지향 프로그래밍 개념을 사용하는 것까지 이해하게 되었다.
JavaScript 프로그래밍은 꽤 느슨한 반면 Vala는 엄격하다. 모듈화를 이용한 더 나은 코드 구조체를 활용하면 개발을 단순화하고 디버깅을 수월하게 만드는 데 도움이 될 것이다.
이 모든 내용을 이해했으니 다음 장에서 학습할 GNOME 애플리케이션 생성의 기반이 되는 GNOME 플랫폼 라이브러리를 사용할 준비가 된 셈이다.