C++ 문자열 나누기

Ruby에도 C#에도 문자열에 대한 split함수가 있다. 그러나 우리의 C++에는 그런 게 없다. 이런 하잖은 기능은 직접 구현해야 한다. 이게 C++이 멋진 이유 아니겠나.

서핑 중, 문자열을 자르는 아주 멋진 코드조각을 stackoverflow에서 발견했다.

vector<string> tokens;

copy(istream_iterator<string>(iss),
    istream_iterator<string>(),
     back_inserter<vector<string> >(tokens));

구차하게 for문을 작성하지 않아도 되는 이런 우아한 방법이 있다니. 쇼크! 내가 얼마나 STL에 무신경했는지 느꼈다.
한눈에 들어오지 않아서 Visual C++ 2005의 STL소스를 참조해서 살펴봤다.

알다시피, copy()함수는 첫 번째 인자로 들어오는 입력 이터레이터를 두 번째 인자로 들어오는 입력 이터레이터까지 순회하면서 그 값을 세 번째 인자인 출력 이터레이터의 위치에 쓴다. back_inserter클래스는 컨테이터에 값을 추가하기 위한 일종의 헬퍼이다.

여기서 내가 가장 헛갈렸던 것은, ‘첫 번째 인자와 두 번째 인자의 값을 비교함에 있어서 istream_iterator는 그 값을 어떻게 가져오며, 비교대상인 istream_iterator클래스의 기본 생성자(두 번째 인자)는 무슨 의미인가’였다.

Visual C++의 STL소스를 뒤져보면, istream_iterator클래스의 기본생성자는 문자열의 char(istream_iterator클래스 기본 템플릿 파라미터)을 가리키는 포인터 변수를 0으로 설정한다. 즉, 종료문자열을 의미한다.

istream_iterator()
: _Myistr(0)
{	
    // construct singular iterator
}

그렇다면, copy()함수는 입력 이터레이터의 값을 어떻게 가져올까? 이는 istream_iterator클래서의 _GetValue()함수를 사용하며, _GetValue()함수는 basic_istream클래서의 >>연산자를 사용한다. 결국, copy()함수는 istringstream클래스에 정의된 >>연산자를 사용하게 된다.

자못 복잡해 보일 수도 있지만, 코드는 아름답다.

여기서 한 걸음 더 나아가서 delimiter를 공백문자가 아닌 다른 걸로 바꾸고 싶었다. istringstream에서는 그러한 능력을 제공하지 않는다. 역시 C++답다. split()함수를 구현하지 않은 마당에, delimiter를 설정하는 것 따위, C++에게는 사치다. 근데, getline()함수에서는 delimiter를 설정할 수 있다. 결국, 다음과 같이 쓸 수 있다.

for (string token; getline(iss, token, 'i'); tokens.push_back(token));

어쨌든, 한 줄은 한 줄. – -;

교훈 : STL소스는 복잡하다.

실행중인 인터넷 익스플로어 제어하기

이거, 간단치 않았던 작업이다. 열려있는 인터넷 익스플로어(이하 IE)를 제어해야 했다. 하다 보니, 이런걸 요청 받기도 하고 또, 겨우겨우 구현하기도 한다. 최단로(路)인지는 모르겠지만, 어쨌든 길은 길이다.

IE::_FindWindow(const TCHAR* tszSubWindowText)는 IE의 윈도우타이틀을 검색해서 핸들을 구한다. IE::Back()함수와 IE::Navigate()함수는 모두 윈도우 핸들을 찾기 위한 윈도우 타이틀을 인자로 요구하며, 각각, IE의 뒤로 가기와 URL이동을 구현한다.

이 코드는 oleacc.lib가 필요하며, Window XP에서 테스트 되었다.

근데…, 이걸 어디에 쓰냐고?

쓸 데가 있더이다~

킨들3 사용기

동기는 가격이 너무 싸다는 것이었다. 단돈 139달러. 배송비(20불)와 관세(약 32,000원)를 포함해도 20만원이 조금 넘는다. 와이파이까지 지원되는데, 이정도 가격이면 정말 만족스럽다. 20만원이면, 겨우(?) IT관련 서적 몇 권이다. 게다가, 무려 한글도 지원한다. 배송은 출발 4일만에 도착. 그것도 토요일과 일요일이 낀 상태. 아마존에서 책을 받는 속도와는 비교 불가. 다만, 당시에는 품절이라 대기시간이 있었다.

