« Previous : 1 : 2 : 3 : 4 : 5 : 6 : Next »

뭐, 무식이 자랑이랄 수는 없겠지만,
본인은 전산학 내지 컴퓨터공학의 여러 분야들 중에서 문외한에 가깝게 제일 모르는 분야는 통신, 네트워크, 웹, 보안 쪽이다.
왜 제일 모르느냐 하면, 저건 컴퓨터 한 대만으로 독학이 가능하지 않고, 뭔가 '감'을 터득할 수 없는 분야이기 때문이다. 그래서 그런 걸 잘하는 사람들이 부럽다..

일례로 완전 최저수준 소켓 API와, 고수준 HTTP API 사이의 중간 과정에 대한 감이 전혀 없다. 후자도 분명 전자를 이용해서 구현됐을 텐데, 내부 구현이 어찌 돼 있는지 난 아는 게 없다.
그리고 네트워크 트래픽이 컴퓨터의 I/O 병목엔 어떤 영향을 끼치는지, 그 패킷이 어떻게 해서 한 운영체제 내부의 특정 응용 프로그램으로 잘 전달이 되는지, DDoS 공격이 서버 컴퓨터에 어떤 물리적인 영향을 끼쳐서 서버를 뻗게 할 수 있는지, (아님 단순히 프로세스/스레드의 무한 스폰으로 인해 소프트웨어적인 자원 고갈만으로 뻗는 건가?)

HTTP 프로토콜에서 파일 업로드는 어떤 절차를 거쳐서 되는지, 방화벽이라는 게 정확히 뭘 하는 물건인지,
왜 구닥다리 윈도 2000/XP sp0을 띄운 채로 랜선을 꽂으면 뭐가 뚫려서 어떻게 되는지..
요즘은 네트워크 패킷은 하부 계층에서 압축이나 암호화를 좀 하는 게 있는지 등등..

이런 것들은 난 하나도 모..른..다. 저런 거 하나도 몰라도 <날개셋> 한글 입력기 개발하는 덴 아무 지장이 없기 때문이다.
그렇다고 해서 내가 컴퓨터 명령어 체계나 운영체제/소프트웨어 자체의 내부 구조나 보안에 대해 전혀 모르느냐 하면 그것도 물론 아니니.. 지식의 분포가 좀 불균형하다면 불균형한 셈이다.

본인은 초딩 중고학년 때 개인용 PC, 중학교 때 모뎀과 PC 통신, 고등학교 때 인터넷과 이메일, 그리고 대학교 때 무선 인터넷과 휴대전화의 순으로 문명의 이기들을 접했다. 랜 선이라는 걸 태어나서 처음으로 구경한 게 고등학교 때부터인데, 그 기간 동안 언젠가 집도 인터넷 접속 방식이 전화 모뎀에서 전용선으로 바뀌었다. 그때가 한창 전국적으로 인터넷 전용선이 깔리던 시절이었으니까.

지금까지 통신 기술은 정말 눈부신 속도로 발전했다.
신문· 방송에서 기자의 이메일을 공개하는 게 대세가 된 게 1990년대 후반부터이다.
지금으로부터 10여 년 전엔 '블로그'라는 단어가 도전 골든벨의 마지막 문제의 답이었다는 게 믿어지시는가? (그것도 학생이 못 맞혔고 그 당시엔 내게도 생소했다)

옛날에는 인터넷 연결을 위해서도 PC 통신을 할 때처럼 먼저 전화를 걸어야 했다. 사용 시간 카운터가 올라가는 자그마한 인터넷 연결 창이 뜬 동안만 인터넷을 이용할 수 있었다.
또한, 모뎀과 마우스를 동시에 사용하려면 두 물건을 서로 COM 포트가 충돌하지 않게 배치를 해야 했다.
Windows 3.x에서는 운영체제 차원의 네트워크 지원이 전무하기 때문에 트럼펫 Winsock인지 뭔지 하는 걸 먼저 설치해야 했다.

이 모든 것들이 지금은 까마득한 옛날 이야기가 됐다.
지금은 뭔가 그렇게 상태를 확인하면서 인터넷을 해야 하는 상황은 스마트폰 태더링으로 무선 인터넷을 쓸 때 정도이고 이것도 제약, 압박감, 부담 같은 건 옛날과는 비교할 수 없이 작아졌다.
메인보드가 어떻게 공간 워프를 했는지, 요즘은 유선 랜과 무선 랜도 전부 내장되어 나온다. 따로 뭘 장착조차 할 필요 없이 바로 접속만 하면 된다.

오늘날 인터넷이라고 불리는 그 통신망은 OSI 레이어 계층 중 제3계층(네트워크 계층)을 차지하는 IP라는 프로토콜을 기반으로 동작한다. IPv4, IPv6 같은 주소 체계도 이 계층에서 규정하는 것이기 때문에 모든 인터넷 통신은 이 체계를 기반으로 구성되어 있다.

그리고 그 아래의 제4계층(전송 계층)에는 인터넷 프로토콜을 따르는 네트워크 패킷을 보내는 방식의 차이를 규정하는 프로토콜이 있는데, 크게 TCP와 UDP가 있다.
TCP는 보낸 패킷이 반드시 순서대로 도착한다는 것은 보장되지만, 보냈던 단위랄까 형태가 그대로 도착하지는 않는다.
aaa, bb, ccccc, ddd, e 이렇게 패킷을 보냈으면 받는 쪽은 aa, ab, bccc, cc, dd, d, e 뭐 이렇게 받을 수도 있고 다른 형태가 될 수도 있다. 조립은 받는 쪽에서 알아서 해야 한다.

UDP는 TCP와는 달리 보낸 패킷이 원래의 형태 그대로 간다는 보장은 되지만.. 일부 패킷이 전송 과정에서 누락될 수가 있다.
즉, 위의 경우 ddd가 누락돼서 aaa, bb, ccccc, e 이렇게 갈지도 모르지만.. 일단 간 놈은 원래 형태 그대로 간다. 패킷의 누락 여부 판단을 받는 쪽에서 알아서 해야 한다.
그래서 TCP는 일종의 스트림 지향적이며, UDP는 개개의 패킷이 모 아니면 도 형태로 가는 메시지 지향적이다.

형태도 보존되고 누락 현상도 없는 만능 프로토콜이 없는 이유야 뭐, 세상에 값도 싸고 성능도 좋은 물건은 존재하지 않기 때문인 것과 같은 맥락일 것이다.
그게 필요하면 UDP 같은 걸 기반으로 패킷 누락을 감지하고 재전송을 요청하는 로직을 응용 프로그램이 별도로 구현해 줘야 한다.

온라인 게임에서는 “기관총 난사 내지 캐릭터 이동 같은 것만 UDP이고 나머지는 다 TCP”라는 말 한 마디로 요약된다.
자주 발생하기 때문에 반응성이 중요하고 적당히 좀 씹혀도 상관 없는 것만 UDP이고.. 나머지 크리티컬한 것들은 다 TCP를 써야 한다는 뜻이다.
그러나 온라인 게임에서 발생하는 트래픽의 상당수, 대략 70% 가까이는 그래도 UDP 방식이라고 한다.

실시간으로 스트리밍되는 대용량 오디오/비디오 데이터도 자명한 이유로 인해 UDP 방식으로 전송된다.
이런 차이를 보면, TCP와 UDP의 관계는 사실상 무손실 압축과 손실 압축의 관계나 마찬가지인 것 같다.

TCP의 경우 응용 프로그램이 아니라 아래의 프로토콜 차원에서 패킷의 누락을 감지하여 누락이 있는 경우 재전송 요청을 한다.
그런데 모바일처럼 네트워크 환경이 원래 워낙 열악해서 패킷 손실이 굉장히 자주 발생하는 곳에서는
TCP 방식에서는 끝도 없이 재전송 요청을 하고 받은 데이터에 결함이 없는지 체크와 빠꾸만 반복하느라 응용 프로그램이 그 동안 멍하니 있어야만 하는 일이 발생한다고 한다.
즉, TCP가 구조적으로 오버헤드가 더 크니, 네트워크가 열악한 곳에서는 그런 동작을 감안해야 한다.

게임에서 그래픽 엔진, 물리 엔진, 동영상/캡처 엔진도 아니고 네트웍 엔진 미들웨어로 먹고 사는 분이 국내에 일단 한 분 계신다. 넷텐션의 대표이사인 배 현직 씨. 이 업계에서는 이미 유명인사이다.

난 네트워크 쪽 프로그래밍을 해 본 건 먼~ 옛날에 소켓 API 대충 뚝딱해서 오목을 만들고..
DirectPlay를 써서 스크래블 정도 보드 게임에다가 네트웍 플레이를 넣어 본 게 전부이다. 그래도 그것만으로도 굉장히 재미있는 경험이었다.
저수준에서 패킷 암호화, 각종 오류 처리 그런 건 모른다. DPlay는 나름 하드웨어 독립을 추구한 통신 API이긴 한데, 요즘은 모뎀이고 시리얼 케이블 그딴 건 다 없어졌으니 그런 추상화 계층이 필요가 없어지면서 자연스레 도태했다. 잘은 모르겠지만 제1계층(물리 계층)은 거의 획일화가 돼 버린 것 같다.

※ 여담. IPX는 어디로 갔는가?

스타크래프트에서 배틀넷 말고 그냥 LAN으로 친구들끼리 팀플을 할 때, 옛날에는 배틀넷 다음으로 위에서 둘째인 IPX를 으레 고르곤 했다. 그러나 어느 패치 때부터인가 맨 아래에 UDP가 추가되었으며 그걸 고르는 걸로 구조가 바뀌었다. IPX는 동작하지 않기 시작했다. 어찌 된 일일까?

IPX라는 프로토콜은 똑같이 이더넷 랜선으로 통신을 하지만, 오늘날의 인터넷과는 다른 방식으로 통신을 한다. 즉, IP와 대등한 제3계층에서 방식이 다른 프로토콜인 것이다. IPX는 옛날에 네트워크 솔루션으로 유명했던 노벨 사에서 개발했고 실제로 매우 널리 쓰이기도 했지만 오늘날은 IP에 밀려서 사라졌다.

Windows 95때까지만 해도 네트워크 구성요소들을 설치하고 나면 기본으로 깔리는 것은 IPX였다. TCP/IP 지원 기능은 운영체제 CD를 넣어서 별도로 설치해야 했다. 무슨 말이냐 하면, 오늘날 당연시되고 있는 이 컴퓨터의 IP 주소를 설정하는 기능이 Windows 95에는 기본으로 없었다는 뜻이다. (심지어 네트워크 기능이 설치된 컴에서도)

사용자 삽입 이미지

그 다음 1990년대 중후반, Windows 98부터는 인터넷의 중요성이 워낙 크게 부각되기 시작했으니 TCP/IP 지원도 같이 포함되었다.
여담이지만 Windows에서는 등록정보/속성을 나타내는 단축키가 R인 편인데, 95에서는 유독 저 대화상자에서만 R은 삭제이고, 등록정보는 P였다. 무척 불편했는데 이 역시 98부터는 다같이 R로 개선됐다.

하긴, 본인도 옛날부터.. Windows가 근거리 네트워크 차원에서 제공하는 컴퓨터 간의 폴더 공유 기능과, 웹브라우저로 띄우는 인터넷은 기술적으로 무슨 관계인가 궁금하긴 했다.
인터넷 열풍 앞에서 IPX는 점점 잉여로 전락했으며, Vista부터는 드디어 IPX 지원이 hlp 도움말만큼이나 짤렸다. 그래서 스타크래프트도 근거리 팀플에 인터넷 프로토콜을 사용하는 UDP 지원이 추가된 것이다.

Posted by 사무엘

2015/02/05 08:37 2015/02/05 08:37
, , , ,
Response
No Trackback , 10 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1058

여러분은 다음과 같은 서로 완전히 다른 분야의 관행들에서 공통된 패턴이 존재한다고 생각하시는가? 만약 존재한다면 공통점이 무엇일까?

  • 컴퓨터에서 각종 계정의 암호를 설정하는데, 암호는 무조건 n글자 이상에 대소문자와 숫자 등이 반드시 골고루 섞여야 한다고 프로그램이 사용자에게 강요를 한다.
  • 출퇴근 시간엔 서울 지하철 사당 역은 2호선과 4호선 사이의 지름길 직통 환승 통로를 폐쇄하고 먼 우회 통로로만 환승이 가능하게 만든다. 또한 사람이 지나치게 많이 몰리는 행사가 열리면 가까운 지하철 역이 통째로 폐쇄되고 열차가 무정차 통과한다.
  • (종교 얘기. 비기독교인은 skip해도 좋음) 예루살렘에서 성령이 강림한 후 신약 기독교회가 갓 태동했다. 그러나 하나님은 역설적으로 그 기독교 성지에서 맹렬한 기독교 박해와 스데반의 순교를 허락하셔서 신자들을 뿔뿔이 흩어 버렸다.

내가 생각하는 이것들의 공통점은... 한데 몰려서는 안 되는 곳에 사람들이 지나치게 많이 몰릴 때 “그 몰리는 선택지 자체를 없애 버려서 분산을 강제로 유도”했다는 점이다. 그리고 이로써 집단 내부의 잠재적 부작용이나 병폐를 해결했다.

