2016년 8월 1일 월요일

[Ruby] 오라클 데이터베이스를 Ruby로 치장하십시오.

오라클 데이터베이스를 Ruby로 치장하십시오.


저자 - Dustin Marx

우아함, 단순성, 다양한 라이브러리, 강력한 OO(object-oriented) 기능을 지원하는 Ruby는 일반 애플리케이션뿐 아니라 스크립트 개발을 위한 강력한 언어 환경을 제공합니다.

게시일: 2007년 2월
강 력한 기능과 신속한 개발을 지원하는 다이내믹 스크립팅 언어의 인기가 치솟고 있습니다. Ruby on Rails가 각광 받으면서, Ruby가 소프트웨어 개발 커뮤니티의 첨병으로 자리잡고 있습니다. 하지만 Ruby는 단순한 웹 개발을 위한 언어가 아닙니다. Ruby는 데이터베이스 전문가들을 위한 강력하고, 우아하고, 유연한 스크립팅 언어로도 활용됩니다. 본 문서는 Ruby의 일부 기능에 대해 자세히 살펴보고, 이 기능을 오라클 데이터베이스에서 활용하는 방법에 대해 설명하고 있습니다.

Ruby: 소프트웨어 개발 환경의 "보석"

최근 들어 Ruby on Rails 웹 개발 프레임워크가 인기를 끌면서 Ruby에 대한 관심이 높아진 것은 사실이지만, Ruby가 실제로 사용되기 시작한 것은 이미 십여 년 전의 일입니다. Ruby는 다양한 빌트-인 기능과, 풍부한 라이브러리, 검증된 환경을 제공하는 오브젝트 지향형 다이내믹 스크립팅 언어입니다.
Ruby는 정규 표현식의 지원, 변수/메소드의 다이내믹 생성 등 Perl, Python과 같은 다른 다이내믹 스크립팅 언어가 제공하는 장점을 공유하고 있습니다. Java, C++, C# 등의 정적 언어를 주로 사용해온 개발자라면 Ruby가 다이내믹 언어에 입문하기 위한 좋은 방편이 될 수 있습니다. Ruby는 Perl에 비해 작성 및 해독이 훨씬 용이합니다.
필자의 동료 카메론 헨드릭스는 "Perl은 프로그래밍 언어의 라스베가스다"라고 말합니다. 다이내믹 언어는 런타임에 우리를 놀라게 하는 특성을 보이곤 합니다. 때로는 이러한 놀라움이 긍정적인 방향으로 나타나서 우리를 마치 잭팟을 터뜨린 것 같은 기쁨에 사로잡히게 합니다. 또 경우에 따라서는 런타임 다이내믹 언어가 경악할 만한 문제를 일으키기도 하며, 이러한 문제들은 디버그 하기도 쉽지 않습니다. Ruby는 Perl의 단점들을 피해 가면서도 Perl의 장점은 그대로 제공하는 특성을 보입니다. 필자에게 있어 Ruby는 보다 예측 가능한 언어이며, 코드 개발 과정에서의 도박을 피할 수 있게 합니다.
본 문서에서는 Ruby를 인터액티브한 방식으로 사용하고, 미리 작성된 스크립트를 이용하여 일상적인 데이터베이스 관련 작업을 수행하는 방법을 설명하고 있습니다. 본 문서에서 사용되는 예제들을 통해 오라클 데이터베이스 관련 Ruby 스크립트를 쉽게 작성할 수 있을 뿐 아니라, 간단하면서도 강력한 Ruby의 기능을 활용할 수 있음을 예시하고자 합니다.
Ruby 설치. Ruby는 특히 마이크로소프트 윈도우즈 환경에서 다운로드 및 설치가 용이합니다. RubyGems는 업데이트, 익스텐션 및 추가적인 Ruby 라이브러리의 설치를 위한 툴로 활용됩니다.
Ruby의 실행. 다운로드/설치한 Ruby를 실행하는 방법은 크게 두 가지가 있습니다. 첫 번째 방법은 .rb 익스텐션을 갖는 스크립트 파일을 작성하여 ruby 커맨드를 통해 실행하는 것입니다. 두 번째 방법으로, Interactive Ruby(irb 커맨드)를 사용하여 커맨드 라인에서 인터액티브한 방식으로 Ruby 커맨드를 실행할 수 있습니다.
Ruby를 설치하지 않은 상태에서 기본적인 Ruby 사용법을 확인하고 싶으시다면, 인터액티브 환경을 제공하는 Ruby 홈 페이지를 참고하시기 바랍니다. 본 문서에서 제공되는 데이터베이스 관련 예제들을 실행할 수는 없겠지만, 기본적인 Ruby의 빌트인 기능과 문법을 테스트하실 수 있습니다.

Hello, Ruby!