생김새는 깔끔하다. 그냥 군더더기 없는 수준. 키보드의 질감은 약간 거칠어서, 만지면, 키보드구나 알 수 있다. 페이지를 넘기는 버튼이 좌우에 두 개씩 있는데, 오른쪽으로 넘기는 버튼이 더 크다. 처음에는 페이지버튼이 좌우 대칭이 더 좋지 않았을까 생각했지만, 책을 읽다가 뒤로 가는 경우가 적은 만큼, 앞으로 넘기는 버튼이 더 커야 하는 게 맞는 것 같다. 240g의 무게는 어떤 자세로 사용해도 부담 없을 정도로, 충분히 가볍다.

화면은 아주 편안하게 다가온다. 해상도가 아주 뛰어나진 않지만(600×800), 그것을 무시할 수 있을 만큼 부드럽게 보인다. 일반 책을 읽는 느낌과 상당히 흡사해서, 모니터화면으로 문서를 읽는 것과는 확실한 차이를 느낄 수 있다. 전자잉크의 특성상, 페이지전환시 잔상이 생기지만, 사용에 문제를 줄만한 수준은 아니다. 다만, 화면의 부분적인 갱신이 일어날 경우(예를 들어 사전을 찾아보는 경우 등), 갱신전의 화면이 깨끗하게 지워지지 않았다. 페이지를 전부 갱신하는 경우는 이런 현상이 생기지 않는다. 북큐브 B-815와 비교해보니, 콘트라스트와 전환속도 모두 두드러질 정도로 차이가 났다(긍정적 의미).

한글을 포함해서 일본어와 중국어도 지원한다. 그러나, 그것뿐이다. 뒷면의 한국 전파인증 마크와 한글 지원은, 배송국가에 한국을 포함시키기 위한 것일 뿐이다. 아마존의 변환 서비스를 이용하면 좀 더 괜찮은 한글폰트를 볼 수 있다지만, 그게 아니라면, 평생 사용해본 적도, 사용할 일도 없는 한글폰트를 맞닥뜨릴 것이다. 이에 비해, 일본어와 중국에는 읽을만한 수준이다.

을 구입하는 과정은 정말 간단하며, 많은 킨들 책들이 무료로 제공되기도 한다. 적어도 아마존에서 책을 사려한다면, 킨들에디션은 매력적인 선택이다. 내가 산 9.99불짜리 책을 새 책으로 사려면, 최소한 9불이상 줘야 한다. 비슷하다고? 아마, YES24에서 구입하는 것이라면, 선뜻 구입하지 않았을 것이다. 내가 북미에 산다고 해도 비슷할 것이다. 킨들로 10불이 넘을 배송비와 보름가량의 배송시간을 아낄 수 있으므로, 분명히 이는 괜찮은 선택이었다. 그러나 일부 책들은, 가격이 비싸다. 아마존에서 개인셀러가 파는 책들의 가격은, 아마존이 파는 가격보다 일반적으로 더 저렴하다. 새 책이라면 그 차이가 큰 경우는 많지 않지만, 그렇지 않다면 이야기가 달라진다. 예를 들어, 위시리스트에 담긴 ‘A Lion Called Christian’은 킨들에디션이 9.06불이다. 페이퍼백이 10.07불이니 그럭저럭 봐줄 만 하지만, 개인셀러가 파는 중고 하드커버는 단돈 0.01불이다. 여기에 해외배송을 하면 대략 14불정도의 금액이 나오는데, 이런 경우는 매우 망설여 진다. 중고 책이기 때문에 어쩔 수 없다면, 최소한 개인셀러가 파는 새 책보다는 저렴해야 한다. 아마존에서 개인셀러가 파는 중고 책들은 상태가 완벽한 경우가 많다. 적어도 내 경험에 Like New라고 설명된 중고 책은 새 책이나 다름 없었고, 이것이 더 싸다면, 난 중고로 산다.

앞으로도 킨들을 계속 사용할 것이다. 단, 내가 줄을 그으면서 읽는 책이 아닌 경우에 한해서다. 나에게 있어서 소설이나 에세이가 바로 그런 책들이다. 학습서를 읽을 때면, 난 언제나 책에 줄을 긋는다. 물론, 킨들에서도 줄을 그을 수 있고, 또 주석을 달 수도 있다. 책갈피를 꽂아둘 수도 있지만, 그것이 내가 원하는 것을 종이 책처럼 빨리 찾을 수 있게 하지는 못한다. 템플릿 파라미터를 갖는 C++ 템플릿을 찾아보기 위해서 수백 번 다음페이지 버튼을 누를 수는 없지 않은가(목차에서 링크를 제대로 지원하지 않으면 정말 이래야 할 수도 있다). 그게 아니라면, 킨들은 정말 쓸만하다. 물론, 책을 읽지 않는 사람이 킨들을 산다고 책을 읽을 가능성은 그다지 많지는 않겠지만 말이다.

Reading Excel using Ruby

There’re some libraries dealing with Excel, but they didn’t work in some conditions. So I’ve just written the code for this.