먼저 종교의 경우다. 먼저 믿고 구원받은 크리스천은 주님의 명령대로 세상 방방곡곡에 흩어져서 복음을 전해야 하는데 그게 말처럼 쉽지 않다. 어지간하면 그냥 신자들끼리만 기득권을 형성하고 교제라는 명목의 친목질만 하면서 고향에서 편하게 살고 싶다. 그러니 하나님이 저런 역경을 허락하신 것이다. 물리적으로만 보자면 그건 자기 신자를 줄이고 세력을 약화시키는 팀킬인데 기독교는 오히려 그런 역경을 통해서 역설적으로 잡초처럼 더 강해지고 널리 퍼져 온 것이다.
(단, 그렇다고 해서 기독교 박해 행위 자체를 정당화할 생각은 하지 마시길.)

교회사뿐만 아니라 바벨 탑 사건도 비슷한 맥락으로 사람들을 강제로 뿔뿔이 흩어 버린 경우에 속한다. 온 인류가 단일 민족 단일 언어이면 지금처럼 복음 전할 때에도 “아프리카 원주민이나 세종대왕, 이 순신 같은 사람도 다 예수 전해듣지도 못했는데 지옥 갔냐?” 이런 쓸데없는 질문을 받을 일이 없었을 텐데.. 하나님이 왜 그런 비효율적인 자충수를 일부러 두신 걸까?

두 말할 나위도 없이 인류가 단일 언어 단일 체계이면.. 다같이 하나님을 믿는 것보다 다같이 순식간이 부패하고 타락하고 썩어 버리는 게 훨씬 더 빨리 진행되기 때문이다. 다 합당한 이유가 있다.
그 대신, 교회가 태동하던 무렵에는 복음이 빨리 퍼져 나가라고 바벨 탑 사건 때와는 정반대로 언어 장벽을 잠시 극복해 주신 것이다. 그게 바로 '방언 은사'이라는 거다. 성경의 방언은 알아들을 수 있는 외국어이지, 울랄날따따따 잡소리가 절대 아니다.

자 그건 그렇고, 교통 얘기로 오면..
사람이 한 장소에 너무 몰리면 꼼짝달싹 못 하고 아무도 이동을 못 하게 될 뿐만 아니라 압사 사고 등 안전상의 위험도 매우 커진다. 제일 가까운 지하철역을 폐쇄하는 것은 사람들을 더 먼 곳까지 강제로 이동시킴으로써 밀집도를 낮추는 효과를 내며, 우회 환승 통로 역시 환승 승객을 수용할 공간을 확보하여 밀도를 낮춰 준다.

새해에 타종 행사를 하고 나면 종각/시청 일대의 지하철역이 폐쇄되고 불꽃 축제가 있을 때는 여의도 근처의 지하철역이 폐쇄되는 이유가 이로써 설명된다. 지난 여름에 교황이 왔을 때에도 광화문, 시청 근처의 지하철역은 당연히 폐쇄크리를 먹었다. 정말 상상을 초월하는 수의 인파가 몰렸기 때문이다.

서로 가깝고 같은 기간에 동일한 십자형으로 건설된 천호 역은 환승 거리가 짧은 반면, 군자 역은 거리가 일부러 꽤 길게 만들어져 있다. 7호선이 8호선보다 더 수요가 많고 혼잡하기 때문일 것이다.
그런데 천호도 마냥 짧기만 한 건 아니다. 천호에서 8호선을 타는 승객의 압도 다수가 잠실 역에서 내리는데, 환승 지점은 열차의 뒷부분이고 잠실에서 빨리 갈아타려면 암사 방면 열차의 맨 앞까지 이동을 해야 한다. 이것 때문에 사람들이 많이 걷는다. 지하철 8호선이 만들어질 때 이런 것까지 다 지능적으로 고려를 했는지는 모르겠다.

자, 그 다음으로 암호 얘기를 하겠다.
모든 사람들이 정말 정보 엔트로피가 높은 완전 무작위한 숫자· 문자를 암호로 사용한다면..
저런 제약은 오히려 암호 공격자에게 좋은 단서로 작용할 수 있다. 비록 암호 조합 문자열이라는 파이 전체에서 차지하는 비중은 여전히 미미하겠지만, 어쨌든 n글자 이하는 절대로 거들떠보지 않아도 되고, 한 종류의 문자만으로 이뤄진 문자열은 처음부터 탐색 대상에서 제끼면 되니까 말이다.

그런데도 굳이 저런 제약이 존재하는 건.. 불행히도 매우 많은 사람들이 저 작은 표본을 벗어나지 않는 범위에서 암호를 허술하게 만들고, 그게 공격자에게 왕왕 털리기 때문이다.
password, qwerty, q1w2e3, asdf, letmein, love 등...;;
그러니 차라리 그 표본을 명시하고서 사용자들로 하여금 강제로 배제하게 하는 게...
공격자에게 새 발의 피 정도의 단서를 던져주고서 전체 보안은 넘사벽급으로 훨씬 더 강화하는 효과를 낸다. 내 발뒤꿈치를 주고 상대방의 머리를 공격하는 전략 되겠다.

암호라는 건 마치 캡챠만큼이나 서로 모순되는 두 이념을 적당히 잘 충족해야 한다.
캡챠가 사람은 쉽게 알아보고 컴퓨터는 못 알아보는 그림이라면, 암호는 공격자--공격자의 주 도구인 컴퓨터도 포함--는 유추하기 무진장 어려우면서 당사자는 기억하기 쉬워야 한다.
주인이 기억하기 쉬우려면 결국 주인은 자기 개성을 표현하는 문자열을 떠올리게 되는데, 이렇게 되면 암호 공격은 거의 사회과학의 영역으로까지 확장되게 된다.

(더 극단적으로는, 기계적으로 완전 철통같은 암호라 해도 암호를 아는 사람을 돈이나 미인계로 매수한다든지, 혹은 아예 물리적으로 잡아 족침으로써 어이없게 뚫어 버리는 예도 있다.;; brute force 테크닉 그딴 것도 필요 없다. 성경에도 삼손의 지인들이 삼손의 수수께끼를 어떻게 풀었던가? 삿 14 참고)

일반적으로 컴퓨터 소프트웨어의 암호 입력란은 IME가 동작하지 않아서 영문· 숫자 이외의 문자를 입력할 수 없다. 이건 여러 모로 바람직한 조치라 여겨진다.
암호는 무슨 문자를 입력하느냐보다는 결국 무슨 keystroke를 입력했느냐가 더 중요하며, 지금 입력하는 글자가 일반적으로 화면에 보이지 않기 때문에 복잡하게 입력 모드 같은 걸 따질 처지가 못 된다.
또한 암호 입력란에서 IME 같은 별도의 소프트웨어 계층이 동작할 경우, 암호 문자열을 악성 프로그램이 가로채는 보안 문제가 커질 수도 있다.

하지만 이런 점에도 불구하고, 운영체제의 자체 GUI를 쓰지 않는 프로그램 중에는 IME가 동작하는 암호 입력란을 가진 경우도 있다. 굳이 한글로 입력을 안 하고 영문 자판에서 한글 입력을 하면.. 무질서도가 상당히 높은 알파벳 문자열이 생성되기 때문에 외국인 공격자가 알아내는 데는 애로사항이 꽃핀다. 이 사용자가 한국인이라는 단서가 없다면 말이다.
특히 세벌식은 사용자가 매우 적은 데다가 자체적으로 4단의 숫자· 기호까지 일부 활용하기 때문에 이런 보안 면에서 아주 좋다. Mac OS가 악성 코드가 별로 안 들끓는 이유도 딴 거 없고 사용자가 심히 적어서 해커들에게 별로 돈이 안 되기 때문이다. 간단하다.

아무도 모르고 내가 기억하기 쉬운 문자열이라는 특성상, 국가를 막론하고 욕설을 암호로 사용하는 사람도 있다. 뭐, 나쁠 것 전혀 없는 발상이긴 하지만 너무 대중적인(?) 욕설은 공격자들도 이미 다 파악하고 있으니 이 역시 조심해야 한다.

암호는 닥치고 20글자 이상으로 엄청 길면.. 무질서도가 기하급수적으로 치솟는다. 어지간히 단순무식한 형태의 암호라고 해도 엄청나게 긴 것에는 답이 없다. 글자수가 몇 자 늘어날 때마다 0.n초이던 예상 공격 시간이 그야말로 수천, 수만 년 이상으로 뻥튀기된다. 그러니 암호라는 건 pass-word가 아니라 최하 phrase나 sentence 정도의 규모로 만드는 게 좋다.

Microsoft Iphone 내지 언어학의 Colorless green ideas sleep furiously처럼 서로 개연성이 없는 생뚱맞은 단어들(저 문장 자체는 절대 쓰지 말 것! ㅋㅋ), 내가 좋아하는 무리수나 엄청 방대한 소수의 x~y째 자리수의 base64 인코딩 등을 섞으면 공격자가 뚫기 대단히 어려운 암호를 만들어 낼 수 있다. 그리고 까먹었더라도 그 암호를 생성하는 공식을 기억하고 있으니 나중에라도 컴퓨터를 돌려 언제든지 다시 만들 수 있다.

그리고 굳이 저런 식으로 머리를 안 굴리더라도, 본인 같은 사람은 직업 특성상 맨날 숫자와 특수문자와 알파벳이 뒤죽박죽 섞인 문자열을 취급하는 게 일이니... 저런 조건을 모두 만족하는 진짜 암호스러운(cryptic) 암호(password)를 의외로 금방 떠올릴 수 있었다.
요즘은 암호 관리자 전용 앱도 많이 나와 있는데, 이런 식으로 암호 생성기가 같이 연계되고 각 포털 사이트별로 암호 변경 주기를 관리도 해 주는 똑똑한 앱이 있으려나 궁금하다.

터치스크린 입력 방식이 시각 장애인에게 악재인 것만큼이나(점자!)..
PC와는 구조적으로 다른 스마트폰의 문자 입력 방식은 무질서도가 높은 암호를 입력하는 데 악재인 것 같다.
작은 화면에서 알파벳, 숫자, 특수문자를 섞어서 수월하게 입력하는 게 압도적으로 불편하고 까다롭기 때문이다.
물론 그 때문에 거기에는 패턴 제스처 같은 다른 암호 입력 방법도 등장했겠지만, 아무래도 문자 입력만치 보안이 강력하지는 못하다.

끝으로 글을 맺으며 든 생각이 있다.
우리말은 엄연히 다른 개념인 password와 encrypt/cryptic이라는 두 의미가 '암호(화)'에 모두 담겨 있다.
이건 어찌 보면 '다른'에 different와 another가 모두 포함돼 있고, 조사 '과/와'에 and뿐만 아니라 with가 섞여 있으며
단순히 시계의 표시 시각이 이르거나 늦은 것까지 다 '빠르다/느리다'로 표현하는 것만큼이나..
일면 좀 부정확하고 어정쩡하게 들린다.

하지만 한국어만 저렇게 한데 싸잡아 표현하는 건 아닌 듯하다.
또한 '암호'는 정보에 대한 접근 가능 여부 자체를 binary로 통제하는 것이고 '암호화'는 정보에 접근했더라도 해독을 못 하게 하는 것인데.
암호화를 해독하기 위한 암호가 있기도 하니, 목표면에서는 굳이 서로 떼어서 생각할 필요가 없는 비슷한 개념인지도 모르니까 말이다.

Posted by 사무엘

2014/11/04 08:37 2014/11/04 08:37
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1025

유니코드 1.x가 제정되었을 때는 믿거나 말거나 한글도 글자마디는 완성형 몇천 자만 지금과는 딴판의 영역에 등록되어 있었다. 단, 글자마디뿐만 아니라 옛한글을 포함한 자모도 자음 134개(물론 초성과 종성의 개수는 서로 다르고 따로 등록됨), 모음 66개가 등록되어서 이 자모들을 아래아한글 2.x가 한컴 2바이트 코드에서 그대로 채택해서 썼다. 한컴 2바이트 코드는 기존 조합형 코드와 유니코드를 적당히 짬뽕한 문자 코드였던 것이다.

유니코드는 첫 도입 배경이 저렇다 보니 “유니코드 = all 2바이트 단일 문자 체계 = wide character 문자 체계”라고 혼동하는 사람이 적지 않다. 그러나 이는 개념적으로 올바른 관계가 아니다. 2바이트 문자 체계로만 치자면 한컴 2바이트 코드도 이에 해당하며, 윈도 이외에 유닉스 계열 OS에서는 wide character는 16비트가 아니라 32비트인 경우도 있다. 왜 32비트이냐 하면 6만 5천여 개라는 집합 크기도 시간이 흐르다 보니 부족해졌기 때문이다.

1996년에 유니코드 2.0이 제정되면서 유니코드는 오늘날 우리가 아는 유니코드와 같은 뼈대가 갖춰졌다. 한번 등록한 문자는 이제부터 재배치나 제거 없이 영구불변으로 굳히기로 했으며, 이때 현대 한글 글자마디 11172자가 순서대로 고스란히 등록되었다. 2바이트 조합형처럼 비트 연산은 못 쓰지만 그래도 테이블 없이 뺄셈과 나눗셈, 나머지 연산으로 한글의 자소 정보를 얻을 수 있으니, 과거 조합형 한글 코드의 장점을 유니코드에서도 물려받은 셈이다.

