Eonil's Blog

여기는 에오닐의 블로그입니다. 컨텐츠를 공유하고 싶으신 분은 About페이지의 라이센스를 확인바랍니다~

헝가리안 표기법의 재발견

오늘 ‘조엘 온 소프트웨어를 넘어서’를 보았습니다. 많은 내용이 ‘조엘 온 소프트웨어’의 재탕이라 실망한 부분도 있었지만, 새로운 내용도 몇 있었고, 꽤 가치있는 내용입니다.

지난 포스트에서 헝가리안 표기법을 무시했었는데, 이 책을 읽고 나니 다시 생각해야겠다는 생각이 들었습니다. 사실 이 책을 구매한 이유의 중 하나가 이 챕터를 보기 위해서였습니다.

내용을 요악하면 헝가리안 표기법의 원래 목적은 변수의 타입을 구분하기 위한 것이 아니라, 변수의 종류를 구분하기 위한 것이었다는 겁니다. 이것이 MS 내에 존재하는 2류 프로그래머들에게 제대로 전달되지 않아 지금 오명을 뒤집어쓰고 있다는 것이죠.

헝가리안 표기법을 처음 봤을 때 세상에 뭐 이런 멍청한 표기법이 있나 하는 생각이 들 정도였습니다. 잘못 알려진 헝가리안 표기법은 컴파일러가 완벽하게 검사해주는 기본 타입을 중복해서 표기합니다.

// Prefix 'ui' means 'unsigned int'.
unsigned int uiVar1 = 0;
unsigned int uiVar2 = 0;

완벽한 낭비인데다가 가독성을 향상시키지도 못합니다. 원래는 이런 것입니다.

// Prefix 'x' means 'x axis'.
// Prefix 'y' means 'y axis'.
unsigned int xVar1 = 0;
unsigned int yVar1 = 0;

같은 타입의 변수라 해도 다른 영역에서 쓰인다면 이들은 구분되어야 하고, 시각적으로도 잘 인지되어야 한다는 것입니다. 조엘은 헝가리안 표기법을 매우 좋아하는데 헝가리안 표기법에도 문제는 있습니다. 그것은 프리픽스를 약어로 사용한다는 것입니다.

// Prefix 'uwc' means 'UI Window Coordinate'.
function func1(int uwcVar1);

약어는 시간이 지나면 작성자도 못알아보는 암호가 되고, 결국 위와 같이 내용을 중복해서 설명하는 주석을 달아야 합니다. 이것보다는 단어 전체를 사용하는 애플 방식이 더 낫습니다.

(void)func1: (int)userWindowCoordinateVar1;

너무 깁니다. 그래서 타이핑이 힘들죠. 하지만 읽는 것은 더 쉽습니다. 작성 후 몇 달이 지났다고 생각하면 더 생각할 필요도 없죠. 애플의 길다란 네이밍 방식에 대해 ‘너무 길다’라고 불평하는 사람은 코드 유지관리를 해본적이 없는 사람입니다. 작성할 때 몇 글자만 더 타이핑하면 두고두고 보기 쉬운 코드를 만들 수 있습니다. 게다가 Xcode, Eclipse, VS.NET 등 대부분의 메이저 IDE는 강력한 이름 완성 기능을 제공합니다. 그래서 실제로 타이핑하는 양은 별 차이가 없습니다. 이름 자동 완성 기능이 없는 편집기를 쓰고 계시다면 할 말이 없네요. 저는 기억력이 모자라서 그런 편집기로는 코딩 못합니다.

헝가리안 표기법의 가치

중요한 것은 컨셉입니다. 변수명을 일종의 수식어로 데코레이트하는 것이 유용하다는 것을 알지만 이 수식어로 무엇을 사용하는 것이 좋은지는 잘 몰랐습니다. 헝가리안 표기법은 이 수식어로는 변수가 사용되는 영역의 종류를 지정하는 것이 적당하다는, 경험에서 우러나온 지혜를 알려줍니다.

컴파일러 형 검사 남용

사실 변수의 종류를 구분하기 위해 별도의 타입을 만들고 컴파일러에게 이러한 타입 체크를 맡겨버리는 방법도 있습니다. 클래스가 하는 일이 그것이죠. 하지만 기본적인 프리미티브 타입에 대해서까지 이러한 방식을 사용하는 것은 코드 작성시 소모되는 오버헤드가 많아 생산성을 떨어뜨리게 될 것 같습니다. 일종의 컴파일러 형 검사 남용이죠. 형 검사는 형 검사일 뿐 로직을 보증할 수는 없습니다. 결국 어느정도의 복잡도는 프로그래머 머리속에서 처리되어야 합니다.

간단한 일인데요… 어쩌구 저쩌구…

일을 하다 보면 이런 멘트를 자주 접하게 됩니다.

이거 간단한 일인데요… 어쩌구 저쩌구… 금방 되겠죠?

일단 이런 말을 들으면 대번에 떠오르는 생각은 한가지입니다.

어떻게든 싸게 부려먹고 싶어하는구나!

물론 이 생각을 표현하지는 않습니다. 하지만 제 계산으로는 간단하고 금방되는 일은 가격이 더 비싸집니다.

간단한 일

간단한 일이라고 하지만 실제로는 간단한 일은 없습니다. 그렇게 간단하면 직접 했겠죠. 직접 처리할 수 없을 정도로 복잡하거나, 힘들거나 어렵기에 외주가 나가는 거죠. 이 경우, 작업자는 두가지를 생각해야 합니다.

  • 발주자가 일의 복잡성을 모릅니다. 발주자에게 전문적인 수준의 교육을 해 주어야 합니다.
  • 발주자가 일의 복잡성을 알고 싶어하지 않습니다. 즉, 작업자가 대신 처리해주어야 합니다.