ExcelData uses win32ole module. It means this work only on Windows and Excel needs to be installed.
There’s only a public class method Load returns row array. Each row consists of hash with column name and value string pair.
Load method requires minimum three parameters. Excel filename(excelFilename), sheet name(worksheetName) and first header column name(firstHeaderColumnName). ExcelData finds first row with first header column name and read cell from there. If empty cell is found(both row and column), it stops reading the file.

require 'win32ole'

class ExcelData
public
  def	ExcelData.Load(excelFilename, worksheetName, firstHeaderColumnName, keySearcher = nil)
    xls       = nil
		ws        = nil
    excelData = []

    begin
      xls         = WIN32OLE.new('Excel.Application')
      xls.visible = false
      ws          = xls.Workbooks.Open(excelFilename).Worksheets(worksheetName)

      excelData   = _CollectData(ws, firstHeaderColumnName, keySearcher)
    rescue
      puts "Failed to open excel file : #{excelFilename}"
    ensure
      xls.quit if xls != nil
    end

    return excelData
  end

private
  def	ExcelData._FindFirstDataRowNum(worksheet, firstHeaderColumnName)
    rowNum  = 1

    while (true)
      row = worksheet.Range("a#{rowNum}")
      break if (row['Value'] != nil && row['Value'] == firstHeaderColumnName)
      rowNum  += 1
    end

    return rowNum + 1
  end
	
  def	ExcelData._FindLastDataRowNum(worksheet, firstDataRowNum)
    rowNum  = firstDataRowNum

    while (true)
      row = worksheet.Range("a#{rowNum}")
      break if (row['Value'] == nil || row['Value'] == "")
      rowNum  += 1
    end

    return rowNum - 1
  end

  def	ExcelData._FindLastDataColChar(worksheet, headerDataRowNum)
    colChar	= 'a'

    while (true)
      row = worksheet.Range("#{colChar}#{headerDataRowNum}")
      break if (colChar == 'z' || row['Value'] == nil || row['Value'] == "")
      colChar.succ!
    end

    return colChar
  end

  def ExcelData._CollectData(worksheet, firstHeaderColumnName, keySearcher)

    firstDataRowNum = _FindFirstDataRowNum(worksheet, firstHeaderColumnName)		
    lastDataRowNum  = _FindLastDataRowNum(worksheet, firstDataRowNum)
    lastDataColChar = _FindLastDataColChar(worksheet, firstDataRowNum - 1)

    puts "Collecting excel data : Total #{lastDataRowNum - firstDataRowNum + 1} rows ..."

    # Column Names
    colNames	= []
    row = worksheet.Range("a#{firstDataRowNum - 1}:#{lastDataColChar}#{firstDataRowNum - 1}")
    row.each do |cell| colNames << cell['Value'] end

    # Collect excel data
    excelData	= []
    for rowNum in firstDataRowNum..lastDataRowNum
      colIndex  = 0
      rowHash   = {}

      row = worksheet.Range("a#{rowNum}:#{lastDataColChar}#{rowNum}")

      row.each do |cell|
        if (!cell['Value'].to_s.empty? && cell['Value'].to_s =~ /^[0-9.]+/)
          rowHash.store(colNames[colIndex], cell['Value'].to_i.to_s)
        elsif
          rowHash.store(colNames[colIndex], cell['Value'].to_s)
        end

        colIndex	+= 1
      end

      excelData << rowHash

      keySearcher.store(rowHash[colNames[0]].to_s, excelData.length - 1) if keySearcher != nil

      print "." if excelData.length % 10 == 0
    end

    puts 

    return excelData
  end
end

You can simply use this code like bellow.

xls = ExcelData.Load('myfile.xls', 'sheet1', 'ITEMID')

Ruby On Rails & 생각

짧은 시간 이었지만, 태어나서 처음으로 웹 프로젝트를 끝냈다. 나에게 ‘끝냈다’는 남들이 사용할 수 있을만한 결과물을 내 놓았다는 의미이다. 이를 기념 삼아 간단히 소회를 풀어보고자 한다.

내가 웹 프로그래밍을 해본 것이라고는 한 십 년쯤 전에 PHP로 뭔가 간단히 끄적여 본 것이 전부였다. 그러다가 Ruby를 접하고 내친김에 Ruby On Rails(이하 RoR)도 시작했지만, 태생이 태생인지라 웹서버와 브라우저간의 통신이라는 다소 미묘한 세계를 이해하는데 상당히 애를 먹었다. 서버프로그래머인 나에게 있어서, 웹 프로그래밍 – 나의 경우에는 좀 더 구체적으로 RoR이 되겠다 – 에서 사용하는 서버와 클라이언트간의 정보전달방법은, 그 추상화의 정도가 너무 심했던 것 같다. 이런 이유로 RoR에 관한 책을…, 그러니깐 다섯 권을 샀다. 물론, Ruby에 관한 책은 별도고. 세 번째 책쯤에서 느낌이 왔고, 네 번째 책에서 감을 잡았다. 그래서 다섯 번째 책은 아직 그냥 있다. 언젠간 읽겠지. 꽤 빨리 읽을 수 있을 것이다.