외국 위원들의 반대를 무릅쓰고 한글에다 유니코드의 금싸라기 영역을 이만치 할당 받기 위해 한국 위원회 사람들이 애를 많이 썼다. 사실 확장완성형도 유니코드 등록 근거를 대기 위해 급히 만들어진 것이라고 한다.
이와 더불어 유니코드 2.0에서는 중요한 개념 내지 꼼수가 추가로 도입되었는데, 바로 surrogate이다.
16비트 공간 중 일부 영역은 그대로 쓰지 말고 따로 떼어 내서 두 글자를 뭉쳐서 더 많은 종류의 글자를 표현하는 데 쓰자는 것이다.

0xD800부터 0xDBFF는 high surrogat이고, 0xDC00부터 0xDFFF까지는 low surrogate이다. high는 언제나 앞에, low는 언제나 뒤에 오는 식으로 역할이 딱 고정되어 있다. 1024개의 앞짝과 1024개의 뒷짝이 만나니, 총 2048개의 독립 글자를 희생하여 2의 20승, 약 100만여 개의 글자 공간을 추가로 확보할 수 있게 된다. 요컨대 유니코드에 U+D800 같은 글자는 없고, U+D7FF 다음에는 곧바로 U+E000이 이어진다. 그 대신 0xD800과 0xDC00이 만나면 U+10000이라는 surrogate, 즉 확장 평면이 시작될 뿐이다. 예전의 16비트 단일 영역은 '기본 다국어 평면'(BMP)이라고 불리게 되었다.

비록 과거의 1바이트/2바이트 혼합 체계보다는 사정이 훨씬 낫고 문자열 처리가 수월한 건 사실이지만, 어쨌든 유니코드도 확장으로 인해 2바이트 단일 표현(UCS2)이라는 깔끔한 장점은 포기해야 하게 되었다. 이쯤 되니 유니코드라는 개념에서 문자 코드와 인코딩을 구분해서 표현해야 할 필요가 생겼다.

유니코드 자체는 UCS, 즉 universal character set이라 불리는 단일 문자 집합이다. 얘는 '가'라는 한글 글자에다가 U+AC00이라는 코드값을 부여해 줄 뿐, 그 코드값이 메모리나 파일에 어떻게 표현되는지에 대해서는 규정하지 않는다. 이를 표현하는 방식이 바로 Unicode transfer format, 즉 인코딩이 된다.

모든 유니코드 번호를 아무 뒤끝 없이 32비트 정수 하나로 균일하게 표현하면 그것은 UTF32이다. 아까 wide character가 4바이트인 플랫폼은 바로 UTF32를 구현하는 자료형인 것이다. 가장 깔끔하고 공간도 넉넉하고 모든 글자를 하나씩 쉽게 접근할 수 있지만 한 글자가 4바이트나 차지하여 메모리 낭비가 심하다.

BMP 영역에 있는 놈은 종전대로 16비트 정수 하나로 표현하고 확장 평면에 있는 놈만 surrogate 두 개로 쪼개서 표현하는 방식은 UTF16이라고 불린다. UCS2를 개념적으로 확장한 것이기 때문에 처음부터 2바이트 wide character를 표방하며 개발된 Windows 플랫폼이 매우 사랑하는 방식이다. 비록 Windows의 wchar_t는 이제 유니코드 코드 포인트 하나를 온전히 표현할 수 있을 정도로 충분히 크지 못한데도 말이다.

끝으로, 그 이름도 유명한 UTF8이 있다. 얘는 U+007F 안에 드는 전통적인 알파벳· 숫자, 제어 문자들은 1바이트로 유지하고, 나머지 BMP 영역의 외국어들은 2~3바이트로 표현하고 surrogate는 BMP와 동일한 4바이트로 늘여 표현하는 진정한 multibyte 인코딩이다.

n째 문자를 찾는 무작위 접근은 안 되겠지만, Windows NT처럼 커널을 처음부터 16비트 문자 단위로 설계하지 않고도 기존 8비트 문자 시스템에서 유니코드를 단절감 없이 표현할 수 있다는 엄청난 장점이 있다. CPU의 엔디언(비트 배열 순서)의 영향을 받지 않으며, tail byte가 기존 영문· 숫자와 충돌을 일으키지도 않는다는 점도 좋고 말이다.
그래서 얘는 웹에서 매우 사랑받고 있고 윈도 이외의 플랫폼에서는 파일뿐만 아니라 메모리 저장용으로도 UTF8이 즐겨 쓰인다. 사실 윈도만 이례적으로 UTF16을 너무 사랑하고 있는 것이고.. ㅎㅎ

여담이지만 UTF32, 16, 8 중 유니코드의 문자 집합이 먼 미래에 또 확장되어야 할 때, 공간이 가장 넉넉하게 남아 있는 놈은 두 말할 나위 없이 UTF32이다. UTF8이야 애초부터 들쭉날쭉 가변 길이 인코딩을 표방했기 때문에 한 글자당 4바이트보다 더 긴 5~6바이트까지 약간 더 확장할 여지가 있다. 지금 당장은 그것까지는 쓰이지 않지만 말이다.

하지만 UTF16은 유일하게 확장의 여지가 전혀 없다. 지금 surrogate 영역이 다 차 버리면 surrogate의 surrogate라도 또 만들지 않는 이상 답이 없다. 그리고 그런 헛짓을 할 바에야 차라리 UTF16을 폐기하고 UTF8 아니면 UTF32로 갈아타는 게 낫지..;; 이런 미묘한 면모가 있다.

다만, 한글 표현의 관점에서는 메모리가 가장 적게 드는 인코딩이 UTF16이라는 것도 감안할 점이다. 현대 한글 글자마디의 경우 UTF8은 3바이트, UTF32는 4바이트이지만 UTF16은 2바이트다. 초-중-종성이 모두 갖춰진 옛한글이라면 UTF8과 UTF32는 각각 9바이트, 12바이트를 차지하지만 UTF16은 6바이트이니 차이가 더 벌어지게 된다. 유니코드에는 한글이 완성형(글자마디+호환용 자모) 방식과 조합형(세벌 한글 자모) 방식이 모두 등록되었으며 완성형 방식도 11172자가 순서대로 모두 등록되었으니, 예전의 조합형· 완성형 논쟁을 완전히 종결짓는 데 성공했다.

Windows NT도 처음에 개발될 때 문자 처리 단위를 wide로 무리해서 넓히느라 고생하지 말고, 그냥 UTF8만 도입했으면 어땠을까 싶지만 이제 와서는 다 지난 일이다. 아무래도 UTF8보다야 UTF16이 컴퓨터의 입장에서는 더 빠르고 간편하게 각각의 문자를 인식할 수 있으며, 이것이 메모리와 성능 소모면에서 UTF32와 UTF8 사이의 완충지대 역할을 해 온 건 부인할 수 없기 때문이다. 덕분에 Windows API의 관점에서는 UTF8조차도 native 인코딩인 유니코드 UTF16으로부터 '변환'되어야 하는 여러 multibyte 인코딩 중의 하나일 뿐이다~! ㅎㅎ

옛날에 인터넷 익스플로러의 버전이 5~6이던 시절엔 한글로 된 URL이 인식이 잘 안 돼서 맨날 “URL을 UTF8로 보냄” 옵션을 끄라는 지시가 팁처럼 공유되곤 했다. 당시엔 Unicode-aware하지 않은 웹 서버가 아직 많아서 말이다. 오늘날로 치면 UAC(사용자 계정 컨트롤)를 끄거나 팝업 창 허용 기능을 사용하는 것과 비슷한 편법이다.
하지만 요즘은 UTF8 방식 URL이 표준으로 정착한 지 오래다. 호환성을 중요시하는 IE에나 그런 옵션이 있지, 크롬 같은 브라우저는 선택의 여지 없이 무조건 UTF8이기도 하고 말이다.

유니코드는 문자 처리 기술의 발전과 함께 더욱 덩치가 커졌으며, 한편으로 더욱 복잡해지고 지저분해지고 있다.
UTF32가 아닌 인코딩으로는 어차피 메모리 배열 인덱스만으로 실제 글자 인덱스를 얻을 수 없는 것은 물론이거니와, 메모리 상으로 존재하는 글자 코드 포인트 수와, 화면에 출력되는 글자의 수도 일대일 대응이 전혀 이뤄지지 않게 되었다. 옛한글이라는 것도 유니코드 관련 기술이 방대해지면서 덤으로 같이 처리 가능해졌을 뿐이다. 진짜 미치도록 복잡한 문자에 비하면 옛한글 쯤이야 양반이지... 동일한 글자를 나타내는 방법이 여러 가지 존재하게 되어서 정규화라는 것도 필요해졌다.

코드 포인트 값만으로 정렬을 하는 것 역시 상당수 의미를 잃었다. 옛한글 자모는 유니코드 5.2 때 한양 PUA 비표준에만 존재하던 것들이 추가 등록되었는데, 이것들의 코드 포인트상의 순서를 따지는 건 부질없는 짓이다. 가뜩이나 부족한 BMP 공간의 여러 틈새에 산발적으로 간신히 등록된 것 자체를 감지덕지해야 할 판이다.

한자는 더욱 가관이다. 유니코드에서 공간을 압도적으로 제일 많이 차지하고 있는 문자인 건 두 말할 나위도 없거니와.. BMP 영역에 이미 등록돼 있고 호환용 같은 이유도 없는데 작업자의 실수로 인해 나중에 surrogate 확장 평면에 중복 등록된 글자도 있고, 일본 문자 코드의 실수 때문에 문헌에 전혀 등장한 적이 없는 유령 한자가 등재돼 있기도 하다.

이것이 오늘날의 컴퓨터 문명을 지배하고 있는 가장 원초적인 규범에 속하는 유니코드의 현실이다. ㅎㅎ
수십 년 주기로 유니코드를 전면 reset하고 코드값을 재정리하면 어떨까 싶지만 그건 기존 컴퓨터 문명이 핵 전쟁 같은 걸로 폭삭 없어지지 않는 한, 상상 속에서나 가능한 일일 것이다.

Posted by 사무엘

2013/12/16 08:25 2013/12/16 08:25
, , , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/909

디지털 컴퓨터는 전자 신호로 0과 1만을 인식할 수 있다. 그렇기 때문에 컴퓨터에서는 문자 역시 0과 1의 조합인 숫자로 표현되며, 문자를 그런 숫자에다가 대응시키는 규칙이 바로 문자 코드이다. 정보 교환을 원활히 하기 위해서는 보내는 쪽과 받는 쪽이 모두 문자 코드가 통일돼 있어야 함은 두 말할 나위가 없다.

정보량의 최소 단위는 1비트이지만 현실적으로 컴퓨터의 기억 장치들이 한 글자로 취급하는 정보량의 최소 단위는 8비트, 즉 바이트이다. 8비트, 2^8승, 즉 256이라는 정보량은 숫자와 알파벳을 정하는 데는 충분하고 상위 1비트를 잉여화하여 오류 검출용으로까지 사용할 수 있을 정도이지만, 한글이나 한자 같은 문자를 담는 데는 턱없이 부족하다.

한자는 글자를 체계적으로 부분글자로 분해할 뾰족할 방도가 없는 상태로 글자 수가 너무 많다. 한글은 글자 생성 체계는 한자보다 훨씬 더 유리한 위치에 있지만, 실용적으로 편하게 다루려면 여전히 1만여 개의 미리 조합된 음절 형태를 그대로 배당해야 한다.

예를 들어 현실에서 '학교'라는 단어는 두 글자로 간주되지, 데이터베이스나 문서 분량 같은 데서 5글자라고 계수되는 곳은 아무도 없다. 코드 차지 영역을 줄이자니 메모리 사용량이 늘며, 그리고 오늘날로 치면 화면에 보이는 글자 길이와 메모리를 차지하는 글자 길이가 서로 완전히 따로 노는 complex script 급의 복잡한 기술이 필요해진다.

결국 한글과 한자는 1바이트만으로 표현할 수 없고 통상 2바이트로 표현된다. 그런데 기존 1바이트 영문· 숫자를 건드릴 수는 없으니 1바이트와 2바이트 문자가 공존하는 multi-byte 코드 체계가 만들어진다. 영문권에서 패리티 비트 내지 유럽 특수문자를 표현하는 데 쓰이는 최상위 1비트는, 이 문자가 1바이트로 끝인지 아니면 2바이트 문자의 첫 바이트(lead byte)인지를 판별하는 비트로 쓰인다.
공교롭게도 한글· 한자는 폭도 균일한 2글자 분량을 차지하여 비주얼 상 잘 어울린다. 전각· 반각이라는 개념이 여기서 유래된다.