보통 발주자가 교육을 원하는 케이스는 거의 없기에, 대부분의 작업자가 할 일이 늘어나는 것입니다. 특히, 이 경우, 발주자가 일처리에 대한 지식이 없다는 것이 확실하기 때문에 복잡성을 추상화하고, 경우에 따라서는 동의 없이 결정을 하는 리스크도 안아야 합니다. 연속되는 컨펌과 크리틱, 재작업은 기본입니다.

뭐 다 충분히 예상 가능한 일이고, 처리 가능하지만, 비용은 최소한 2배 이상 들어갑니다. ‘간단한 일’은 실제로는 ‘저는 아무것도 모르니 알아서 잘해주세요’입니다. 물론 실제로 결정권을 주면 정말 간단한 일이 되겠지만, 결정권을 주는 클라이언트는 거의 없고, 대부분은 그날그날의 기분에 따라 결정이 바뀝니다. 이런 일은 최소한 2배의 비용을 받아야 하며, 기획서도 부실하고 이상할 정도로 리젝트 비율이 높으므로, 수지가 맞으려면 3배 이상은 받아야 합니다.

금방 되는 일

한 가지 잘 알려지지 않은 사실은, 모든 일에는 적절한 작업기한이 있다는 것입니다. 그리고 이 기한에서 늘거나 줄어드는 것은 양쪽으로 비용이 추가됩니다. 보통 발주자는 기한이 늘면 비용이 추가되고, 기한이 줄면 비용이 줄어드다고 생각합니다. 하지만 그렇지 않습니다. 기한이 늘면 어떻게든 작업이 연장되므로, 신경쓰고 서비스해야 하는 부분이 늘어납니다. 작업자의 시간을 구매하는 것이니 당연히 비용이 추가됩니다. 하지만 적정기간보다 기한이 줄면, 작업자는 절대마감에 맞춰 없는 시간을 만들어내는 마술을 부려야 합니다. 그런데 클라이언트가 ‘금방 될 것’이라고 언급하는 경우, 대부분은 발주자가 업무에 걸리는 시간을 잘 모르기 때문입니다. 그래서 ‘금방 되는 일’을 원하는 경우, 이 말의 의미는 ‘어떻게든 알아서 마감을 맞춰주세요!’ 라는 뜻입니다.

이 경우, 추가되는 비용은 곤란할 정도로 계산하기 어렵습니다. 작업자의 여가와 휴식 시간을 사용해야 하기 때문이며, 최악의 경우, ‘정신과 시간의 방’을 사용해야 하기 때문입니다.

비용 추가

사실 발주자가 이러한 사항을 잘 모르지는 않습니다. 특히, 시간과 비용은 어디에서나 중요하기 때문에, 제대로 된 발주자라면 이런 부분에는 탁월한 감각을 가지고 있습니다. 이들은 이런 단어를 함부로 언급하지 않습니다. 이런 단어를 언급해야 하는 상황은 고비용을 불러오기 때문에, 항상 적절한 난이도와 기간을 계산합니다.

그래도 이러한 ‘간단한’이나 ‘금방 되는’ 같은 단어를 언급하는 것은 실제로 ‘알아서’, ‘빠르게’를 원하기 때문입니다. 높은 비용을 청구해도 이상하지 않습니다.

간혹 가다 이런 단어로 수식하면 실제로 일의 복잡성이 줄어보여서 비용을 싸게 할 수 있지 않을까- 하고 생각하는 초보 발주자도 있는데, 그렇게 보일지는 몰라도 실제로 일의 복잡도가 줄어드는 것은 아니므로, 발주자가 실제로 이런 생각을 갖고 있다면 계약이 성립되지 않습니다. 이 경우 작업자는 위와 같은 사항을 언급해 실제는 어떻다는 것을 알려야 합니다. 대부분의 경우, 작업자 입장에서는 발주자가 어느 정도의 감을 갖고 있는지 알 수 없으므로 일단 항상 이런 사항들을 언급해야 합니다. 이런 것을 다 알고도 낮은 비용으로 일을 해결하려 한다면 정당한 가격을 지불하려 하지 않는 ‘악덕 업주’일 따름입니다. 그런사람과는 일을 하지 마십시오.

Stop on Objective-C Exceptions

Xcode에는 Stop on Objective-C Exceptions라는 옵션이 있습니다. 처음에 이 옵션을 보고  멋모르고 이걸 켰다가 지금까지 고생을 했네요.

일단, 이 옵션은 로우레벨 예외에서 멈추는 옵션입니다. Cocoa는 Objective-C의 예외를 캐치해 적절한 예외 메시지를 생성해 보여줍니다.

그런데 이 옵션을 체크하면 예외가 생성된 그 순간에 디버거가 멈춥니다. 그래서 예외 메시지가 전혀 표시되지 않고 EXC_BAD_어쩌구 하는 메시지나 SIGABRT만 보이게 됩니다. 최악의 경우에는 아무것도 안나옵니다.

로우 레벨 개발을 지원하기 위해 존재하는 옵션인거죠. 더 강력한 기능에 엑세스할 수 있도록 하는 옵션이지만, 평소에는 쓸 일이 없습니다.

예외 메시지 없이 디버깅하기…

너무 힘들었었습니다. 로우레벨 옵션을 켜놓고 지금까지 코코아에 원래 예외 메시지가 없는 줄 알았던 겁니다! 생각해보면 말이 안되는 거죠. 플랫폼 시스템용 API인데 예외 메시지가 전혀 없다는 건 있을 수 없는 일입니다. 그런데도 그려러니 하고 넘어갔던 건 이상한 프레임워크를 너무 많이 봐와서일겁니다.

이 문제를 발견하게 된 것은 Fast Enumeration 덕분입니다. for…in으로 컬렉션을 조회하고 있는데 조회문에서 계속 예외가 발생하는 거죠.