회사에서 게임데이터를 엑셀로 관리하고 있었다. 같은 실수가 몇 번이고 반복되고 있었고, 기획자는 툴의 필요성을 강조했다. 그래서 내가 RoR을 꺼냈다.

어느 게임회사나, 시간이 넉넉한 곳은 없다. 따라서 게임개발에 사용되는 툴은 그 중요도에 따라 완성도가 결정되는 경우가 많다. 프로그래밍지식이 없는 기획자나 그래픽담당자는 자신이 하는 일이 막노동인줄도 모르고 그냥 열심히 하며, 설사 이를 프로그래머가 알게 되었다 하더라고 전담자가 배치되지 않는 이상 제대로 된 툴이 나오지 않는다. 어지간한 규모가 아니라면, 툴을 전담하는 개발팀을 두는 회사는 거의 없다. 결국, 툴 개발은 게임프로그래머에게 추가작업을 요구하게 된다.

게임데이터를 다루는 툴은 단순해 보이지만, 이것이 DB와 연결되고 데이터 검증과정이 들어가고, 또 사용자 인증 및 히스토리기능까지 넣으려 한다면, 이를 독립된 어플리케이션으로 만드는데 꽤 많은 시간이 소모될 것은 뻔한 일이다. 더구나 원활한 편집을 위한 UI는 게임프로그래머가 구현하기엔 결코 간단한 기능이 아니다.

나는 조인을 사용하는 쿼리문을 제대로 작성할 줄 못하고, 서로 다른 DBMS의 특성도 잘 모른다. Ajax는 거의 들어만 본 수준이다. HTML은 기본적인 것만 알고, 당연히 웹 표준은 딴 세상 이야기다. 프로젝트 시작전이나 지금이나 이 사실은 변함이 없다. RoR이 멋진 것은, 이런 상태에서도 프로젝트를 끝낼 수 있었다는 것이다. 물론, 아는 것이 많을수록 더 괜찮은 프로그래밍을 할 수 있는 것이 당연하겠지만, 그렇지 않아도 in-house툴로는 충분한 결과물을 만들어 낼 수 있다.

이후에, 테스트 삼아 만들어본 또 다른 프로젝트는, 반나절 만에 동작하는 프로토타입을 만들어 낼 수 있었다. 서버의 덤프파일이 생기면 이를 모아서 관리하고 그 통계를 보여주는 웹 프로그램이었다. 이를 웹으로 구현하지 않으면, 파일전송을 위해서 FTP를 사용하든가 아예 이를 구현해야 한다. 그리고 또 이를 정보화하기 위한 작업도 필요하다. 개발자가 덤프분석을 위해 이를 다운받는 건 또 어떻게 할 것인가.

대부분의 게임프로그래머는 웹 프로그래밍에 대해 알지 못한다. 게다가 웹 프로그래밍에 대한 기술적 하대는 게임프로그래머로 하여금 웹 프로그래밍에 대한 관심을 점점 더 멀게 한다. 안타까운 점은, 동일한 목적에 대해서, 어플리케이션개발보다 웹 개발이 훨씬 더 빠르고 효율적인 경우가 많다는 것이다. 더구나, RoR처럼 생산성이 높은 웹 프레임워크를 사용한다면, 그 효과는 배가되기 마련이다. 이 일을 사내의 웹팀에게 넘기지 않고 직접 해야 하는 이유는, 그들도 바쁘거니와, 프로그래밍을 할 줄 아는 사람 중에서는 게임프로그래머가 그 게임과 만들 툴을 가장 잘 이해하고 있기 때문이다. 웹팀이 각 팀에 특화된 툴을 만들어 줄 수 있을 리도 요원하다.

나의 경우는 Ruby가 마음에 들어서 RoR을 사용한 것이지만, 어떤 언어든, 어떤 프레임워크든, 어떤 방식이든, 빨리, 그리고 사용자가 편하게 사용할 수 있는 물건을 내놓는 것이 최고 아니겠나.

마지막으로, 그 툴이 지금 어떻게 지내고 있는지 살짝 덧붙인다. 툴만 완성되면 실수가 없을 것처럼 이야기하던 기획자는 아직 그 툴을 제대로 사용하고 있지 않다. 요청한지 몇 주가 지나도록 툴 설정에 관련된 자료는 오지 않고 있으며, 그럴 의지가 있는지도 잘 모르겠다. 스크립트에 관련된 각종 자료를 정리해주는 것보다, 실수를 하더라도 지금까지의 방식을 고수하는 것이, 차라리 더 편하다고 생각하는 것 같다.

