[Cocoa] 키밸류 코딩에 대한 오해와 진실

키밸류코딩(Key-Value Coding : KVC)은 Objective-C의 어찌보면 가장 중요한 디자인 패턴 중의 하나이다. 굉장히 편리한 Objective-C의 기능들 중 다수가 키밸류코딩에 기반하거나, 많은 부분을 의존한다. 특히 키밸류옵저빙, 코어데이터, 코코아바인딩 등 코딩의 수고스러움을 비약적으로 줄여주는 기술들이 키밸류코딩을 기반으로 하고 있다.

하지만 이에 대해서 자세한 내용을 다루고 있는 글들이 상당히 많음에도 많은 이들이 이 키밸류 코딩에 대해 잘못 이해하고 있다는 사실은 상당히 놀랍다. 이 글에서는 키밸류 코딩에 대한 오해와 실체에 대해 살펴보고, 키밸류 코딩을 이해하는 시도를 해 보고자 한다.

프로퍼티

원칙적으로 객체는 그 내부에 구현이나 정보들을 외부에서 알지 못하도록 숨긴다. 따라서 객체 내부의 변수나 값에 접근하기 위해서는 접근자라는게 필요하다. 보통 코코아 책에서는 다음과 같은 모양으로 클래스를 만든다.

#import
@interface Person : NSObject
{
  NSString *firstName, *lastName;
} 
-(NSString *)firstName;
-(NSString *)lastName;
-(void)setFirstName:(NSString *)newFirstName;
-(void)setLastName:(NSString *)newLastName;
@end

코드 1 ) Person의 인터페이스

#import "Person.h"

@implementation Person
-(id)init {
  self = [super init];
  firstName = @"";
  lastName = @"";
  return self;
} 

-(NSString *)firstName
{
  return firstName;
}

-(NSString *)lastName
{
  return lastName;
}

-(void)setFirstName:(NSString *)newFirstName
{
  firstName = [newFirstName copy ];
}
-(void)setLastName:(NSString *)newLastName
{
  lastName = [newLastName copy];
}
@end

코드 2) Person의 구현부

이 Person 이라는 클래스는 firstName, lastName 이라는 이름을 구성하는 두 개의 문자열 변수를 인스턴스 변수로 가지고 있다. 그리고 객체 외부에서 이 이름들에 접근하기 위해 인스턴스 변수 1개당 2개씩의 메소드를 만들어주었다. 각각의 이름은 -firstName, -lastName, -setFirstName:, -setLastName: 으로 인스턴스 변수의 이름과 유사한 형태를 띄고 있다. 하지만 외부에서 액세스해야하는 정보의 양이 많아지면 이런 간단하고 단순한 접근자 메서드들을 계속해서 만들어주어야 한다는 사실은 게으른 우리로서는 슬프기 그지 없다. 그래서 Objective-C에서는 “외부에서 사용가능한 객체의 정보”라는 개념으로 프로퍼티라는 것을 마련해 두고 있다. 이는 마치 마법처럼 위의 코드를 비약적으로 간결하게 줄여준다.

#import
@interface Person : NSObject
{
  NSString *firstName, *lastName;
}
@property (copy, nonatomic) NSString *firstName;
@property (copy, nonatomic) NSString *lastName;
@end

코드 3) 프로퍼티 구문을 이용한 Person의 인터페이스

#import "Person.h"
@implementation Person
@synthesize firstName;
@synthesize lastName;
-(id)init {
  self = [super init];
  firstName = @"";
  lastName = @"";
  return self;
}

코드 4) @systhesize 구문으로 노가다를 제거한 Person의 인터페이스

즉, @property~@synthesize 쌍으로 이루어진 구문으로 각각의 인스턴스 변수를 “이것은 프로퍼티다”라고 말해주면 컴파일러는 알아서 맨 위의 코드에서 노가다로 만들었던 getter, setter 메서드들을 자동으로 생성해준다. 이런 메서드들을 접근자(accessor)1라고 한다.