그런데 lead byte는 그렇다 쳐도 2바이트의 다음 둘째 바이트인 tail byte도 기존 순수 1바이트 문자들과(영문· 숫자 같은) 전혀 겹치지 않게 할 것인지? 아니면 어차피 얘는 lead byte에 의해 변별이 됐으니 좀 문맥 의존적으로 겹치는 걸 허용할 것인지가 문제가 된다. 한글 코드의 경우 완성형은 겹치지 않으나 조합형은 겹친다.

그렇기 때문에 조합형은 tail byte가 대문자 알파벳과 소뭇자 알파벳이 제각기 따로 될 수 있고 심지어 \ 역슬래시 문자가 나올 수도 있다. 이런 이유로 인해 조합형은 1비트 + 초중성 5비트씩 한글의 구성 원리를 매우 잘 반영하여 설계된 문자 코드임에도 불구하고 당시 8.3 제약이 있던 시절에 파일 이름을 사실상 한글로 지정할 수 없었으며, 심지어 이론적으로는 // 주석을 조합형 한글로 지정한 경우 \ 때문에 2-byte aware 하지 않은 컴파일러에서는 충돌이 발생할 수도 있었다.

옛날에 한글 MS-DOS 시절에 한글 도스 셸이나 QBasic처럼 텍스트 GUI(?)를 구현한 프로그램들을 실행해 보면 한국 마소가 굉장히 잘 만든 게 있었다. 메뉴나 대화상자 같은 게 표시되어서 배경에 있던 2바이트 텍스트를 반토막 내더라도 깨진 문자열을 보여주는 법이 결코 없었다. 늘 짝수 바이트가 유지돼야 하는 한글/한자 영역이 홀수 바이트로 줄어들면 가장자리를 하나 삭제해 주면 된다.

그러나 문맥 의존적인 문자 코드로 그런 걸 구현하려면 문자열을 처음부터 읽어 봐야 하고, 두 바이트 중 하나가 소실됐을 때의 뒷처리가 문맥 독립 코드보다 더 어려우면 어렵지 쉽지는 않을 것이다. 불가능하다는 뜻은 아니지만.

조합형을 옹호하고 완성형을 까기에만 바쁜 분들의 심정이야 세벌식+한글 에반젤리스트로서 본인은 적극 이해한다. 그러나 과거의 2바이트 한글 코드의 이면엔 이런 면모도 있었음을 지적하고자 한다. 완성형은 여러 이슈가 얽혀서 그렇게 만들어졌지, 작정하고 한글을 난도질하려는 악의적인 의도로 만들어진 건 아니었다. 문맥 독립성뿐만 아니라 굉장히 보수적인 국제 규격 권장 사항을 만족하는 방식으로 문자 코드를 만들려다 보니, 한글 11172자에다 한자와 각종 특수문자들까지 넣을 절대적인 공간 자체가 부족했기 때문이다.

나중에 완성형에다 어거지로 한글 부족분을 채워 넣은 cp949 확장완성형은 어차피 조합형처럼 문맥 독립성이 깨졌다. 물론, 어차피 이렇게 만들 거면 처음부터 조합형으로 가지 하는 비판은 피할 수 없게 된 게 사실이다. 그런데, 이런 비슷한 삽질을 일본도 문자 코드를 만들면서 했으며, 이를 우리나라도 그대로 답습했을 뿐이다.

조합형과 완성형은 11172자와 2350자의 차이뿐만 아니라 한글 낱자를 표현하는 방식에서 매우 큰 차이가 있다.
조합형은 글자마디와 자모의 차이가 사실상 없다. 그냥 초 중 종성 중 한 성분만 있으면 자모이고, 초성과 중성이 갖춰졌으면 글자마디이다. 초성 ㄱ과 종성 ㄱ을 구분해서 표현할 수 있고 초성+종성이나 중성+종성 같은 '미완성 한글'도 얼마든지 표현할 수 있다.

그러나 완성형엔 그런 개념이 없다. 한글 입력을 처음 시작했을 때 쓰라고 '호환용 한글 자모'라는 게 있는데 그건 한글이 아니라 명목상으로는 '특수문자' 영역이다. 그냥 한글 자모 모양인 그림일 뿐이다. 초성+종성 구분이나 미완성 한글 표현 같은 건 없다.

메모리 1바이트가 아깝던 16비트 도스 시절에 국내에서는 조합형 한글 코드+글꼴이 쓰였지 마소의 윈도처럼 2350자 완성형 코드+글꼴로 돈지랄을 하는 프로그램은 전혀 없었다. 이런 압도적인 인지도의 차이로 인해 조합형은 1992년에 한국의 복수 표준으로 승격되어서 cp1361이라는 이름으로 나름 운영체제의 코드 페이지에 등록도 됐다.

자, 이렇게 컴퓨터는 1바이트 인코딩에다가 한중일 문화권을 위한 1+2바이트 멀티바이트 인코딩 구도가 유지되었고 마소에서는 이를 도스 시절부터 코드 페이지라고 불렀다. 그런데 이런 문자 코드들은 (1) 알파벳· 숫자 같은 공통 문자를 제외하면 같은 문자라 해도 국가마다 배당된 코드값이 같을 수가 없고, (2) 문자 집합 자체의 크기가 너무 작아서 세계 모든 문자들을 한 코드 페이지에다 담을 수 없다는 문제가 있었다.

人(사람 인)이라는 한자가 한국· 중국· 일본의 표준 코드에서의 코드값이 같을 리가 없으니 (1)은 자명한 문제다. (2)의 경우, 2바이트 코드라 해도 앞서 보았듯이 1바이트 문자와의 충돌을 피하기 위해 매우 제한된 영역의 바이트들만 묶을 수 있다. 이 때문에, 문맥 의존까지 시킨다 해도 추가로 만들 수 있는 문자는 1만여 자에 불과하다. CJK에서 쓰는 상용 한자들이나 간신히 다 집어넣을 정도이다.

그래서 1980년대 말부터 이런 발상의 전환이 이뤄졌다. “앞으로 컴퓨터의 메모리는 증가하고 소프트웨어 국제화의 중요성은 커질 테니, 글자 하나의 크기를 16비트로 쿨하게 확장하고 6만여 자의 공간에다가 전세계 언어 문자들을 집어넣고 동일 문자 동일 코드값을 실현하는 게 어떨까?”

이것이 바로 유니코드의 존재 목적 되시겠다. 컴공 전공자가 아니더라도 컴퓨터에서 '유니코드'라는 말은 한 번쯤은 다들 들어 보셨을 것이다. 이건 그야말로 컴퓨터 문자 코드계의 바벨 탑 내지 에스페란토 같은 물건이다.

마이크로소프트는 소프트웨어의 국제화에 굉장한 혜안이 있던 기업이었다. 그래서 OS/2와 결별하고 Windows NT를 첫 개발할 때, 애초부터 8비트 문자가 아니라 유니코드를 담을 수 있는 16비트 문자를 기반으로 설계했다. 심지어는 NTFS 파일 시스템까지도. 그리고는 이를 독자적으로 wide character이라고 부르기 시작했다. 어차피 8비트 문자만으로 충분하던 라틴 문화권에서는 별로 득이 되는 것도 없이 메모리 사용량만 두 배로 뻥튀기되는 대가를 감수하고라도 말이다.

Win32 API는 기존 16비트 윈도 API를 최대한 닮게 설계되었지만, 문자열을 다루는 모든 API에 A 버전과 W 버전 구분이 생겼다. 다만, 32비트 시절에 처음으로 도입된 OLE/COM 같은 기술은 처음부터 오로지 유니코드(= wide) 문자열만 취급하게 만들어졌다.

PE 방식으로 만들어진 32비트 EXE/DLL은 string 테이블, 메뉴, 대화상자 같은 리소스의 저장 포맷도 다 유니코드 방식으로 바뀌었다. 윈도 3.1에다가 Win32s를 설치하면 32비트 커널 DLL뿐만 아니라 각종 코드 페이지 변환 테이블도 설치되는 걸 볼 수 있다. 그게 바로 리소스를 변환하기 위해서이다.

윈도 NT가 3.x나 9x보다 메모리 사용량이 매우 많았던 이유 중 하나도 유니코드 때문이며, 반대로 윈도 9x가 유니코드 API를 지원하지 않았던 이유도 가정용 PC의 부족한 메모리 문제 때문이다.
이 때문에 컴파일 스위치만 바꿔서 유니코드와 기존 멀티바이트를 모두 커버할 수 있는 TCHAR, _T 같은 개념도 그때 생겼다. 두 개의 문자 포맷을 모두 지원하는 작업은 정말 엄청난 대공사였을 것 같다.

Posted by 사무엘

2013/12/13 08:24 2013/12/13 08:24
, , , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/908

지뢰찾기 연구

요즘 팔자에도 없던 지뢰찾기에 살짝 재미가 붙었다.
본인은 비슷한 학력· 경력으로 IT 업계에 종사하는 여느 사람들과는 달리, 머리 싸움을 즐기는 스타일이 전혀 아니었으며 복잡한 퍼즐 게임 따위와도 담을 쌓고 지내는 편이었다. 이런 점에서 본인은 완전 퍼즐 게임 매니아인 T모 님과는 성향이 다르다.

그래도 지뢰찾기 정도면 요령을 알고 나니까 은근히 재미있다. 초보 레벨로는(9*9, 지뢰 10개) 40초~1분 남짓한 시간 동안 대략 60~70%대의 승률로 깨겠다. 처음엔 초보 레벨조차도 5분이 넘게 끙끙대기도 했으나, 마치 경부선 서울-부산 열차의 운행 시간이 17시간에서 6시간대~4시간대로 줄어들듯이 시간이 단축되었다.

사용자 삽입 이미지
지뢰찾기는 소련에서 개발된 테트리스와 더불어 시간 죽이기용으로 상당히 적절한 컴퓨터용 퍼즐인 거 같다. 여느 보드 게임과는 달리, 물건이 먼저 존재하다가 컴퓨터로 옮겨진 게 아니라 처음부터 컴퓨터용으로 만들어진 게임이라는 차이가 있다.

맥북의 터치패드 격인 트랙패드로는 도저히 게임을 할 수 없는 듯했다.
두 손가락을 동시에 누르거나 패드 우측 하단을 지그시 누르면 우클릭이 되긴 하는데, 이게 생각보다 정확하게 인식되지가 않는 듯하기 때문이었다.

지뢰가 있다는 깃발만 꽂으려고 우클릭을 했는데, 그게 좌클릭으로 인식되어 지뢰를 밟고 장렬히 죽는 참사가 한두 번 발생하는 게 아니어서 말이다. 단, Windows Vista 이후부터 새로 개발된 지뢰찾기는 Shift+클릭으로 우클릭, 더블클릭으로 좌우 클릭도 돼서 조작이 훨씬 더 편해졌다.

키보드로는 Space는 셀 개봉(좌클릭)이고, Shift+Space가 깃발(우클릭)이다.
그런데 이번엔 깃발이 꽂힌 것을 제외한 모든 인접 셀들을 한꺼번에 개봉하는 건 키보드로 어떻게 하는지 모르겠다. 게임에 익숙해지고 나면 셀 개봉은 하나씩 클릭하는 것보다 저렇게 개봉을 훨씬 더 즐겨 하게 되는데 말이다.

지뢰찾기라는 게임은 풀이 순서를 논리적으로 명확하게 유추 가능한 상황이 대부분이지만, 가끔은 주어진 정보만으로는 정확한 지뢰 배치를 알 수 없어서 찍기(guessing)를 해야 하는 경우도 있다. 지뢰가 정확하게 어떤 조건으로 배치되어 있을 때 그런 상황이 생기는지는 잘 모르겠다.

숫자 정보로부터 유추 가능한 지뢰 배치 가짓수는 기본적으로 폭발적으로 증가할 수 있으며, 어떻게 될 수 있는지 백트래킹으로 일일이 하나하나 때려박아 넣으며 추적을 하는 수밖에 없다. 뭔가 네모네모 로직을 푸는 것 같은 느낌이 들기도 한다. 이 때문인지 이 문제는 전산학적으로 봤을 때 NP 완전 문제라는 것까지 증명되었다.

그리고 찍기가 필요 없는 명확한 상황일 때 사람이 지뢰를 찾는 절차는 의외로 아주 명료하고 기계적이다.
딱 이 정도 영역이 개봉되고 인접 셀의 지뢰 정보가 이렇게 주어졌을 때, '명백한 해법' 하나만 동원해서라도 컴퓨터가 게임 진행을 충분히 도와 줄 수 있겠다는 생각이 들었다.

그래서, 막간을 이용해 지뢰찾기를 푸는 프로그램을 짜 봤다.
초-중급 수준의 간단한 클래스 설계와 알고리즘 구현이 동원되니 심심풀이 땅콩 코딩용으로 꽤 적절한 거 같다!
'명백한 해법'을 적용할 수 없어서 '찍기'를 해야 할 때, 지뢰가 있을 만한 위치를 가장 유력하게 유추하는 것 정도까지 구현해야 비로소 중급-고급 사이를 넘볼 수 있지 싶다.

대략의 코딩 내역은 이러하다.
지뢰 답을 알고 있는 MineSource 클래스(각 칸마다 지뢰 여부를 실제로 담고 있는 2차원 배열),
그리고 그 MineSource에다가 쿼리를 해 가며 1~8 숫자와 자기가 찾아낸 지뢰 위치 정보만을 알고 있는 MineSolver 클래스를 만들었다.
이들은 다 2차원 배열과 배열의 크기는 공통 정보로 갖고 있으므로 MapData라는 동일 기반 클래스를 설정했다.