for(id <Discrete> e in elements)    // 여기에서 예외
{
    [e tick];
}

메시지도 없고, 모든 요소들을 다 검사해봐도 문제가 없었습니다. 4시간 정도를 고민하다가 그냥 잠들어버렸습니다. 좀 자다가 일어나서 다시 이걸 쳐다보니 조회 도중에 컬렉션을 수정하고 있다는 것이 생각났습니다. C#의 for…in에서는 조회 중 컬렉션을 수정하면 예외가 생겼죠. 곧바로 이런 기본적인 예외에 대한 메시지조차 표시하지 않는 이유가 뭘까? 하는 생각이 들었습니다. 그리고 예외 메시지가 없을리가 없다는 생각을 하게 됐죠. 그리고 생각난게 Stop on Objective-C Exceptions 옵션입니다.

생각난 김에 바로 이 옵션을 끄고 테스트를 해 봤죠.

‘컬렉션 조회 중 컬렉션을 수정하였기에 예외가 발생했습니다’라는 문구가 보이시나요? 제가 4달 동안 보고 싶었던 것도 이겁니다. 정말 반가워요! 지금가지의 어려웠던 세월이 힘겹게만 느껴집니다…

더 놀라운것은 이 옵션을 켰더라도 이 메시지를 볼 수 있다는 겁니다. 그냥 디버깅 컨티뉴를 하면 됩니다.

그러면 옵션을 켜지 않았을 때와 마찬가지의 메시지를 보여줍니다.

물론 경우에 따라서 그냥 종료되는 경우도 있는데, 그런 경우는 정말로 해당 예외에 대한 메시지가 없는 경우이겠죠. 또한, 어떤 경우엔, 안좋아 보이는 로그가 계속 뜨는데 예외로 잡히지 않는 경우도 있습니다. 이 경우엔 이 옵션을 켜야만 예외가 잡힙니다.

기본 세팅

처음 이 옵션을 봤을 때, Xcode가 여러 언어를 지원하므로 Objective-C에 특화된 기능인지 알고 그냥 켰습니다. 물론 왜 기본으로 꺼져 있을까… 하는 의심이 있긴 했지만요. 덕분에 지금까지 예외 메시지 하나 없이 계속 개발을 했던 것이죠. 플렉스 빌더같은 이클립스 계열은 뭔가 많이 세팅해주지 않으면 제대로 작동하는게 없을 지경이라 버릇대로 했던 것이 이런 결과를 불러왔습니다.

역시 애플의 성향대로, 기본 세팅이 최적의 세팅이었습니다. 웬만해서는 아무것도 건드리지 않는 것이 좋습니다.

결과적으로 이 옵션은 정말 로우 레벨입니다. 강력한 대신 불친절하죠. 일단 한동안 옵션을 끄고 사용해봐야겠습니다. 필요할 때만 켜서 쓰도록 말이죠.

언어와 생산성

간혹 JavaScript같은 동적 타이핑 언어들이 타입을 명시하지 않아도 되기에, 타이핑을 절약해 생산성 향상을 도모할 수 있다는 글을 봅니다.

글쎄요, 아주 작은 규모의 프로그램이라면 모르겠지만, 규모가 조금만 커져도 이 말은 헛소리가 되고 맙니다. 이러한 형식 미지정 언어 기능은 타이핑 시간은 줄여주겠지만, 코드만으로 필요한 형식을 알 수 없게 되기에 타이핑이 끝난 그 순간부터 고통이 시작됩니다.

다음 코드를 보아 주세요.

var queryAboutSomething = function(server, dataToFill, optionByCustomer)
{
    //...
}

인수들이 어떤 타입이어야 하는지 알 수 있나요? 이 함수를 호출하기 위해서는 어떤 개체들을 준비해야 하나요? 제대로된 타입의 개체를 넣지 않으면 함수는 오류를 내고 뻗어버립니다. 아무거나 넣으면 알아서 처리해주는 매직 함수를 작성할 수도 있겠지만, 그러면 인수를 검사하는 코드가 본문보다 길어질 것입니다.

이 인수들의 타입을 알아내기 위해서는 함수 전체를 리뷰해야 합니다. 그래서 결국 타입 정보를 주석이나 문서에 써넣게 되는데, 이런 주석은 코드와 동기화되지 않은 가능성이 매우 높기에 ‘나쁜 주석’의 표본이 되며, 문서에 있다면 함수를 호출할때마다 문서를 뒤적여야 하기에 생산성은 극도로 떨어지게 됩니다. 형식을 지정하는 어떤 표준적인 방법 또한 없으므로 코드 편집 툴이 자동으로 형식을 알려줄 수 있는 방법 또한 없습니다. 컴파일러나 인터프리터가 인수의 형식을 검사해 잘못된 호출을 자동으로 방지할 수 있을만한 기능도 없습니다. 작성은 편하지만 호출할 때는 몇 배의 시간이 필요합니다. 모든 함수의 인수 형식들을 기억할 수 있는 천재적인 머리를 가지고 있다면 이야기가 좀 다르긴 합니다. 설마 헝가리안 표기법을 생각하고 있는 분은 없겠죠?

제가 보기엔 이런 식으로 형식 정보를 알 수 없는 언어들의 생산성은 기계어보다 나을 것이 없어보입니다.

OCaml 같은 암시적 정적 타이핑의 경우, 자동 완성 기능이 잘 갖춰진 편집기가 있다면 명시적 타이핑과 같은 효과를 누릴 수 있을 겁니다.

생산성의 핵심