툴이 사용되지 못해도, 이를 통해서 내가 배운 것들로 충분히 행복하다. 애당초, 거기에 더 큰 비중이 있었다. 다만, 계속 부지런 떠는 그들이 마음에 걸릴 뿐이다.

아이러브스쿨은 왜 잊혀졌는가?

한창 주가를 올리던 시기엔, 원하는 친구는 아이러브스쿨을 통해서 다 찾을 수 있을 정도였다. 그러다 좀 있으니 다모임이라는 사이트가 그 자리를 대신하기 시작했고, 동시에 아이러브스쿨은 내리막길을 향해 발걸음을 내딛고 있었다.

보통, 이렇게 회사가 어려워지면, 여러 이유로 회사를 그만두는 사람이 늘어나기 시작한다. 그들 대부분은 그 길로 또 다른 삶을 시작한다. 그 중에는 드물게 스스로의 뒤를 돌아보는 사람도 있기 마련인데, 바로 아이러브스쿨의 개발팀장이었던 서영수님이다.

오늘, 뒤늦게 발견한 그의 소중한 회고는 몸담고 있는 회사에 대해서 많은 것을 생각할 수 있게 해주었다.

다운로드 PDF: 아이러브스쿨은 왜 잊혀졌는가?

Critical Section Block 2

이전 포스트에서 CSBLOCK 매크로를 사용해서 좀 더 간편하게 크리티컬 섹션을 정의할 수 있도록 했었다. 이에 조금 덧붙여서, 어떤 데이터형이든지, 그에 대응하는 크리티컬 섹션 변수를 만들어 줄 수 있도록 하고 싶었다. 이 작업도중 Variadic macro to count number of arguments을 읽게 되었고, 옳타구나 하고 여러 개의 자료형을 매크로의 파라미터로 사용할 수 있도록 수정했다.

다음은 이 아이디어를 구현한 코드이다.

locker 클래스는 주소값에 대응하는 값(이 코드에서는 int형 key를 사용했지만, 윈도우의 크리티컬 섹션을 위해서는 CRITICAL_SECTION 타입의 변수가 될 것이다)을 등록/삭제/검색 한다.

class locker
{
public:
    locker()
    : _key(0)
    {}

    ~locker()
    {
        map<unsigned int, int>::iterator    itr, itrend;
        itr     = _users.begin();
        itrend  = _users.end();
        for (; itr != itrend; ++itr)
        {
            unregister_addr((void*)itr->first);
        }
    }

    int register_addr(void* address)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr != _users.end())    return itr->second;

        cout    << "register: " << hex << (unsigned int)address << endl;
        _users[(unsigned int)address]   = ++_key;

        return _key - 1;
    }

    int unregister_addr(void* address)
    {
        int key = 0;
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr == _users.end())    return key;

        key = itr->second;

        cout    << "unregister: " << hex << (unsigned int)address << endl;
        _users.erase((unsigned int)address);

        return key;
    }

    int key(void* addr)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)addr);
        if (itr == _users.end())    return 0;

        return itr->second;
    }

private:
    map<unsigned int, int>  _users;
    int             _key;
};// class locker

이 클래스에는 약간의 문제가 있는데, _users, _key 변수 역시 크리티컬 섹션이라는 점이다. 따라서 이에 대한 처리도 필요하다. 또한, 주소값에 대한 key를 동적으로 생성하는데 대한 성능상의 문제도 존재한다. 그렇지만, 어디까지나 이는 아이디어를 구현하기 위한 과정으로 판단해서 일단 패스. :)

scope_lock_args 클래스는 CSBLOCK 매크로가 여러 개의 인자를 받을 수 있도록 해준다.

class scope_lock_args
{
friend class scope_lock;
public:
    scope_lock_args(int count, ...)
    {
        va_list addresses;
        va_start(addresses, count);

        for (int i = 0; i < count; ++i)
            _addresses.insert(va_arg(addresses, void*));

        va_end(addresses);
    }

    typedef std::set<void*> data_type;
private:
    std::set<void*> _addresses;
};// class scope_lock_args

scope_lock 클래스의 생성자와 소멸자에서 크리티컬 섹션의 시작과 종료를 알린다. 각 데이터는 scoke_lock_args 클래스가 주소에 따라 정렬해서 관리하기 때문에 항상 같은 순서로 락을 걸고 풀 수 있다.

class scope_lock
{
public:
    scope_lock(scope_lock_args arg)
    : _arg(arg)
    {
        scope_lock_args::data_type::iterator    itr, itrend;
        itr     = _arg._addresses.begin();
        itrend  = _arg._addresses.end();
        for (; itr != itrend; ++itr)
        {
            _locker.register_addr(*itr);
            cout    << "scope_lock::scope_lock " << hex << *itr << endl;
            // EnterCriticalSection
        }
    }