MineSource는 특정 위치 x,y에 대한 쿼리가 온 경우, MineSolver에다가 인접 셀들의 지뢰 개수를 써 준다. 인접 셀에 지뢰가 하나도 없다면 여느 지뢰찾기 프로그램이 그러는 것처럼 인접 셀 8개도 한꺼번에 개봉하면서 flood fill을 한다.
곧바로 지뢰를 찍었다면 당연히 곧바로 게임 오버라고 알려 준다. 그리고 요즘 지뢰찾기 게임 구현체들이 그런 것처럼, 첫 턴에서는 절대로 지뢰를 찍는 일이 없게 내부 보정을 하는 것도 이 클래스에서 하는 일이다.

지뢰찾기의 '명백한 해법'은 딱 두 가지이다.

  1. 열리지 않은(지뢰 마크가 있는 놈 포함) 인접 셀의 개수와 자기 숫자가 '같은' 셀은 주변 미개봉 셀이 다 지뢰임이 100% 확실하므로 그것들을 전부 지뢰 마크(깃발)로 표시한다.
  2. 깃발이 꽂힌 주변 셀의 개수와 자기 숫자가 같은 셀의 경우, 지뢰 마크가 없는 나머지 열리지 않은 인접 셀은 지뢰가 아닌 게 100% 확실하다. 따라서 전부 개봉한다.
  3. (위의 명백한 해법만으로 개봉할 만한 셀이 존재하지 않는 건 운이 나쁜 케이스다. 패턴을 기반으로 랜덤 추측을 해야 하는데, 이건 일단 보류.)

텍스트 모드에서 자기 스스로 무작위하게 지뢰밭을 만들고 지뢰찾기를 풀기도 하는 자문자답 프로그램을 만드니, 200줄이 좀 안 되는 코드가 완성되었다.
이 프로그램은 인접 셀에 대해서 뭔가 조건을 만족하는 셀의 개수를 세거나, (getCount) 일괄적으로 동일한 조치를 취하는(doAction) 패턴이 많다.

이걸 그냥 for(j=y-1; j<=y+1; j++) for(i=x-1; i<=x+1; i++)이라는 2중 for문만으로 돌리기에는 i나 j가 boundary 밖인 경우도 고려해야 하고, (i,j)가 (x,y)와 같은 위치인 경우도 피해 가야 하기 때문에 loop 자체가 생각보다 복잡하다.
그러니, 그 loop 자체만 하나만 짜 놓고 loop 안에서 하는 일을 그때 그때 달리 지정하는 것은 템플릿-람다로 깔끔하게 설계했다.

다음은 프로그램의 간단한 실행 결과이다.

after the first turn:
+ + 1 . . . . . .
+ + 1 . 1 1 1 . .
+ + 1 . 1 + 2 1 .
+ + 1 . 1 2 + 1 .
1 1 1 . . 1 + 2 1
. . . . 1 1 + + +
. . . . 1 + + + +
. 1 1 2 2 + + + +
. 1 + + + + + + +

(중간 과정 생략)

picking 7 9
@ @ 1 . . . . . .
2 2 1 . 1 1 1 . .
1 1 1 . 1 @ 2 1 .
1 @ 1 . 1 2 @ 1 .
1 1 1 . . 1 2 2 1
. . . . 1 1 3 @ 2
. . . . 1 @ 3 @ 2
. 1 1 2 2 2 2 1 1
. 1 @ 2 @ 1 . . .
You Won!


이 정도 초보적인 지뢰 찾기 풀이 프로그램은 이미 다 개발되고도 남았으니,
유튜브를 뒤지면 신의 경지 수준의 속도를 자랑하는 지뢰찾기 TAS (매크로 프로그램 내지 역공학을 동원한 게임 스피드런) 동영상들이 나돌고 있다.

여담이다만, 지뢰찾기를 하다가 지뢰를 밟아서 게임 오버가 될 때 본인은 깜짝 깜짝 잘 놀란다. =_= 마치 옛날에 페르시아의 왕자를 하는데 타이밍을 잘못 잡아서 왕자가 쇠톱날(chopper)에 두 동강 나서 죽는 것 같은 그런 느낌이다.

Posted by 사무엘

2013/09/26 08:32 2013/09/26 08:32
, , , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/881

지금으로부터 수십 년 전에는 동네마다 컴퓨터 학원이 있었고, 꼬꼬마가 프로그래밍을 공부하겠다고 하면 으레 GWBASIC부터 시작하곤 했다. 베이직은 16비트 MS-DOS뿐만 아니라 각종 가정용 8비트 컴퓨터에도 특유의 인터프리터 환경이 내장되어 있기도 해서 접하기가 한결 쉬웠다.

그때와는 달리 오늘날의 컴퓨터 교육은 이미 만들어진 소프트웨어들을 활용만 하는 실무에만 치우친 편이다.
지금 컴퓨터 프로그래밍을 처음부터 공부하고 싶다면 무엇부터 시작하는 게 좋을까?
이 질문에 대한 답변은 그 사람이 무슨 프로그램을 작성하고 컴퓨터로 무엇을 하고 싶은지 목적에 따라 크게 달라진다.
이 글은 나의 지극히 좁은 편견만을 반영하고 있으므로, 당연히 프로그래머마다 생각이나 견해가 다를 수 있다.

1. 비전문가/비전공자로서 그냥 최소한의 시간 투자로 개인적인 컴퓨터 활용도만 높이고 싶다면(고급 계산기 + 기초 알고리즘 실습 + 파일 자동 조작 + 매크로/자동화 도구 등)

개인적으로 파이썬을 추천한다. 복잡한 자료형을 다루기가 쉬워서 여타 언어들에 비해 짧은 코드만으로 복잡한 일을 한번에 끝낼 수 있다. 방대한 크기의 파일을 읽어서 내가 원하는 처리를 한 뒤 출력을 뱉어내는 수십 줄 남짓한 프로그램만 짤 줄 알아도 인생이 굉장히 편해질 수 있다.
좀 수학 덕후 기질이 있다면, 함수형 프로그래밍 언어를 건드려 봐도 될 듯.

2. 1보다는 좀 더 나아가서 가성비가 뛰어난 개발 환경에서 최소한의 GUI까지라도 만들어 보고 싶으면

Windows 플랫폼 한정으로 C#급 언어가 가장 좋겠다.

3. 웹브라우저에서 어지간한 애니메이션이나 프레젠테이션을 다 띄우고, 글이나 그림을 계산 결과로서 출력하고 싶으면

HTML + 자바스크립트.
요즘은 HTML이 단순히 화면에 뿌려지는 글과 그림, 하이퍼텍스트 문서일 뿐이라고 생각하는 건 큰 오산이다.
문서에다 서식을 주는 건 이제 CSS라는 방대한 별도의 규격으로 독립해 나가고, HTML은 문서 반 코드 반이다. 실시간으로 내용이 업데이트되고 화면 끝까지 스크롤됐을 때 추가로 컨텐츠를 로딩하고.. 사용자의 조작에 반응하여 그림을 뿌려 주는 등, 예전에는 ActiveX나 하다못해 플래시라도 써야 했을 컨텐츠들이 지금은 저것만으로 다 된다.

웹브라우저가 거의 플랫폼 독립적인 프로그램 구동 플랫폼처럼 바뀌었으니, 이를 활용할 줄 알면 역시 컴퓨터 활용 능력이 크게 향상될 수 있다. 웹 프로그램은 다른 언어나 런타임, IDE 같은 걸 설치할 필요조차 없이, 그냥 메모장에서 코딩 후 웹브라우저에서 곧바로 돌려보면 된다.

4. 맥 OS용 응용 프로그램이나 아이폰 앱을 개발하고 싶다면

Objective C + xcode + COCOA API 등등으로 고고씽이다. 일단 맥북을 장만해야 할 것이고 Windows와는 너무 이질적인 개발 환경 때문에 처음에 고생 많이 할 것이다.

5. 안드로이드 스마트폰용 앱을 개발하고 싶다면

자바 + 이클립스 IDE에 익숙해져야 할 것이다. Java는 요즘 스마트폰 앱 개발용 언어로 입지가 확 되살아난 듯하다. 이게 완전한 웹용 언어도 아니고(자바스크립트는 자바와 전혀 다른 언어임), 자바 애플릿이 플래시/ActiveX를 완전히 대체하는 RIA (rich internet application) 프레임워크로 자리잡은 것도 아니고, 로컬에서는 느리고 성능이 안 좋다 보니 전통적인 기계어 프로그램들에 밀려서.. 예전까지는 위상이 좀 어정쩡했기 때문이다. 로컬에서는 일부 크로스플랫폼 소프트웨어의 GUI(프런트 엔드)를 돌릴 때나 좀 쓰이곤 했다.

6. 끝으로, PC + Windows 환경에서 네이티브 코드 + standalone으로 실행되는 프로그램을 개발하고 싶다면

아래아한글이나 <날개셋> 한글 입력기나 어지간한 온라인 게임과 같은 급의 기계어 실행 파일을 만들고 싶다면..
역시나 재래식 Visual C++이나 최소한 델파이 같은 툴로 가야 한다.
개발 환경, 언어 문법과 기본 라이브러리, Windows API, 그 뒤 개발 분야에 따라 추가적인 라이브러리 공부까지 산 넘어 산이다.

오늘날 프로그램 개발 환경이 결국 로컬 + 웹 + 앱이라는 세 양상으로 구분된다고 예전에 글을 쓴 적이 있는데.. 그것과도 관계가 있다.
프로그래밍, 더 나아가 소프트웨어 개발에 관심이 있는 분이라면 이런 아이템들을 참고하면 되겠다.
그런데 정작 이런 글을 쓴 본인은 6만 빼고 나머지 분야는 여전히 너무 모른다는 게 함정..ㅎㅎ

Posted by 사무엘

2013/09/17 08:37 2013/09/17 08:37
,
Response
No Trackback , 13 Comments
RSS :
http://moogi.new21.org/tc/rss/response/878

수학에서 행렬은 굉장히 흥미로운 물건이다.
행렬끼리의 덧셈이나 행렬의 상수배는 어려울 게 없는 쉬운 연산이지만, 행렬끼리의 곱셈은 그렇지 않다. 행렬 A와 B사이의 곱셈은 A의 가로 크기와 B의 세로 크기가 같아야 정의되며, 새로 생기는 행렬의 크기(dimension)는 반대로 B의 가로 크기와 A의 세로 크기로 결정된다.

이런 특성상 행렬의 크기는 세로, 즉 row부터 먼저 써 주는 게 직관적이다. 세로 x줄 가로 y줄짜리 x,y 행렬과 y,z 행렬의 곱은 x,z 크기가 된다고 표기가 가능하기 때문이다.

또한, 앞에 있는 행렬과 뒤에 있는 행렬이 원소가 서로 연산되는 방향이 다르기 때문에 행렬의 곱셈은 교환 법칙이 성립하지 않는다. A×B가 일반적으로 B×A와 같지 않다는 뜻. 그러나 결합 법칙은 성립한다. (A×B)×C와 A×(B×C)는 동일하므로, 같은 방향만 유지하면 아무 순서로나 행렬을 곱해 줘도 된다.

그래서 이것과 관련하여 흥미로운 문제가 하나 있다.
크기가 들쭉날쭉 다르지만 순서대로 곱셈은 가능한(= 인접한 행렬끼리는 앞 행렬의 가로 크기와 뒤 행렬의 세로 크기가 일치) N개의 행렬들이 있다. 우리는 이들을 모두 최소의 계산량만으로 곱하고 싶다.

역행렬이나 행렬식 값을 구하는 비용에 비할 바는 아니겠지만 행렬의 곱셈은 꽤 비싼 연산이다. 일반적으로 x,y 크기와 y,z 크기의 행렬을 곱하는 데는 원소들간에 x*y*z회의 곱셈이 필요하다. n 크기의 정사각행렬의 경우 이는 n^3으로 귀착된다. (뭐, 분할 정복법을 활용하여 n^2.x승으로 줄이는 복잡한 알고리즘이 있긴 하지만 이것은 초기 준비 오버헤드가 굉장히 크기 때문에 행렬이 무진장 클 때에나 의미가 있다.)

예를 들어 A는 4*2 크기, B는 2*3 크기, C는 3*1크기의 행렬/벡터라고 치자.
이것을 A*B*C 순으로 진짜 순서대로만 곱하면 A*B를 곱하는 데 4*2*3=24회의 곱셈이 동원되고, 그 결과물인 4*3 행렬을 C와 곱하느라 12회의 곱셈이 필요해서 계산량은 총 36이 된다.

사용자 삽입 이미지

그러나 B*C부터 먼저 곱한 뒤 A를 거기에다 곱하면 열수가 적은 C 덕분에 B*C는 겨우 6회 만으로 끝나고, 거기에다 4*2*1=8회의 곱셈이 추가되어 총 14의 계산량만으로 A*B*C를 구할 수 있다. 답은 결국 똑같은데도 (AB)C보다 A(BC)가 훨씬 더 나은 전략인 것이다.