생산성의 핵심은 코드를 on-the-fly로 작성할 수 있는지 여부에 달려 있습니다. 코드 작성중에 문서나 헤더, 또는 StackOverflow를 뒤적여야 한다면 이미 생산성은 바닥을 기고 있는 겁니다. 또한, 리팩토링도 중요합니다. 내가 만든 식별자의 이름을 원하는 때에 마음껏 바꿀 수 있다면 이름을 정하느라 고민하지 않고 아무거나 쓴 다음 조금씩 고쳐갈 수 있습니다.

가장 큰 도움이 되는 것은 에디터의 이름 자동 완성 기능입니다. 함수를 호출할 때 타입을 포함한 함수의 시그니쳐가 뜹니다. 이걸 한번 쓱 보고 필요한 개체들을 만들 수 있습니다. 자동 완성 기능은 아주 빠른 레퍼런스 문서의 역할을 합니다. Visual Studio는 이를 극으로 밀어붙여 인텔리센스는 시그니쳐 뿐 아니라 문서 대부분을 바로 보여줍니다. 또한 자동 완성 기능은 타이핑 오타도 없애줍니다. 타이핑하는 순간 자동 완성이 되지 않는다면 정의되지 않은 이름이란 것을 순간적으로 알 수 있습니다.

두번째는 코드가 얼마나 읽기 쉬우냐에 달려 있습니다. 디버깅이든 유지관리든 간에 코드는 한번만에 끝낼 수 없습니다. 어떤식으로든 꾸준한 리뷰가 필요합니다. 리뷰시 인수 검사와 같은 데코레이션 사이에 파묻혀 있는 로직을 찾아내야 한다면 역시 생산성이 추락하고 있는 겁니다. 그래서 코드는 가능한한 필요한 로직만 기술해야 합니다. 어떠한 비-로직 코드가 많이 필요하다면 별도의 메서드로 분리해 읽기 쉽게 만들어야 합니다.

세번째는 강력한 디버거입니다. 런타임에 작동하는 인터랙티브 콘솔이 주어진다면 더 좋겠죠. 물론 이 콘솔은 위에서 언급한 이름 제시 및 완성 기능을 완전히 지원해야 합니다. 만약 메모리 변화가 트랜젝션이 가능해서 이러한 콘솔로 작동한 부분을 롤백시킬 수 있다면 실시간으로 소스코드를 수정하는 것도 가능할 겁니다.

동적 타이핑의 유용성

하지만 동적 타이핑도 유용할 수 있습니다. 아주 작은 규모의 프로그램에서만 말이죠. 함수도 필요없이 단순히 순차적으로 실행되고 끝나는 진짜 ‘프로그램’ 말이죠. HTML에서의 스크립트의 역할이 원래 그런 것이었기에 자바스크립트가 암시적 타이핑 언어가 되었을 겁니다. 함수를 정의하더라도 항상 코드 전체를 리뷰하기에 부담없는 사이즈였겠죠. 하지만 그 이상의 규모에서는 안됩니다. 함수같이 구조를 정의하려면 명시적 타이핑이 반드시 필요합니다. 물론 동적 타이핑보다는 OCaml같은 암시적 정적 타이핑이 훨씬 더 낫습니다.

이런 작은 규모에서 유용함을 지키려면 구조 작성 기능이 없어야 할겁니다. 있어도 최대한 사용을 절제해야겠죠.

인터랙티브 프로그래밍

프로그래밍이 시간이 많이 걸리고 어려운 것은 이것이 기본적으로 예측에 기반하기 때문입니다.

프로그램은 명령의 목록인데, 실제로 해당 명령이 수행되기 전에는 환경의 상태가 어떨지 알 수 없습니다. 프로그래머는 여러가지 조건과 졔약을 통해 실행상태를 예측합니다. 명령을 통해 변화하는 상태도 예측합니다. 예측이 틀리면 오류가 발생하고, 그러면 프로그램을 고쳐야 합니다. 그리고 처음부터 다시 실행합니다.

버그를 없애기 위해서는 실행순간의 상태를 정확하게 예측해야 합니다. 결국 프로그래머는 정교한 예언가입니다. 몰려오는 비구름을 보고 폭풍우를 예측하는 것과 같은 일입니다. 하지만 막상 비구름이 오기 전에 사라질 수도 있으며, 폭풍이 아니라 소나기로 끝날지도 모릅니다.

프로그램이 실행되고 있는 순간의 상태를 알 수 있고, 해당 순간에 프로그램을 직접 고칠 수 있다면 수정을 위해 프로그램을 처음부터 돌리지 않아도 되며, 무엇보다 미래의 상태를 예측하는 점쟁이같은 짓을 하지 않아도 됩니다.

새로운 프레임워크

현재의 프로그래머는 언어가 아니라 프레임워크를 배웁니다. 언어는 아무리 어려워도 한달이면 배울 수 있습니다. 익숙해지는데 시간이 걸릴 뿐이죠. 하지만 자고나면 새로나오는 프레임워크를 배우는 것은 고역입니다. 문법이 문제가 아닙니다. 문제는 이 프레임워크가 어떻게 움직이는지 모른다는 것입니다. 많은 경우에는 문서가 충분하지 않습니다. 치명적인 오류만 간신히 피할 수 있는 정도의 문서만 있는 경우도 많죠. 이 경우 반복적인 관찰로 동작을 알아내야 합니다. 물론 동작이 잘 설명된 문서가 딸려 있는 좋은 SDK도 있지만, 어떤 순서로 사용하는 것이 옳은 것인지 확신을 갖기 위해서는 여전히 관찰을 해야 합니다.

그래서 프레임워크를 배우는 것은 문법을 배우는 것이 아니라 그 코드가 어떻게 작동하는지 관찰하는 것입니다. 동물학자들이 사막여우의 생태를 관찰하기 위해 사바나 한가운데서 몇달동안 지내는 것과 같죠. 명령을 수행시켜보고 어떻게 움직이는지 관찰하는 일련의 일련의 실험을 통해 코드의 동작을 파악합니다. 10년간 사막여우를 관찰한다면 어떤 확신이 생길 것이고 100만Km 떨어진 도시 한가운데에서도 책을 쓸 수 있을 겁니다.