즉 프로퍼티는 그 자체로 실체가 있는 개념이라기보다는 객체 외부에서 그 객체를 살펴봤을 때 객체 내부에 이렇게 활용 가능한 정보들이 있다라는 명세서와 같은 개념이라고 볼 수 있다. 어차피 객체 내의 인스턴스 변수는 숨겨져 있으므로, 접근자메서드를 통해 액세스할 수 있는 정보들만 “프로퍼티”가 될 수 있고, @property~@synthesize  구문은 “이런 이런 것들이 프로퍼티로 존재한다”라는 것을 명시해서 컴파일러로 하여금 이에 대한 접근자를 자동으로 만들어주라고 하는 것이다.

심지어 프로퍼티는 반드시 인스턴스 변수일 필요도 없다. 예를 들어 맨 처음의 예에서 인스턴스변수는 두개 뿐이지만,

-(NSString *)fullName
{
  return [NSString stringWithFormat:@"%@ %@",firstName,lastName];
}

이라는 메서드를 추가해준다면, Person의 외부에서는

NSString *hisFullName = [aPerson fullName];

과 같은 식으로 풀네임을 얻어올 수 있는데, 이 fullName 역시 프로퍼티가 된다!

@property (nonatomic, readonly) NSString *fullName;

이라고 인터페이스 파일에서 선언해주면 별도의 fullName을 위한 인스턴스 변수를 만들지 않아도, fullName 이라는 프로퍼티를 이용할 수 있는 것이다. 프로퍼티는 개념적으로 객체외부에서 정보를 얻는 방법일 뿐이다.

키밸류 코딩에 대한 오해

문제는 인터넷에서 찾아볼 수 있는 많은 글에서 이렇게 접근자 메서드를 자동으로 생성해주는 @property~@synthesize 구문을 키밸류 코딩이라고 설명하고 있는 경우가 너무 많다는 것이다. 분명히, 이는 키밸류 코딩이 아니고 “프로퍼티”라고 하는 Objective-C의 다른 기능을 의미하는 것이다

키밸류 코딩에 관한 진실

키밸류코딩은 사실 별게 아니다. 그런데 별 게 아니기 때문에 이를 명확하게 설명하기가 힘들 뿐이다. 키-밸류 코딩은 말 그대로 키를 가지고 값에 접근한다는 의미이다. 실질적으로는 NSString 문자열의 내용을 가지고 객체 내부에 존재하는 값에 접근하게 된다.

따라서 @propery~@synthesize 구문이 자동으로 접근자를 생성해주는 것은 키밸류 코딩이 아니라고 명확하게 말할 수 있다. 하지만 진정으로 중요한 것은 이 구문이 생성해주는 접근자 메소드의 이름이 왜 하필 인스턴스 변수와 같은 이름을 갖느냐는 것이다. 실제로 이 구문을 사용하면 -firstName, -setFirstname: 과 같이 메소드가 만들어진다. 이름이 이렇게 되는 이유, 그것은 키밸류 코딩으로부터 시작하게 된다. 이 시점에서는 둘의 연관관계는 별로 없지만, 나중에 가면 이런 규칙은 정말 ‘위대해’지기 까지 한다.

valueForKey:, setValue:forKey:

키 밸류 코딩은 이 두 메소드를 사용해서 객체의 프로퍼티 혹은 인스턴스 변수에 접근한다. 여기서 ‘혹은’이라는 단어도 중요하다. 위에서 만든 Person 클래스의 인스턴스인 person에게 이름을 지어준다고 생각해보자.

Person *person = [[Person alloc] init];
person.firstName = @"KILDONG";
person.lastName = @"KIM";

키밸류 코딩에서는 접근자 메서드를 키 이름을 기준으로 만든다고 했다. 이 때 person.firstName 과 같이 구두점으로 구분하는 문법을 사용하면 이는 키밸류 코딩의 접근자 이름 규칙을 따른다고 간주하고 실제로는 [person setFirstName:@"KILDONG"]; 이라는 구문과 완전하게 동일하게 동작하게 된다. 다만, 이는 키밸류 코딩의 이름 규칙으로 얻을 수 있는 부가적인 장점이지, 키밸류 코딩의 핵심적인 내용은 아니다.

즉, 구두점을 통한 액세스는 키밸류 코딩에서 사용하는 이름 짓기 방식을 그대로 따르고 있기 때문일 뿐이고, 실제 해당 객체가 키밸류 코딩을 따르지 않고 있더라도 사용이 가능하며, 키밸류 코딩을 따른다고 구두점 구분법을 사용해야 할 이유또한 없다. (둘은 완전히 별개이다.)