막 설치를 끝낸 Ruby에서 Interactive Ruby(irb)를 실행하여 그림 1처럼 멋진 계산기로 활용하실 수 있습니다. 그림 1과 그림 2는 Interactive Ruby가 지원하는 두 가지 프롬프트 스타일(default와 simple)을 보여 주고 있습니다. 그림 1은 Ruby의 산술 연산자를 이용하는 방법을, 그림 2는 문자열 연산자와 문자열/숫자 포맷 간 변환 기능을 예시하고 있습니다. Ruby 홈 페이지에서도 그림 1과 그림 2에서 사용된 irb 기능을 테스트해 보실 수 있습니다.
Figure 1
그림 1 Ruby 인터프리터와 Ruby 수학 연산자
Figure 2
그림 2 Ruby 인터프리터와 Ruby 문자열 연산자
Ruby 는 스크립트의 작성/유지보수에 매우 유용한 글로벌 상수들을 제공합니다. 그림 3은 사전 정의된 글로벌 상수의 활용 예를 보여 주고 있습니다. 이 세 가지 글로벌 상수들은 Ruby 인터프리터의 플랫폼, 버전, 출시일 정보를 표시하고 있습니다. 그림에는 예시되지 않았지만, 글로벌 상수 ENV를 사용해서 운영 체제의 환경 변수를 확인할 수도 있습니다. 문서의 뒷부분에서 ENV의 해쉬 버전을 사용하는 방법을 설명하겠습니다.
Figure 3
그림 3 유용한 Ruby 상수
그 림 4는 Interactive Ruby의 또 다른 유용한 기능을 보여 주고 있습니다. (그림에서는 TAB 버튼을 클릭하는 모습을 직접 보여 드릴 수 없었지만) IRB 프롬프트에서 "0."을 입력한 이후 TAB 버튼을 두 번 클릭하면, 0 또는 다른 숫자 값에 대한 메소드가 그림과 같이 표시됩니다. 이 기능을 JDeveloper와 같은 Java 통합 개발 환경에서 제공하는 메소드 네임의 자동 완성 기능과 유사한 것으로 생각하시면 되겠습니다. 그림에서 숫자를 위한 ceiling (ceil), floor, round 메소드 그리고 to_s (to string), to_a (to array)와 같은 변환 메소드가 제공되고 있음을 확인할 수 있습니다.
Figure 4
그림 4 Ruby IRB ? 탭을 이용한 자동 완성 기능

Ruby와 오라클 데이터베이스의 인터액션

본 문서에서는 Ruby와 오라클 데이터베이스의 커뮤니케이션을 위해 Ruby/OCI8을 사용하고 있습니다. Ruby/OCI8은 http://rubyforge.org/projects/ruby-oci8/에 서 다운로드할 수 있습니다. 인터액티브 IRB에서 데이터베이스에 접근하기 위한 Ruby 코드를 직접 작성할 수도 있지만, 그리 깔끔한 방법은 아닙니다. 보다 좋은 방법은 Ruby 스크립트 파일에 데이터베이스 액세스 코드를 작성해 넣은 후 ruby 인터프리터 커맨드를 사용해서 이 스크립트 파일을 실행하는 것입니다.
Ruby와 오라클 DDL SQL. 먼저 새로운 테이블을 생성하는 Ruby 스크립트 파일을 작성해 보겠습니다. 우선 require 구문을 사용해서 Ruby/DBI를 사용함을 선언한 후 Ruby/DBI API를 사용합니다. 구현된 코드가 Listing 1과 같습니다.
Listing 1 createStatesTable.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
dbh.do("CREATE TABLE states (
           id CHAR(2) PRIMARY KEY,
    name VARCHAR2(15) NOT NULL,
    capital VARCHAR2(25) NOT NULL)")
dbh.disconnect 
Listing 1의 코드 중 대부분은 SQL*Plus 또는 SQL Developer에서 새로운 데이터베이스 테이블을 생성하기 위해 사용하는 SQL 코드와 동일합니다. 데이터베이스 연결 문자열은 "DBI:OCI8:ORCL" 문자열과 유저네임/패스워드(hr/hr)로 구성되어 있습니다. "DBI"는 표준적으로 정의되며, "OCI8"은 Ruby/OCI8 드라이버를, 그리고 "ORCL"은 데이터베이스 서비스를 의미합니다. 이 파일(createStatesTable.rb)은 커맨드 라인에서 간단한 createStatesTable.rb 명령으로 실행될 수 있습니다. 커맨드가 성공적으로 실행되면 아무런 출력도 제공되지 않지만, HR 스키마에 새로운 테이블이 생성된 것을 확인할 수 있습니다.
다른 데이터베이스 작업을 설명하기 전에, ruby 인터프리터 커맨드가 명령을 실행하기 전에 신택스를 점검할 수 있는 기능을 제공한다는 점을 미리 언급해 두는 것이 도움이 될 듯 합니다. 예를 들어, 테이블을 실제로 생성하지 않고 createStatesTable.rb 스크립트의 신택스만을 점검하고 싶다면 아래와 같이 -c 옵션을 사용하면 됩니다. ruby -c createStatesTable.rb.
Listing 1에서 dbh 변수의 데이터타입이 정의되어 있지 않음을 주목하시기 바랍니다. 다른 다이내믹 스크립팅 언어와 마찬가지로, Ruby는 변수의 데이터타입 선언을 요구하지 않으며 변수의 컨텍스트에 따라 데이터타입을 다이내믹하게 적용합니다. 나머지 Ruby 코드 예제에서도 이러한 "오리 타입(duck typing)"이 사용될 것입니다. 무슨 말인고 하니, 오리처럼 울고 걷는다면 그건 오리임에 분명하다는 뜻입니다.
Ruby와 오라클 DML SQL. 이제 생성된 STATES 테이블에 데이터를 입력해 봅시다. 몇 가지 미국의 주 정보를 입력하기 위한 코드가 Listing 2와 같습니다.
Listing 2 populateStatesTable.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
sqlInsert = "INSERT INTO states (id, name, capital)
                         VALUES (?, ?, ?)"