그래서 좋은 프레임워크의 조건은 일관적인 사용법입니다. 한 가지 방법만 알면 나머지는 비슷하게 쓰면 된다는 것이죠. 하지만 세상은 깔끔하지 못하고, 프레임워크들은 각각 자기만의 방식으로 움직입니다. 그래도 이것저것 많이 다루어보다보면 대략 뻔한 동작 패턴들을 익히게 되니 점차 사용이 쉬워지긴 합니다.

인터랙티브 프로그래밍

현재는 미래보다 더 알기 쉽습니다. 프로그래머가 실행 순간의 상태에 서 있다면 프로그래밍은 쉽고 빠르며 재밌는 일이 될 것입니다. 코딩을 미래에 대한 예측이 아니라 현재의 동작을 정의하는 것으로 만들어야 합니다. 그러면 새로운 프레임워크에 관찰이 실시간으로 이루어집니다. 또한, 지금 작성하고 있는 코드가 바로 프로그램에 추가되기에, 관찰하면서 원하는 동작을 이끌어내면 프로그램 작성이 완료됩니다. 마치 워드프로세서의 매크로 같은거죠. 원하는 동작을 녹화하면 프로그램이 자동으로 작성되고, 다음에는 이것을 재생하기만 하면 똑같은 동작이 만들어지는 것이죠.

이것은 디버깅에도 탁월합니다. 오류가 나면 프로그램이 일시정지하면서 콘솔이 나옵니다. 프로그램에서 오류가 난 부분도 바로 보입니다. 프로그래머는 잘못된 부분을 수정합니다. 수정하는 즉시 프로그램이 고쳐집니다. 일종의 트랜젝션 기능이 있다면 특정한 단위로 상태를 롤백하고 재개할 수도 있을 것입니다. 그러면 최종 트랜젝션 때부터 프로그램을 재개하면 됩니다. 정상적으로 작동하는 것이 확인되면 프로그램 작성이 완료됩니다. 일상적인 디버깅 과정중 가장 많은 시간을 보내는 부분이 컴파일과 상태 재현입니다. 코딩하는 것은 사실 별 문제도 아니죠. 가끔씩 고난이도의 논리 버그를 만나 몇일씩 생각만 하기도 하지만, 이 때도 상태 재연에 집중력을 빼앗기지 않는다면 더 빨리 문제를 해결할 수 있습니다.

이 사실을 깨닫는 데는 리스프를 알게 된 것이 가장 큽니다.

필요한 것들

인터랙티브 프로그래밍을 위해서는 일단 언어가 중요합니다. 실행중에 프로그램 자체를 수정하고 저장할 수 있는 기능이 필요합니다. 부분적으로가 아니라 완전하게 수정될 수 있어야 합니다. 이를 위해서 프로그램은 반드시 인터프리터 방식이어야 합니다. 또한, 현재 메모리된 전역/지역 변수들을 확인할 수 있고 실행되고 있는 코드가 모듈이나 프로시저상에서 어디에 위치하고 있는지도 확인할 수 있어야 합니다. 이러한 정보를 보여주고 변경을 즉시 적용할 수 있는 콘솔이 필요합니다. 많은 디버거가 실행중에 메모리를 수정하고 재개하는 기능을 제공하지만, 동시에 코드를 수정하고, 수정된 코드를 바로 프로그램에 주입하는 기능은 없습니다. 이러한 콘솔이 있으면 디버거가 따로 있을 필요가 없습니다. 코딩과 디버깅이 하나의 콘솔에서 동시에, 같이 이뤄집니다.

제가 알기로는 인터프리터 언어는 모두 이러한 가능성을 갖고 있습니다.

동적 언어의 태생적인 성능 한계

동적 언어는 간단히 말하면 동적 분기를 지원하는 언어입니다. 여러가지 폼이 있지만, 핵심은 그 하나입니다. 그리고 이 동적 분기는 그 컨셉 자체가 CPU가 최적화할 수 없는 컨셉입니다. 그래서 동적 언어는 정적 언어에 비해 느릴 수 밖에 없게 됩니다.

최적화 요약

최적화에 대해 이런저런 말이 많지만 어려운 부분은 저로서는 감당하기 힘드네요.

개발시 간편하게 쓸 수 있는 최적화 기법만 정리해 봅니다.

  1. 하드웨어 활용
  2. 컴파일러 최적화
  3. 동적 분기 최소화
  4. 연산 결과 캐시

하드웨어 활용

첫째로 해야 하는 것이 하드웨어 활용입니다. 소프트웨어 레벨에서 아무리 뭘 하더라도 하드웨어를 따라갈 수는 없습니다. 단, 이것은 프로젝트 초기에 결정해야 합니다. 어떤 플랫폼과 프레임워크를 쓰는지에 대한 문제이니까요.

일반적으로 특정 하드웨어에 종속적인 해결법은 하드웨어가 바뀜에 따라 무효해지므로, 구현비용을 잘 생각해서 결정해야 합니다. 간단히 구현할 수 있다면 조건부 컴파일로 쓸 수 있겠죠. 표준 스펙에 기반한 하드웨어 가속 기능을 사용하는 것이 가장 이상적입니다. 이런 관점에서 볼 때 크로노스 그룹의 오픈 표준 시리즈는 상당히 괜찮습니다.

만약 가상 하드웨어 샌드박스 플랫폼 안에 안에 있더라도 동일합니다. 이경우 일반적인 하드웨어와는 달리 종류를 막론하고 샌드박스 밖에서 바로 수입된 기능의 성능이 가장 좋겠죠.