실질적인 키밸류 코딩은 “문자열”을 통해 키에 접근하는 방식이다. 따라서 person의 이름 짓기는 이렇게도 가능하다.

[person setValue:@"KILDONG" forKey:@"firstName"];
[person setValue:@"KIM" forKey:@"KIM"];

그런 다음, 실제로 제대로 된 이름이 들어가 있는지 확인해볼 수도 있겠다.

NSLog(@"person's full name : %@ %@", person.firstName, person.lastName]);

여기까지는 별로 놀랍지도 않다. 다만 되려 ‘편리함’을 강조하는 키밸류 코딩이 사용하는 setValue:forKey: 를 사용하는 것이 더 번거롭지 않느냐는 이야기를 하는 것이 맞을 수도 있겠다.

이번에는 진짜 마법을 볼 차례이다. 위에서 작성한 코드3), 4)를 아래와 같이 대폭 간소화하자.

#import
@interface Person : NSObject
{
  NSString *firstName, *lastName;
}
@end

코드 5) 프로퍼티 선언을 제거한 인터페이스

#import "Person.h"
@implementation Person
-(id)init{
  self = [super init];
  firstName = @"";
  lastName = @"";
  return self;
}
@end

코드 6) 프로퍼티의 합성을 제거한 구현부. 이제 Person의 인스턴스 변수는 완전히 숨겨진다.

이렇게 변경하더라도,