    ~scope_lock()
    {
        scope_lock_args::data_type::reverse_iterator    itr, itrend;
        itr     = _arg._addresses.rbegin();
        itrend  = _arg._addresses.rend();
        for (; itr != itrend; ++itr)
        {
            cout    << "scope_lock::~scope_lock " << hex << *itr << endl;
            // LeaveCritialSection
        }
    }

    operator bool() const
    {
        return true;
    }

private:
    static locker   _locker;
    scope_lock_args _arg;
};// class scope_lock

locker  scope_lock::_locker;

하일라이트. CSBLOCK는 최소 하나부터 최대 세 개까지의 데이터를 인자로 받을 수 있다. 물론, 필요하면 늘릴 수 있다.

#define CSBLOCK(...)    CSBLOCK_IMPL(__VA_ARGS__, 3, 2, 1)
#define CSBLOCK_IMPL(_1, _2, _3, N, ...)    if (scope_lock __sl = scope_lock_args(N, _1, _2, _3))

이제는 CSBLOCK 매크로에 여러 개의 인자를 넣을 수 있고, 그 순서에 상관없이 항상 같은 순서로 락을 사용한다.

#include <iostream>
#include <map>
#include <set>
#include <cstdarg>

using namespace std;

int main()
{
    int k   = 0;
    int l   = 0;

    CSBLOCK(&k)
    {
        cout    << "<1>" << endl;
        CSBLOCK(&l, &k)  // 순서 상관 없음
        {
            cout    << "<2>" << endl;
        }

        CSBLOCK(&k)
        {
            cout    << "<3>" << endl;
        }

        CSBLOCK(&k, &l)  // 순서 상관 없음
        {
            cout    << "<4>" << endl;
        }
    }

    return 0;
}

성능상의 문제로 실제로 사용하기에는 많은 개선이 필요하겠지만, 꽤 매력적이지 않나? ㅋㅋㅋ
C++은 확실히, 이런 것들이 재미있는 것 같다.

Authlogic 사용하기

Authlogic이 쉽고 간단하단다. 그래서 썼다. Railscasts #160 Authlogic를 따라 하는 데, 이거 영 안 된다. 그렇다. 난 레일즈 초보다. ㅠㅠ

Authlogic은 사용자세션을 ActiveRecord클래스와 동일한 방법으로 다룰 수 있도록 Authlogic::Session::Base클래스를 제공한다. 이 클래스를 자동으로 생성하기 위해서는 generate스크립트를 사용할 때 generator로 session을 지정하면 된다.

$ ruby script/generate session user_session

이는 사용자의 로그인/로그아웃을 ActiveRecord클래스가 데이터를 추가하거나 삭제하는 것처럼 처리할 수 있게 해주며, 실제 사용자 정보를 가질 (진짜 ActiveRecord를 상속한)모델클래스도 필요하다. 이 모델클래스를 Authlogic가 사용하게 하기 위해서는 클래스에 acts_as_authentic지시자를 추가해 주어야 한다.

class User < ActiveRecord::Base
  acts_as_authentic
end

데이터모델클래스의 칼럼에는 string타입의 persistence_token이 존재해야 하며, 이 외에 다음과 같은 값을 사용할 수 있다.

t.string    :login,               :null => false                # optional, you can use email instead, or both
t.string    :email,               :null => false                # optional, you can use login instead, or both
t.string    :crypted_password,    :null => false                # optional, see below
t.string    :password_salt,       :null => false                # optional, but highly recommended
t.string    :persistence_token,   :null => false                # required
t.string    :single_access_token, :null => false                # optional, see Authlogic::Session::Params
t.string    :perishable_token,    :null => false                # optional, see Authlogic::Session::Perishability

# Magic columns, just like ActiveRecord's created_at and updated_at. These are automatically maintained by Authlogic if they are present.
t.integer   :login_count,         :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
t.integer   :failed_login_count,  :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
t.datetime  :last_request_at                                    # optional, see Authlogic::Session::MagicColumns
t.datetime  :current_login_at                                   # optional, see Authlogic::Session::MagicColumns
t.datetime  :last_login_at                                      # optional, see Authlogic::Session::MagicColumns
t.string    :current_login_ip                                   # optional, see Authlogic::Session::MagicColumns
t.string    :last_login_ip                                      # optional, see Authlogic::Session::MagicColumns

또한, 이 모델클래스가 Authlogic의 세션모델과 연동되기 위해서는 세션모델의 이름이 데이터모델의 이름에 Session이 덧붙은 형태여야 한다. 즉, 데이터모델이 User라면, 세션모델의 이름은 UserSession이어야 하는 식이다.