컴파일러 최적화

Code Complete 책에서도 현대 컴파일러 최적화는 너무 다양해 예측하기 힘드므로 프로파일링해야 한다고 나와 있습니다. 컴파일러 최적화는 제가 아는 한 가장 쉽습니다. 하나의 룰만 지키면 됩니다.

  • 수동 최적화하지 말 것

일반적으로 수동 최적화는 컴파일러 최적화를 방해합니다. 컴파일러나 하드웨어에 대해 통달하지 않았다면 항상 컴파일러 최적화가 더 똑똑합니다. 수동 최적화된 코드는 특이하므로 컴파일러가 인지하기 어렵습니다. 현재 컴파일러들은 배열 언롤이나 함수 인라이닝은 물론, SIMD를 자동으로 활용하는 자동 벡터화 기능도 포함하고 있습니다. 각 하드웨어에 맞는 SIMD를 수동 작성하는것보다는 이쪽이 훨씬 낫겠죠.

동적 분기 최소화

Dynamic Dispatch는 동적 언어의 핵심입니다. 언어가 유연할수록 느려질 수 밖에 없는 이유가 바로 이것입니다. 유연성의 핵심은 분기가 얼마나 자유로우냐인데, 자유로운 분기는 CPU상에서 실행시 최적화되기 힘드므로 빨라질 수 없습니다. 가상 함수 계열은 모두 동적 분기이므로 이를 줄이면 큰 성능 향상을 가져올 수 있습니다. 그래서 이러한 가상 함수를 지원하는 언어는 모두 가상 함수를 finalize 해 성능을 높이는 키워드를 지원합니다. 보통은 final 키워드이죠. 함수명에 사용해 함수의 오버라이드를 금지시키거나, 클래스명에 사용해 모든 멤버의 오버라이딩을 한번에 봉쇄하기도 하죠.

Objective-C의 메시징 메커니즘은 기본적으로 동적 분기입니다. 여기에는 final 같은 키워드도 없으니 분기 예측을 통한 실행 성능 형상은 기대할 수 없습니다. 그러므로 성능이 중요한 코드는 Objective-C로 작성해서는 안되며, C로 작성되어야 합니다. 하지만 보통 어플리케이션 로직과 UI는 성능보다 유연성을 필요로 하므로 Cocoa의 역할은 알맞다고 할 수 있습니다.

연산 결과 캐시

사실상 프로그래머가 로직으로 간단히 할 수 있는 유일한 최적화 기법이라 할 수 있습니다. 대부분의 경우 소개되는 최적화 기법은

  1. 컴파일러 최적화를 방해하거나
  2. 캐시하거나

둘 중에 하나입니다. 캐시는 일종의 연산 제거입니다. 동일한 연산의 불필요한 중복 수행을 방지하는 컨셉이죠. 단점이라면 메모리가 필요하다는 것입니다. 제 생각에는 일반적인 프로그래머의 입장에서 캐시 외에는 답이 없다고 봅니다. 하드웨어에 밀접하고 컴파일러가 최적화를 잘 못해준다면 이야기가 좀 다르겠지만요.

캐시를 잘 작성하기 위한 방법은 한 가지입니다.

  • 최적화하지 말것. 느리다면 그 때 프로파일링하고 병목을 제거할 것.

맥코넬씨가 강조하는 핵심이죠. 일단 프로그램 작성시에는 효율을 생각하지 않고, 로직과 구조에만 집중해서 제작합니다. 예를 들면:

데이터가 필요하다면 그때그때 파일을 열어 DB에서 읽어옵니다.

  1. 이게 느리면 파일 핸들을 캐시하고,
  2. 그래도 느리면, DB 연결도 캐시하고,
  3. 그래도 느리면 쿼리 결과도 캐시합니다.
  4. 그래도 느리다면 결과셋이 표시된 UI도 캐시해야겠죠.

DB 내에도 일종의 캐시 구조가 있을 것입니다. 인덱스라는 기능이 있죠. 미리 계산된 데이터 위치를 따로 저장하는 것입니다. 캐시는 메모리와 프로세싱 사이에서 트레이드 오프가 가능하므로, 전체적인 시스템 사양에 따라 동적으로 조절될 수도 있습니다. 하지만 무엇보다도 만들기 쉽다는 것이 장점입니다. 전체 시스템 구조를 변경하지 않고 캐시를 추가하는 것은 어렵지 않습니다.

정점 데이터 사용법

GLES에서 그려질 정점 데이터를 전송하는 방법은 두가지가 있습니다.

  1. 직접 전송
  2. 버퍼로 캐시

1. 직접 전송

기본 방식입니다.
정점을 GPU로 바로 전송해 그리도록 합니다. 매번 그릴 때마다 정점이 전송되므로 VRAM이 필요없는 대신, 전송 대역이 필요합니다.
정점이 매우 많이 필요한 데이터가 아니라면, 대역이 많이 필요하지 않으므로 VRAM을 아끼기 위해서도 좋은 방법일 수 있습니다. 또한, 매번 정점 데이터를 보낼 때마다 갱신할 수도 있기에 동적인 정점이 필요하다면 이쪽이 더 좋습니다.

2. 버퍼로 캐시

정점이 많아지면 전송 대역에 한계가 오고 성능 저하로 이어집니다. 이에 정점을 VRAM에 캐시하고 이를 사용하도록 하는 방법입니다. 이 캐시를 VBO(Vertex Buffer Object)라 하며, DirectX의 VertexBuffer의 역할을 합니다.

  • 그리기 전, 정점을 미리 보내 두어야 합니다.
  • VRAM이 필요하며,
  • 드로잉 시에는 대역을 아낄 수 있습니다.
  • 정점이 많이 필요할 경우 좋습니다.