[person setValue:@"KILDONG" forKey:@"fistName"];
[person setValue:@"HONG" forKey:@"lastName"];
NSLog(@"person's name : %@ %@",[person valueForKey:@"fistName"], [person valueForKey:@"lastName"];

와 같이 시험해보면 에러없이 컴파일 되며, 심지어 결과 역시 정확하게 나온다. 이 Person 이라는 클래스는 내부에 무슨 변수가 있는지 알수도 없고, 접근자 메서드도 제공하지 않지만 키-밸류 코딩은 마치 마법처럼 person  객체에 값을 쓰고, 또 읽어왔다.

키밸류코딩, 마법의 비밀

어떤 NSObject의 하위 클래스가 (대부분의 코코아 클래스는 NSObejct의 하위클래스인데, 이는 엄청나게 다행인 것이다!) valueForKey: 메시지를 받게되면, 이 메소드는 다음과 같이 동작하게 된다. 예를 들어 키 이름이 myFavoriteMovie 라고 하자. (일부러 길게 씀) 즉 어떤 객체가 -valueForKey:@”myFavoriteMovie” 라는 메시지를 받았다면,

  1. 먼저 그 객체에 -myFavoriteMovie 라는 메소드가 있는지 살펴본다. 만약 존재한다면 객체는 [self myFavoriteMovie]라고 메시지를 보내고 그 결과값을 리턴해준다.
  2. 그런 메소드가 없다면 비슷한 다른 이름들을 찾아본다. -getMyFavoriteMovie, -isMyFavoriteMovie 라는 메소드를 찾아본다. 이런 이름이 있다면 그 메소드를 실행해서 나온 결과값을 리턴해준다. 그마저도 없다면 여러 다양한 가능성을 찾아본다. 예를 들어 -countOfMyFavoriteMovie, -objectInMyFavoriteMovieAtIndex:, -myFavoriteMovieAtIndexes 등등 키밸류 코딩의 이름 규칙이 적용되는 여러 메소드들을 찾아본다. 특히 이들은 배열이나 세트와 관련된 메소드들인데, 이런 메소드가 있다면 이 메소드 내에서 사용하는 배열의 프록시 객체를 리턴해준다.
  3. 메소드 중에서 일치하는 이름을 찾아내지 못한다면, 이번에는 인스턴스 변수를 찾는다. myFavoriteMovie, isMyFavoriteMovie, _myFavoriteMovie, _isMyFavoriteMovie 등의 인스턴스 변수를 찾는다. 있다면 그 인스턴스 변수에 저장된 “객체”를 리턴한다. (-valueForKey: 는 id형을 반환하므로 객체를 받게 된다.)
  4. 그마저도 없다면 그 키는 이 객체에 없다는 것이다. 객체는 [self valueForUndefinedKey:@"myFavoriteMovie"]; 를 호출하고 그 메소드는 (따로 오버라이드하지 않았다면) 예외를 발생시킨다.

이와 같이 다소 복잡한 경로를 거쳐서 값을 액세스하는 방법이다. 따라서 직접적인 액세스에 비해서는 느리지만, 활용하기에 따라서는 상당히 강력하게 활용할 수 있다. 특히 객체들의 정보를 표로 보여주는 경우에 테이블의 칼럼명 (identifier)을 객체의 키 이름으로 정한다면 키밸류 코딩을 사용하여 여러 케이스를 나눌 필요 없이 한 방에 이를 처리할 수도 있다.

실제로 valueForKey: 메소드를 사용할 경우는 그리 많지가 않을수도 있다. 그러나 키밸류 코딩이 따르고 있는 이름 만들기 규칙만큼은 키밸류 코딩에 의존하는 수많은 기술들에서 활용되고 있으며, 구두점을 통한 액세스와 같이 (person.firstname) 이와 무관해 보이는 부분들에 있어서 엄청 많은 편의성을 제공해주게 된다. 특히 모델의 복잡도나 규모가 커지면 키패스를 사용해 많은 양의 코드를 절약하게 되고, 성능의 향상도 도모할 수 있다.

키밸류 코딩 따르기

키밸류 코딩은 valueForKey;, setVale:forKey: 의 메소드를 사용하는 것보다, 키밸류 코딩에서 정하는 이름 규칙에 맞게 프로퍼티나 메소드를 만들어 사용하는 것에서 더 큰 잇점을 취할 수 있다. 상당히 유용한 키밸류 옵저빙 매커니즘은 특정 객체의 일부 정보가 변경되는 것을 원격으로 알아차리는 방법인데, 이 역시 키 밸류 옵저빙 이름 규칙을 따를 때 효율적으로 이용할 수 있게 된다. 따라서 이런 이름 규칙을 습관화 해 놓으면 별도의 노력이나 주의를 기울이지 않고도 많은 장점들을 쉽게 얻을 수 있다는 점에서 알아두는 것이 좋을 것 같다.

  1. 접근자라는 말은 딱딱한 느낌이 들어서 좋은 번역이라 할 수는 없는데, 마땅한 단어가 생각이 나지 않는다.

[iOS/OSX] 앱 로컬라이징 하기

nib 파일 로컬라이징

nib 파일을 선택하고 인터페이습 빌더에서 언어를 추가하면 해당 언어로 표시할 nib 파일이 복사된다. 이에 따라 버튼 레이블이나 뷰에 올라가는 이미지와 텍스트를 변경하면 기기에서 설정한 언어에 따라 자동으로 해당 언어의 nib 파일이 선택되어 로컬라이징된 UI를 보여주게 된다.

문자열 테이블

예를 들어 경고창이나 검색 결과 문구에 대해서는 문자열 테이블을 사용하여 로컬라이징한다. 문자열테이블은 .strings 라는 확장자를 가지고 있다. 예를 들어 코코아 앱에서 찾기 패널을 만들었다고 한다면 Find.strings 파일을 각 언어별로 만들어 둘 수 있다. 문자열 테이블은 키-값 쌍의 모음이다. 각각의 키와 값은 겹따옴표로 둘러진다.

이를 코드에서 사용하려면 NSBundle 을 사용해야 한다.

NSBundle *main = [NSBundle mainBundle];
NSString *aString = [main localizedStringForKey:@"Key1" value:@"DefaultValue1" table:@"Find"];

이 코드는 Find.strings 파일에서 Key1 의 키를 찾는다. 만약 값이 발견되지 않았다면 디폴트로 DefaultValue1 이라는 문자열이 된다.

문자열 테이블 만드는 법

Xcode에서 빈 파일을 하나 만든다음, 이름을 Localizable.strings 라고 하고 English.lproj 디렉토리에 저장한다. 파일의 내용은 앞서 설명한 바와 같이 키와 값 쌍으로 입력하며, 각각의 쌍은 세미콜론으로 끝나야 한다.

"DELETE" = "Delete";
"SURE_DELETE" = "Do you really want to delete %d people?";
"CANCEL" = "Cancel";

이제 만들어진 문자열 테이블을 Xcode에서 선택하고, 인스펙터 패널에서 다른 언어 버전을 추가한다. 신규로 추가한 언어를 선택한 다음, 내용을 편집한다.

"DELETE" = "삭제";
"SURE_DELETE" = "%d명의 인원을 삭제하겠습니까?";
"CANCEL" = "취소";

알파벳이 아닌 문자가 있는 경우에는 유니코드로 변환할 것인지 묻는 경우가 있는데 변환을 해야 한다.

이렇게 저장된 문자열은 다음과 같이 사용한다. (문자열 테이블이 하나 밖에 없는 경우) 문자열 테이블이 더 있는 경우에는 테이블 이름도 명시해야 한다.

NSString *deleteString;
deleteString = [[NSBundle mainBundle] localizedStringForKey:@"DELETE" value:@"Delete?" table:nil];

이렇게 쓰는게 상당히 번거롭기 때문에 매크로로 처리할 수도 있다.

#define NSLocalizedString(key, comment) [[NSBundle mainBundle] localizedStringForKey:(key) value:@"" table:nil]

이 때, comment 부분은 그냥 무시된다. 하지만 genstrings 툴을 사용할 때, 코멘트를 보고 활용할 수 있다.

ibtool 을 사용하여 nib 생성하기

ibtool 명령은 터미널을 통해 다국어 적용 문자열을 덤프할 수 있는 명령이다. 아래 예는 MyDocument.nib 파일로부터 Doc.strings 파일을 생성해 내는 명령이다.

$ cd SomeProject/English.lproj
$ ibtool --generate-stringsfile Doc.strings MyDocument.nib

이렇게 생성된 string 파일에서 만약 스페인어 버전을 만들겠다면, 스트링 파일의 값들을 수정해준다. 그런 다음,

$ mkdir ../Spanish.lproj
$ ibtool --strings-file Doc.strings --write ../Spanish.lproj/MyDocument.nib MyDocument.nib

즉, 현재 디렉터리의 MyDoument.nib 파일을 Doc.strings 파일을 참고하여 일부 레이블을 번역한 버전을 자동으로 생성하는 것이다.

문자열 포맷의 토큰 순서 변경하기

예를 들어 페이지 번호를 표시한다고 할 때,

NSString *theFormat = NSLocalizedString(@"PAGE", "%d of %d ");
x = [NSString stringWithFormat:theFormat, currentPage, totalPage];

와 같이 포맷팅할 수 있다. 하지만 언어별로는 순서가 달라질 수 있는데, 이는 다음과 같이 문자열 테이블 작성시에 토큰의 순서를 달러 표시와 함께 써서 변경할 수 있다.

"PAGE" = "%2$d 중 %1$d";

 

[iOS/OSX] Timer 사용하기

특정한 시간 이후에 작업을 시행하는 타이머를 사용하는 방법에는 몇 가지 방법이 있을 수 있는데, 코코아에서 타이머는 NSTimer 객체를 통해 구현된다.

타이머와 런루프

타이머는 런루프(Run Loop)와 밀접한 관련을 맺고 있다. 타이머는 스스로 동작기한을 가지고 있고, 그 상태로 런루프에 등록된다. 런루프는 그 기한이 지나는 시점에 타이머를 지켜보고 있다가, 타이머가 실행하기로 한 액션을 지정된 객체로 보내게 된다.

런루프는 (역시나 런루프와 스레드를 혼동하는 불편한 진실 때문에 이에 대한 별도의 포스팅도 필요할 듯) 사용자의 입력 등에 적절히 반응하기 위해서 계속해서 “기다리는” 일종의 루프이다. 예를 들면 키보드는 풀링이라는 방식으로 일종의 런루프를 가지고 있다. 즉 사용자가 키보드를 누르지 않는 상황에서 키보드는 ‘최선을, 총력을 다해서’ 사용자 입력을 반복해서 기다린다. 런루프는 이와 유사하게 스레드 내에서 사용자의 UI 입력을 최선을 다해 기다린다.

타이머가 지정한 액션은 런루프에 의해서 확인된다. 타이머를 사용할 때 가장 유의해야 하는 점은 NSTimer는 실시간 타이머가 아니라는 점이다. 만약 반복되는 주기를 가진 타이머의 특정 작업이 있다고 할 때, 런루프는 단지 그 타이머만을 감시하는 것이 아니라 아주 많은 종류의 입력을 처리해야 하기 때문에 이를 놓치는 경우가 있을 수 있다. 따라서 NSTimer를 통해 이뤄지는 지연된 작업이나, 반복작업은 실제 시간과 다소 차이가 있을 수 있으며, 경우에 따라서는 “상당히 큰” 차이가 발생할 수 있다.

타이머 사용하기

스케줄된 타이머 사용하기

타이머를 사용하는 가장 일반적인 방법은 NSTImer의 scheduledTimerWithTimeInterval:targer:selector:userInfo:repeats: 를 사용하는 방법이다. 이는 현재 메인 런루프에 타이머를 추가한다.

-(void)startOneOffTimer {
  [NSTimer scheduledTimerWithTimeInterval:1.0
                                   target:self
                                 selector:@selector(timerFired:)
                                 userInfo:[self userInfo]
                                   repeat:NO];
}
-(void)startRepeatingTimer {
  NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5
        target:self selector:@selector(timerFired:)
      userInfo:[self userInfo] repeat:YES];
  self.repeatedTimer = timer;
}