dbh.do(sqlInsert, "AL", "Alabama", "Birmingham")
dbh.do(sqlInsert, "AZ", "Arizona", "Phoenix")
dbh.do(sqlInsert, "CO", "Colorado", "Denver")
dbh.do(sqlInsert, "FL", "Florida", "Tallahassee")
dbh.do(sqlInsert, "MA", "Maine", "Augusta")
dbh.do(sqlInsert, "PA", "Pennsylvania", "Philadelphia")
dbh.do(sqlInsert, "UT", "Utah", "Salt Lake City")
dbh.do(sqlInsert, "WA", "Washington", "Seattle")
dbh.do(sqlInsert, "WY", "Wyoming", "Cheyenne")
dbh.commit
dbh.disconnect
Listing 2에서 Ruby/DBI가 제공하는 몇 가지 새로운 기능을 확인할 수 있습니다. 먼저, INSERT 구문에서 위치지정자(placeholder)로 물음표 기호가 사용되었음을 주목하시기 바랍니다. 이 기호는 구문을 먼저 생성한 뒤, 각각의 주(state) 정보를 테이블에 입력하는 과정에서 매우 유용하게 활용됩니다. INSERT 구문은 DML 구문이므로 (Listing 1의 DDL CREATE 구문과 달리) 뒷부분에 반드시 COMMIT을 붙여 주어야 합니다.
스크립트 실행이 완료되면, 데이터가 입력된 STATES 데이터베이스 테이블을 얻게 됩니다. 이제 사용자가 입력 과정에서 몇 가지 실수를 했다고 가정해 봅시다. (MA는 Massachusetts의 약자이지만, 위에서는 MA가 Maine의 약자로 잘못 사용되었습니다. 그리고 주 정부가 위치한 도시의 이름들도 잘못 입력되었습니다.)
Listing 3는 이 문제를 해결하기 위한 간단한 UPDATE 스크립트를 예시하고 있습니다. Listing 3는 위치지정자를 사용해서 주 정부가 위치한 도시 이름을 업데이트하고, 위치지정자를 사용하지 않고 Maine 주의 약자를 수정하고 있습니다.
Listing 3 updateStatesTable.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
sqlCapitalsUpdate = "UPDATE states SET capital = ? WHERE id = ?"
dbh.do(sqlCapitalsUpdate, "Montgomery", "AL")
dbh.do(sqlCapitalsUpdate, "Harrisburg", "PA")
dbh.do(sqlCapitalsUpdate, "Olympia", "WA")
dbh.do("UPDATE states SET id = 'ME' WHERE name = 'Maine'")
dbh.commit
dbh.disconnect
이미 예상하셨겠지만, DELETE는 INSERT, UPDATE와 같은 다른 DML 구문과 매우 유사한 방법으로 실행됩니다. 따라서 DELETE에 대한 설명은 생략하고 SELECT 구문으로 바로 넘어가겠습니다. SELECT 구문은 데이터베이스 쿼리를 통해 결과 셋을 가져온다는 점에서 다른 DML 구문과 차이를 갖습니다.
Listing 4는 데이터가 입력된 STATES 테이블에 간단한 쿼리를 수행하는 방법을 예시하고 있습니다. 빌트인 P 함수는 반환된 로우를 프린트합니다(그림 5 참고). 또는 빌트인 puts 메소드 도는 pp("pretty printer") 라이브러리를 사용해서 쿼리 결과를 디스플레이할 수도 있습니다. Listing 4에서는 쿼리 결과의 프린트를 위한 대안적인 방법들을 "#" 기호로 커멘트 처리하고 있습니다.
Listing 4 queryStatesTable.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
rs = dbh.prepare('SELECT * FROM states')
rs.execute
while rsRow = rs.fetch do
   p rsRow
   #Alternative output: puts rsRow
   #Alternative output: pp rsRow
end
rs.finish
dbh.disconnect   
Figure 5
그림 5 쿼리 결과
Ruby DBI는 결과 출력을 위한 멋들어진 포맷팅 기능을 별도로 제공하고 있습니다. Listing 5, 6는 DBI를 사용하여 동일한 쿼리 결과를 테이블 포맷과 XML 포맷으로 출력하고 있습니다. Listing 5, 6에서는 결과 출력을 위해 DBI::Utils를 사용하고 있을 뿐 아니라, 결과 셋의 조회 및 처리 방법도 Listing 4와 조금 다릅니다.
Listing 5 queryStatesTableFormatter.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
rs = dbh.execute('SELECT * FROM states')
rows = rs.fetch_all
column_names = rs.column_names
rs.finish
DBI::Utils::TableFormatter.ascii(column_names, rows)
dbh.disconnect
Listing 6: queryStatesTableXML.rb
require 'dbi'

dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
rs = dbh.execute('SELECT * FROM states')
states_rows = rs.fetch_all
rs.finish
DBI::Utils::XMLFormatter.table(states_rows)
dbh.disconnect   
위 스크립트에 의해 생성된 테이블 출력과 XML 출력은 각각 그림 6, 7에서 확인하실 수 있습니다.
Figure 6
그림 6 쿼리 결과의 테이블 출력
Figure 7
그림 7 쿼리 결과의 XML 출력
지금까지 오라클 환경에서 Ruby를 이용하여 기본적인 CRUD (CREATE, RETRIEVE, UPDATE, DELETE) 작업을 실행하는 방법에 대해 설명했습니다. 이제 Ruby에서 오라클 스토어드 프로시저를 사용하는 방법을 배워 볼 차례입니다.
Ruby와 오라클 스토어드 프로시저. PL/SQL은 다양한 오라클 데이터베이스 기반 애플리케이션에서 적극적으로 활용되고 있는 언어입니다. 오라클 스토어드 프로시저에 관련하여 사용자들이 축적한 경험과 지식이 상당한 수준에 이르고 있기 때문에, 어떤 언어를 사용하든 오라클 스토어드 프로시저의 강력한 기능을 함께 활용하는 것이 매우 중요합니다.
Listing 7은 빌트인 PL/SQL 스토어드 프로시저 DBMS_UTILITY.DB_VERSION에 접근하기 위한 코드를 예시하고 있습니다.DBMS_UTILITY.DB_VERSION은 두 개의 OUT 매개변수를 제공하고 있으며, 코드는 이 매개변수들을 통해 전달된 결과를 출력합니다.
Listing 7 builtInDBVersionCompat.rb
require 'dbi'

db_read_str = 'BEGIN DBMS_UTILITY.DB_VERSION(?, ?); END;'
dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
sth_db = dbh.prepare(db_read_str)
sth_db.bind_param(1, ' ' * 50)  # allow for up to 50 chars
sth_db.bind_param(2, ' ' * 50)  # allow for up to 50 chars
sth_db.execute
version = sth_db.func(:bind_value, 1)
puts "Oracle DB Version: " + version
compatibility = sth_db.func(:bind_value, 2)
puts "Oracle DB Compatibility: " + compatibility
dbh.disconnect   
위의 Ruby 코드를 실행한 결과가 그림 8과 같습니다.
Figure 8
그림 8 DBMS_UTILITY.DB_VERSION에 액세스하는 Ruby 코드의 실행 결과
Listing 7에서 흥미로운 점을 한 가지 발견할 수 있습니다. 이 코드는 Ruby 심볼(코드의 :bind_value)을 사용하는 방법과 DBI::Handle(코드의 sth_db) func 메소드를 사용해서 Ruby 데이터베이스 드라이버의 기능을 호출하는 방법을 예시하고 있습니다.
Listing 8의 코드는 Ruby DBI를 이용하여 빌트인 스토어드 함수 DBMS_METADATA.GET_DDL을 실행하는 방법을 예시하고 있습니다. 이 빌트인 함수는 여러 가지 관점에서 매우 유용합니다. 그 이유의 하나로 RUBY DBI의 함수로부터 반환된 값을 가져올 수 있다는 점을 들 수 있습니다. 이 함수는 오브젝트 타입과 오브젝트 네임이 다른 옵션 매개변수와 함께 전달될 것을 요구하고 있습니다. 아래 코드에서는 필수적으로 요구되는 두 개의 매개변수만을 전달하고 있으며, 매개변수 전달을 위해 "name notation"을 사용하고 있습니다.
Listing 8 builtInGetDDL.rb
require 'dbi'

db_read_str = 'BEGIN :out1 := DBMS_METADATA.GET_DDL(object_type=>:in1, '
db_read_str += 'name=>:in2); END;'
puts db_read_str
dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
sth_db = dbh.prepare(db_read_str)
sth_db.bind_param("out1", ' ' * 20000 )
sth_db.bind_param("in1", "TABLE")
sth_db.bind_param("in2", "STATES")
sth_db.execute
puts sth_db.func("bind_value", "out1")
dbh.disconnect
DBMS_METADATA.GET_DDL 함수는 테이블 생성을 위한 DDL 구문을 반환합니다. 이 DDL 구문을 이용하면 필요할 때 테이블을 재생성할 수 있으므로 유용합니다. 반환된 DDL 구문은 앞에서 우리가 사용한 CREATE TABLE보다 더 자세하며, 우리가 가정한 설정을 명시적으로 정의하고 있습니다. 이 결과를 통해 오라클 데이터베이스의 내부적인 동작 원리에 대한 이해를 높일 수 있을 뿐 아니라, 어떤 매개변수를 튜닝해야 하는지 판단하기 위한 정보로 활용할 수도 있습니다.
Listing 8은 오라클 스토어드 함수로부터 반환된 값에 접근하는 방법과 함께, 앞에서 사용한 변수의 바인딩을 위한 새로운 방법을 예시하고 있습니다. 앞에서 바인드 변수를 사용한 코드(Listing 7)에서, 우리는 "positional binding"을 사용했습니다. 다시 말해, 데이터베이스 실행 문자열에 물음표 기호(?)를 위치시키고 1로 시작하는 연속적인 정수를 사용하여 값을 참조하였습니다. Listing 8에서는 "name binding"을 사용하고 있습니다. 즉, Ruby 심볼(:out1, :in1, :in2)을 위치시키고 이름 정보를 이용하여 바인드 변수들을 참조하고 있습니다. Ruby의 "positional/name binding" 지원 기능은 PL/SQL의 "positional/named parameter notation" 지원 기능과 유사합니다. 그림 9는 Listing 8(STATES 테이블 생성을 위한 DDL)의 실행 결과를 보여 주고 있습니다.
Figure 9
그림 9 DBMS_METADATA.GET_DDL로부터 생성된 DDL CREATE 구문