정점을 수정해야 할 경우, 새로 보내야 하므로 버퍼로 인한 이득이 사라집니다. 그래서 고정된 정점에만 사용해야 합니다.

버텍스 셰이더

그래픽 피델리티가 올라가면서 대량의 정점을 사용하는 것이 불가피하기에 버퍼를 쓸 수 밖에 없는데, 버퍼(=VRAM)에 있는 정점은 CPU가 엑세스할 수 없기에 동적인 변화를 줄 수 없습니다. 그래서 GPU가 버퍼에 있는 정점을 동적으로 사용할 수 있도록 하는 방법을 찾게 됩니다. 이것이 정점 프로그램, 즉 버텍스 셰이더입니다.

Objective-C++

Objective-C는 C 기반 언어이므로 C는 자연스럽게 섞어쓸 수 있습니다. C에 존재하는  기능은 아예 있지도 않을 정도니까요. (예: 정적 필드)

하지만 C++는 이와 비슷하게 C에서 갈라져 나온 언어로 Objective-C와는 다른 길을 걷어왔습니다. 이들은 기본적으로 호환이 되지 않습니다. 하지만 이들은 결국 C의 확장이므로, 포인터와 기본형식, 함수 호출이라는 최소의 공통분모가 있습니다. 기를 기반으로 애플은 이들을 섞어쓸 수 있도록 하는 옵션을 주었고, 실제로는 이들을 섞어서 사용할 수 있습니다. 여기에서 가장 기반이 되는 것은 포인터=개체라는 룰입니다. 하지만 제약이 있습니다.

  • 별도의 처리가 필요하므로 컴파일러가 이를 인지할 수 있도록 소스파일 확장자를 .mm으로 주어야 합니다.
  • 타입 시스템이 호환되지 않습니다. 그래서 클래스 정의와 상속은 완전히 분리되어 섞을 수 없습니다.
  • C++ 키워드는 인스턴스 변수 식별자로 사용할 수 없습니다. 다른 컨텍스트에서는 허용됩니다.
  • id라는 이름의 C++ 템플릿을 만들 수 없습니다. Objective-C의 프로토콜 정의와 문법이 겹치기 때문입니다.
  • 예외 처리가 분리됩니다. C++예외는 C++코드에서만, Objective-C 예외는 Objective-C 코드에서만  캐치할 수 있습니다.
  • Objective-C의 클래스 안에 C++클래스를 정의해도 전역 클래스로 간주됩니다.

더 자세한 내용은 레퍼런스 페이지에서 확인하실 수 있습니다:
http://developer.apple.com/mac/library/documentation/Cocoa/Conceptual/ObjectiveC/Articles/ocCPlusPlus.html#//apple_ref/doc/uid/TP30001163-CH10-SW1

코코아 터치에서 간단한 로컬 데이터 다루기

플래시에는 e4x가 있습니다. 그래서 웬만한 데이터는 전부 XML로 만들어서 처리해 버립니다. 플래시에서 XML을 주로 사용하는 이유는 e4x라는 강력한 도구 때문이며, XML 자체의 특징이나 매력과는 큰 상관이 없습니다.

그러면, 코코아 터치에서는 무엇으로 데이터를 다루나요?

결론부터 말씀드리자면 Property List입니다. 이것은 일종의 규격화된 XML로, 작은 데이터를 다루기에 알맞습니다. 데이터셋 크기가 커지면 다소 비효율적이라 예상합니다. 이 프로퍼티 리스트는 기본 컬렉션 개체인 NSArray와 NSDictionary에서 직접 지원하며, 정수, 문자열 등의 기본적인 프리미티브 형식 몇 가지를 지원합니다. 또한 계층적 개체로 구조화된 데이터도 저장할 수 있습니다. 설정 파일이나 그 외 간단한 용도로 제격이죠. XML로 저장되지만, 기능이나 사용 형태는 JSON에 가깝습니다. 예를 들어 전체 데이터셋을 로드하기 위해서 다음 중 하나의 메서드를 사용하면 됩니다.

[NSArray arrayWithContentsOfFile:]
[NSDictionary dictionaryWithContentsOfFile:]

사실 정수, 실수, 문자열과 단순 목록, 연관 목록을 지원하면 모든 종류의 데이터를 다 담을 수 있습니다. 데이터베이스란 것도 사실 별 거 없죠. 하지만 프로퍼티 리스트의 존재를 알기까지는 생각보다 힘든 과정이 필요했습니다.

혼란

처음에 필요했던 것은 간단한 로컬 데이터를 목록으로 만들어 읽는 것이었습니다. 하지만 이런 일이 자주 발생될 것이 너무 뻔하게 예상되므로 기왕이면 플래시의 e4x처럼 프레임워크가 자연스럽게 지원하는 가장 간편한 방식을 알아내고 싶었습니다. 또한, 웹상에 존재하는 많은 종류의 공개 API는 대부분 XML을 기본 데이터 형식으로 사용합니다. 그리고, 이러한 데이터는 프로그래머만 다루는 것이 아니며 때때로 기획자나 디자이너도 다룰 수 있게 해야 합니다. 이런 것을 종합하면, XML외에는 대안이 없습니다. XML도 어려워하는 사람이 많지만, 그나마 가장 널리 퍼져 있는 것이 XML이기 때문입니다. 타 직업군의 사람과의 호환성 확보에 가장 좋다고 할 수 있죠. 결국 제가 원한 프레임워크는 언어 내에서 간단히 다를 수 있으면서도 XML로 읽고 쓸 수 있는 프레임워크였죠. 일단 XML 지원은 필수였습니다.

코어 데이터