타이머의 반복이 없는 경우에는 타이머가 종료된 후 해당 액션을 호출해주고 난 후 타이머는 자체적으로 해제된다. 그렇지 않은 경우에는 추후에 반복되는 타이머를 해제해주기 위해서 따로 참조점을 갖고 있어야 한다.

스케줄링 된 타이머는 생성 즉시 런루프에 추가되어 카운트다운을 시작하게 된다. 이와 별도로 특정 시점부터 동작을 시작하는 타이머는 다음과 같이 만들 수 있다.

-(void)startFireDateTimer {
  NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:3600];
  NSTimer *timer = [[NSTimer alloc] 
                        initWithFireDate:fireDate
                                interval:0.5
                                  target:self
                                selector:@selector(timerFiredMethod:) 
                                userInfo:[self uesrInfo] 
                                  repeat:YES];
  NSRunLoop *theRunLoop = [NSRunLoop currentRunLoop];
  [theRunLoop addTimer:timer forMode:NSDefaultRunLoopMode];
}

시작된 타이머를 멈추기

위와 같이 만들어지고 시작된 타이머는 반복되지 않는다면 종료시 호출해야 하는 액션을 호출하고, 정지된다. 하지만 아직 정해진 시간을 다 채우지 않았거나, 반복해서 실행되는 타이머는 멈출 필요도 있다. 타이머를 멈추기 위해서는 해당 타이머에 invalidate 메시지를 보내면 된다.