신기하지 않은가? 그래서 이런 configuration을 일반화하여 {4, 2, 3, 1}이라고 표현하고, 더 나아가 n>=3인 n개의 자연수라고 치자.
이 입력에 대해서 최소 곱셈 횟수와 실제 곱셈 순서를 구하는 것이 문제이다.

정올 공부를 한 분이라면 아시겠지만, 이것은 다이나믹 프로그래밍, 혹은 동적 계획법이라는 알고리즘 설계 방법론을 학습하면서 예시로 다뤄지는 아주 기본 문제이다. 다이나믹 프로그래밍은 다음과 같은 경우에 유용하다.

  • 전체 구간에 대한 최적해가 부분 구간의 최적해에다가 추가 연산을 함으로써 구하는 게 가능하다.
  • 그리고 한번 답을 구해 놓은 부분 구간의 최적해는 더 바뀌지 않는다는 게 보장된다.

이 행렬의 곱셈 문제에서 가장 작은 구간은 3이며, 이때의 답은 그냥 두 말할 나위 없이 세 정수의 곱이다.
그리고 전체 구간 [1..n]에 대해서 최적해는 바로..

  • 1을 [2..n]과 곱했을 때의 계산량 (맨 앞의 행렬과 나머지)
  • [1..n-1] 과 n을 곱했을 때의 계산량 (앞의 행렬들과 맨 뒤의 행렬)

중 더 작은 놈이라고 간주하면 된다.

그럼 [2..n]과 [1..n-1]은? 각 구간에 대해서 또 동일한 해법을 적용하여 재귀적으로 구간을 계속 쪼개 나가는 것이다. 언제까지? 구간의 길이가 3이 될 때까지 말이다.
이렇듯, 다이나믹 프로그래밍은 재귀성을 띠고 있다. 이것은 수학적으로는 점화식으로 표현되며, 코드로는...

const int dat[]={4,2,3,1,2,6,5,8,3,2}; //배열

int GetMin(int f, int t)
{
    int i=t-f, j;
    if(i<3) return 0; //should not reach here
    else if(i==3) return dat[f]*dat[f+1]*dat[f+2]; //obvious case
    else {
        //사실은 i가 3인 경우도 이 조건의 특수한 케이스라고 간주할 수 있다.
        //단지 GetMin값이 0이고, t-2와 f+1이 동일한 값이 될 뿐이다.
        i=GetMin(f,t-1) + dat[f]*dat[t-2]*dat[t-1]; //(A*B)*C
        j=GetMin(f+1,t) + dat[f]*dat[f+1]*dat[t-1]; //A*(B*C)
        return i<j ? i:j;
    }
}

int answer = GetMin(0, 10);

과연 이렇게 하면 답이 구해질까?
프로그램을 돌려 보면, 10개의 정수로 표현된 9개의 서로 다른 크기의 행렬들의 곱은..
146회의 곱셈만으로 계산이 가능하다고 나온다.

구체적인 계산 순서는 이러하다.

4 (2 (3 (((((1 2 6) 5) 8) 3) 2)))

이 경우, 각 단계별 계산 순서는 다음과 같이 되기 때문에,

x y z x*y*z
1 2 6 12
1 6 5 30
1 5 8 40
1 8 3 24
1 3 2 6
3 1 2 6
2 3 2 12
4 2 2 16

곱을 전부 합하면 진짜로 146이 맞다!
참고로, 이런 전략을 쓰지 않고 진짜 FM대로 앞에서부터 뒤로 행렬을 순서대로만 곱하면 계산량은 최적해의 세 배를 넘는 492에 달한다.
이것이 바로 알고리즘이 만들어 내는 차이이다.

다이나믹 프로그래밍에는 반드시 수반되어야 하는 작업이 있다. 바로 예전에 구했던 구간 계산값들을 배열에다 저장해 두는 것이다. 그렇게 하지 않으면, 마치 피보나치 수열을 f(x) = f(x-1)+f(x-2)라고만 구현하는 것만큼이나 계산량이 n이 커짐에 따라 기하급수적으로 커지게 된다. 그것도 예전에 한번 했던 똑같은 계산을 매번 반복하느라 말이다.
그래서 이 방법을 사용한 알고리즘은 대체로 시간 복잡도와 공간 복잡도가 모두 O(n^2)이 된다. 시간 복잡도가 지수함수에서 그래도 다항함수로 바뀐다.

구간별로 최적해 자체뿐만이 아니라 구간 분할을 어떻게 했는지에 대한 정보도 따로 보관해 놓으면 아까와 같은 구체적인 계산 순서도 그 정보를 추적함으로써 구할 수 있다.

정올에서 다이나믹 프로그래밍의 중요성은.. 두 말하면 잔소리이다.
본인은 20세기에 정올 공부를 한 세대인지라 그 시절의 문제밖에 기억을 못 한다만..

1997년 한국 정보 올림피아드의 고등부 3번인 벽장 문제는 최적해를 구하고자 할 경우 공간과 시간 복잡도가 O(n^3)인 다이나믹 프로그래밍으로 풀 수 있다. 이 때문에, 16비트 환경임을 감안하더라도 이 문제는 입력의 범위가 작다. 벽장의 개수와 벽장 사용 순서가 최대 겨우 20까지밖에 안 올라가는 소규모이다. 실용적인 상황에서는 이런 부류의 시뮬레이션 문제는 휴리스틱이 동원되어야 할 것이다.

이 외에,

1999년 고등부 1번 검은 점 흰 점 연결,
2000년 고등부 1번 수열 축소

도 다이나믹으로 푸는 문제이다.
국제 정보 올림피아드의 기출 문제 중에는
10회(1998)의 둘째 날 마지막 문제인 폴리곤 게임,
11회(1999)의 첫째 날 첫 문제인 꽃 진열이 기억에 남는다. 특히 꽃 진열은 상당히 기초적인 다이나믹 프로그래밍 문제로, <날개셋> 타자연습의 문장 정확도 측정도 이와 거의 같은 발상의 알고리즘을 사용하고 있다.

난 이 바닥은 손 놓은 지가 너무 오래 돼서 기억이 가물가물하다.
정보 올림피아드에서 경시와 공모는 마치 과학과 공학, 어학과 문학의 차이와 비슷한 것 같다.

Posted by 사무엘

2013/08/14 08:34 2013/08/14 08:34
, ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/866

한자어로 '원'이라고 부르는 동그라미라는 도형은 시각적으로나 수학적으로나 아주 신기한 도형이다.
둥글다는 게 무슨 의미인지 기계가 계산으로 표현할 수 있을 정도로 엄밀하게 정의하자면 결국 '어떤 점에서 거리가 같은 점들의 집합'이라는 정의가 등장하게 되고, n차원 직교 좌표에서 거리라는 건 결국 차원을 구성하는 각 축의 거리들의 제곱의 합의 제곱근이라고 정의된다.

원의 지름과 원의 둘레의 비율은 그 이름도 유명한 '파이'이며, 3.141592... 로 시작하는 이 값은 무리수인 동시에 초월수라는 것도 상식이다.

그런데 이 원을 정사각형 격자 모양의 래스터 그래픽 장치에서 어떻게 하면 효율적으로 그릴 수 있을까? 그런 물건의 내부엔 컴퍼스 같은 직관적인 도구가 없는데 말이다.
중심이 x, y이고 반지름이 r인 원을 구성하는 좌표들을 어떤 계산을 통해 얻어 올 수 있을까?
원점이 중심인 원의 방정식은 x^2+y^2=r^2. 따라서 y=sqrt(r^2-x^2) 방정식을 이용하면 사분원 내지 반원을 구성하는 점을 구할 수 있다. 그리고 이 값을 바탕으로 나머지 방향의 점을 그리면 될 것이다.

#include <math.h>
template<typename T>
void Draw_Circle(int x, int y, int r, T f)
{
    double R_2 = r*r;
    for(int i=0;i<r;i++) {
        int v = (int)(sqrt( R_2 - i*i )+0.5);
        f(x-i, y-v); f(x-i, y+v);
        f(x+i, y-v); f(x+i, y+v);
    }
}

for문 자체는 0부터 r까지 사분원의 x좌표만 돌고, 이를 바탕으로 점을 찍는 함수 f를 4개 방향으로 모두 호출한다.
r의 제곱 값은 한 번만 계산하면 되므로 for문 밖에서 별도로 선언해 주는 센스도.
소숫점은 버림이 아니라 반올림이 되도록 0.5를 더한 뒤에 int로 캐스팅하는 게 좋다. 그래야 당장 그려지는 원도 90*n도 부근이 더 탱탱하고 보기 좋아진다.

함수의 사용은 MFC 기준으로 이런 식으로 하면 된다. 함수 안에서 또 다른 함수를 내부적으로 호출할 때 함수 포인터보다 람다가 참 깔끔하긴 하다. (너무 남발한 게 꼬이면 code bloat은 피할 수 없겠지만)

CPaintDC dc(this);
auto x = [&](int x,int y) { dc.SetPixel(x,y,0); };
Draw_Circle(220,220, 200,  x);
Draw_Circle(420,330, 160,  x);

사용자 삽입 이미지

그런데 아뿔싸, 역시 기울기가 1보다 더 커지는 곳에는 점이 듬성듬성 떨어져 있게 된다.
이 틈을 점 찍기가 아니라 선 그리기 같은 다른 함수로 메운다는 건 있을 수 없는 일이고..
결국, 우리의 원 그리기 알고리즘은 언제나 기울기가 1보다 작은 구간에서만 동작하게 loop 구조를 바꿀 필요가 있다.
우리는 원을 4등분했는데, 그렇게 4등분된 조각도 한쪽 끝과 맞은편 끝이 완벽하게 대칭으로 이들을 동시에 그려 보자.

가령, 1사분면에서는 x좌표를 1씩 증가시키면서 r로 근접하고(위의 코드에서 i) y좌표는 r이다가 점점 0으로 작아지는데(위의 코드에서 v),
이와 동시에 반대편에서는 y좌표를 1씩 증가시키면서 r로 근접하고, x좌표는 r에서 0으로 근접시키도록 점을 같이 그리는 것이다.
이제 loop는 변수 i의 값이 r에 도달한 지점에서 끝나는 게 아니라 v와 값이 같아지는 지점에서 끝나면 된다. (정확히는 sqrt(2)*r/2 지점이 됨)

{
    double R_2 = r*r;
    for(int i=0; ;i++) {
        int v = (int)(sqrt( R_2 - i*i )+0.5);
        f(x-i, y-v); f(x-v, y-i);
        f(x-i, y+v); f(x-v, y+i);

        f(x+i, y-v); f(x+v, y-i);
        f(x+i, y+v); f(x+v, y+i);
        if(i>v) break;
    }
}

사용자 삽입 이미지
와, 이로써 굉장히 찰진 모양의 원이 그려졌다. 한 번 루프를 돌 때마다 점이 8개가 그려지는 것이다.
그러나 이런 원 하나 그리는데 부동소숫점에, 곱셈에, 심지어 제곱근까지 꽤 부담스러운 연산이 많이 들어갔다.
이걸 좀 줄일 수는 없을까?