처음에 접한 것은 코어 데이터였습니다. 코어 데이터는 대량의 데이터셋을 위해 최적화된 데이터 엑세스 프레임워크입니다. SQLite DB나 바이너리 또는 XML을 백엔드 저장소로 사용할 수 있습니다. 개별 개체의 데이터 변화를 모두 추적하고, 컨트롤에 자동 바인딩도 지원합니다. 네트워크와는 크게 상관없어 보이지만, 어딘가 그런 기능이 있을지도 모릅니다. 애플에서 권장하는 데이터 엑세스 방식이죠. 일견 기능은 매우 강력해 보이지만, 헛점이 있습니다. 너무 어렵다는 것입니다.

코어 데이터를 다루기 위해서는 기본적으로 코코아 바인딩을 마스터해야 합니다. 그런데 이 코코아 바인딩이란 놈이 만만치 않죠. 그 외에도 몇 가지의 복잡한 개념을 이해해야 합니다. 사실 이 기술들의 컨셉은 별 거 없지만, 애플 엔지니어들에게 딱 맞춰진 개념 구현을 익혀야 한다는게 정말 어렵습니다. 그리고 이런 종류의 데이터 바인딩 프레임워크가 다 그렇듯, 유연성이 떨어져 실제 사용에는 제약이 많습니다. 바인딩은 정말 폼 기반 어플리케이션같은 특정 목적의 응용이 아니면 적합하지 않습니다.

그러면 코어 데이터에서 남는 것은 대량의 데이터셋을 효과적으로 다룰 수 있는 아키텍쳐 뿐입니다. 하지만 이것은 데이터가 로컬에 있을 때에나 해당하는 것이며, 데이터가 원격 서버에 있다면 데이터셋 스쿠핑은 서버에서 해결하므로 사실 큰 의미가 없습니다.

XML지원도 사실 어떤 식으로 되는지 모릅니다. 샘플을 슬쩍 봤는데, 개별 개체를 인코딩하고 계층 구조는 개체 아이디로 레퍼런싱하는 방식을 사용하는 듯 했습니다. 이런건 프로그래머도 못봅니다. 오로지 기계만 읽을 수 있습니다.

결국 뭔가 강력한 기능을 제공한다는 것은 알겠지만, 아직 저는 정말로 필요한 때가 아닌 것 같습니다.

아카이브

아카이브는 개체를 바이너리로 저장하는 기능입니다. 코어 데이터의 주 구성 요소 기술 중 하나이죠. 이건 말 그대로 바이너리입니다. 이걸로 커스텀 데이터셋을 간편히 만든다는 것 자체가 불가능하고, XML과도 관련이 없습니다. 그래서 패스했습니다.

JSON

애플 푸시 서비스의 샘플이 JSON으로 되어 있어서 JSON에 관심을 가져 보았으나, 제조사인 애플에서 제공하는 기반 프레임워크가 없는데다, 사용되는 분야가 너무 한정적이라 그만두었습니다. 그리고 웹에서 필수적으로 발생하는 인코딩과 이스케이프 문제에 대한 정의가 너무 허술한 듯 했습니다. 특히 언어에 종속된 문법으로 인해 슬래시를 모두 이스케이프해야 한다는 것은 치명적입니다.

간략한 데이터셋은 프로그래머만 사용하는 것이 아닙니다. 때로는 디자이너나 기획자에게 전달하고, 그들에게 내용을 받아와야 하는 때도 있습니다. 자기 스스로 데이터를 편집하기를 윈하지만, 어려운 것은 다루지 못하는 사람들이 많죠. 여기에 http://로 시작하는 링크조차 모두 이스케이프해야 하는 JSON은 적용하기 힘듭니다.

NSXMLParser

코코아 터치에서 정말깜짝 놀란 것이 XML DOM 파서가 없다는 것이었습니다. 대신 이벤트 드리븐 파서를 제공하는데, 말이 파서지 사실 쓸게 못됩니다. 스트림 프로세서를 만드는 것 외에는 쓸 수 있는 곳이 거의 없고, 결국엔 이 파서를 이용해 작은 전용 DOM 을 만드는 식으로 일이 진행됩니다. 처음에는 forwardInvocation 기능을 이용해 e4x처럼 동적으로 개별 노드에 엑세스하는 DOM 개체를 만들려 했습니다. 그런데 Objective-C 컴파일러는 선언되지 않은 셀렉터에 대해 경고를 내보냅니다. 경고를 무시하는 것은 잠재적인 위험을 무시하는 것이므로, 결국 이 방식을 쓰지 못하게 되었습니다.

이 작은 DOM은 매우 작은 커스텀 XML 메시지를 만들기에 적합하므로 XML API를 사용할 때 유용하리라 생각해서 없애지는 않았습니다만, 기본 데이터 엑세스 용도로는 사용하지 않습니다. 프로퍼티 리스트를 발견했기 때문이죠.

NSArray/NSDictionary 그리고 프로퍼티 리스트

모든 것을 다 포기하고, 기본적인 배열과 사전 개체로 해결을 보려 했습니다. NSArray의 오토릴리즈된 생성자 목록을 보던 중arrayWithContentsOfFile이라는 메서드가 보이더군요. 순간 감이 왔습니다. 애플 엔지니어가 이런것을 생각하지 않았을리 없죠. 미리 다 준비해뒀을 건데 왜 안보이는 걸까. 그건 그게 너무 가까이 있어서 보지 못했기 때문이 아닐까? 하는 생각이 들었죠. 바로 레퍼런스를 뒤적였고, 이 메서드의 설명 근처에서 Property List라는 키워드를 찾을 수 있었습니다. 사실 프로퍼티 리스트는 전부터 알고 있었는데 잊어버리고 있었습니다. e4x의 기능을 가진 프레임워크를 찾는 데 너무 집착했기 때문이죠. 프로퍼티 리스트를 구성 설정에 주로 쓰이며, 간략한 데이터셋을 다루는데 최적입니다. 이제 모든 문제를 해결했습니다.