이렇게 Authlogic의 사용준비를 마쳐서 User라는 데이터모델클래스와 UserSession라는 세션모델클래스가 마련되었다면, 사용자의 계정정보는 User클래스를 사용하여 저장하고, 세션정보는 UserSession클래스를 사용하여 다룰 수 있게 된다.

음, 이렇게 쓰고 보니 정말 쉬운 것 같기도 하다. – -;

애플의 혁신 – iPhone OS 4

iPhone OS 4

Multitasking, Folders, Unified inbox, iBooks, Enterprise, Game Center, iAd.

‘겨우 이걸 신기술이라고 발표한 거야? 멀티태스킹은 애플의 모바일 디바이스에서만 안 되는 거고, 폴더도 니들만 없던 거고, 오죽하면 통합이 메일을 Top7에 포함시켰냐. iBooks는 자기들 책 팔아먹으려고 만든 게 뻔하고, 엔터프라이즈기능은 니들이 안 해도 아이폰을 사용할 기업이라면 어느 정도 당연히 하지 않겠어? 게임센터는 기존의 서비스를 죽일 테고, 콰트로를 인수했다더니 결국 광고기능을 넣는구먼.’이라고 쓰려고 했다. 스티브잡스가 대단한 건 알았지만, 이 정도로 일반적인 것들로 미디어의 대서특필을 이끌어 낼 수 있다는 게 어이없다고 생각했다. 나 또한 아이폰의 열렬한 지지자인동시에 사용자이지만, 솔직히 이건 아니라고 생각했다. 스티브잡스가 소개하는 iPhone OS 4의 대표적인 7가지 기술에 대한 수많은 기사와 글들을 봤을 땐, 정말인지 저렇게 쓰려고 했다. 진정하고, 키노트 동영상을 보기를 잘했다. 안 그랬으면 난 바보 될 뻔했다.

다른 건 제쳐두고, 세 가지에 놀랐다.

멀티태스킹이 어떻게 배터리킬러가 되지 않을 수 있는지에 대한 건 관심 없다. 그냥 그럴 수도 있다고 생각한다. 정말 그렇다면, 그것도 쬐끔 대단하긴 하다. 개발자에게 이에 대한 API를 제공한다는 것도 괜찮은 아이디어 같다. 내 아버지의 경우라면, 멀티태스킹이 없는 것이 오히려 이익이다. 윈도폰을 사용하시는, 나이에 비해서 매우 기술 친화적인, 작은아버지는 윈도폰의 인터페이스를 전혀 이해하시지 못하시는 듯 하다. 좀 더 정확히 말하면, 노력은 하시지만 이해가 안 된다고 해야겠다. 수십 년 젊은 나도 모르는 판에 오죽하겠나. 삼성이 만든 거지 같은 UI가 없었다면 그나마 나았을 수도 있었겠다. 과거에 내가 사용하던 WM6.1을 OS로 사용하던 스마트폰에서는 현재 어떤 프로그램이 실행 중인지 알 수가 없었다, 물론 설정으로 들어가서 메모리를 확인하면 어떤 프로그램이 돌고 있는지 찾을 수는 있다. 아니면 배터리소모를 감수하고 서드파티앱을 설치하면 PC용 윈도 비슷하게 구현되긴 했다. MS는 이런걸 누구나 알고, 또 할 수 있다고 생각한 것 같다. 그런데 말이지, 스티브잡스는, 혹은 애플은 그렇지 않다는 걸 알고 있었던 듯싶다. 아이폰의 몇 안 되는 버튼인 홈버튼을 더블클릭(이것도 나이가 있는 분들에겐 어려울 수 있지만, 버튼을 하나 늘리는 것보단 훨씬 낫다)하면 실행중인 프로그램이 보인다. 현재화면을 가리지도 않는다. 이건 윈도모바일 세계에선 상상도 할 수 없는 일이다. MS라면 새로운 창을 띄웠지 않았을까. 아니면 좀 더 복잡한 방법을 골랐을 것이다. 물론, 이것도 현재의 것을 버린다는 가정하에서나 가능하지만. 어쨌든, 이런 식의 멀티태스킹이라면 누구나 이해할 수 있을 것이다. 더 이상 언제 실행시켰는지도 모르는 앱을 발견하는 일은 없어지고, 또 그것을 확인하기 위한 복잡한 과정도 없어졌다.

