04 기능 두큰술 - 직원 급여 관리 애플리케이션 (2)
October 2nd, 2007
지난 글: 03 친해지길 바래 - 직원 급여 관리 애플리케이션 / 루비 코코아 프로그래밍
지난 예제에 이어서 직원 급여 관리 애플리케이션 RaiseMan을 개선한다. 지난 번에 워낙 많은 시행착오를 거쳤으니 이번에는 좀 더 쉽게 따라갈 수 있기를 기대한다. 8장 아카이브, 9장 Nib 파일과 NSWindowController, 10장 사용자 기본설정에 걸쳐서 설명된 내용이다.
NSCoding 프로토콜
입력한 내용을 디스크에 저장하고 다시 읽어오는 Load/Save 기능을 구현한다. 당연히 코코아 프레임워크에서 제공하는 몇몇 객체를 이용하면 많은 코딩없이 쉽게 구현할 수 있다. 자신의 모델 객체(이 예제에서는 Person)을 인코딩/디코딩하는 부분만 구현해주면 나머지 처리(예를 들면 메뉴 아이템 등)는 프레임워크가 다 알아서 해주는 식이다. 적은 양의 코딩으로 많은 일을 할 수 있다.
- class Person < OSX::NSObject
def initWithCoder(coder)
super_init
@name = coder.decodeObjectForKey('name')
@expected_raise = coder.decodeFloatForKey('expected_raise')
self
end
def encodeWithCoder(coder)
coder.encodeObject_forKey @name, 'name'
coder.encodeFloat_forKey @expected_raise, 'expected_raise'
end
end
위 코드는 NSCoder 객체를 이용해 Person 객체를 인코딩하고 디코딩한다. 이렇게 하면 Person의 상태를 담은 변수를 이진 스트림으로 저장한다. 하지만 가능하다면 루비 커뮤니티에서 자주 쓰이는 YAML이라 JSON 등으로 사람이 읽을 수 있는 형식으로 직렬화하는 것도 좋을 것 같다. 그렇게 하면 RaiseMan 프로그램이 없어도 텍스트 에디터 등으로 편집도 가능할 것이기 때문이다. 어쨌든 이 예제에서는 코코아식으로 모두 따라해봤다.
도큐멘트 아키텍쳐
이제 NSDocument의 몇가지 메서드를 구현해서 Open, Save, Save As, Save All, Open, Open Recent, Revert To Saved 등의 기능을 제공해보자. dataRepresentationOfType, loadDataRepresentation_ofType 메서드만 구현하면 앞에서 나열한 모든 기능을 공짜로 얻을 수 있었다.
저장하는데는 NSKeyedArchiver, 이를 다시 불러올 때는 NSKeyedUnarchiver를 사용했다.
- class MyDocument < OSX::NSDocument
def dataRepresentationOfType(aType)
@people_controller.commitEditing
OSX::NSKeyedArchiver.archivedDataWithRootObject(@employees)
end
def loadDataRepresentation_ofType(data, aType)
target = OSX::NSKeyedUnarchiver.unarchiveObjectWithData(data)
return false unless target
self.employees = target
true
end
end
그리고 XCode에서 확장자도 정해주고, 아이콘도 달아주고, 문서 형식도 설정해주니 그럴싸한 프로그램이 되었다. 굿!
메모리 관리
Objecive-C의 dealloc 메서드를 루비 코코아로 포팅해야할까? 말아야할까? 갑자기 궁금해졌다. 다행히 하지 않아도 된다는 결론을 Examining RubyCocoa에서 찾을 수 있었다.
Objective-C의 객체들은 참조 카운트(Reference Count)를 통해 관리된다. 하지만 이를 개발자에 맡기는 방식보다는 가비지 컬렉션이 있는 언어가 훨씬 생산적이다. 루비 코코아가 빛나는 순간이다. 보통 Objective-C의 객체는 아래와 같이 생성한다.
- @preference_controller = PreferenceController.alloc.init
Objective-C의 경우는 dealloc 메시지를 받았을 때 위 객체를 릴리즈해야만 한다. 하지만 루비는 Objective-C 객체 모두를 루비 래퍼(Wrapper) 클래스에 담아서 저장한다. 그리고 루비 객체가 지워질때(가비지 컬렉터에 의해) 이 객체가 담고 있던 Objective-C 객체도 릴리즈한다. 루비 코코아에서는 직접 객체를 릴리즈할 필요가 없는 이유이다.
환경설정 패널
이번에는 환경설정 패널을 추가하는 것을 따라해보며 새로운 윈도우 또는 뷰를 추가하는 방법을 실습했다.
적당한 Nib파일과 컨트롤러를 만들면 된다. 어렵지 않았다. 재미있는 점은 OSX의 Nib 파일이 늦게 필요한 시점이 되야 로딩된다(Lazy loading)는 사실이다. 요즘같은 풍족한 시기에는 감흥이 적기는 하지만, 그래도 내가 사용하는 기능만 메모리에 올라온다는 점에서 효율적으로 보인다.
루비 코코아에서는 인터페이스(*.h)를 구현하고 이 파일을 인터페이스 빌더에 드래그 & 드랍해서 반영하는 패턴을 사용할 수 없어서 아쉽다. 인터페이스 빌더에서 아웃렛과 액션을 지정하고 rb 파일에서 또 구현체를 입력한다. 반복이다. 줄일 방법이 있을텐데 nib_tool.rb가 그런 역할을 대신해주는 툴로 보이는데, 아직 사용해보지 않아서 정확히 모르겠다.
- class AppController < OSX::NSObject
ib_outlet :preference_controller
def showPreferencePanel(sender)
preference_controller.showWindow(self)
end
protected
def preference_controller
@preference_controller ||= PreferenceController.alloc.init
end
end
AppController는 PreferenceController를 한번만 로드한다.
- class PreferenceController < OSX::NSWindowController
def init
initWithWindowNibName('Preferences')
end
end
PerferenceController에서는 자신이 메모리에 올라올 때 Nib 파일을 함께 로드하고 이 파일의 Owner가 된다.
OSX::NSObject#to_ruby
Objective-C에서 기본적으로 사용되는 객체들은 모두 루비 객체로 변환할 수 있다. 예를 들면 NSDate는 Time으로, NSString은 String으로 변환하는 식이다. 그 중 특히 유용한 것은 NSArray과 NSDictionary를 각각 Array와 Hash 객체로 변환해서 사용하는 것이다.
루비의 Enumerable은 강력하고 편리한 메서드들을 많이 구현하고 있으므로 NSArray, NSDictionary에 믹스인된 Enumerable 모듈이 많은 즐거움을 줄 것이다. 이 내용은 oc_attachment.rb에 구현되어 있다. 이 파일을 읽어보면 Objective-C 객체를 좀 더 루비스럽게 사용하기 위한 첨가 메시지들이 구현되어 있다. 한번 읽어보는 것이 도움이 되는 것 같다.
- class NSDictionary
include Enumerable
end
어쨌든 NSDictionary는 루비 해시로 생각하고 코딩하면 큰 무리가 없을 것 같다. 이를 이용해 사용자 기본설정을 구현한다.
사용자 기본설정
윈도 애플리케이션이 레지스트리를 이용해 설정값을 저장하는 것처럼, 코코아 애플리케이션은 ~/Library/Preference에 기본 설정 데이터베이스를 저장한다. 그리고 이 값은 NSUserDefaults 객체를 통해 접근가능하다. ObjectiveC의 경우는 setBool_forKey, boolForKey 등의 메서드로 값을 얻겠지만, 루비 코코아는 마치 루비 해시 객체를 다루듯 편하게 설정을 읽고 쓸 수 있다.
RaiseMan에서는 두 종류의 설정을 제공한다. 첫번째는 TableView의 배경색(KEY_TABLE_BG_COLOR)이고, 두번째는 프로그램 시작시 빈 문서를 띄울지 말지를 결정하는 플래그(KEY_EMPTY_DOC)다. 먼저, 사용자 설정을 다룰 클래스를 하나 정의했다. 이 클래스는 NSUserDefaults 객체를 이용해 두가지 설정값을 읽고 쓴다. 배경색은 OSX::NSColor 객체를 직렬화해서 저장하고, 플래그는 체크박스의 값은 정수로 그래도 저장한다.
- class Preference
class <<self
KEY_TABLE_BG_COLOR = 'TableBackgroundColor'
KEY_EMPTY_DOC = 'EmptyDocumentFlag'
def register_defaults
defaults = { KEY_TABLE_BG_COLOR => color_to_data(:yellow), KEY_EMPTY_DOC => true }
user_defaults.registerDefaults(defaults)
end
def background_color
data_to_color(user_defaults[KEY_TABLE_BG_COLOR])
end
def background_color=(color)
user_defaults[KEY_TABLE_BG_COLOR] = color_to_data(color)
end
def empty_document_flag
user_defaults[KEY_EMPTY_DOC]
end
def empty_document_flag?
empty_document_flag.to_i > 0
end
def empty_document_flag=(flag)
user_defaults[KEY_EMPTY_DOC] = flag
end
protected
def user_defaults
OSX::NSUserDefaults.standardUserDefaults
end
def color(obj)
obj.is_a?(OSX::NSColor) ? obj : OSX::NSColor.send("#{obj}Color")
end
def color_to_data(obj)
OSX::NSKeyedArchiver.archivedDataWithRootObject(color(obj))
end
def data_to_color(data)
OSX::NSKeyedUnarchiver.unarchiveObjectWithData(data)
end
end
end
몇가지 헬퍼 메서드를 protected로 구현하고, background_color와 empty_document_flag 값에 대한 접근자(읽기/쓰기)를 구현했다. 이제 이 값을 적당한 곳에서 호출하기만 하면 된다. 그리고 애플리케이션이 최초로 실행될 시에는 register_defaults를 호출하여 기본값을 저장한다.
이렇게 저장한 값은 명령행에서 defaults 툴을 이용해서도 접근할 수 있다.
- defaults read com.deepblue.RaiseMan
아래는 위에서 만든 패널에 대한 컨트롤러 코드로 PreferenceController.rb의 일부다.
- class PreferenceController < OSX::NSWindowController
def initialize
Preference.register_defaults
end
- def change_background_color(sender)
Preference.background_color = sender.color
end
end
사용자 기본 설정 사용
이 설정값은 주로 NIB 파일이 로드된 시점에 적용한다.
- class MyDocument < OSX::NSDocument
def windowControllerDidLoadNib(aController)
super_windowControllerDidLoadNib(aController)
@table_view.backgroundColor = Preference.background_color
end
end
이렇게 하면 다음부터 열리는 창은 설정값에 따른 배경색을 갖게 된다. 좀 더 예쁜 색을 고를걸 그랬다. 하하.
후기
이제 저장도 되고, 사용자 별로 따로 설정도 할 수 있는 정말 쓸만한 애플리케이션이 만들어졌다. 적은 코딩양으로 많은 일이 일어나서 신기한 마음이 드는게 처음 레일스를 공부할 때와 비슷한 느낌이 들어 재밌다. 지금까지 만든 소스 코드다.
이제 루비 코코아, 정확히 말하면 루비-ObjectiveC 브리지에 어느 정도 적응했는지, 브리지 이상을 바라게 된다. 바꿔 말하면 조금 더 루비스러운 코드의 간결한 wrapper 클래스와 DSL이 있으면 좋겠다는 생각을 많이 한다. 위에서 정의한 Preference 클래스만해도 충분히 줄일 수 있어 보이는 기계적인 중복들이 보인다. 더 개선하고 싶은 마음이 들었지만, 지금은 코코아 맛을 보는 중이니 참고 진도를 나가야겠다 :)
다음 편은 지금까지 만든 RaiseMan을 좀 더 꾸며볼 차례다(05 완성도를 높여라 - 직원 급여 관리 애플리케이션 (3)). 또 어떤 기능을 더할지 기대된다(전체보기 - 루비 코코아 프로그래밍)




October 2nd, 2007 at 03:28 PM (myRuby.net) 슬슬 모양을 갖춰가는 RaiseMan입니다.
October 2nd, 2007 at 03:33 PM 직원들 급여관리는 잘되고 계신가요? ㅋㅋ
October 2nd, 2007 at 04:57 PM 꽃띠앙// 5편에서 완성될꺼예요 ^^
October 3rd, 2007 at 03:53 AM 징하네… 광고좀 고만하시징…
October 3rd, 2007 at 06:50 AM 케이ㅋ// 왠 광고요? 그런거 한 적 없는데염
October 3rd, 2007 at 02:36 PM 제 급여도 관리해주세요