Ruby와 오라클: 데이터베이스가 전부는 아니다.

오라클 전문가들의 관심은 단지 오라클 데이터베이스에만 한정되는 것은 아닙니다. 설정 파일이 있고, 환경 변수가 있고, Oracle Application Server와 같은 다른 오라클 제품들도 있습니다. 이러한 영역에서 Ruby를 활용하는 방법에 대해 알아 보기로 합시다.
Ruby와 환경 변수. 를 이용하면 운영 체제의 환경 변수에 쉽게 접근할 수 있습니다. Listing 9은 정의된 환경 변수의 목록을 확인하기 위한 간단한 Ruby 프로그램의 예를 보여 주고 있습니다.
Listing 9 displayOracleENV.rb
ENV.each {|key ,value| print key, "=", value, "\n"}  
위 코드는 ENV 해쉬에 대해 반복 실행되며, 라인별로 하나씩 환경 변수를 출력합니다. 이 한 줄의 코드만으로도 Ruby 언어가 갖는 파워와 간결성을 확인할 수 있습니다.
Ruby와 운영 체제 파일. 오라클 데이터베이스를 관리하다 보면 init.ora와 같은 설정 파일의 컨텐트에 접근해야 하는 경우가 많습니다. Listing 10은 Ruby를 이용하여 파일의 key-value 설정을 조회하는 방법을 예시하고 있습니다.
Listing 10 displayInitOra.rb
=begin
This Ruby script demonstrates several Ruby features while performing the
functionality of returning values and their setting from the init.ora file.
This multiline comment block is demarcated by the =begin and =end.
Normal, single-line comments are also shown in this code (pound sign).
=end

fileName = ARGV.to_s
# example of if statement
if fileName.size < 1
  print "Enter init.ora path and name: "
  fileName = gets.chomp!
end

#example of unless statement
unless fileName.size > 1
  fileName = "C:\\oracle\\product\\10.1.0\\admin\\orcl\\init.ora"
end
print "File Name: " + fileName + "!"
theFile = File.new(fileName, "r")  # read the file
text = theFile.read  # copy file's contents into string
theFile.close        # string holds file contents; close file
regExp = Regexp.new('^.+=.+$')
puts text.scan(regExp)
Listing 10에서는 Ruby의 여러 가지 기능이 활용되고 있습니다. 그 중에서도 가장 중요한 기능으로 정규 표현식 처리를 위한 Regexp의 활용을 들 수 있을 것입니다. Regexp 인스턴스는 매치되는 문자열에 가장 적합한 패턴(^.+=.+$)으로 인스턴스화(instantiate) 됩니다. 정규 표현식은 문자열의 scan 메소드에 적용되어 정규 표현식에서 정의된 문자열과 매치되는 서브문자열을 반환합니다. Perl과 마찬가지로, Ruby는 환상적인 정규 표현식 기능을 제공합니다.
Listing 10은 또 커맨드 라인 또는 콘솔 기반 사용자 프롬프트로부터 입력을 받아 들이는 방법을 예시하고 있습니다. 코드는 먼저 사전 정의된 ARGV 변수를 이용하여 커맨드 라인으로부터 파일 경로와 파일 이름을 입력 받고 있습니다. 이 방법이 성공적이지 못한 경우, 다음으로 사용자에게 경로를 묻습니다. 사용자가 경로와 파일 이름을 입력하지 않았을 때에는 하드 코딩된 경로가 사용됩니다. 이 과정에서 if 구문과 (Perl과 유사한) unless 구문이 사용되고 있습니다.
또 Listing 10의 코드는 chomp! 메소드의 활용 예를 보여 주고 있습니다. 이 String 메소드는 사용자 콘솔로부터 문자열로 입력된 파일 이름의 뒷부분에서 구분자(separator)를 제거하기 위해 사용됩니다. 이 테크닉을 사용하지 않는 경우, 입력을 통해 전달된 newline 문자로 인해 File 오브젝트를 여는 과정에서 문제가 발생하게 됩니다. Ruby 메소드에 사용되는 느낌표(!)는 메소드가 오브젝트에 잠재적으로 위험한 작업을 수행함을 표시합니다. 또 Ruby의 메소드 뒤에 물음표(?)가 사용된 경우는 True 또는 False의 결과가 반환됨을 의미합니다.
이번 섹션의 마지막 예제에서는 두 개의 init.ora 파일을 비교하고 그 차이를 보고하는 간단한 Ruby 스크립트를 작성해 보겠습니다. Listing 11은 Listing 10과 유사한 방법을 사용하고 있지만, 커맨드 라인 매개변수를 통해 두 init.ora 파일의 컨텐트를 읽어 들인다는 점에서 다릅니다.
Listing 11 diffInitOra.rb
=begin
This script compares two files and returns from each file what it has that
the other provided file does not have.  Original order of either file does
not matter.
=end

require 'pp'

unless ARGV.size == 2
  print "Usage: ruby arrayMagic.rb firstFile secondFile"
  exit
end