-(void)stopRepatedTImer
{
  [self.repeatedTimer invalidate];
  self.repeatedTimer = nil;
}

타이머를 사용하지 않은 예약된 작업 호출

타이머를 사용하지 않고도 특정한 시간 지연 후에 간단히 어떤 기능을 실행할 수 있다. 이 기능은 NSObject에 이미 정의된 메소드이다. (역시 코코아의 루트 클래스가 NSObject라는 사실은 매우 다행스럽다!) 이는 -performSelector: withObject : afterDelay: 이다. 이는 다음과 같이 사용할 수 있다.

[self.tableView performSelector:@selector(reloadData)
                     withObject:nil
                     afterDelay:10.0];

이미 예약된 작업 취소하기

NSObject의 클래스 메소드인 +cancelPreviousPerformRequestsWithTarget:selector:object :+cancelPreviousPerformRequestsWithTarget: 을 사용한다. 이는 지연 후에 일을 하게되는 객체가 이 메시지를 받고 예약된 작업을 실행하지 않게 된다.

[스터디] 후위식을 사용하지 않는 공학용 계산기 로직

공학용 계산기 엔진 만들기

예전에 미진하게 만들다가 말았던 계산기 만들기와 관련해서. 지난 번 글은 사실 스탠포드의 iOS 앱 개발 강의 내용을 바탕으로 만들었다. 물론 일부 내용을 좀 수정하기는 했는데. 그 강의에서는 스택에 숫자와 연산자를 넣는 형태로 동작했다. 예를 들면 3 + 5 를 계산하기 위해서는 3, enter, 5, enter, plus 의 순서로 값을 먼저 입력하고 연산자를 뒤에 입력하는 형태의 약간 이상한 방식으로 동작했다. 이는 사실 그 계산기 프로젝트를 계속 발전시켜 공학용 계산기로 만들기 위해서이다.