여러 개의 앱을 폴더에 넣어둔다는 개념은 너무 당연해서 그걸 새로운 기능이라고 말하는 것조차 우습지만, 잡스가 보여준 대로라면, 이건 새로운 기능이 맞다. 자꾸 WM이랑 비교하게 된다. 어쩔 수 없다. 지금까지 너무 당연하게 생각하던 것들조차 애플은 더 단순하게 만들었다. WM에서 폴더란 것은, 그리고 모든 PC환경의 폴더는 별도로 만드는 것이다. 그래서 빈 폴더가 존재할 수 있다. 곱씹어보면, 빈 폴더가 존재해야 할 이유는 없다. 그래서 iPhone OS 4의 폴더기능에는 빈 폴더가 없다. 죽이는 발상이다. 서로 다른 앱의 아이콘을 합치면 자동으로 폴더가 만들어진다. 게다가 폴더이름은 앱의 카테고리를 참조해서 자동으로 기본값이 정해진다. 폴더를 펼치면? 기존의 화면에 폴더의 내용이 보인다. 지금까지 폴더를 클릭하면 그 내용이 그 창에서 표시되는 것이 일반적이었다. 당연히 기존의 화면은 보이지 않는다. 적어도 WM6.1에서는 그랬다. ‘화면이 작아서’라고 이해했지만, 아이폰에서는 그렇지 않다. 작은 화면은 여전해도, 폴더의 내용은 팝업으로 펼쳐진다. 폴더의 앱이 많으면? 그래서 9개로 최대개수를 제한하는 듯 하다. 확실치는 않지만, 그렇다고 하더라도, 아이콘의 수를 제한하는 것이 기존의 화면을 가리는 것 보단 훨씬 낫다. 더 이상 뒤로 가기 위한 버튼을 만들지 않아도 되니깐.

애드몹을 이용하여 앱에 광고를 게재하면 클릭당 0.01불을 준단다. 10원이 조금 넘는 돈이다. 근데 이걸 누가 클릭하지. – -; 누구도 광고로부터 방해 받고 싶어하지 않는다. TV에서 광고를 보지 않아도 된다면 누가 광고를 보겠는가. PC에서라면, 어쩌다 광고를 클릭하기도 한다. 화면이 넓으니깐, 팝업이 튀어나와도 그 정도를 참아줄 아량은 있다. 그러나 스마트폰이라면 얘기가 달라진다. 더 느려질게 뻔하고, 더욱이 튀어나온 브라우저를 꺼야 한다. 당연히 난 한번도 아이폰에서 광고를 클릭해 본적이 없다. 그러나 잡스가 시연한 iAd형식의 광고라면 얘기가 좀 달라진다. 여전히 광고를 클릭하려고 하진 않겠지만, 실수로라도 클릭하는 것에 대한 짜증은 훨씬 덜할게 분명하다. 광고에 방해 받고 싶지 않다는 말에는 광고로 내가 사용하던 앱의 실행을 중단해야 한다는 것도 포함된다. 그런데, 간단히 ‘X’버튼을 누르는 것으로 끝난다면? TV광고보다는 훨씬 좋다. 최소한, 클릭에 대한 부담을 상쇄시킬 수 있을 것이다. 이 정도라면, 정말 관심이 있는 건 클릭할 것 같다. 게다가 광고료의 60%를 개발자에게 준단다. 분명, 구글보다는 100배쯤 너그러운 조치다. 구글이 뭔가 바구지 않는다면, 앱개발자는 더 이상 애드몹을 사용하지 않을 것이다. 안드로이드 개발자라면 또 모르겠지만.

키노트 초반, 잡스가 iPad의 성과를 소개하기 전에 보여준 USA Today의 Ed Baig라는 사람의 iPad리뷰는 다음과 같다.

“The iPad is not so much about what you can do – browse, do e-mail, play games, read eboks and more – but how you can do it. That’s where Apple is rewriting the rulebook for mainstream computing.”

잡스횽이 how를 힘주어 말했다. 그럴 만 했다. 애플의 디바이스에 구현된 기능은 대단치 않지만, 그 기능을 사용하기 위한 UI는 분명 ‘혁신적’이라 할만하다.

RoR: remote_form_for와 테이블

루비온레일즈에서 테이블의 별도의 셀에 입력 폼을 나눠 배치했을 때, form_for를 사용하면 문제가 없었지만, remote_form_for를 사용하면 파라미터 값이 넘어오지 않는 문제가 있어서 엄청나게 삽질했다. 인터넷에 검색해보니 다음과 같은 내용이 있다.

Tables and forms can be nested either way. But if you put forms into tables, each form must be completely included into a single table cell (one TD element in practice). Thereby the forms are each completely independent.

결국, 테이블 밖으로 remote_form_for 블록을 빼니 파라미터 전달이 잘 된다. form_for는 왜 문제가 없는지 잘 모르겠지만, 테이블과 폼을 중첩시키는 것이 옳은 방법은 아닌 듯하다.

웹이라는 또 다른 패러다임에 적응하느라 삽질 중. 낯선 것이 너무 많다.