firstFile = File.new(ARGV[0], "r")
secondFile = File.new(ARGV[1], "r")
regExp = Regexp.new('^.+=.+$')
text1 = firstFile.read.scan(regExp)
text2 = secondFile.read.scan(regExp)
firstFile.close
pp firstFile
puts text1-text2, "\n"
pp secondFile
puts text2-text1
secondFile.close
위의 예제 코드는 pp 라이브러리를 이용해서 파일 이름을 출력하고, 두 파일 중 하나가 아직 열리지 않은 상태이면 파일이 닫혀있음을 보고합니다. 또 Ruby의 강력한 마이너스 연산자의 활용 예를 확인하실 수 있습니다. 파일로부터 읽어 온 두 개의 문자열 사이에 마이너스 기호를 적용하기만 하면, 파일 간의 차이를 쉽게 확인할 수 있습니다.
Listing 11의 실행을 위해, 필자는 기존 init.ora 파일의 복사본을 생성한 후 복사본에서 3 개의 매개변수(open_cursors, db_name, job_queue_processes)를 변경하였습니다. 또 좀 더 흥미를 돋구기 위해, init.ora 파일 복사본의 매개변수 순서를 변경해 보았습니다. 그림 10는 이 두 개의 init.ora 파일에 대해 Listing 11의 코드를 실행한 결과를 보여 주고 있습니다. 그림에서 확인할 수 있듯 아주 간단한 코드 만으로 (심지어 파일의 라인 순서가 뒤바뀐 상태에서도) 파일 간의 차이를 비교할 수 있었습니다.
Figure 10
그림 10 두 init.ora 파일의 차이 비교
위 예제에서 확인한 것처럼, Ruby는 매우 단순하고도 강력한 파일 처리 기능을 제공합니다. 스풀링된 출력과 다른 유형의 파일에서도 Ruby를 유용하게 활용할 수 있습니다.

Ruby의 예외 처리 기능

Ruby 는 스크립트 및 OO(object-oriented) 애플리케이션에서 활용 가능한 강력한 기능을 제공하고 있습니다. 그 대표적인 예의 하나로 Ruby의 예외 처리(exception handling) 기능을 들 수 있습니다. Listing 12는 Ruby의 예외 처리 메커니즘을 구현한 예입니다.
Listing 12 rubyExceptions.rb
=begin
This script is similar to Listing 10, but demonstrates Ruby exception handling
using functionality originally demonstrated in Listing 10.
=end

fileName = ARGV.to_s

#obtain file name if not provided on command-line
if fileName.size < 1
  print "Enter init.ora path and name: "
  fileName = gets.chomp!
end

#ensure something has been provided for file name
begin
  if fileName.size < 1
    raise ArgumentError, "No valid file name provided" + "\n"
  end
rescue ArgumentError => argErr
  print argErr
  raise  # re-raise this exception and force script termination
end

#get file contents
begin #Begin exception handling block for file I/O
  theFile = File.new(fileName, "r")
  text = theFile.read
  theFile.close
rescue IOError
  print "I/O Error: Problem accessing file " + fileName + "\n"
  exit
rescue Errno::ENOENT
  print "ENOENT: Cannot find file " + fileName + "\n"
  exit
rescue Errno::EPERM
  print "EPERM: Insufficient rights to open " + fileName + "\n"
  raise
rescue Exception  # Catch-all: More exceptions captured than with "rescue" alone
  print "Generic error rescued (captured) during file I/O attempt." + "\n"
  raise
else
  print "Good news!  There was no problem with file I/O for " + fileName + "\n"
ensure
  print "Good or bad, file handling attempt is now complete!\n"
end #End exception handling block for file I/O

#obtain text string for regular expression
print "Enter regular expression pattern: "
pattern = gets.chomp!

begin #Begin exception handling block for regular expression
  regExp = Regexp.new(pattern)
rescue RegexpError
  print "Problem with regular expression " + regExp.to_s + "\n"
  exit  # Nothing to be done with a bad regular expression
ensure
  print "Regular expression evaluated was: " + regExp.to_s + "\n"
end #End exception handling block for regular expression

puts text.scan(regExp)
Listing 12에서는 begin 키워드를 사용하여 익셉션(exception)이 발생될 수 있는 코드 블록의 시작 부분을 표시하고 있습니다. 또 raise 키워드를 사용하여 스크립트가 자체적으로 익셉션을 발생시킬 수 있도록 하였습니다. 위 코드에서 여러 차례 사용된 rescue 키워드는 Java의 catch 키워드와 유사합니다. Ruby의 ensure 키워드는 Java의 finally 키워드와 유사하며, 이 코드에서는 익셉션 발생 여부에 관계없이 기능을 실행하기 위해 적용되었습니다.
Listing 12에서 확인할 수 있는 것처럼, Ruby는 begin과 end로 묶인 코드 블록에서 익셉션이 발생하지 않는 경우 특정 기능을 실행하기 위한 else 키워드를 지원합니다. 코드에 포함된 커멘트를 통해 Ruby의 예외 처리 기능에 대한 자세한 정보를 얻으실 수 있을 것입니다.
그림 11는 Listing 12의 스크립트를 3 회에 걸쳐 실행한 결과를 보여 주고 있습니다. 첫 번째 스크립트에서는 예외 처리 기능을 호출하기 위해 존재하지 않는 파일 이름을 지정해 보았습니다. 그런 다음 올바른 파일 이름을 이용하여 스크립트를 재실행하여, else 블록을 실행하고 정규 표현식 처리를 위한 다음 블록으로 이동하는 과정을 테스트하였습니다. 스크립트를 두 번째 실행하는 과정에서, RegexpError를 발생시키기 위해 올바르지 않은 정규 표현식을 적용하였습니다. 세 번째로, 익셉션이 전혀 발생하지 않는 조건 하에서 스크립트를 실행하였습니다. ensure 블록은 세 번의 실행 과정 모두에서 실행됩니다. 이 블록은 익셉션의 발생 여부와 무관하게 항상 호출되도록 정의되어 있기 때문입니다.
Figure 11
그림 11 Ruby 예외 처리 기능의 실행
Ruby DBI 예외 처리. 바로 앞의 섹션에서 Ruby의 예외 처리 기능에 대해 알아 보았습니다. Ruby/DBI는 자체적인 예외 처리 계층을 제공합니다(Listing 13 참고). 실제 환경에서는 Listing 13과 같이 "database catch all" 익셉션(DBI::DatabaseError)만을 캡처하고 그 속성에 접근하여 데이터베이스 코드와 에러 메시지를 출력하는 방법이 자주 활용되고 있습니다.
Listing 13 rubyDbExceptions.rb
=begin
Listing13-rubyDbExceptions.rb