보통의 공학용 계산기는 ‘후위식’을 이용한 계산 방법을 사용한다. 우리가 흔히 사용하는 1 + 2와 같은 수식의 형태를 연산자가 가운데에 들어간다고 해서 중위식이라고 하는데 이를 12+ 라 쓰고 뒤에서부터 계산하는 방식을 후위식이라 한다. 중위식을 후위식으로 변환할 때 연산의 우선 순위를 고려하게 되므로, 곱하기/나누기가 더하기/빼기와 섞여있는 식이나 괄호가 들어있는 식도 후위식으로 전환하고 나면 뒤쪽에서부터 차근차근 계산해서 한 번에 계산할 수 있는 장점이 있다. 물론 후위식 자체는 사람이 계산하기에는 무척 불편한 형태의 수식 표현이지만 컴퓨터는 스택에서 연산자와 피연산자를 꺼내서 쉽게 처리할 수 있다.

문제는… 이 후위식으로 변환하는 과정이 꽤나 골치아프다는 게 문제. (연산 우선순위를 정의한 기준과 별도의 스택이 추가로 필요) 그래서 생각한게 사람이 계산하는 방식과 마찬가지로 (단순 무식하게) 각 항별로 버퍼에서 우선 계산하고, 다음 항을 계산해야 할 때 버퍼에 있는 값을 결과값에 더해 버리면 되지 않을까? 하는 것이었다.

즉 이를 정리하면 다음과 같다.

  1. 버퍼의 값은 1로 초기화한다. 버퍼에는 숫자값과 현재 항이 음수인지, 양수인지를 구분하는 플래그, 그리고 새로 버퍼에 넣는 값을 곱할 것인지, 나눌 것인지를 결정할 플래그. 이렇게 3개의 값이 필요하다.
  2. 곱하기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가 버퍼값과 곱해진다. 버퍼의 계산방식을 곱하기로 변경한다.
  3. 나누기 연산자가 수식에서 나오면 다음에 나오는 숫자는 버퍼에 들어가면서 기존 버퍼값을 나눈다. 버퍼의 계산 방식을 나누기로 변경한다.
  4. 더하기 연산자가 수식에서 나오면 이는 하나의 항에 대한 처리가 끝나고 다음 항이 시작되기 때문에 버퍼에 계산된 값을 꺼내 결과 값에 더해준다. 다음 버퍼는 “더해지는” 항이므로 버퍼의 부호를 양수로 설정해놓고, 값을 초기화한다.
  5. 빼기 연산자가 수식에서 나오면 이 역시 하나의 항에 대한 처리가 끝났다. 더하기와 같이 처리하고 다음 항의 부호는 음수이므로 버퍼의 부호를 음수로 설정한다.
  6. 숫자값이 나올 때는 이 값을 버퍼에 집어 넣는다. 버퍼의 곱하기/나누기 플래그에 따라 이값이 버퍼에 들어갈 때 기존의 버퍼의 값에 곱하거나 나눈다.

이 규칙에 따라 곱하기나 나누기로 연결된 식은 하나의 항으로 취급되어 버퍼에서 계산되고, 더하기나 빼기가 나타날 때 결과 값에 더해진다. 이 과정을 수식의 전체에 대해 진행하면 자동으로 식이 계산된다.

이 방식을 사용하면 괄호처리 역시 어렵지 않게 할 수 있다. 여는 괄호가 나타나면 그 괄호를 닫을 때까지 (따라서 열린 괄호의 수를 카운트하며 추적해야 한다) 수식에 들어있는 식을 별도의 수식으로 만든다. 그런 다음 이 수식을 수식 처리 함수 (즉 그 함수 자신)에게 넘겨 재귀적으로 처리하고 그 결과값을 버퍼에 집어넣으면 된다.

기타 파이, 로그, 제곱, 제곱근 등의 처리도 버퍼에서 별도의 필터를 통해 기능을 추가해 나갈 수 있을 것이다.

계산기엔진 소스코드 : http://www.box.com/s/2neu29qfmblaeoiaj5a8