...
    int R_2 = r*r;
    int v = r;
    for(int i=0; ;i++) {
        if(i*i + v*v > R_2) --v;
...

loop의 앞부분을 이렇게 고쳐 보자.
x축에 속하는 i의 값이 1증가할 때마다 y축에 속하는 v의 값은 그대로 유지되거나 1 감소하거나 둘 중 한 변화만을 겪을 것이다.
i가 증가함에 따라 원점에서 i, v까지의 거리가 R보다 확 커지게 됐다면, 이 궤적은 원의 범위를 벗어나는 것이므로 y축에 속하는 v를 1 줄여 준다.

실질적으로 행해지는 연산을 이렇게 최적화해 주면 최소한 부동소숫점과 제곱근 연산은 없어진다.
그러나 최적화의 여지는 그래도 여전히 남아 있다. 저 꼴도 보기 싫은 곱셈을 없애려면 어떡하면 좋을까?

방법이 있다.
결국, i*i는 0, 1, 4, 9, 16 ...의 순열을 생성해 낼 텐데, 얘는 덧셈을 두 번 하는 걸로 대체할 수 있다. 한 번 덧셈을 한 뒤엔 증가치가 2씩 늘어나니까 말이다(1과 4의 차는 3, 4와 9의 차는 5, 9와 16의 차는 7). x^2의 도함수가 괜히 2*x가 아니다.

그리고 v는 초기값이 아예 R_2와 같으니 약분이 가능하다. 그 뒤에 v의 값이 줄어들면서 차이만이 발생할 뿐이다. 그런데 얼마를 빼 줘야 할까?
x^2가 (x-1)^2로 바뀌었을 때 감소하는 값은 잘 알다시피 2*x-1이다. 따라서 이 값만 초기에 계산해 놓은 뒤, v가 1 감소하게 됐을 때 가상의 v_square은 그만치 빼 주고, 그 델타값 자체도 2 감소시키면 된다.

...
    int v = r, i_square = 0, i_delta = 1;
    int v_delta=2*r-1, v_square_delta = 0;
    for(int i=0; ;i++) {
        if(i_square + v_square_delta > 0) {
            --v; v_square_delta-=v_delta, v_delta-=2;
        }

... //점 여덟 군데를 찍어 준 뒤

        if(i>v) break;
        else i_square+=i_delta, i_delta+=2;
    }

이로써 그 부드러운 원을 오로지 정수의 덧셈만으로, 그리고 곱셈이라고는 loop 돌기 전에 *2 단 한 번밖에 안 하는 깔끔한 원 그리기 알고리즘이 완성되었다. 놀랍지 않은가? 게다가 고정적인 두 배 연산은 잘 알다시피 bit shift로도 수행 가능한 아주 가벼운 연산이기도 하고 말이다.

GWBASIC, Windows GDI API, 옛날 볼랜드 BGI 등 모든 그래픽 라이브러리에 들어있는 원 그리기 함수는 기본적으로 이 알고리즘을 이용하여 원을 그린다. 각종 알고리즘 서적에 예제로 실려 있는 소스들도 세부적인 변수 활용이나 계수 계산에 차이가 있을지언정 기본적인 아이디어는 동일하다.

사실, 이건 거의 대학교 학부 수준이고 정보 올림피아드 공부라도 했다면 중· 고등학교 시절에라도 접했을 기초적인 내용이다. 진짜 어려운 건 이걸 응용하여 안티앨리어싱을 적용한다거나 타원을 그리거나, 아예 부채꼴 내지 회전된 원을 그리는 알고리즘이다.

단, Windows GDI가 그리는 원은 왠지 좀 엉성하고 덜 예쁜 것 같다. 비교를 할 때 반올림 보정을 안 하는지 경계가 아주 약간 덜 통통하며, 특히 기울기가 1(45도)에 가까워지는 지점에 점의 배치가 지저분하다.
차이를 보이기 위해 움짤을 만들어 보았다. 파란색 원은 GDI 함수로 그린 것이고, 빨간색 원은 우리가 작성한 함수로 그린 것이다.

사용자 삽입 이미지

Posted by 사무엘

2013/07/28 08:27 2013/07/28 08:27
, , ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/860

※ 들어가는 말

정렬은 검색과 더불어 컴퓨터가 인간에게 유용한 결과물을 내놓기 위해 내부적으로 가장 빈번히 수행하는 계산 동작에 속한다. 다른 알고리즘의 내부 과정으로 즐겨 쓰이기도 하고 말이다. 전산학 내지 컴퓨터 과학에서 정렬 문제가 얼마나 중요한지에 대해서는 더 말이 필요하지 않다.

정렬은 문제의 목표가 너무나 명확하고 실용적이며, 다양한 관점에서 문제의 접근이 가능하고 좋은 알고리즘과 나쁜 알고리즘의 차이도 아주 드라마틱하게 알 수 있기 때문에... 예로부터 그 특성과 해법이 연구될 대로 연구되어 왔다. 시간 복잡도 관념이 없던 초짜 프로그래머가 O(n^2)와 O(n log n)의 어마어마한 차이를 깨우치는 계기도 대체로 정렬 알고리즘을 공부하고부터이다.

n개의 원소에 대한 정렬 작업은 n개의 원소를 임의의 방식으로 늘어놓는 n!가지의 순열 중에, 원소들의 값 순서가 오름차순이나 내림차순이 유지되는 순열을 선택하는 작업이라고 볼 수 있다. 그리고 일반적인 정렬 알고리즘은 임의의 두 원소와의 비교를 통해 거기서 가능한 선택의 범위를 좁혀 나간다.

이런 원론적인 분석을 통해, 비교 연산 기반 정렬 알고리즘의 시간 복잡도는 아무리 기가 막힌 알고리즘을 고안하더라도 O(n log n)보다는 결코 더 좋을 수가 없다는 것이 증명되어 있다. 그리고 정렬 알고리즘 중, 제자리(in-place)라는 특성을 지닌 알고리즘은 교환(swap)이라는 동작도 공통적으로 사용하게 된다.

정렬 문제는 NP 완전 문제라고 알려져 있는 외판원 문제(TSP)에서 정점(vertex)들이 일렬로 쭉 나열되어 있는 특수한 경우라고 볼 수도 있다. 가까운 순서대로 순서대로 방문하는 게 정답일 테니 결국 정점들이 정렬된 것이나 마찬가지이다. 비록 domain이 1차원이 아닌 2차원 이상으로 가면 난이도가 곧바로 안드로메다 급으로 치솟지만 말이다.

※ O(n^2) 또는 O(n log n)인 비교 기반 알고리즘

역사적으로 굉장히 많은 수의 정렬 알고리즘이 고안되었으며 이들은 제각기 장단점과 특성이 있다. 알고리즘을 평가하는 주 잣대로는 자료 개수 n에 대한 시간 복잡도와 공간 복잡도가 있으며, 이들도 평균적일 때와 최악의 상황일 때를 따로 평가한다. 이 외에도 자료의 상태에 성능이 민감하게 달라지는지, 그리고 값이 같은 원소의 상대적인 순서가 보존되는지를 나타내는 순서 안정성(stability)을 따지기도 한다.

시간 복잡도가 O(n^2)에 속하는 정렬 알고리즘은 일명 '발로 짠 알고리즘'에 속한다. 직관적이고 구현하기 매우 쉬우나 성능이 쥐약이라는 뜻.
거품 정렬, 선택 정렬, 삽입 정렬이 대표적인데, 거품의 경우 배열이 아니라 아예 random access가 불가능한 연결 리스트 같은 컨테이너에다가 적용해도 좋을 정도로 바로 옆 원소와의 비교와 교환밖에 하지 않는다. 그 때문에 성능이 대단히 나쁘다.

선택 정렬은 비교에 비해 대입 연산이 적고 자료의 상태에 그리 민감하지 않은 게 특징이다. 그에 반해 삽입 정렬은 자료 상태에 따른 성능 편차가 크고 O(n^2) 알고리즘 중에서는 성능이 나은 편이기 때문에, 작은 범위의 입력에 한해서 종종 쓰이는 경우가 있다. 실제로 비주얼 C++의 qsort 함수 구현을 보면, 퀵 정렬을 쓰다가 구간이 8개 이하의 원소로 감소하면 거기는 삽입 정렬로 때운다.

O(n^2) 알고리즘들은 원리가 간단하기 때문에 공간 복잡도는 대체로 O(1)인 in-place이다. 한 쌍의 원소를 그때 그때 교환하기 위한 고정된 크기의 메모리밖에 쓰지 않는다는 뜻 되겠다. 시간이 비효율이면 공간 오버헤드라도 없어야 하지 않겠는가.

이론적인 시간 복잡도에 부합하는 O(n log n)급 알고리즘으로는 힙, 병합, 퀵 등이 있다. 이들은 시간 복잡도만 동일할 뿐 내부적인 특징은 정말 제각각이다.

일단 힙 정렬은 위의 세 알고리즘 중에서 유일하게 메모리 복잡도가 O(1)인 검소한 녀석이다. 그 대신 한 배열 안에서 왔다 갔다 하는 작업이 많아서 그런지 속도는 미세하게 다른 알고리즘보다 더 느린 편. 한 배열 안에서 heap 자료구조를 만든 뒤, 이것으로부터 정렬된 형태의 배열을 역순으로 만드는 두 단계의 과정이 무척 기발하며, 인간의 머리로 어째 이런 걸 생각해 낼 수 있는지 놀라움을 느낀다.

병합 정렬은 동급 시간 복잡도 알고리즘 중에서는 꽤 직관적인 편이고 또 유일하게 안정성도 있어서 좋다. 그러나 FM대로 구현한 녀석은 배열 복사본이 하나 더 필요하기 때문에 메모리 복잡도가 O(n)이나 되며, 대입에 대한 비용이 큰 자료구조에 대해서는 성능 하락의 폭이 큰 게 흠이다.

※ 퀵 정렬

한편, Tony Hoare이라는 영국의 전산학자가 1960년대에 20대 중반의 나이에 고안한 퀵 정렬은 정렬 알고리즘계의 종결자, 야생마, 이단아 같은 존재이다. pivot이라 불리는 중간값을 설정하여, 주어진 구간을 “pivot보다 작은 값, pivot, pivot보다 큰 값” 조건을 만족하게 swap 연산을 통해 바꾼다. 그 뒤, pivot을 기준으로 구간을 양분하여 양 구간도 재귀적으로 똑같은 작업을 한다. 알고리즘도 너무 명쾌하고 깔끔하지 않은가?

이 알고리즘은 대충 부분적으로 정렬되었거나 아예 완전히 무작위인 데이터에 대해서 매우 대단히 좋은 성능을 자랑한다. 그러나 pivot을 어떻게 정하느냐에 따라서 알고리즘의 성능이 크게 좌지우지되며, 자료의 상태에도 매우 민감해진다는 점이 간과될 수 없는 특성이다.

pivot이 데이터의 적당한 중간값으로 설정되지 못하고 하필이면 최소값이나 최대값으로 설정된 경우, 알고리즘 수행 후에도 구간은 깔끔하게 양분되지 못하고 하나씩만 줄어들게 된다. 이 경우 알고리즘의 수행 시간은 O(n log n)이 아니라 O(n^2)에 가까워진다! 역순으로 정렬된 데이터를 정렬하는데 구간의 맨 앞이나 맨 뒤의 값을 pivot으로 쓴다고 생각해 보자.

문제는 이때 시간 복잡도만 늘어나는 게 아니라는 것이다. 분할 정복법을 쓴다는 특성상 퀵 정렬은 재귀호출을 써서 구현되는데, 구간이 반씩 시원하게 안 쪼개지고 하나씩만 쪼개지면 재귀호출의 깊이도 자칫 n회가 될 수 있다는 뜻이다. 이 경우 프로그램은 stack overflow 오류가 발생하며, 이는 프로그램의 보안에도 악영향을 끼치게 된다.

다만, 쪼개진 구간 중에 원소 수가 많은 구간이 아니라 의도적으로 적은 구간부터 골라서 재귀적으로 처리하는 경우, 메모리 복잡도는 O(log n)으로 원천적으로 줄일 수 있다. 퀵 정렬 함수의 구현체 자체에 딱히 동적 배열 같은 게 없더라도 재귀호출 때문에 메모리 복잡도가 올라가며, 원소들이 정확하게 반씩 분할될 경우에 log n에 해당하는 깊이까지 간다는 뜻이다.

일반적으로 퀵 정렬의 구현체는 그냥 구간의 정중앙에 있는 원소만 pivot으로 지정하는 게 보통이다. 이렇게만 하더라도 O(n^2)의 최악 시간 복잡도를 만드는 입력 데이터를 일부러 만들기란 대단히 어려우며, 수학적으로 발생하기도 불가능에 가까운 건 사실이다.

하지만 공격자가 퀵 정렬 구현체의 알고리즘을 알고 있는 경우, 의도적으로 해당 알고리즘이 pivot을 요청할 만한 위치에 일부러 구간의 최대값이나 최소값을 집어넣어서 매 단계별로 퀵 정렬을 엿먹이는 게 불가능하지는 않다! 세상엔 그것만 전문적으로 연구한 사람도 있다. anti quick sort라고 검색해 보셈.. 이것이 퀵 정렬의 진정 오묘하고 이상한 면모라 하겠다.

이걸 이용하여 비주얼 C++의 qsort 함수로 테스트하면, 평소 같으면 인텔 i5 기준 눈 깜짝할 사이에 끝나는 정수 10만 개의 정렬이 수 초 대로 떡실신하는 기현상이 벌어지는 걸 볼 수 있다. 그런데 xcode의 C 라이브러리가 제공하는 qsort는 퀵 정렬을 쓰지 않는지 그런 것의 영향을 받지 않더라..

※ C/C++ 언어에서의 지원

C 라이브러리에 있는 qsort 함수는 콜백 함수에 전달해 줄 사용자 데이터--가령, 비교 옵션 같은 것--를 받는 부분이 없어서 무척 불편하다. 그래서 별도의 사용자 데이터는 전역 변수나 TLS(thread local storage)를 통해 얻어 와야 하는 번거로움이 있다. 이것이 비주얼 C++ 2005부터 도입된 qsort_s에서는 개선되었다.

한편, C++ 라이브러리에도 잘 알다시피 std::sort라는 함수가 있다. C 함수보다 type-safe할뿐만 아니라 iterator를 통해 포인터보다 더 추상적인 자료형도 정렬할 수 있으며, 비교도 직관적인 비교 연산자 아니면 functor로 편리하게 지정할 수 있어서 좋다. 또한 이건 템플릿 형태이기 때문에 정렬 코드가 해당 프로그램의 번역 단위에 최적화된 형태로 embed된다는 것도 더욱 좋다.

C의 경우 비교 연산 함수의 리턴값은 뺄셈 연산을 모델로 삼아서 '음수, 0, 양수' 중 하나를 되돌리게 되어 있다. 그러나 C++ 버전은 < 연산을 모델로 삼아서 그냥 true/false boolean값만 되돌리면 된다는 차이가 있다. 사실, 그것만 있어도 정렬이 되니까 말이다.

C++ 라이브러리에는 sort뿐만이 아니라 stable_sort도 있다. 하지만 실생활에서 꼭 stable_sort를 써야만 할 상황이 있는지는 모르겠다. 실제로 정렬 성능은 굳이 안정성이 지켜지지 않아도 되는 sort가 더욱 뛰어나다.

※ 기타 정렬 알고리즘

정렬 알고리즘의 시간 복잡도는 굳이 O(n^2) 아니면 O(n log n) 중 하나로만 떨어지는 게 아니다. 그 범주에 속하지 않는 대표적인 알고리즘은 셸 정렬이다. 고안자의 이름을 따서 명명된 이 알고리즘은 삽입 정렬이 대충 정렬된 자료에 대한 성능이 뛰어나다는 점을 응용하여, 삽입 정렬을 일정 구간별로 띄엄띄엄 반복해서 적용해 준 뒤 최종적으로 삽입 정렬을 full scale로 한번 돌려서 정렬을 끝낸다.

퀵 정렬이 pivot을 정하는 것이 판타지라면, 셸 정렬은 그 구간을 정하는 방식이 판타지이다. 셸은 분명 O(n^2)보다는 훨씬 더 뛰어난 성능을 보이지만 그렇다고 O(n log n)급은 아니다. 사실, 셸은 구간을 어떻게 설정하느냐에 따라서 시간 복잡도를 계산하기가 대단히 chaotic하고 어렵다.

구간을 두 배씩 좁히는 게 제일 나쁜 방법이이기 때문에 최악의 경우 도로 O(n^2)까지 떨어져 버리나, 약간 머리를 쓰면 O(n^1.5) 정도는 된다. 구간을 가장 잘 잡았을 때 최대 O(n (log n)^2)까지는 갈 수 있다는 것이 알려져 있다. 그래도 셸은 메모리 복잡도가 깔끔한 O(1)이고, 코딩이 상당히 짧고 간결하면서도 O(n^2)보다는 성능이 확실히 낫다는 데 의의가 있다.

앞서 말했듯이 정렬 알고리즘의 시간 복잡도의 한계가 O(n log n)이라는 것은 비교 연산을 사용하는 일반적인 알고리즘이 그렇다는 소리이다. 그런 방식으로 정렬을 하지 않는 알고리즘의 경우, O(n)짜리 알고리즘도 충분히 존재할 수 있다.

가령, 데이터의 도메인이 메달이어서 '금, 은, 동'이라는 세 종류밖에 없는 경우, 자료를 일일이 뒤져 볼 필요 없이, 각 메달의 개수를 세어서 금 a개, 은 b개, 동 c개라고 써 주기만 하면 될 것이다. 부동소숫점이나 문자열처럼 도메인이 굉장히 넓은 자료형은 그런 식으로 정렬할 수 없겠지만, 좁은 범위의 정수 정도면 그런 식으로 발상을 전환하여 비교 연산을 요청하지 않는 정렬 알고리즘을 쓸 수도 있다.

여기에 속하는 대표적인 알고리즘은 기수(radix) 정렬이며, 이 외에도 유사한 전략을 사용하는 알고리즘이 더 있다.

정렬 알고리즘에 대해서는 메아리 풉에도 수학적으로 더 엄밀한 개념 기술이 있으므로 참고하시고, 또 이 홈페이지에는 이미 아시는 분도 있겠지만 본인이 학부 시절에 정렬 알고리즘 모음집이라는 간단한 프로그램을 짜서 올려 놓은 게 있다. 일부 검색엔진에서는 '사이트'로도 등록되어 있다. ㅎㅎ 관심 있으신 분은 거기 소스도 참고하시기 바란다.

* 여담이지만, 전산학 덕후와 해커들의 머리 싸움 덕질에는 끝이 없는지라, 퀵 정렬뿐만 아니라 hash 알고리즘을 엿먹이는 연구도 이미 될 대로 돼 있다.. 특정 해싱 알고리즘에 대해서 충돌만 골라서 일으키는 입력을 생성하는 것 말이다.

Posted by 사무엘

2012/10/04 08:24 2012/10/04 08:24
, , ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/740

굳이 전산학이나 컴퓨터과학· 공학의 전공자가 아니어도, 그리고 현업 프로그래머가 아니더라도 조금만 기계 다루는 것에 조예가 있는 컴덕후라면 요즘 컴퓨터의 발전 추세가 어떤지는 다들 알 것이다. 사실은 <날개셋> 한글 입력기의 개발자인 본인보다도 더 잘 알 것이다.

튜링 완전하며 메모리로부터 프로그램을 읽어 와서 구동하는(프로그램 내장 방식) 범용 컴퓨터가 발명된 지가 아직 100년은커녕 반세기 남짓밖에 지나지 않았다. 그러나 컴퓨터의 속도와 메모리 용량은 가히 넘사벽급으로 발달했다. 컴퓨터는 지난 수십 년 동안 자연이 아니라 인간의 발명한 물건의 내부에서, 시간에 따른 지수함수 스케일의 발전을 볼 수 있던 얼마 안 되는 엄청난 분야였다.

그러나 그것도 한계는 있어서 컴퓨터 성능의 대표적인 지표이던 클럭 속도가 2003년 즈음에 놀랍게도 지수함수적인 성장이 멈췄다. 10년까지는 아닌 거 같다만 8, 9년 전의 PC나 지금의 PC나 다 3GHz대이다. 오늘날과 같은 반도체 회로라는 근본적인 패러다임이 바뀌지 않는 한, 전력 소모와 발열 때문에 컴퓨터의 기본 동작 자체만의 속도가 더 빨라질 수는 없다.

우리가 다루는 컴퓨터는 생각보다 굉장히 정밀하고 빡세게 만들어져 있다. 예전부터 파이프라인, pre-fetching 등 온갖 있는 테크닉, 없는 꼼수를 다 동원해서, 어떻게든 낭비되는 자원이 없이 모든 자원이 계산에 동원되고, 1ms라도 속도를 더 올리려고 공돌이들의 온갖 공밀레 연구가 동원되었다. 이런 수직적인 성장이 한계에 달하니 요즘 컴퓨터는 코어 수를 늘려서 분업이라는 수평적인 성장을 추구하고 있다.

100이라는 일이 있어서 이게 2GHz짜리 컴으로 다 처리하는 데 10이라는 시간이 걸린다면, 1GHz짜리 컴으로는 20만큼의 시간이 걸릴 것이다. 그러나 1GHz 짜리 컴이 2개 있어서 그 일을 정확히 50씩 분담할 수 있다면, 각각의 컴퓨터의 최대 속도는 비록 1GHz밖에 안 되더라도 10에 가까운 시간 만에 일을 끝마칠 수 있어서 2GHz에 준하는 효과를 낼 수 있다. 이것이 기본적인 아이디어이다.

물론, 딱 10으로 줄이지는 못한다. 일을 분할하고 두 대의 컴퓨터를 세팅하고 굴린 후, 두 컴퓨터(코어)가 내놓은 결과를 취합하는 오버헤드가 역시 결코 작지 않기 때문이다. 그리고 세상에 상호 의존성이 없이 역할을 저렇게 딱 분담할 수 있는 작업이란 흔치 않다.

방대한 계산이 필요한 작업을 다수의 컴퓨터 기계들을 여러 대 동원해서 수행하는 건, 분산 컴퓨팅이라 하여 예전부터 그리 생소한 개념이 아니었다. 3D 그래픽으로 영화를 만드는 업계에서는 rendering farm이라 하여 수많은 컴퓨터들을 농장에다 비유할 정도로 열나게 굴려서 영상을 만들어 낸다. 빌드 속도가 느린 걸로 악명 높은 C++ 언어를 위해서 Incredibuild라는 이스라엘산 분산 빌드 시스템도 있다.

그런 것처럼 개개의 컴퓨터도 이제 코어가 여러 개 돌아가고 멀티스레딩 능력이 충분히 뛰어나니, 이제는 프로그램 차원에서 멀티코어-aware한 개발이 필요해졌다. 왜냐하면 분산 처리 시스템을 설계하는 건 정말 머리가 뽀개질 정도로 복잡하기 때문에 컴퓨터나 컴파일러가 자동화를 해 줄 수 없다. 예전 계산 결과에 의존적인 다음 계산을 병렬로 동시 수행시킬 수는 없는 노릇이니까 말이다.

그렇기 때문에 멀티코어-aware하지 않은 옛날 프로그램은 컴퓨터가 언제나 최악의 case로 가정하고 보수적으로 돌려서 코어를 하나만 할당하여 돌아가게 된다. 한 프로그램이 while(1); 만 하고 가만히 있다 해도 CPU 사용률이 100%로 치솟지 않는다.

특히 C/C++의 포인터는 그야말로 아무 메모리 주소나 가리킬 수 있고, *a, *b가 있으면 둘이 가리키는 주소가 같을지 아닐지 등 최적화나 병렬화를 할 여지가 없을 정도로 너무 generic하기 때문에 오히려 처리가 어렵다고 한다. 파스칼이나 포트란처럼 언어 표현력이 C/C++보다 부족한 대신에 컴파일러가 소스 코드만 보고서 그 복잡도를 어느 정도 파악할 수 있는 언어가 병렬화에는 오히려 더 유리하다고.

그래서 소스 코드의 특정 구간에 대해서 컴파일러나 CPU가 안심하고 병렬화를 할 수 있게 중간 중간에 부가정보를 인위적으로 넣어 줄 필요가 생겼는데, 이런 부가정보를 기술하는 표준 규격 중 하나가 그 이름도 유명한 OpenMP이다. #pragma omp 같은 컴파일러 지시자도 있고, OpenMP 라이브러리보부터 호출해서 쓰는 함수도 있다.

옛날에 C언어가 처음 발명되던 시절엔 최적화를 위해서 변수를 가능한 한 레지스터에 올려라고 지시하던 register라는 카워드가 있었는데, 앞으로 C/C++ 같은 네이티브 코드 언어는 멀티코어를 언어 차원에서 수용하는 규격이 덩달아 생겨야 할 것이다.

물론, CreateThread 같은 함수를 써서 코드의 로직 차원에서 대놓고 여러 작업 스레드를 만들었다면, 단일 프로세스가 여러 개의 코어를 자연스럽게 점유하는 게 응당 가능하다. 그러나 디자인의 성격상, 멀티스레딩은 보통 일하는 스레드, 사용자 UI 스레드, 입출력 신호를 기다리는 스레드처럼 용도별로 달리 배당하는 게 바람직하지, 동일한 일을 하는 코드 내부에서 굳이 다중 코어 활용을 위해 스레드를 분할하기에는 동기화를 비롯해 일이 너무 복잡해진다. 서버 같은 게 아니라면, 제각기 CPU를 full로 사용하는 여러 작업 스레드를 만들 일 자체가 매우 드물다.

이때 멀티코어 프로그래밍을 잘 하면 이런 단일 코드가 명시적인 스레드 생성을 하지 않고도 CPU의 자원을 골고루 전부 활용해서 고성능으로 돌아가게 된다. 다만 앞서 말했듯이 이렇게 작업을 분할하는 오버헤드부터가 큰 편이기 때문에, 소규모 작업보다는 동영상 인코딩이나 대용량 파일 압축이나 컴파일 같은 진짜 방대한 작업에서 멀티코어의 진가는 더욱 두드러질 것이다.
일례로, 비주얼 C++은 2005부터 빌드할 때 병렬 컴파일이 수행된 경우, 빌드 로그에 해당 빌드를 수행한 코어의 번호가 뜬다.

O(n^3)짜리 행렬 곱셈은 워낙 단순 무식하고 복잡한 의존도 따위도 없는 순수 노가다이다 보니, “나 병렬화 해 주세요”라고 온몸으로 외치는 문제라고 볼 수 있다. 퀵 정렬이나 병합 정렬처럼 구간이 딱딱 나뉘는 알고리즘이 병렬화에도 적합한 구조일 것 같긴 하다만, 요즘 컴퓨터의 성능이라면 겨우 internal sort 규모의 정렬 작업은 굳이 멀티코어를 쓸 필요도 없을 정도로 금방 끝날 듯.

요컨대 이제는 고성능 컴퓨터를 쓰는 것만으로 프로그램의 수행 속도가 저절로 올라가는 시대는 끝났다. 그 다음으로 레지스터 배당이라든가 캐시나 파이프라인 적중률 같은 건 CPU 설계에 맞춰 컴파일러가 더 똑똑해야 하는 영역인데, 이 역시 튜닝의 수준은 예술의 경지에 도달해 있다. 그리고 그 다음으로 필요해진 것이 코드에 등재되어 있는 병렬화 가능성 정보라고 생각하면 되겠다. 멀티코어는 프로그래머가 잘 숙지하고 있어야 하는 프로그래밍 패러다임이 되었음이 틀림없다.

Posted by 사무엘

2012/04/13 08:24 2012/04/13 08:24
Response
No Trackback , 7 Comments
RSS :
http://moogi.new21.org/tc/rss/response/668

« Previous : 1 : 2 : 3 : 4 : 5 : 6 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2026/07   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Site Stats

Total hits:
3940823
Today:
1491
Yesterday:
2118