This script demonstrates the use of exceptions with DBI::DatabaseError
and database-related exception classes that inherit from DBI::DatabaseError.
Often, it is enough to capture DatabaseError, but this example shows all
of the inherited exception classes.  DatabaseError is rescued last so that
a more particular database-related exception might be rescued first.

This "try" block of this code includes an "infinite" while loop that will
open connections without closing them until a DBI::DatabaseError is forced.
=end

require 'dbi'

begin
  counter = 0
  while 0  # "infinite" loop because 0 resolves to "true" for Ruby conditional
    dbh = DBI.connect('DBI:OCI8:ORCL', 'hr', 'hr')
    counter += 1
    puts "DB Connection #" + counter.to_s + "\n"
    #Intentionally NOT closing with dbh.close to force DatabaseError.
  end
rescue DBI::DataError => dataErr
  dbh.rollback
  puts "DB error due to problem with data"
  puts "Error Code: #{dataErr.err}"
  puts "Error Message: #{dataErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::IntegrityError => integErr
  # Example: Trying to insert same value for unique column twice.
  dbh.rollback
  puts "DB error due to integrity problem."
  puts "Error Code: #{integErr.err}"
  puts "Error Message: #{integErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::InternalError => internErr
  dbh.rollback
  puts "DB error database internal error."
  puts "Error Code: #{internErr.err}"
  puts "Error Message: #{internErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::NotSupportedError => notSuppErr
  dbh.rollback 
  puts "DB feature not supported."
  puts "Error Code: #{notSuppErr.err}"
  puts "Error Message: #{notSuppErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::OperationalError => opErr
  dbh.rollback
  puts "DB error due to problems with operation of database."
  puts "Error Code: #{opErr.err}"
  puts "Error Message: #{opErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::ProgrammingError => dbProgErr
  # Example: Bad column name in SQL statement.
  dbh.rollback
  puts "DB error due to programming problem.\n"
  puts "Error Code: #{dbProgErr.err}"
  puts "Error Message: #{dbProgErr.errstr}"
  puts "DB rollback.\n"
rescue DBI::DatabaseError => dbErr
  # Catch-all for all database exceptions.
  dbh.rollback
  puts "Database exception encountered."
  puts "Error Code: #{dbErr.err}"
  puts "Error Message: #{dbErr.errstr}"
  puts "DB rollback."
rescue DBI::InterfaceError => ifError
  dbh.rollback
  puts "Problem with DBI interface encountered."
rescue RuntimeError
  dbh.rollback
  puts "Unknown error (not DB or DBI) encountered."
else
  puts "DB commit.\n"
  dbh.commit
ensure
  puts "Disconnecting database handler."
  dbh.disconnect
end  
Listing 13에서 사용된 DBI 익셉션은 DBI::DatabaseError로부터 유도된 것 말고도 (데이터베이스가 아닌 DBI 인터페이스와 관련된 에러를 캐치하기 위한) DBI::InterfaceError 익셉션을 포함하고 있습니다.
Listing 13의 코드를 실행하면 익셉션이 발생됩니다. 익셉션이 발생될 때까지 "while 0" 루프(Ruby에서 0은 "true"를 의미합니다)가 반복적으로 실행되기 때문입니다. 익셉션이 발생되는 이유는 오픈된 연결을 닫지 않은 상태에서 DBI:DatabaseError 익셉션이 발생할 때까지 DBI.connect가 반복적으로 호출되기 때문입니다. 이때 발생되는 에러 코드는 12520(오라클의 데이터베이스 에러 코드), 에러 문자열은 "ORA-12520: TNS:listener could not find available handler for requested type of server"입니다. 이 코드와 문자열 값은 각각 DBI::DatabaseError의 err, errstr 속성을 이용해서 출력할 수 있습니다.
Listing 13에서 의도적으로 데이터베이스 에러를 발생시키기 위해 추가된 while 루프를 정상적인 데이터베이스 구문(예: SELECT 구문)으로 대치한다면, Listing 12의 경우와 마찬가지로 rescue, else, ensure 블록이 활성화될 것입니다. 혹시라도 (무결성 제약 조건 위반 등으로 인해) 익셉션이 발생하는 경우 "rescue" 블록이 호출되어 처리됩니다. rescue 블록이 지정되지 않은 익셉션이 발생하는 경우, else 블록의 코드가 실행되고 트랜잭션은 커밋 처리됩니다. 어떤 경우든, 다시 말해 익셉션의 발생 여부에 관계없이 ensure 블록의 코드는 무조건적으로 실행되어 핸들러와의 연결을 해제합니다.
Ruby 스크립트를 작성할 때, 반드시 익셉션을 캐치(rescue)해야 하는 것은 아닙니다. (Java의 "unchecked" 익셉션을 상기하시기 바랍니다.) Listing 12 이전의 모든 코드에서도 예외 처리 기능은 구현되지 않았습니다. 하지만, 특정 익셉션이 발생할 가능성이 있고 이에 대해 어떤 식으로든 대응하고자 한다면, Ruby를 이용하여 간단하게 예외 처리 기능을 구현할 수 있습니다. 익셉션이 발생되는 코드 블록에 rescue-else-ensure 블록을 연결시키지 않는 경우, 스크립트는 그 블록 내에서의 실행을 중단하고 "general exception" 정보를 표시한 후 다음 코드 블록을 실행합니다.
그림 12는 Ruby의 주요 익셉션 중 몇 가지를 보여 주고 있습니다. 그림 13은 Ruby DBI 익셉션을 보여 주고 있습니다. 이러한 표준 익셉션 이외에도 직접 Ruby 익셉션을 생성하여 활용할 수도 있습니다.
Figure 12
그림 12 몇 가지 Ruby 익셉션
Figure 13
그림 13 Ruby/DBI 익셉션
Java와 마찬가지로 Ruby는 오브젝트 지향형(OO) 언어로 구현되었습니다. 덕분에 필자는 그림 12, 13의 클래스 다이어그램을 Oracle JDeveloper 10g의 UML 모델링 툴을 이용하여 생성할 수 있었습니다.

Ruby와 O-R(Object-Relational) 매핑

본 문서를 통해 Ruby와 Ruby/DBI를 이용해서 데이터베이스 작업을 수행하는 방법을 배워 보았습니다. 데이터베이스에 직접 스크립트를 실행하는 일반적인 환경이라면, 지금까지 설명한 방법이 가장 합리적인 선택이 될 것입니다. 하지만 Ruby와 오라클 데이터베이스에서 오브젝트-관계형(object-relational) 매핑 접근법을 사용하고자 한다면, ActiveRecord가 적절한 선택이 될 수 있습니다.
ActiveRecord는 Ruby on Rails에서 처음 소개되었지만, 현재는 Rails가 구현되지 않은 환경에서도 사용이 가능합니다. ActiveRecord는 ORM 환경을 단순화하기 위한 여러 가지 관례와 가정에 의존하고 있습니다. ActiveRecord에서 사용되는 대부분의 관례는 Ruby 모델 클래스, 오라클 데이터베이스 테이블 및 컬럼의 명명 규칙과 관계되어 있습니다. ActiveRecord는 또 기존 관례 및 가정에서 벗어난 예외 규칙을 적용하기 위한 단순화된 override 메커니즘을 제공하고 있습니다.
ActiveRecord는 Ruby의 OO 기능을 활용한 대표적인 사례이며, 특히 상속(inheritance)와 믹스-인(mix-in)을 지원합니다. 개발자들은 ActiveRecord를 사용하면서 ActiveRecord 모델 클래스에 거의 코드를 추가할 필요가 없습니다. 대부분의 기능이 전체 모델에 의해 상속되는 베이스 ActiveRecord 클래스를 통해 제공되고 있기 때문입니다.

Ruby와 웹 개발

최근 들어 Ruby on Rails의 인기가 급증하면서, 이제 Ruby를 위한 다른 웹 프레임워크의 대안은 고려할 수조차 없게 되었습니다. 다른 웹 프레임워크도 Ruby를 지원하고 있지만, Rails는 이 분야에서 분명한 강자로 인정받고 있습니다. 실제로 Ruby on Rails는 Ruby가 명성을 얻게 되는 계기가 되었을 뿐 아니라, Python, Java와 같은 다른 언어에서도 이와 유사한 프레임워크를 개발하기 위한 동기를 제공하였습니다. Ruby를 이용하여 웹 개발을 수행한다면, Ruby on Rails가 유력한 선택이 될 수밖에 없습니다.

Ruby는 단순한 스크립팅 언어가 아닙니다.

본 문서에서는 데이터베이스 접근 및 데이터베이스 파일의 처리에 관련한 Ruby의 스크립팅 기능에 초점을 맞추고 있습니다. 하지만 Ruby는 그 이상의 것을 제공합니다. 우아함, 단순성, 다양한 라이브러리, 강력한 OO(object-oriented) 기능을 지원하는 Ruby는 일반 애플리케이션뿐 아니라 스크립트 개발을 위한 강력한 언어 환경을 제공합니다.
Rails를 사용하든 사용하지 않든, Ruby는 배우고 사용할 가치가 충분한 유용한 언어입니다. Ruby는 복잡한 오브젝트 지향형 애플리케이션이나 웹 애플리케이션의 개발을 위해 사용될 수 있으며, 또 한편으로 오라클 데이터베이스와 기타 오라클 툴에 관련한 스크립팅을 위한 매우 강력한 도구로 활용됩니다.

Dustin Marx는 Raytheon Company의 수석 소프트웨어 엔지니어 겸 아키텍트입니다.

댓글 없음:

댓글 쓰기