« Previous : 1 : 2 : 3 : 4 : 5 : ... 25 : Next »

전자식 컴퓨터에서 CPU가 정보를 한 번에 처리하는 단위의 크기는 4~8비트부터 시작해서 16, 32비트를 거친 뒤 64비트의 순으로 두 배씩 증가해 왔다.
컴덕후, 전산 전공자는 누구나 다 아는 기초 교양 컴퓨터 역사 이야기이다만.. 수 년 전에 썼던 글을 첨삭· 보완해서 다시 올린다.

1. 32비트

임베디드 같은 거 말고.. 키보드와 모니터가 달려서 사람이 직접 다룰 수 있는 범용 컴퓨터 급에서, 편법이나 제약 없이 완벽한 보호 모드, 선점형 멀티태스킹/멀티스레딩 따위가 구현 가능해진 것은.. 아무래도 32비트부터이다. CPU가 4GB 정도의 넉넉한 주소 공간은 직통으로 취급할 수 있어야 가상 메모리라는 걸 구현할 수 있고, 다른 멀티태스킹 기능들도 이를 토대로 구현 가능하기 때문이다.

가상 메모리라는 건 실제로 존재하는 메모리보다 더 많은 메모리를 쓰게 해 주며, 메모리에 로드되는 응용 프로그램의 유연성과 안전을 향상시켜 주는 매우 유용한 개념이다. 하지만 이건 결과적으로 메모리 접근 과정에 계층을 하나 더 추가하는 것이고 이에 따른 엄청난 실행 오버헤드가 필요하다. 또한, '메모리를 관리하기 위한 메모리'가 추가로 더 필요하다.

결국 이걸 지원하려면 컴퓨터의 메모리가 최소한 수 MB 이상 단위로 필요하며, 포인터의 크기도 16비트로는 어림도 없다. 당시에 OS/2, Windows NT 같은 운영체제가 DOS/Windows 3.x와는 비교할 수 없이 많은 메모리가 필요하여 일반 PC 사용자가 범접할 수 없었던 건 다 이유가 있었다. 단순히 GUI 셸이나 유니코드 지원 때문만이 아니다.

뭐, GUI고 뭐고 다 때려치우고.. 옛날 '알테어'처럼 천공 카드와 패널 스위치로, 오실로스코프 화면으로 소통하는 극한의 특수 컴퓨터라면.. 비좁은 16비트 공간에서도 운영체제의 고급 개념들을 구현 자체는 할 수 있을 것이다. 하지만 그건 실용적인 가치가 없으며, 마치 16비트 word 크기에서 부동소수점을 구현하는 것만큼이나 굉장한 무리수일 것이다.

80386 CPU 자체는 1980년대 중반에 등장했다. 얘는 머신 워드의 크기가 32비트로 확장됨과 동시에 가상 메모리 주소로부터 실제 물리 메모리를 매핑하는 기능, context switching을 순식간에 수행하는 기능도 하드웨어 차원에서 직통으로 지원하기 시작했다. 286에 비하면 엄청난 발전이다. 멀티스레딩이 하드웨어적으로 지원이 돼야 당장 에디터나 워드 프로세서에서 오타를 빨간 점선으로 표시해 주는 기능부터가 랙 없이 수월하게 구현할 수 있다.

물론 16비트 시절에도 사용자가 보기에 UI와 백그라운드 작업이 동시에 수행되는 프로그램이 없지는 않았다. 하지만 그런 기능은 마치 16비트 시절에 64KB가 넘는 데이터를 다루던 것처럼 굉장히 삽질스럽고 성능 오버헤드가 큰 편법으로 구현된 것이었다. (찔끔찔끔 idle time processing을 직접 구현)
더구나 Windows용 프로그램의 경우, 메뉴 하나만 꺼내도 그 작업은 계속되지 못하고 중단돼야 했다. idle time processing이 없는 message loop에 진입했기 때문이다.

이렇듯 80386은 PC의 새로운 가능성을 열었지만.. 그때는 CPU의 가격은 말할 것도 없고 메모리(RAM)의 가격도 너무 비쌌다. 그래서 가정용 PC는 1990년대를 한참 넘기고 RAM이 수십MB 이상 급으로 증가한 뒤에야 16비트의 잔재를 벗겨내고 Windows NT의 이상이 차츰 실현되었다.

그 전에 Windows 3.x는 핵심부가 명백한 16비트 기반이며, 사실 온전한 운영체제도 못 됐다. 명령의 실행 자체라기보다는 가상 8086처럼 멀티태스킹에 필요한 몇몇 기능만 80386 CPU의 기능을 활용하는 게 있었다. 가령, 도스창을 여러 개 띄우고 Windows용 프로그램과 따로 동시에 돌아가는 것 말이다.
그렇게 386 CPU의 기능을 부분적으로나마 활용하는 386 확장 모드라는 게 있으니 Win32s 같은 익스텐더 프로그램을 추가로 설치하는 것이라도 가능했을 것이다.

2. 16비트

16비트는 짧게는 포인터 자체의 한계인 64KB 메모리 제한, 그리고 x86 환경 한정으로는 640KB 기본 메모리 제약을 벗어나려고 가난하고 지저분한 프로그래밍 테크닉이 많이 동원돼야 했던 컴퓨터 환경이다.
그래도 뭔가 외형이 오늘날의 PC와 동급으로 갖춰졌다고 볼 수 있는 최소한의 마지노 선이 16비트이긴 했던 것 같다.

그 전에 8비트 컴은.. 개인적으로 그냥 롬 베이직 아니면 카트리지 게임밖에 써 본 기억이 없다. IBM 호환 PC 같은 단일 규격이 나온 것도 없어서 기계별로 파편화가 왕창 심했으며, 본격적인 프로그래밍은 C 정도의 고급 언어도 없이 그냥 어셈블리어밖에 선택의 여지가 없었다.
CPU 자체와 직접적인 관계가 있는 요소는 아니지만.. 16비트 컴퓨터에서 실현된 대표적인 특성은 바로.. 640*400급 이상의 소위 '고해상도' 그래픽임이 틀림없다. 게임기용 CPU까지 두루 생각하더라도 말이다.

8비트 컴퓨터의 화면에서 폭이 14/16픽셀급인 영문 글꼴 내지.. 영문과 동등한 자형으로 출력되는 한글을 본 적이 없다. 8비트 오락기의 상징이던 아타리/남코 폰트도 딱 CGA스러운 8*8 크기이다.

사용자 삽입 이미지

또한 Atari ST나 Apple II 같은 명품 8비트 컴도 IBM PC와 달리 컬러에다 GUI는 지원했을지언정, 해상도는 320*200이 채 안 되는 저해상도였지 않던가?

사용자 삽입 이미지
사용자 삽입 이미지

CPU의 속도와 그 시절 컴들의 메모리 용량, 디스플레이 기술, 가성비 등등을 감안했을 때 8비트로는 고해상도 그래픽이 실현되지 않았거나 못했던 것 같다. 이는 우리나라에서 옛날에 교육용 PC 보급 사업을 하던 시절에, 8비트는 민간에 많이 퍼져 있음에도 불구하고 진작에 배제하고 16비트로 앞을 내다보는 선택을 하는 데도 영향을 줬다. 고해상도 그래픽이 지원돼야 한글· 한자를 표현하는 데 애로사항이 없기 때문이다.

옛날에 초등학교 저학년에서 고학년으로 올라가면서 교과서의 글자 크기가 확 작아졌던 것처럼.. 8비트에서 16비트로 넘어가니 모니터에 찍히는 글자의 크기부터가 확 작아지고 화면이 큼직해졌다.

사용자 삽입 이미지

Commodore 64는 1982년에 출시된 8비트 컴퓨터의 상표명이다. 램 용량이 64KB -_-;;라고 64라는 숫자가 붙은 것이다. ㅡ,.ㅡ;;

16비트 IBM PC 호환 기종은 저런 8비트 기종들과 달리, 처음부터 고해상도로 시작했다. 비록 CGA는 저해상도였지만 그건 IBM PC가 처음부터 그래픽이나 사운드에 별 관심이 없던 업무용 컨셉이어서 그랬던 것이고, 텍스트 모드는 고해상도였다. 그리고 그래픽도 EGA/VGA가 나오면서 한계가 금세 완화되면서 8비트와 격차를 벌렸다.

3. 64비트

이렇게 8에서 16, 16에서 32에 비해, 32에서 64비트로의 이동은 큰 격변 없이 그냥 4GB가 넘는 메모리를 사용 가능하게 하는 선에서 자연스럽게 진행되었다. 현대적인 컴퓨터의 뼈대와 근간은 32비트 레벨에서 사실상 다 완성됐기 때문이다.

그래도 32비트에서 64비트로 넘어가던 2000년대 중반 비슷한 시기에 멀티코어가 시작됐고, 모니터는 와이드 화면으로 차츰 넘어갔다는 점을 주목할 만하다.
그 과도기에 32비트 전용 멀티코어와 64비트 싱글코어 CPU도 잠깐 나왔었지만 얼마 못 가 묻혔다. 컴덕이 아닌 대부분의 일반인들은 두 요소가 모두 갖춰진 인텔 Core2 Duo CPU부터 써 보셨을 것이다.

PC에서 x86-64라는 아키텍처가 보급되기 전에 인텔에서 Itanium이라는 CPU를 내놨다가 대차게 망한 흑역사가 있다. 그리고 수십 년 전 Cray 같은 슈퍼컴 전용 아키텍처가 있던 시절에는 PC보다 훨씬 전부터 32/64비트 CPU가 일반적이긴 했다.
그래 봤자 그때의 슈퍼컴이 지금의 PC보다 성능이 못하지만 말이다. 그리고 이제는 스마트폰마저도 64비트 CPU를 쓰고 있다.

옛날에 386과 486에 이어, 펜티엄 CPU가 등장했을 때는 이게 64비트라고 말이 많았다. 대우 전자에서는 펜티엄 CPU가 달린 64비트 컴퓨터만 있으면 회사에서 업무도 광속으로 끝내고 칼퇴근이 가능하다고 말도 안 되는 유치찬란 과장 광고를 내보냈던 적이 있다. 486은 각진 로봇 머리이고, 펜티엄은 부드러운 진짜 인간 머리라고! ㅋㅋㅋ

하지만, 펜티엄이 64비트로 확장한 건 주메모리와 CPU 사이의 데이터 버스의 대역폭이다. 명령 집합, 레지스터 같은 내부 구조와 실질적인 동작이 64비트 단위로 돌아가는 건 당연히 아니다. 반대로 옛날에 386 SX는 원가를 낮추기 위해 CPU만 32비트이고 데이터 버스는 16비트였다.

Windows NT의 구버전은 DEC Alpha 같은 64비트 CPU를 지원했던 적이 있다. 하지만 운영체제 자체는 여전히 32비트 기준으로만 동작했기 때문에 64비트 성능을 제대로 발휘하지 못했다. 마치 동시대의 Windows 3.x가 386/486에서도 16비트 코드 기준으로 동작했던 것과 비슷하게 말이다.

그렇기 때문에 그때 시스템 설계 차원에서 LLP64냐 ILP64냐를 고민한다거나 INT_PTR 같은 추상 자료형을 만드는 일은 없었다. 그런 개념이 등장한 건 최소한 Visual C++ 6.0 이후에 Windows 2000 타이밍쯤의 일이다. 그때 Windows가 IA64용으로 포인터 크기가 8바이트인 진정한 64비트로 포팅됐기 때문이다.

4. 여담: 클럭 속도와 오버클럭질

컴퓨터의 비트 수는 자동차의 성능으로 치면 그 자체가 마력· 토크를 나타낸다기보다는.. 그냥 배기량을 나타내는 것 같다. 2행정이냐 4행정이냐, 피스톤 왕복이냐 반켈이냐 이런 것까지 따지지는 않고 그냥 연소실의 부피이다.
오늘날이야 컴퓨터들이 코어 수가 워낙 늘어서 단편적인 클럭 속도만으로 성능의 모든 것을 나타내는 건 무리가 있다. 전력이나 발열 걱정이 없는 가정용 데스크톱 PC는 3~4GHz가 일반적이고, 노트북은 2GHz 안팎이 일반적인 듯하다.

본인이 올해 초에 지른 노트북 PC는 약간 크고 무겁더라도 나름 성능에도 약간 비중을 두고 고른 물건이었는데.. 사 놓고 보니 클럭 속도가 2GHz가 안 된다는 걸 뒤늦게 알게 되어 약간 실망(?)했다. 어쩐지 각종 광고 문구에도 CPU에 대한 언급은 눈에 잘 띄는 곳에 나와 있지 않고 제원표의 작은 글자를 꼼꼼히 살펴봐야 했다.

2010년대가 다 끝나 가고 Windows 10이 돌아가는 세상에 램은 8GB이고 CPU 속도가 2GHz도 안 되는 건 문제가 있지 않나 하는 생각이 들었다. 그래도 하드 대신 SSD가 달려 있으니 다른 쪽에서 병목이 줄어서 컴의 속도가 상대적으로 빠르게 느껴졌다.

2019년 현재, AMD의 FX-9370이 기본 클럭이 4.4GHz에 달해서 세계에서 가장 빠른 x64 CPU로 일컬어진다. 4GHz의 장벽은 메모리 4GB의 장벽하고는 아무 관계 없는 한계이며, 클럭 속도는 일단은 CPU의 비트수와도 당연히 무관한 잣대이다.
x86 진영 밖에서는 IBM zEC12라는 메인프레임용 CPU가 기본 클럭이 5.5GHz여서 세계에서 가장 빠른 마이크로프로세서라고 한다. 메인프레임은 Cray도 아니고 x86도 아니고.. 몇 비트짜리에 무슨 아키텍처와 어떤 특성을 가진 컴퓨터인지 난 잘 모르겠다..;;

자동차에도 차의 한계를 시험할 정도로 과속· 과적을 일삼는 사람이 있듯이, 컴퓨터 CPU에 대해서도 액체 질소 같은 특수한 냉각 시설까지 장만해서 오버클럭질을 즐기는 매니아 괴수 괴짜들이 있다. 세계 기록은 8GHz를 넘어 있다고 하나.. 이 상태로 컴퓨터를 지속적으로 사용하는 건 불가능하다. 그 기록을 세운 뒤에 컴퓨터의 상태도 장담할 수 없으니, 그냥 기록 수립에만 의의가 있다. 그야말로 싱글 코어만으로, latency와 throughput 중에서 오로지 latency에 속하는 성능의 극한을 시험하는 셈이다.

Posted by 사무엘

2019/08/01 08:35 2019/08/01 08:35
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1646

Trackback URL : http://moogi.new21.org/tc/trackback/1646

Leave a comment

1. 완전히 새로운 알고리즘 분야

컴퓨터는 정말 대단한 기계이다.
정보의 최소 단위인 0과 1을 분간하고, 임의의 주소가 가리키는 메모리의 값을 읽거나 쓸 수 있고 프로그램의 실행 지점도 메모리의 값을 따라서 변경 가능하게 했더니(튜링 완전) 그야말로 무궁무진한 양의 정보를 가히 무궁무진한 방식으로 처리가 가능해졌다.

이런 이론적인 근간이 마련된 뒤에 반도체의 집적도가 더 올라가고 메모리와 속도가 더 올라가고 가격이 더 내려가는 건 그냥 시간 문제일 뿐이었다.
그런데.. 단순히 복잡한 계산이나 방대한 검색을 빠르게 해내는 것만으로 컴퓨터가 인간의 고유 영역을 완전히 침범하고 대체했다고 보기는 곤란하다. 그건 그냥 자동차가 인간보다 더 빠르고 중장비가 인간보다 더 힘센 것만큼이나, 기계가 인간의 역할을 일부 보조하고 확장하는 것일 뿐이다.

물론 단순히 동력과 관련된 분야는 말이나 소 같은 동물도 인간보다 약간이나마 더 우위에 있긴 했다. 그에 비해 정보 처리 분야는 자연계에 지금까지 인간의 라이벌 자체가 아예 존재한 적이 없었다는 차이가 있다.
그러나 인간도 속도가 느리고 개인의 능력이 부족할 뿐이지.. 많은 인원을 동원하고 많은 시간만 주어지면 기계적인 정보 처리 정도는 '유한한 시간' 안에 언젠가 다 할 수 있다. 일을 좀 빠르고 정확하게 수행하는 것만 갖고 '창의적이다', '인간을 닮았다'라고 평가해 주지는 않는다.

정렬과 검색에, 다이나믹이니 분할 정복이니 하는 최적해 구하기처럼 고전적인 분야에서 고전적인(?) 방법론을 동원하는 알고리즘은 이미 수십 년 전에 다 연구되어서 깔끔한 결과물이 나왔다. 그런 건 이미 대학교 학부 수준의 전산학에서 다 다뤄지는 지경이 됐으며, 정보 올림피아드라도 준비하는 친구라면 아예 중등교육 수준에서 접하게 됐다.

그런데 현실에서는 그렇게 깔끔하게 떨어지지 않는 더 복잡하고 난해한 문제를 풀어야 한다. 깔끔하게만 접근했다가는 시간 복잡도가 NP-hard급이 되어 도저히 감당할 수 없는 문제를 적당히 타협하여 풀어야 한다.
중· 고등학교의 고전역학 문제에서는 "공기의 저항은 무시한다" 단서가 붙지만, 대학교에 가서는 그런 것까지 다 고려해야 하는 것처럼 말이다.

대수적으로 답을 구할 수 없는 문제에 대해 근사치를 효율적으로 구하기 위해 수치해석이라는 기법이 등장했듯, 전산학에도 각종 휴리스틱과 근사 알고리즘이라는 게 존재한다. 압축 알고리즘으로 치면 무손실이 아닌 손실 분야인 셈이다. 구체적인 건 학부 수준을 넘어 대학원에 소속된 별도의 연구실에서 다룰 정도로 난해하다.

그런데.. 이런 것들은 여전히 사람이 범접하지 못하는 분량의 계산 문제를 최대한 빠르게 효율적으로 푸는 방법에 대한 연구이다. 그런 것 말고 사람은 간단히 하는데 컴퓨터에게는 굉장히 난해해 보이는 일이 있다.
컴퓨터로 하여금 텍스트나 음성 형태의 인간의 자연어를 알아듣고 타 언어로 번역한다거나, 그림으로부터 글자 같은 정보를 알아보게 할 수 없을까?
컴퓨터를 바둑· 장기· 오목 같은 게임의 고수로 키울 수 없을까?

이건 단순히 TSP(순회하는 세일즈맨 문제)를 더 그럴싸한 가성비로 다항식 시간 만에 푸는 것과는 분야가 다르다.
저런 걸 기계가 인간과 비슷한 속도와 정확도로만 해내더라도 굉장한 이득이다. 기계를 부리는 비용은 인간을 고용하는 인건비보다 넘사벽급으로 저렴한 데다, 기계는 인간 같은 감정 개입이 없고 지치지 않고 실수도 전혀 하지 않기 때문이다.

하물며 속도와 정확도가 인간 전문가의 능력을 능가하게 된다면 게임 끝이다. 기계적인 단순 노동력이나 판단력만을 요구하는 일자리는 사라지며, 인간은 기계가 대신할 수 없는 더 창의적이고 전문적인 일자리로 갈아타야 할 것이다.

2. 흑역사

소위 '인공지능'에 대한 연구는 진공관이니 트랜지스터니 하던 무려 1950년대 컴퓨터의 초창기 때부터 천조국의 날고 기는 수학자, 컴퓨터 공학자들에 의해 진행돼 왔다. 특히 세부 분야 중 하나로서 기계번역도 연구됐으며, 1954년에는 조지타운 대학교와 IBM이 공동 연구 개발한 기계번역 솔루션이 실제로 출시되기도 했다.

인류 역사상 최초로 기계번역이란 게 연구된 언어는 러시아어 → 영어이며, 이는 전적으로 냉전 덕분이다. 하긴, 2차 세계 대전 때는 번역이 아니라 암호를 해독하는 기계가 개발되긴 했었다. 적성국가들의 언어 중 일본어는 영어와의 기계번역을 연구하기에는 구조가 너무 이질적이고 어려웠을 것이다.

그래도 인간 번역가가 아닌 컴퓨터가 러시아어 텍스트로부터 영어 텍스트를 허접하게나마 뱉어 내자 학계와 업계는 흥분했다. 이런 식으로 조금만 더 연구하면 컴퓨터가 금방이라도 세계의 언어 장벽을 다 허물어 줄 것 같았다.

그때는 학자들이 자연어에 대해서 뭔가 순진 naive하게 원리 원칙대로 규칙 기반으로, 낭만적으로 접근하던 시절이었다. 인간의 언어도 무슨 프로그래밍 언어처럼 유한한 문법과 생성 규칙만으로 몽땅 다 100% 기술 가능하고 parse tree를 만들고 구문 분석이 가능할 거라고 생각했다. 물론 그 규칙이 간단하지는 않겠지만, 촘스키 같은 천재 언어학자 몇 명을 외계인과 함께 골방에다 갈아 넣고 며칠 열나게 고문하면 다 찾아낼 수 있을 것이고.. 그러면 언어의 기계 분석은 게임 끝이지 않겠냐 말이다.

궁극적으로는 전세계 모든 언어들의 교집합과 합집합 요소를 망라하는 중간(intermediate) 언어도 만들 수 있을 것이라고 생각했다. 세계 각국의 언어들을 그 중간 언어와 번역하는 시스템만 만들면 전세계 사통발달 언어 번역 시스템을 만들 수 있지 않겠는가? 이 정도 생각은 나조차도 한 적이 있다.

그랬으나.. 뚜껑을 열어 보니 영광은 거기서 끝이었다.
기계번역은 빵점 백지 상태에서 4, 50점짜리 답안을 내놓는 것까지는 금방 할 수 있었지만, 거기서 성적을 7, 80점짜리로라도 올려서 실용화 가능한 상품은 오랫동안 연구비를 아무리 투입해 줘도 선뜻 나오지 않았다.
인간의 언어라는 게 절대로 그렇게 호락호락 만만하지 않고 매우 불규칙하고 복잡한 구조라는 게 연구하면 연구할수록 드러났기 때문이다. 지금까지 사용한 연구 방법론 자체가 약발이 다하고 한계에 다다랐다.

이 때문에 1970년대에는 돈줄을 쥔 높으신 분들이 "인공지능"이란 건 밥값 못 하는 먹튀 사기 허상(hoax)일 뿐이라고 매우 비관적이고 보수적으로 생각하게 됐다. 컴퓨터는 그냥 계산기일 뿐, 그 돈으로 그냥 인간 번역가나 더 양성하고 말지..
이 단어 자체가 학계의 흑역사 급으로 금기시되어 버렸다. 인공지능이란 게 키워드로 들어간 논문은 저널에서 믿고 걸러냈으며, 관련 연구들의 연구비 지원도 모조리 끊길 정도였다.

이 현상을 학계에서는 제1차 AI 겨울(혹한기, 암흑기, 쇼크, 흑역사 등등...)이라고 부른다. 과거의 무슨 오일 쇼크 내지 게임 업계 아타리 쇼크처럼 말이다.
그렇게 고비를 겪었다가 더 발달된 연구 방법론으로 활로가 발견되고, 그러다가 또 2차 겨울을 극복한 뒤에야 요 근래는 인공지능의 중흥기가 찾아왔다고 여겨진다.

3. 문제는 데이터

지금은 기계번역이건, 게임 AI이건, 패턴인식이건 무엇이건.. 인공지능의 주재료는 규칙이 아니라 데이터이다.
기계번역 시스템을 개발하는데 언어학 문법 지식이 동원되지 않으며, 보드 게임 AI를 만드는데 통상적인 게임 규칙 기반의 알고리즘이 동원되지 않는다. 이상한 노릇이 아닐 수 없다.

그러는 게 아니라 요즘 인공지능이라는 것은 아이디어는 매우 간단하다. 기출문제와 정답을 무수히 많이, 인간이 상상할 수도 없을 정도로 많이 주입시켜 준 뒤, 이로부터 컴퓨터가 규칙성을 찾아내고 새로운 문제가 주어졌을 때 정답을 추론하게 하는 방법을 쓴다. 해결하고자 하는 문제를 기술하고 정답의 조건을 명시하고 알고리즘을 구현하는 걸 인간이 일일이 하는 게 아니라.. 그 자체를 컴퓨터가 알아서 하게 하는 것이다.

이것이 '기계학습', 그 이름도 유명한 machine learning이다. 이것이 이전에 인공지능을 구현하는 방법론이던 '전문가 시스템'을 대체했다. 이런 무지막지한 방법론이 적용 가능할 정도로 요즘 컴퓨터의 속도와 메모리가 매우 크게 향상된 덕분이다.

인간의 입장에서 기계학습을 시키는 방식은 지도(supervised learning) 또는 비지도 학습으로 나뉜다.
그리고 기계의 입장에서 학습(?)을 실제로 수행하는 방법으로는 인공 신경망, 앙상블, 확률(은닉 마르코프 모델), 경사/기울기 하강법 같은 여러 테크닉이 있는데, 기울기 하강법은 복잡한 선형 방정식을 푸는 심플렉스와 비슷하다는 느낌도 든다.

인공 신경망은 생물의 신경망이 동작하는 원리에서 착안하여 만들어진 기계학습 모델이라고는 하지만 당연히 실제 인간 뇌의 작동 방식에 비할 바는 못 된다.
MLP니 CNN이니 RNN이니 하는 신경망 용어들이 존재하며, 그리고 이 인공 신경망을 어떻게 하면 잘 갖고 놀 수 있을까 고민하는 연구 분야를 '딥 러닝'(심층학습)이라고 한다. 마치 네트워크 계층의 다양한 기술 용어만큼이나 AI에도 계층별로 다양한 기술 용어가 존재한다.

게임 AI라면 단순히 뭔가를 인식하고 분류만 하면 장땡인 게 아니라 뭔가 극한의 최적해를 찾아가야 할 텐데.. 이런 걸 학습시키는 건 딥 러닝의 영역이다. 알파고처럼 말이다. 그런데 알파고 하나가 지구상의 최고의 인간 바둑 기사를 이긴 것은 물론이고, 다른 재래식 알고리즘으로 십수 년간 개발되어 온 기존 바둑 AI들까지도 다 쳐발랐다니 무서운 일이 아닐 수 없다. 뭐, 개인용 PC 한 대만으로 그렇게 동작하는 건 아니긴 하지만 말이다.

오늘날 연구되고 있는 인공지능은 무작정 인간과 동급으로 생각하고 창조하는 기계는 당연히 아니고, 컴퓨터의 막강한 메모리와 계산 능력으로 지금까지 주어진 데이터를 토대로 새로운 상황에 대처하는 답안을 꽤 그럴싸하게 제시하는 '약한 인공지능'일 뿐이다.

쉽게 말해 "서당 개 삼 년이면 풍월 읊는다"처럼 말이다.
추리소설를 한 1000편쯤 읽고 나니 다른 새로운 추리 퀴즈에도 설정이 뻔히 보이고 답이 보인다.
드라마를 1000편쯤 보고 나니 비슷비슷한 드라마들은 스토리 전개가 어찌 될지 '안 봐도 비디오'처럼 된다. 그런 것 말이다.

그런데 저게 말처럼 쉬운 일인 건 아니다.
학습 대상인 무수한 텍스트· 이미지· 음성 데이터 내지 각종 게임 복기 데이터를 어떤 형태로 수치화해서 벡터 형태로 표현할 것인가?
그리고 '학습'이라는 걸 하는 동안 해당 AI 엔진의 내부에서는 구체적으로 무슨 계산 작업이 행해지는가?
컴파일러만 해도 결과물로 OBJ 파일이라는 게 생기는데, 그 내부적인 학습 상태는 어떤 형태로 표현되며, 이것만 따로 저장하고 불러오는 방법이 존재하는가? 본인은 AI알못이다 보니 전혀 모르겠다. ㅡ,.ㅡ;;

수천, 수만, 아니 그 이상 셀 수 없이 많은 숫자들로 이뤄진 벡터 I가 또 수없이 많이 있다고 치자. 이 숫자들은 현실 세계를 표현하는 미세한 자질을 표현한다.
그런데 어떤 블랙박스 함수 f가 있어서 f(I_1..n)에 대한 결과가 O_1..m라는 벡터 집합으로 나왔다고 한다.

컴퓨터는 이 I와 O 사이에서 규칙성을 찾아서 I에 대해 O와 최대한 비슷한 결과를 산출하는 함수 f를 구한다. 그러면 이제 임의의 다른 아무 input에 대해서도 수긍할 만한 출력 결과를 얻을 수 있을 것이다.
패턴 인식? 기계번역? 유사 작곡이나 창작? 현실에서 해결하려는 문제가 무엇이건 machine learning이 하는 일은 본질적으로 저걸로 요약된다. 내가 AI 쪽으로 아는 건 이게 전부이다.

지금은 TensorFlow 같은 범용적인 기계학습 엔진/라이브러리까지도 구글 같은 괴물 기업에 의해 오픈소스로 몽땅 풀려 있으며, 이걸 파이썬 같은 간편한 스크립트 언어로 곧장 돌려볼 수도 있는 세상이 됐다.
그런 라이브러리를 직접 개발하고 유지보수 하는 것까지는 바라지도 않는다. 방대한 현실 데이터를 수집해서 저기에다 적절하게 집어넣고, 이로부터 고객이 원하는 유의미한 추세나 분석 결과를 얻는 것만 해도 뭔가 프로그래밍 코딩과는 별개로 아무나 할 수 없는 전문 영역의 일이 돼 있다.

오늘날 AI 엔진의 연구를 위해서는 근간 이론이라 할 수 있는 선형대수학, 편미분, 확률 통계는 무조건 먹고 들어가야 된다. 엔진 코드를 직접 다루지 않고 쓰기만 하는 사람이라도 엔진이 어떤 원리로 돌아가는지 정도는 알아야 가장 적절한 방법론/알고리즘을 선택할 수 있을 테니 저런 것들을 맛보기 수준으로라도 알아야 할 것이다.

과거에 정보 사냥 대회가 있었던 것처럼 이제는 주어진 데이터로부터 새로운 문제를 잘 푸는 기계학습 모델을 설계하는 것이 경진대회의 아이템 내지 학교와 직장의 과제가 될 것으로 보인다. 컴퓨터가 할 수 있는 일이 더 늘어난다면 사람만이 할 수 있는 일은 그보다 더 수준 높고 추상적인 쪽으로 이동할 수밖에 없으니 말이다.

아무리 그래도 그렇지 자연어의 문법과 어휘 자체를 전혀 모르는 상태에서 데이터 학습만으로 댓글이 선플인지 악플인지를 기계가 분간할 수 있는 걸까? 그래도 클레멘타인 영화에 늘어선 댓글이 선플인지 악플인지 판단하려면 그에 대한 특별한 학습-_-;;이 필요할 것 같다.

그리고 변화무쌍 복잡한 필기체의 인식이 아니라, 그냥 자동차 번호판의 정향화된 숫자 내지 겨우 QR 코드의 인식 정도는.. '영상 처리 기술'의 영역이지, 저 정도의 거창하게 기계학습이니 뭐니 하는 인공지능의 영역은 아니다. 그건 구분해서 생각할 필요가 있다.

오래 전부터도 각종 전산학 알고리즘 용어를 검색할 때 종종 걸려 나오긴 했는데.. 국내 개인 사이트 중에 AIStudy라는 곳이 있다. 나모 웹에디터가 있던 시절부터 존재했던 정말 옛날 사이트이다. 그런데 운영자가 내 생각보다 굉장히 어린 친구이다. 정말 대단할 따름이다.
당연히 과학고-카이스트 라인이려나 생각했는데 그렇지는 않고 일반고-서울대 테크를 타 있다. 앞날에 건승을 빌어 본다.

Posted by 사무엘

2019/07/06 08:33 2019/07/06 08:33
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1637

Trackback URL : http://moogi.new21.org/tc/trackback/1637

Leave a comment

Windows용 GUI 프로그램은 가시적인 GUI 구성요소인 윈도우/창이라는 걸 생성해서 화면에 띄울 수 있다.
윈도우는 메뉴, 제목 표시줄, 테두리 같은 '껍데기' non-client 영역과, 그 내부에 프로그램이 재량껏 내용을 표시하는 '알맹이' client 영역으로 나뉜다. 윈도우에다 스타일을 무엇을 주느냐에 따라 non-client 영역의 구성요소와 차지 면적이 달라진다. 똑같이 테두리를 주더라도 크기 조절이 가능한 창은 그렇지 않은 창보다 테두리가 더 두껍게 그려지곤 한다.

GetWindowRect 함수는 어떤 창이 화면에서 문자 그대로 차지하는 스크린 좌표(non-client와 client 모두 포함)를 되돌리며, GetClientRect는 창 내부의 클라이언트 영역의 크기를 되돌린다.
후자의 경우 RECT를 되돌리긴 하지만 left와 top 멤버의 값은 언제나 0으로 설정된다. 그러니 얘는 진짜 RECT라기보다는 SIZE의 성격에 더 가깝다. 하지만 창의 client 영역을 원점(0,0) 기준으로 DC에다 그림을 그릴 일이 많으며 GDI 함수 중에도 RECT를 받는 놈이 있다. 그렇기 때문에 저런 형태의 RECT를 받는 건 여러 모로 유용하다.

어떤 창의 클라이언트 영역이 자기 창의 non-client 영역을 기준으로 얼마나 떨어져 있는지, 그리고 원점 또는 전체 스크린 좌표 기준으로 어디에서 시작하는지를 얻기란 쉽지 않다.
또한, 어떤 child 윈도우가 부모 창의 클라이언트 영역 좌표를 기준으로 어디쯤에 있는지를 얻는 것도 바로 가능하지 않다(예: 대화상자의 어떤 컨트롤이 대화상자를 기준으로 어느 위치에 있는가?). 기준이 되는 창들을 모두 스크린 좌표로 얻어 온 뒤에 수동으로 좌표 오프셋 보정을 해야 답을 구할 수 있다.

뭐 이런 식이다.
크기와 스타일이 동일한 창에 대해서 전체 영역 대비 클라이언트 영역이 위치와 크기가 어떻게 결정될 것인지는 얼추 고정적으로 예상 가능하다.
그런데 이렇게 직관적이고 명백해 보이는 영역에도 뭔가 골치 아프고 지저분한 가상화, 보정, 샌드박스라는 게 존재한다는 게 믿어지시는가?

이 문제는 지금으로부터 10수 년 전, Windows Vista가 등장하면서 시작됐다.
과거의 XP가 단조로운 고전 테마를 탈피하여 non-client 영역과 각종 표준 컨트롤에 임의의 테마 비트맵을 도입했는데, Vista/7은 더 나아가서 그래픽 가속 기능의 도움으로 프로그램 창 주변에 짙은 그림자를 그리고, 테두리에는 뭔가 청명한 느낌이 나는 반투명(?) 유리 재질을 구현했다.

그래서 법적으로(?) 똑같은 크기의 창이라도 창이 화면에다 실제로 1픽셀이라도 그림을 그리는 영역이 더 커졌으며, 테두리의 두께도 더 두꺼워졌다.
특히, 크기 조절이 되지 않는 창이라도 "크기 조절이 되는 창과 동급으로" 테두리가 두꺼워졌다.
이로 인해 window rect라는 개념을 해석하는 방식에도 변화가 불가피해졌다.

그럼 골치 아픈 설명 대신 예를 들도록 하겠다.
대화상자를 생성하여 (1, 1) 위치에다 300, 200픽셀 크기로 배치(SetWindowPos 호출)하는 프로그램을 돌려 보았다.
이걸 Visual C++ 2010 이하, 또는 2012 이상이라도 Windows XP 호환 툴체인으로 빌드하면 실행 결과가 다음과 같이 나온다.

사용자 삽입 이미지

그러나 동일한 코드를 Visual C++ 2012 이상의 정규 툴체인으로 빌드하면 실행 결과가 다음과 같다. 둘의 차이가 뭘까?

사용자 삽입 이미지

옛날 방식으로 빌드하면.. window rect를 계산할 때 Windows Vista/7의 Aero가 추가적으로 그려 주는 껍데기 공간이 영역에 포함되지 않는다. 그래서 window rect가 음수가 아니라 분명 (1, 1)임에도 불구하고 창의 좌측 상단이 좀 짤린다.
이 창이 실제로 차지하는 영역을 얻으려면 DwmGetWindowAttribute(m_hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rc, sizeof(rc))를 호출해 줘야 한다. 보다시피 실제 영역은 (-4, -4)에서부터 시작하면서 5픽셀이나 더 크다는 걸 알 수 있다.

그러나 최신 방식으로 빌드하면 창의 모든 영역이 window rect에 포함된다. 이 때문에 창의 크기가 더 작아지며, 클라이언트 영역의 크기도 10픽셀씩이나 차이가 나게 된다. 그 대신 얘는 GetWindowRect와 DwmGetWindowAttribute의 결과가 서로 일치한다.
두 프로그램의 실행 결과를 한 화면에 포개 놓으면 다음과 같다.

사용자 삽입 이미지

이런 차이를 만들어 내는 것은 바로.. 실행 파일의 PE 헤더에 기록되어 있는 운영체제/서브시스템의 버전이다. 링커에서 /SUBSYSTEM:WINDOWS,6.0 옵션으로 지정 가능하다.
Visual C++ 2010의 컴파일러는 5.1 (XP)이 최저값이기 때문에 필요한 경우 6.0 (Vista)으로 올려서 지정할 수 있다. 그러나 2012 이상 후대의 컴파일러는 선택의 여지 없이 6.0이 최소값이며 더 낮은 값으로 지정이 되지 않는다. 툴체인 자체를 더 낮은 것으로 골라야 한다.

의외로 매니페스트 XML이 아니라는 게 신기하다. 공용 컨트롤 6.0 사용 여부, 고해상도 DPI 인식 여부, 요구 보안 등급 같은 건 매니페스트 속성이니까 말이다.
당연한 얘기이지만 공용 컨트롤 6.0과 최소 운영체제 6.0은 서로 다른 개념이라는 걸 유의하도록 하자. 공용 컨트롤의 버전은 차라리 과거 IE의 버전(6)과 비슷한 구도이다. 일개 웹브라우저가 운영체제의 셸까지 마개조하던 시절의 잔재이다(DLL hell도 존재했었고..).

그래도 현재의 Visual C++ 2019도 최소 운영체제 버전이 6.0에서 값이 더 오르지는 않고 있다. Vista를 진짜 하한선 마지노 선으로 잡고 있는 듯하다.
그럼 다음으로 Aero를 끄고 Basic 테마에서 두 프로그램을 실행한 결과는 다음과 같다.

사용자 삽입 이미지사용자 삽입 이미지

desktop window manager가 동작하지 않으니 DwmGetWindowAttribute 함수는 실행이 실패했다.
두 경우 모두 프로그램의 외형은 짤리는 것 없이 동일하고, 테두리의 두께만이 차이가 날 뿐이다. 최신 방식으로 빌드된 프로그램은 더 두껍고, 그렇지 않은 레거시 프로그램은 얇다.

단, Aero의 동작 여부와 무관하게 최신 방식과 레거시 방식이 클라이언트 영역 크기는 동일하다는 걸 알 수 있다. 전자는 테두리가 두꺼우니 클라이언트 영역이 (284, 162)로 더 작고, 후자는 (294, 172)로 더 크다.

사용자 삽입 이미지사용자 삽입 이미지

고전 테마는 Windows 8~10에서는 더 찾아볼 수도 없는 모드가 됐는데.. 얘는 빌드 방식에 따른 차이가 전혀 존재하지 않는다. DwmGetWindowAttribute도 당연히 동작하지 않고 말이다.
단지, 얘는 크기 조절 가능한 창에 대해서는 테두리의 두께가 1픽셀.. 눈꼽만치 더 늘어난다. 그래서 창의 크기가 같을 때 클라이언트 영역의 크기가 (294, 175)에서 (292, 173)으로 2픽셀씩 더 줄어드는 걸 알 수 있다.

사실은, 대화상자를 크기 조절 가능하게 만들면 6.0이 아닌 옛날 방식으로 빌드된 프로그램이라도 앞의 Aero 내지 Basic 테마에서 창이 최신 6.0 버전이 지정된 프로그램과 동일하게 동작한다.
리소스 편집기에서 대화상자의 Border 속성을 Dialog Frame 말고 Resizing으로 지정했을 때 말이다.

그럼 마지막으로 Windows의 종결자 버전인 10에서는 창이 어떻게 뜨는지 살펴보자.
Windows 10의 프로그램 창은 외관상으로 Vista/7 같은 두툼한 테두리가 없고 슬림하다. 그럼에도 불구하고 10에서도 6.0이 지정된 프로그램은 클라이언트 영역의 크기가 레거시 방식 프로그램보다 더 작게 설정된다. 다시 말하지만 GetWindowRect의 결과는 둘 다 동일한데... 이렇게 동작해야 할 이유가 없어 보이는데도 말이다.

사용자 삽입 이미지사용자 삽입 이미지

더욱 괴이한 것은 DwmGetWindowAttribute의 실행 결과이다.
나름 DWM은 동작하고 있는지 이 함수도 값을 되돌리기는 하는데... 보다시피 리턴된 사각형이 GetWindowRect의 리턴값보다도 더 작다~! Vista/7의 Aero와는 결과가 딴판이다.
최신 6.0 방식으로 빌드된 프로그램은 (1, 1) 위치에서 (300, 200) 픽셀 크기를 찍더라도 실제로 생기는 창의 크기는 (8, 1)에서 (286, 193) 픽셀 크기 남짓으로 예상보다 꽤 작게 된다.

지금까지 나열한 실험 결과를 표로 한데 요약하면 다음과 같다. 96dpi (100%) 확대 배율에서 굴림 아니면 맑은 고딕의 완전 기본 metric 기준이다.
GetWindowRect의 값이 다 똑같이 [1,1, 301,201]인 윈도우이더라도 실제 외형과 클라이언트 영역 크기는 운영체제와 빌드 방식에 따라 저렇게 차이가 날 수 있다는 것이다.

  레거시 (2010/XP) 신형 (2012+/Vista+)
DWM Attribute client 크기 DWM Attribute client 크기
Vista/7 Aero [-4,-4, 306,206] [294, 172] [1,1, 301,201] [284, 162]
Vista/7 Basic N/A [294, 172] N/A [284, 162]
Classic N/A [294, 175] N/A [294, 175]*
Windows 10 [3,1, 299,199] [294, 171] [8,1, 294,194] [284, 161]

* resizable하지 않은 대화상자의 경우, 고전 테마에서는 레거시 및 신형 방식 모두 대화상자 외형 차이가 없다. 단지, resizable한 대화상자는 프레임 두께가 1픽셀 더 두꺼워져서 클라이언트 영역이 2픽셀씩 줄어든다.
그리고 고전 이외의 다른 테마에서 resizable한 대화상자는 레거시 방식도 신형 방식과 동일한 결과가 나온다.

그러니, 똑같은 소스 코드이더라도 최신 Visual C++ 컴파일러로 다시 빌드를 하면 예전보다 창 크기가 더 작게 나와서 UI 구성 요소의 오른쪽/아래쪽이 짤리는 부작용이 발생할 수 있다. 그런 상황이 발생하면 놀라지 말고 SetWindowPos 같은 함수에 숫자를 무식하게 하드코딩 하지 않았는지를 침착하게 점검해 보기 바란다.

옛날에는 SetWindowPos, GetClientRect 같은 함수의 msdn 설명을 보면 Windows Vista에서 보정으로 인한 동작 차이가 발생할 수 있다는 말이 '비고'란에 분명 있었다. DWM 쪽의 API를 참고하라는 말도 봤었는데 2010년대 이후부터는 삭제된 듯하다. 지금은 그런 말을 찾을 수 없다.

하지만 그런 말이 있건 없건, 창의 크기를 지정할 때는.. non-client 요소가 일체 없고 픽셀값을 반드시 곧이곧대로 지정해야만 하는 불가피한 상황이 아니라면 어지간해서는 SetWindowPos에다가 하드코딩된 값을 무식하게 전하는 일이 없어야 한다.
이 글에서 소개한 non-client 영역 크기 보정도 있고, 화면 확대 배율(DPI)도 있고.. 하드코딩 픽셀값을 쓸 수 없게 만드는 요인은 여럿 존재한다. 잘 만들어진 프로그램이라면 창 크기를 계산할 때 그런 변수들을 잘 감안해야 한다.

가령, 대화상자 리소스 템플릿에서 사용되는 단위인 DLU를 픽셀수로 보정하려면 MapDialogRect을 사용하면 된다. 이것만으로도 화면 확대 배율은 커버 가능하다.
그리고 특정 클라이언트 영역의 픽셀수와 정확하게 대응하는 window rect 크기를 구하려면 AdjustWindowRect(Ex)를 사용하면 된다.

Posted by 사무엘

2019/05/27 08:34 2019/05/27 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1624

Trackback URL : http://moogi.new21.org/tc/trackback/1624

Leave a comment

Windows 운영체제에서 쓰이는 EXE나 DLL 같은 실행 파일들은 잘 알다시피 Portable executable이라는 포맷을 따라 만들어져 있다. 그래도 맨 앞에는 MZ로 시작하는 MS-DOS EXE 헤더가 호환성 차원에서 여전히 있으며, 거기서 가리키는 오프셋을 따라가 보면 그제서야 PE 헤더가 시작된다.

그런데 아무 EXE/DLL이나 찍어서 바이너리 에디터로 들여다 보시라. 도스 EXE stub과 실제 Windows용 실행 파일의 사이 공간에는 뭔가 4바이트 간격으로 규칙성이 느껴지는 정체불명의 데이터가 수십~수백 바이트가량 있으며, 끝에는 약속이나 한 듯 "Rich"라는 문자열이 있다. 이건 도스/Windows 어느 플랫폼에서도 쓰이지 않는 잉여 데이터이다.
이놈의 정체는 도대체 무엇일까...??

사용자 삽입 이미지

더욱 괴이한 것은.. 이건 Visual Basic, Visual C++ 같은 마소의 개발툴(링커)로 생성한 바이너리에만 존재한다는 것이다.
게다가 마소 개발툴조차도 처음부터 이랬던 게 아니다. Visual C++의 경우, 5.0의 마지막 서비스 팩(아마 3)부터 적용된 거라고 한다. 그러니 지금으로부터 20여 년 전, 대략 1997~98년 사이부터이고.. 그냥 편의상 6.0부터라고 생각해도 무방하다.

본인의 고딩 시절, Visual C++ 4.2로 빌드된 날개셋 한글 입력기 1.0 내지 PentaCombat 오목 게임의 실행 파일을 들여다보면.. 아니나다를까 일명 "Rich 헤더"가 존재하지 않는다. MZ와 PE 사이에 아무 공백이 없다.

사용자 삽입 이미지

그러나 VC++ 6으로 빌드된 날개셋 1.2부터는 응당 Rich 헤더가 추가되어 있다.

과거 Windows 9x의 경우, 내부의 프로그램들을 빌드할 때 Visual C++이 아닌 다른 자체 컴파일러를 사용했다. kernel32, gdi32, user32 같은 DLL이라든가 메모장 같은 간단한 프로그램 말이다.
여기서 유래된 바이너리들은 Rich 헤더가 존재하지 않으며, 이 관행은 Visual C++ 6이 출시된 뒤에도 Windows 98/ME까지 변함없이 이어졌다.

하지만 내장 프로그램 중에서 워드패드와 그림판, EUDC 편집기처럼 Program Files\Accessories에 들어있던 프로그램은 MFC도 사용하고 나름 Visual C++ 냄새가 났었다.
얘들은 Windows 95 시절엔 대외적으로 공개된 적이 없는 MFC 짝퉁을 사용하다가, Windows 98부터 깔끔하게 Visual C++ 5 sp3으로 빌드되기 시작했다. 그래서 얘들은 예외적으로 Rich 헤더가 포함되기 시작했다.

이런 9x와 달리 Windows NT는 운영체제 차원에서 처음부터 Visual C++ 팀과 잘 연계하는 편이었다.
9x 계열은 98에 와서야 msvcrt와 mfc42같은 Visual C++ 출신의 배포용 DLL들이 최초로 정식 포함돼 들어간 반면, NT 계열은 처음부터 메모장도 진작부터 msvcrt를 사용해 왔다. NT4는 직접 확인해 보지 않아서 모르겠지만, 2000은 모든 EXE/DLL의 내부에 Rich 헤더가 존재한다.

한편, Visual C++ 말고 델파이 같은 타 개발툴로 빌드된 실행 파일에는 Rich 헤더 같은 건 당연히 존재하지 않는다.

사용자 삽입 이미지

그럼 본론으로 들어가도록 하겠다.
마소에서는 이 헤더? 데이터? chunk?를 왜 집어넣기 시작했으며, 이 정보의 의미는 도대체 무엇일까? 엄밀히 말하면 헤더라고도 볼 수 없는 단순 데이터일 뿐인데 말이다.
놀랍게도 마소에서는 이에 대해서 지금까지 공식적인 답변을 한 번도 제공하지 않았으며 undocumented, 묵묵부답으로 일관한 듯하다. 빌드 시에 이 헤더를 제외시키는 옵션도 없다.

그래서 일각에서는 흉흉한 음모론까지 나돌기 시작했다. 제일 유명한 게 뭐냐 하면, 이건 이 바이너리를 빌드한 컴퓨터 환경을 식별하는 정보라는 것이다.
그래서 이 exe/dll이 악성 코드로 밝혀져서 경찰에 수사를 의뢰하게 되면.. 이 정보로부터 개발자의 컴퓨터를 추적할 수 있고, 따라서 악성 코드를 만든 사람도 아무 단서가 없는 것보다는 더 용이하게 색출할 수 있다고 한다..;;

이거 마치 컬러 복사기 얘기처럼 들린다. 컬러 복사기의 결과물에는 아주 정교한 워터마크가 사람 눈에 안 띄게 몰래 새겨진댄다. 그래서 어설픈 컬러 복사 위조지폐가 발견되면 그 워터마크를 토대로 복사기의 일련번호를 추적할 수 있으며, 이를 통해 범인도 색출할 수 있다고 한다. 허나 본인은 그런 게 실제로 존재한다고 믿지는 않는다.

뭐, 복사기는 그렇다 치고.. 실행 파일의 경우, 상식적으로 생각해 봐도 저건 개발 컴퓨터의 색출 목적으로 사용하기에는 보안이 너무 허술하다.
진짜 나쁜 마음 품은 악성 코드 개발자라면.. 그 Rich 헤더 부분을 후처리로 몽땅 0으로 칠해서 일부러 지워 버리기만 해도 자기 정체를 숨길 수 있으며, 그래도 악성 코드의 동작에는 하등 문제될 것 없다.

이 경우 PE 헤더의 다른 필드에 존재하는 checksum 정보가 어긋나서 파일이 변조되었다는 것이 감지되겠지만, 이것도 일부러 기재하지 않았다고 0을 집어넣어 버리면 그만이다. 더구나 보안을 위해 소프트웨어에서 이미 있던 이스터 에그도 다 없앴고 요즘은 바이너리 차원에서 철저히 예측 가능한 reproducible build까지 추구하는 마당에, 이런 식의 비밀 식별 정보는 마소의 개발 방침 이념과도 어울리지 않는다.

그러니 프로그램을 빌드할 때마다 내 컴퓨터의 맥 어드레스가 유출된다는 식으로 불안해할 필요는 없다. 하지만 Rich 헤더의 정체는 여전히 베일에 싸여 있었다.
그래서 전세계의 많은 컴덕과 해커들이 의문을 품기 시작했으며, 어떤 용자는 MS에서 개발한 링커 프로그램을 아예 근성으로 리버스 엔지니어링까지 했다. 그래서 이 데이터에 대한 여러 사실들을 밝혀냈다.

가장 먼저.. Rich 헤더는 4바이트 덩어리 단위로

A B B B X1 Y1 X2 Y2 ... "Rich" B

대체로 요런 형태로 돼 있다.
끝의 Rich 다음에 나오는 마지막 double word인 B가 일종의 난수이며, 암호화 key이다. 그리고 Rich 앞에 있는 숫자들은 바로 그 B와 xor을 해 주면 실제값을 얻을 수 있다.

그러면 맨 첫째 값 A는 언제나 0x44 0x61 0x6E 0x53.. 문자 형태로 늘어놓으면 "DanS"라는 시그니처가 된다.
다음으로 시그니처 뒤에 이어지는 몇 개의 B는 16바이트 단위 padding을 맞추기 위한 0값인 것 같다. 자기 자신을 xor 하면 결과는 언제나 0이 되니 말이다.

그 뒤 이어지는 같은 값들은 숫자 2개가 X, Y 형태로 한 pair를 이룬다. X는 이 바이너리를 생성하는 데 쓰인 툴(C 컴파일러, C++ 컴파일러, 리소스 컴파일러, 어셈블러 등..)과 버전(빌드 번호)을 나타내며, Y는 그 도구를 이용하여 생성된 아이템.. 이를테면 obj 파일의 개수를 나타낸다고 한다.

그럼 이런 정보들은 링커가 어디에서 얻어서 집어넣는가 하면.. 당연히 자기가 input으로 받아들이는 obj 파일로부터이다. 그렇잖아도 1997~98년을 전후해서 obj 파일 포맷이 바뀌었다고 한다.
obj야 소스 코드를 번역한 기계어 코드의 뭉치일 뿐이니 20년 전이나 지금이나 컴퓨터 아키텍처가 변함없는 한 바뀔 일이 없으며, 내부 구조가 바뀌어 봤자 새 버전의 컴파일러/링커가 인식하는 새로운 chunk 정도나 추가되는 게 전부일 것 같은데.. 꼭 그렇지는 않은 모양이다.

하긴, 같은 양의 코드를 빌드해도 요즘 컴파일러는 예전에 비해 obj의 파일 크기가 점점 더 커지고 있어 보이긴 하다. 디버깅 또는 최적화와 관련된 온갖 힌트와 메타정보들이 첨가되어서 그런 것 같다.
그래도 obj는 소스 코드를 빌드할 때마다 새로 생성되는 일회용 임시 파일일 뿐이니, 포맷에 하위 호환성 걱정 따위는 할 필요가 없을 것이다. 날려 봤자 빌드만 다시 하면 될 일이고..

아무튼 흥미로운 사실을 알게 됐다.
개인 정보 유출은 아니고, 그렇다고 디버깅과 관련된 힌트도 아니고 저런 정보가 마소의 개발툴에서 도대체 왜 들어갔는지는 여전히 오리무중이다만.. 그래도 내역을 까맣게 모르던 시절보다는 상황이 나아졌다.
본인이 이 글을 쓰기 위해 검색하고 참고한 외국 사이트는 다음과 같으니,  더 자세한 관심이 있는 분은 참고하시기 바란다.

The devil’s in the Rich header
Microsoft's Rich Signature (undocumented)
The Undocumented Microsoft "Rich" Header
Article: Things They Didn't Tell You About MS LINK and the PE Header

저 외국 사이트들의 설명에 따르면, Rich와 DanS는 모두 1990년대에 마소의 Visual C++ 팀에서 근무하고 이 데이터의 구현에 직접 관여했던 프로그래머의 이름에서 유래되었을 가능성이 높다고 한다. 그 이름은 각각 Richard Shupak와 Dan Spalding이다. ㄷㄷㄷ;;
MZ, PK만큼이나 프로그래머의 이름이 파일 포맷에 각인된 사례라 하겠다.

참고로 전자 리처드의 경우, Windows SDK에서 psapi.h의 작성자로 이름이 나와 있기도 하다.
PSAPI는 Windows NT 계열용으로 실행 중인 프로세스 정보를 조회하는 EnumProcesses, GetModuleFileNameEx 등의 함수를 정의하는 라이브러리인데, 작성 날짜가 무려 1994년이라고 적혀 있다.

Posted by 사무엘

2019/05/21 08:31 2019/05/21 08:31
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1621

Trackback URL : http://moogi.new21.org/tc/trackback/1621

Comments List

  1. nyam 2019/05/23 08:23 # M/D Reply Permalink

    저는 이게 좀 보기에 걸리적거려서 Imagine 실행파일에서 아예 Rich 시그니처 부분을 다 삭제해 버립니다.. ㅎㅎ

    그러고 보니 이렇게 Rich 시그니처 부분을 삭제하면 이후 zip 파일에 압축해서 넣을 때 몇 바이트 정도 더 작게 압축이 되더군요.. ㅎ

    1. 사무엘 2019/05/23 10:20 # M/D Permalink

      아.. 아예 0으로 덧씌우는 후처리를 해 버리시는군요.. ㅋㅋㅋ
      보안 관련도 아니고.. 저 시그니처는 도대체 왜 들어가는지 모르겠습니다.
      nyam 님~! 잘 지내시죠? 정말 오랜만에 뵙네요. 반갑습니다 ^^;;

  2. nyam 2019/05/23 14:25 # M/D Reply Permalink

    저야 그냥저냥 잘 지내고 있습니다. ㅎㅎ
    정말 오랫만입니다.

    사실 드러내진 않지만 사무엘님 블로그에 거의 매일 들러서
    아주 잘 정제되어 있는 유익한 글들을 읽고 있습니다.

    감사합니다.

Leave a comment

1.
먼 옛날에 기술이란 게 지금보다 비싸고 희귀하던 시절에는 컴파일러 자체가 유료화 대상이었다. Windows 플랫폼 SDK에 같이 들어있는 무료 기본 컴파일러는 상업용으로 따로 판매되는 Visual C++의 컴파일러와 같은 빡센 최적화 기능이 없었다. 사용자가 작성한 코드대로 돌아가는 실행 파일은 만들어 주지만 최적의 성능을 발휘하는 형태로 만들지는 않았다는 것이다.

그러다가 Visual C++ 2005 Express를 기점으로 고성능 컴파일러도 무료화되고 기본 제공되기 시작했다.
요즘 컴파일러의 최적화 테크닉은 학교에서 배우는 것처럼 (1) 쓰이지 않는 변수나 코드 제거, (2) 값이 뻔한 수식은 한 번만 미리 계산해 놓고 loop 밖으로 옮기기, (3) 변수는 적절하게 레지스터 등재, (4) 함수는 적절하게 인라이닝, (5) 최대한 병렬화, (6) CPU 명령 하나로 처리 가능한 단위로 묶기, (7) switch 분기를 매번 번거로운 if문이 아니라 테이블 참고 방식으로 변경... 같은 미시적인 것만이 그림의 전부가 아니다.

그야말로 번역 단위(소스 코드) 간의 경계를 넘나들고 컴파일러와 링커의 역할까지 재정립하면서까지 저런 최적화 테크닉을 적용할 부위를 판단할 필요가 있다. 그래서 컴파일러에는 전역 최적화라는 옵션이 도입되고, 링커에는 link time code generation이라는 옵션이 추가되어 서로 연계한다. 뭐, 대단한 기능이긴 하지만 이 정도로 최적화를 쥐어짜서 성능이 더 나아진 것 대비 프로그램의 빌드 타임이 너무 길어지는 건 별로 마음에 안 든다.

그리고 세상에 C++ 컴파일러가 MS 것만 있는 것도 아니고, 경쟁사들의 제품도 갈수록 성능이 좋아지고 가격이 저렴해지고 있으니 optimizing compiler 정도는 충분히 대중적인 영역으로 내려가게 됐다.

2.
한편, Visual Studio Express 에디션은 기업에서 사용이나 상업용 소프트웨어 개발까지 아무 제약 없는 무료인 대신, 리소스 편집기와 MFC 라이브러리가 없다. 지금도 그런지는 모르겠지만 처음엔 심지어 64비트 빌드 기능도 없었다.
그 뒤 VC++ 2013부터는 MFC와 리소스 편집기가 다 있는 Community 에디션이란 게 나왔다. 얘는 기능 제약이 없는 대신 개인 개발자나 스타트업 중소기업 수준까지만(인원 얼마 이하, 연 매출 얼마 이하..) 무료이다.

서로 조건이 다른 무료이기 때문에 VC++ 2013, 2015, 2017까지는 한 버전에 대해서 express와 community 에디션이 모두 나왔다. 하지만 express는 장기적으로는 community로 흡수되고 없어질 것으로 보인다.

3.
이렇게 프로그램을 기본적으로 만들고 빌드하는 데 필요한 도구들은 슬슬 무료화 단계에 들어섰고, 그 다음으로는 그냥 빌드만 하는 게 아니라 제품의 품질을 높이는 데 도움을 주는 도구, 대규모 공동 작업과 테스트를 위해 필요한 전문 도구들이 아직 유료이다. 어떤 것은 Visual Studio의 엔터프라이즈 같은 제일 비싼 에디션에서만 제공된다.

Visual C++ 2008에서는 GUI 툴킷이 feature pack을 통해 무료로 풀렸더니 2012부터는 정적 분석 도구가 무료로 풀렸다. 이걸 처음 써 본 소감은 꽤 강렬했다.

사실, 본인도 그저 닥치는 대로 코딩과 디버깅만 하는 것 말고 소프트웨어공학적인 개발 프로세스라든가 기술 문서 잘 쓰는 요령, 변수와 함수의 이름 잘 짓는 요령, 전문적인 테스트 절차와 프로파일링 같은 것을 잘 알지 못한다. 더 발전하려면 그저 무료로 풀려 있는 도구들만 쓰는 게 아니라 그 너머에 있는 도구들도 뭔지 알고 필요성을 공감할 정도는 돼야 할 텐데..

그렇잖아도 날개셋 한글 입력기도 어떤 형태로든 버그가 없었던 적은 거의 없었다. 그리고 개인 프로젝트뿐만 아니라 회사 업무도 늘 깔끔하게 마무리 짓지는 못해서 버그가 있는 채로 제품을 내고 후회한 적이 적지 않았다.
만들어진 제품의 품질을 검증하는 절차 자체는 소프트웨어뿐만 아니라 건축· 건설을 포함해 공학의 어느 분야에서나 다 필요하고 존재할 것이다. 그 가운데에 무형의 지적 산물인 소프트웨어만이 갖는 특수성 때문에 이 바닥에서만 통용되는 방법론도 있겠지만 말이다.

아무튼, 프로그래밍 툴의 제작사들이 흙 파서 장사하는 건 아닐 테니.. 이런 어마어마한 컴파일러쯤은 딴 데서 개발 비용을 회수할 통로가 다 있으니까 무료로 풀어 놓는 게 가능할 것이다.

4.
(1) 이렇듯, Visual Studio를 포함해 개발툴들은 갈수록 기능이 좋아지고 기술들이 상향평준화되고 있다. 일례로, 2017인가 2019부터는 코드 편집기에 일반 컴파일러의 경고/에러뿐만 아니라 정적 분석 결과까지 초록색 밑줄로 띄워 주는 걸 보고 무척 감탄했다. (NULL 포인터 역참조가 일어날 수 있습니다 따위..)

(2) 2017/2019부터는 도움말과 API 레퍼런스는 몽땅 인터넷으로 처리하고 로컬 오프라인용 Help Viewer를 더 관리하지 않으려는지? 설치 화면에서는 기본 선택돼 있지도 않으며, 컨텐츠 역시 2015 내용 이후로 달라진 게 없어 보인다.

(3) 그리고 Spy++의 64비트 버전은 왜 도구 메뉴에서 숨겨 버린 걸까?
한 프로그램만으로 32비트와 64비트를 통합해서 잘 동작하는 것도 아니고, "이 창의 메시지를 들여다보려면 64비트 Spy++를 실행해 주십시오" 안내를 해 주는 것도 아닌데 왜 이런 조치를 취했는지 이해할 수 없다. 사실은 32와 64비트를 가리지 않고 아주 seamless하게 동작하는 Visual Studio의 디버거가 정말 예술적인 경지의 대단한 도구이긴 하다.

(4) 시대 유행과 별 관계 없는 부분은 계속해서 안 바뀌기도 하는 것 같다.
도구상자를 customize할 때 콤보 박스의 길이 조절을 200x 시절처럼 마우스 드래그로 할 수 없고.. 2010 이래로 그냥 픽셀수 입력으로 때운 걸로 굳히려는가 보다.

무려 7년 전 글에서 지적했던 2. 메뉴 편집기의 우클릭 버그, 3. 툴바 편집기의 화면 잔상 역시, Visual Studio 2019 현재까지도 전혀 고쳐진 게 없다.
그리고 예전엔 안 그랬던 것 같은데 2019는 Visual Studio 텍스트 에디터의 폰트를 딴 걸로 바꾸고 껐다가 다시 켜서 곧장 반영되는 걸 확인까지 했는데.. 켠 채로 절전 상태로 몇 번 갔다가 돌아오면 폰트가 컴터에 따라 아주 가끔은 돋움이나 Courier New 같은 기본 글꼴로 돌아오는 것 같다.

Posted by 사무엘

2019/05/10 08:36 2019/05/10 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1617

Trackback URL : http://moogi.new21.org/tc/trackback/1617

Leave a comment

Windows의 Region

Windows API에는 region(영역)이라는 일종의 자료구조 라이브러리가 있다. 얘는 2차원 래스터(픽셀/비트맵) 그래픽 공간에서 각 픽셀별로 "영역에 포함되냐 안 되냐"라는 일종의 '집합'을 표현한다.
그리고 운영체제는 이 자료구조를 이용하여 각종 그래픽이 그려지는 영역을 정한다. 즉, region은 클리핑(clipping) 영역을 표현하는 데 쓰인다는 것이다.

도스 시절의 여느 그래픽 라이브러리에도 간단한 사각형 영역에만 그림이 그려지게 하는 초보적인 수준의 클리핑 기능은 있었다. 하지만 Windows의 region은 여러 사각형이 겹친 것, 임의의 다각형, 원 등 아무 모양이나 표현하고, 그 영역 안에만 그림이 그려지게 만들 수 있다.

그도 그럴 것이 Windows 같은 GUI 운영체제라면 창들의 Z-order 같은 걸 구현하는 과정에서 밥 먹고 맨날 하는 짓이 정교한 클리핑일 수밖에 없게 된다. 뒤쪽에 있는 창 내용은 앞쪽에 있는 창의 영역을 침범하지 않고 그려져야 하기 때문이다. 그러니 그런 기능을 사용자들에게도 쓰라고 제공해 주는 게 결코 이상한 일이 아니다.

region은 가장 먼저, (1) 직사각형, 타원, 모서리가 둥근 직사각형, 다각형(CreatePolygonRgn, CreatePolyPolygonRgn)처럼.. 속이 닫힌 도형을 그리는 다양한 API를 통해 생성할 수 있다. 당연히 그 도형의 모양이 영역의 모양이 된다.

다음으로, GDI가 제공하는 기능인 (2) path로부터 region을 생성할 수 있다(PathToRegion). path란 마치 윤곽선 글꼴 글립처럼 직선(MoveTo, LineTo)과 곡선(PolyBezirTo)을 임의로 조합하여 어떤 궤적이나 경계선을 기술하는 자료구조이다. region과 달리 벡터 기반이며, 별도의 자료구조로 존재하는 게 아니라 DC의 내부 상태에 종속인 형태로 보관된다는 차이가 있다.

path를 사용하면 경계선이 베지어 곡선인 region을 만들 수 있으며, TextOut 같은 글자 출력 함수를 path에다 넣으면 임의의 글자의 윤곽선도 따서 고스란히 region으로 만들 수 있다. 커다란 두 글자를 포개 놓은 뒤, 겹치는 영역만 다른 색깔로 칠하는 게 region으로는 가능하다. 그 비결은 바로...

사용자 삽입 이미지

(3) CombineRgn이라는 함수를 통해 region 간에 일종의 집합 연산을 할 수 있기 때문이다. 두 region의 교집합, 합집합, 차집합을 구함으로써 더 복잡한 형태의 region을 만들 수 있다.

위의 그림을 보아라. 운영체제나 하드웨어 차원에서 제공되는 layer이나 alpha channel 합성 같은 걸 쓴 게 아니다. 옅은 회색(B), 짙은 회색(S), 검정(겹침)이 차지하는 영역을 2차원적으로 완전히 따로 떼어내서 서로 완전히 다른 색깔과 무늬로 칠할 수 있다. 뚝 떨어진 영역도 당연히 같이 감안해서 말이다. 이런 게 평범한 글자 찍기 API로는 가능하지 않을 것이다.
다만, region은 anti-aliasing을 지원하지 않는 boolean 흑백 자료구조이다 보니, 글자 경계가 거친 것은 아쉬운 점이며 요즘 그래픽 기술의 트렌드와 맞지 않다.

그리고 끝으로.. region은 내부 자료구조를 어느 정도 노출해 주고 있기까지 하다. 그래서 그걸 직통으로 저장하고 불러오는 식으로 생성할 수도 있다. 데이터를 얻는 함수는 GetRegionData이고, (4) 그걸로부터 region을 다시 생성하는 함수는 ExtCreateRegion이다.

어떤 방식으로 region을 생성했건, 얘는 내부적으로 크게 세 부류로 나뉜다. region을 생성하거나 받아들이는 함수들이 그 region의 유형을 리턴값을 통해 알려주기도 한다.

  • 아무 영역도 없는 공집합인 NULLREGION
  • 직교좌표 직사각형 하나로만 구성된 SIMPLEREGION
  • 그 외의 다른 모든 모양을 표현하는 COMPLEXREGION.. 얘는 내부적으로 2개 이상의 사각형, 아니 scan line들로 구성된다.

자연에서 관찰되는 힘이라는 것들이 중력이나 원자력과 관계 있는 게 아니면 나머지는 출처가 몽땅 전자기력이듯이(폭발력, 마찰력, 탄성, 자석, 정전기, 표면장력, 생물 근육 등등등..),
그리고 사람이 생성(...)되는 방식이 흙을 빚어서 직통, 여자의 씨 같은 극소수 예외를 제외하면 나머지 수십~수백 억의 인간들은 몽땅 남자의 씨 기반이듯이.. 그런 것처럼 region도 일상생활에서 보는 단순하지 않은 물건들은 몽땅 complex라고 생각하면 되겠다.

region이 내부적으로 구현된 방식의 특성상(벡터/오브젝트 기반이 아닌 비트맵/픽셀 스캔라인 기반) 무한을 구현할 수 있지는 않으니, 집합 연산에서도 not 연산인 여집합이 지원되지는 않는 걸 볼 수 있다. 차라리 이미 있는 집합끼리 차집합이 대신 지원되고 말이다.

region을 식별하는 핸들 내지 포인터 자료형은 HRGN이다. 그런데 Create...처럼 HRGN을 리턴값으로 주는 함수 말고, Get...Rgn, CombineRgn 이런 이름이면서 HRGN을 인자로 받는 함수들은... 이미 있는 HRGN에다가 값만 바꿔서 넣어 준다. 그런 함수를 쓰려면 null region 하나라도 미리 미리 생성해서 전해 줘야 한다.

그런데 Windows API에는 많고 많은 region 생성 함수들 중에 null region만을 달랑 생성하는 함수는 의외로 없다. 좌표가 모두 0인 직사각형.. CreateRectRgn(0,0,0,0) 이게 그냥 텅 빈 region을 생성하는 역할을 한다. 좀 교묘한 점이다.

그럼 이 region는 어떤 용도로 쓰이며, 할 수 있는 일이 무엇일까? 본인이 생각하기에 다음과 같다.

1. 단독

그냥 저거 자체만으로 뭔가 2차원 공간 상의 기하/집합 알고리즘 구현체로.. 다른 GDI API와의 연계 없이, 심지어 명령 프롬프트용 프로그램에서도 쓰일 수 있다. 펜, 브러시, 글꼴, 비트맵 같은 타 오브젝트들이 DC와의 연계 없이는 거의 쓸모없는 것과 굉장히 대조적이다.
하지만 region이 그렇게 단독으로 쓰이는 경우는 그리 많지 않아 보인다.

2. 창의 invalid 영역 표현

어떤 창의 앞을 가리던 다른 창이 없어지고 내 창의 내용이 다시 그려지게 됐을 때.. 그 이름도 유명한 WM_PAINT 메시지가 날아온다.
BeginPaint와 함께 제공되는 DC는 창 전체가 아니라 정확하게 다시 그려져야 하는 영역에만 그림이 그려지도록 클리핑 처리가 돼 있는데, 이 영역이 말 그대로 region으로 표현되며 GetUpdateRgn 함수를 통해 얻어 올 수 있다.

WM_PAINT 때 이 영역에 대해서 PtInRegion이나 RectInRegion을 적절히 호출하면서 그림을 그리면, 무식하게 화면 전체를 그리는 것보다 프로그램 성능과 반응성을 향상시킬 수 있다.
물론 DC 차원에서 클리핑 처리가 되는 것만으로도 화면 전체를 그리는 것보다는 속도가 향상되지만, 애초에 그리기 요청을 안 하고 CPU 계산을 안 하는 게 더 낫기 때문이다.

3. 내부 클리핑

운영체제뿐만 아니라 사용자 역시 임의의 region을 생성해서 클리핑 용도로 쓸 수 있다. 비트맵이 사각형 모양이 아니라 원 모양으로만 뿌려진다거나, 특정 글자 모양으로만 뿌려지게 할 수 있다는 것이다.
그렇게 하려면 HRGN을 DC에다가 지정하면 된다. 이것은 SelectObject로 해도 되고 SelectClipRgn으로 해도 된다. 완전히 동일하다.
단지, 클리핑을 해제하는 것은 SelectClipRgn로만 가능하다. HRGN 값으로 NULL을 전해야 하기 때문이다. null region은 그림이 전혀 그려지지 않게 하는 효과를 낼 테니까..

HRGN은 기술적으로는 HPEN, HBRUSH, HBITMAP, HFONT와 마찬가지로 여러 GDI 오브젝트 중 하나로 취급된다. 상술한 바와 같이 DC에 SelectObject 될 수 있으며, 소멸 함수가 DeleteObject인 것까지도 동일하다.
하지만 얘는 다른 오브젝트들과 달리 select나 get 될 때 내부 메모리가 복사될 뿐, 핸들값 자체를 주고 받지는 않는다. 즉, HRGN은

HRGN oldRgn = (HRGN)SelectObject(dc, newRgn);
(.....)
SelectObject(dc, oldRgn);

이런 식으로 운용되지 않는다는 것이다. 옛날 핸들값을 보관하고 되돌리는 식의 절차가 필요하지 않다.
DC와 region은 서로 따로 논다. 이렇게 설정한 뒤에 원래 있던 HRGN 핸들은 곧장 삭제해 버려도 된다.

본인은 MFC의 CGdiObject처럼 GDI 객체 핸들만 한데 뭉뚱그린 템플릿 클래스를 만들어서 쓰고 있다. (소멸자에는 DeleteObject가 있고..)
그런데 다른 오브젝트들은 template<T> Handle(T v=NULL) 이런 식으로 NULL이 default 인자인 생성자를 만들어서 초기화할 수 있는 반면,
HRGN에 대해서는 인자가 없는 경우에 대해 specialize된 생성자를 따로 만들어서 이때는 null region을 생성해 놓게 했다. 그래야 이놈을 region을 얻어 오는 다른 함수에다가 인자로 줄 수 있기 때문이다.

4. 칠하고 그리는 공간

그리고 region 자체가 도형을 나타내니 그 모양대로 클리핑이 아니라 내부를 칠하는 용도로 응당 활용할 수 있다. 내부를 칠하는 FillRgn과, 내부의 경계선을 그려 주는 FrameRgn이라는 함수가 제공된다.
흥미로운 것은 경계선을 그릴 때도 pen 대신 brush가 쓰인다는 것이다. region은 path와 달리 벡터 드로잉이 아니기 때문이다. 경계선은 그냥 픽셀 차원에서 색깔이 변하는 곳을 얼추 감지해서 표시해 주는 것일 뿐이다.

5. 창 자체의 외형

끝으로, region은 윈도우의 모양을 지정하는 용도로도 쓰인다. 통상적인 사각형 모양이 아니라 리모콘 같은 다른 물건처럼 생긴 프로그램 창, 현란한 스플래시 윈도우, 내부에 구멍까지 있는 윈도우.. 전부 SetWindowRgn 함수의 산물이다.
한번 SetWindowRgn에다 전해 준 HRGN은 이제 운영체제가 관리하기 때문에 사용자가 DeleteObject 하지 말아야 한다고 문서에 거듭 명시되어 있다.

SetWindowRgn를 지정하는 순간부터 그 창은 운영체제가 non-client 영역과 테두리 경계에 기본 제공하는 각종 테마와 반투명 프레임, 둥그런 테두리, 그림자 효과들로부터 완전히 열외된다. 그 대신 고전 테마의 완전 무미건조한 테두리만이 그려진다. 일반적으로는 당연히 아래처럼 그려질 프로그램 창이 위처럼 그려지게 된다는 뜻이다.

사용자 삽입 이미지

그런 창은 어차피 non-client 영역이 전혀 없이 외형을 사용자가 완전히 customize하는 형태로 쓰일 테니 말이다. 제목 표시줄과 테두리의 외형은 보급품 그대로이면서 중앙에만 region을 지정해서 총알 구멍 같은 게 숭숭 뚫린 프로그램 창 같은 건 만들 수 없다는 뜻이다.

보통은.. 공용 컨트롤 6.0 매니페스트가 없는 프로그램의 경우, 버튼 같은 컨트롤들이 구닥다리 고전 스타일로 그려지고 non-client 영역은 운영체제가 자동으로 쌈빡하게 그려 준다. (그림에서 아래 오른쪽) 그런데 SetWindowRgn을 지정하면 반대로 컨트롤들은 정상적으로 그려지는데 non-client 영역이 고전 스타일로 돌아간다는 게 흥미롭다.

단, 지난 Windows 2000부터는 SetWindowRgn가 아닌 다른 방법으로도 사각형 모양이 아닌 윈도우를 표현할 수 있게 되었다. 바로 layered window이다. WS_EX_LAYERED 스타일을 준 윈도우에다가 SetLayeredWindowAttributes 함수를 호출하면 (1) 이 창에 대해서 투명도를 지정할 수 있고, (2) 특정 RGB 값을 color key로 지정해서 그 색깔은 투명으로 처리할 수 있다. 배경색을 칠하는 것만으로 그 부위가 투명해지니, 번거로운 region보다 훨씬 더 편리해지기까지 했다.

과거 MS Office 97~2000 시절의 흑역사 중에는 'Office 길잡이'라는 "취지만 좋았다" 급의 물건이 있었다. 애니메이션 캐릭터가 화면에 나타나서 돌아다니는데.. 첫 도입되었던 97은 캐릭터가 사각형 창 안에 갇힌 형태였던 반면, 2000부터는 배경 없이 창이 시시각각 애니메이션 캐릭터 모양으로 변했다.

사용자 삽입 이미지사용자 삽입 이미지

이게 Windows 2000에서는 하드웨어빨을 탄 layered window로 비교적 간편하게 구현됐던 반면.. 9x에서는 일일이 window region을 바꿔 가면서 동작했다. 그 밑의 창은 매번 WM_PAINT가 발생하면서 성능 페널티가 장난이 아니었을 것이다.

본인은 먼 옛날에 이런 프로그램도 구경한 적이 있었다. 실행하면 만화영화 그림체로 그려진 병아리 한 마리가 마치 저 Office 길잡이처럼 튀어나왔는데, 그냥 가만히 있는 게 아니라 각종 프로그램 창들 위를 돌아다녔다. 나름 중력도 구현했던지라, 마우스로 집어다가 공중에다 옮겨 놓으면 아래의 근처에 있는 창으로 떨어지기까지 했다. 내 기억이 맞다면 꽤 재미있는 프로그램이었는데.. 지금 인터넷으로 다시 검색하고 구할 길이 없다.

이 프로그램에 대해서 본인이 현재 기억하고 있는 건 아마 일본에서 만들어진 걸로 추정된다는 것, 그리고 무려 Windows 3.x용 16비트 프로그램이었다는 것이다. 그 열악한 Windows 3.x에서도 타이머를 걸어서 최소화 아이콘에다가 애니메이션을 구현하고, 창의 경계가 시시각각 곡선 윤곽으로 바뀌는 저런 액세서리 프로그램이 만들어지기도 했다.

이렇듯, SetWindowRgn을 이용해서 이런 재미있는 활용을 할 수 있는데.. 날개셋 한글 입력기에서 사각형이 아닌 모양의 창이 나타나는 곳은 마우스 휠을 눌렀을 때 나타나는 동그란 자동 스크롤 앵커가 유일하다.

사용자 삽입 이미지

에디트 컨트롤은 자동 스크롤 모드가 없고, MS 오피스 제품들은 동그란 테두리 없이 그냥 배경에다가 회색 화살표가 나타나는 듯하지만.. 마소에서 만든 웹 브라우저(IE, Edge)에서는 앵커 윈도우가 나타난다. 날개셋의 앵커 윈도우도 먼 옛날에 얘를 참고해서 만들어진 것이다. 맨 처음에는 region만 쓰다가 이내 layered window도 사용하도록 형태가 바뀌었다.

여담: 좌표계 관련 문제

아, region의 경계면과 관련해서 주의해야 할 점이 있다.
같은 좌표를 줬을 때, 직사각형은 pen으로 그려지는 테두리와 region의 영역이 픽셀 단위로 정확하게 일치한다. 다시 말해 같은 RECT rc에 대해서 CreateRectRgn + SetClipRgn을 한 뒤에Rectangle을 호출한 결과는 클리핑을 안 했을 때와도 동일하다.

하지만 타원(Ellipse vs CreateEllipticRgn)이나 폴리곤(Polygon vs CreatePolygonRgn) 같은 다른 도형에 대해서는 이것이 성립하지 않는다. region은 오른쪽과 아래 끝의 1픽셀이 미묘하게 잘린다.

//직사각형
CRgn rg; CRect rc(10, 10, 80, 80);
rg.CreateRectRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Rectangle(rc);
rc.OffsetRect(40, 40); dc.Rectangle(rc);

//원
CRgn rg; CRect rc(110, 10, 180, 80);
rg.CreateEllipticRgnIndirect(rc);
dc.SelectClipRgn(&rg); dc.Ellipse(rc);
rc.OffsetRect(40, 40); dc.Ellipse(rc);

//폴리곤으로 표현한 직사각형
CRgn rg; POINT pt[4] = {
{10, 100}, {80, 100}, {80, 170}, {10, 170} };
rg.CreatePolygonRgn(pt, 4, ALTERNATE);
dc.SelectClipRgn(&rg); dc.Polygon(pt, 4);

이 코드를 실행한 결과는 다음과 같이 차이가 난다.

사용자 삽입 이미지

폴리곤으로 직사각형 좌표를 지정해 줘도, 아예 직사각형 전용 생성 함수를 줬을 때와 달리, region은 영역이 살짝 덜 생긴다. 그래서 그 region 안에서 동일 좌표로 도형을 직접 그려 보면 테두리의 오른쪽과 아래쪽이 잘린다.

이를 감안해서 원형 region을 생성할 때는 그리기 함수일 때보다 1픽셀 정도 더 크게 원을 그리면 잘리는 현상은 막을 수 있다. 하지만 그래도 그리기 함수와 region 함수는 경계 계산 결과가 서로 미묘하게 달라서 직사각형일 때처럼 깔끔하게 일치하는 모양이 나오지 않는다.
그러니 region 자체의 경계를 그려 주는 FrameRgn 함수를 대신 쓸 수밖에 없다. 허나, 얘가 그려 주는 테두리는 전문적인 원 그리기 함수에 비해 표면이 거칠며 별로 예쁘지 않다.

본인은 처음에 이런 특성을 몰라서 한동안 삽질을 했었다. 이럴 때도 region 대신 layered window는 순수하게 그리기 결과에 따라서 투명색을 자동으로 처리해 주니 더욱 유용하다.

Posted by 사무엘

2019/04/12 08:34 2019/04/12 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1607

Trackback URL : http://moogi.new21.org/tc/trackback/1607

Leave a comment

1. 재부팅 이벤트

본인은 지금까지 Windows가 시스템 종료/재시작/로그아웃 될 때.. 메시지에 즉각 응답하면서 정상적으로 돌아가는 프로그램이라면 종료 처리도 당연히 정상적으로 되는 줄로 알고 있었다.
사용자가 [X] 버튼이나 Alt+F4를 눌러서 종료할 때와 완전히 동일하게 말이다. WM_CLOSE에 이어 WM_DESTROY, WM_NCDESTROY가 날아오고, WM_QUIT이 도달하고, message loop이 종료되고, MFC 프로그램으로 치면 ExitInstance와 CWinApp 소멸자가 호출되고 말이다.

왜냐하면 시스템이 종료될 때 발생하는 현상이 언뜻 보기에 정상적인 종료 과정과 동일했기 때문이다. 저장되지 않은 문서가 있는 프로그램에서는 문서를 저장할지 확인 질문이 뜨고, 운영체제는 그 프로그램 때문에 시스템 종료를 못 하고 있다고 통보를 한다. 좋게 WM_CLOSE 메시지를 보내는 걸로 반응을 안 하는 프로그램에 대해서만 운영체제가 TerminateProcess 같은 강제 종료 메커니즘을 동원할 것이다.

그런데 알고 보니 그게 아니었다.
정상적인 종료라면 ExitInstance 부분이 실행되어야 할 것이고 프로그램 설정들을 레지스트리에다 저장하는 부분도(본인이 구현한..) 실행돼야 할 텐데, 내 프로그램이 실행돼 있는 채로 시스템 종료를 하고 나니까 이전 설정이 저장되어 있지 않았다.

꼭 필요하다면 WM_ENDSESSION 메시지에서 사실상 WM_DESTROY 내지 ExitInstance와 다를 바 없는 cleanup 작업을 해야 할 듯했다. 뭐, 날개셋 한글 입력기에서 이 부분이 반영되어 수정돼야 할 건 딱히 없었지만 아무튼 이건 내 직관과는 다른 부분이었다.

시스템이 종료될 때 발생하는 일은 딱히 디버거를 붙여서 테스트 가능하지 않다. =_=;; 로그 파일만 기록하게 해야 하고, 매번 운영체제를 재시작해야 하니 가상 머신을 동원한다 해도 몹시 불편하다. 꽤 오래 전 일이 됐다만, 날개셋 외부 모듈이 특정 운영체제와 특정 상황에서 시스템 종료 중에 운영체제 시스템 프로세스 안에서 죽는 문제가 발생해서 디버깅을 한 적이 있었다. 몹시 골치 아팠었던 걸로 기억한다.

2. 파일의 동등성 비교

파일 이름을 나타내는 어떤 문자열이 주어졌을 때,

  • 상대 경로(현재의 current directory를 기준) vs 절대 경로
  • 과거 Windows 9x 시절이라면 ~1 같은 게 붙은 8.3 짧은 이름 vs 원래의 긴 이름
  • 대소문자 (Windows는 대소문자 구분이 없으므로. 그런데 이것도 A~Z 26자만 기계적으로 되나? 언어별로 다른 문자의 대소문자는?)
  • 그리고 옵션으로는 정규 DLL search path와 환경 변수

이런 것들을 몽땅 다~ 감안해서 다음과 비스무리한 일을 하는 가벼운 API가 좀 있으면 좋겠다. 보다시피 동일한 파일을 표현하는 방법이 굉장히 다양하기 때문이다.

  • 두 개의 파일 명칭이 서로 동일한 파일인지 여부를 판단. 당연히 파일을 직접 open/load하지 않고 말이다.
  • 그 파일의 대소문자 원형을 유지한 절대 경로. 일명 '정규화된' 이름을 되돌린다. 이게 동일한 파일은 물리적으로, 절대적으로 동일한 파일임이 물론 보장된다.
    참고로 Windows API 중에서 얼추 비슷한 일을 한다고 여겨지는 GetFullPathName이나 GetModuleFileName 함수는 절대 경로 말고 파일의 대소문자 원형 복원이 제대로 되지 않는다.
  • 혹은 그 파일의 시작 지점이 디스크에서 물리적으로 어디에 있는지를 나타내는 64비트 정수 식별자를 구한다. 포인터가 아니지만 파일의 동등성 여부 판단은 가능한 값이다. 정수를 넘어 UUID급이어도 무방함.

본인은 비슷한 목적을 수행하기 위해, 그냥 두 파일을 CreateFileMapping으로 열어 보고 리턴된 주소가 동일하면 동일한 파일로 간주하게 한 적이 있었다. 핸들 말고 MapViewOfFile 말이다. 본질적으로 동일한 파일이라면 운영체제가 알아서 같은 주소에다 매핑을 하고 레퍼런스 카운트만 증가시킬 테니까..
Windows 9x에서는 주소가 시스템 전체 차원에서 동일하겠지만 NT 계열에서는 한 프로세스 내부에서만 동일할 것이다.

하지만 내가 필요한 건 파일의 내용이 아니라 그냥 두 파일의 동등성뿐인데 이렇게 매핑을 하는 건 overkill 삽질 같다는 인상을 지울 수 없었다.
비슷한 예로, LoadLibrary도 같은 파일에 대해서는 같은 리턴값이 돌아온다. HMODULE도 오늘날은 핸들이 아니라 메모리 map된 주소이니까.. 다만, 오버헤드를 줄인답시고 LoadLibraryEx + LOAD_LIBRARY_AS_DATAFILE 이렇게 열어서는 안 된다. 그러면 로딩 방식이 크게 달라지더라.

3. JNI에서 문자열 처리하기

Java 언어는 JNI라고 해서 자기네 바이트코드 가상 머신이 아닌 C/C++ 네이티브 코드를 호출하는 통로를 제공한다.
프로그램 자체를 C/C++로 짜던 시절에는 극한의 성능을 짜내야 하는 부분에 어셈블리어를 집어넣는 게 관행이었는데.. 이제는 일반적인 코딩은 garbage collector까지 있는 상위 계층에서 수행하고, 극한의 성능을 짜내야 하는 부분에서만 C/C++ 코드를 호출한다는 게 흥미롭다.

JNI는 그냥 언어 스펙에 가까운 광범위한 물건이다. Windows 환경에서는 그냥 Visual C++로 빌드한 DLL이 export하는 함수를 그대로 연결할 수도 있다. 물론 그 DLL을 빌드하기 위해서는 Java SDK에서 제공하는 jni 인터페이스 헤더와 static 라이브러리를 사용해야 한다.
한편, 안드로이드 앱 개발에서 쓰이는 NDK는 JNI 스펙을 기반으로 자체적인 C++ 컴파일러까지 갖춘 네이티브 코드 빌드 도구이다.

Java의 문자열은 JNI에서는 jstring이라고 내부 구조를 알 수 없는 자료형의 포인터 형태로 전달된다. C++에서는 UTF-8과 UTF-16 중 편한 형태로 바꿔서 참조 가능하다.
UTF-8로 열람하려면 JNIEnv::GetStringUTFChars를 호출하면 된다. 길이를 알아 오려면 GetStringUTFLength부터 호출한다. 전해받은 문자열 포인터는 ReleaseStringUTFChars로 해제한다.

그 반면, UTF-16 형태로 열람하려면 위의 함수 명칭에서 UTF를 빼면 된다. GetStringChars, GetStringLength, ReleaseStringChars의 순이다. Java가 내부적으로 문자를 2바이트 단위로 처리하기 때문에 이들이 주로 취급하는 자료형은 jchar*이다. 그러니 얘는 char16_t 자료형과 호환된다고 간주해도 좋다. 참고로 wchar_t는 NDK 컴파일러의 경우 4바이트로 처리되더라.

UTF-16이나 UTF-8이나 다 UTF이긴 마찬가지인데, Java는 변별 요소인 8을 생략하고 함수 이름을 왜 저렇게 지었나 개인적으로 의구심이 든다. 물론 GetStringChars는 Java가 내부적으로 문자열을 원래부터 2바이트 단위로 처리하다 보니 우연히 UTF-16과 대응하게 됐을 뿐, 대놓고 UTF-16을 표방했던 건 아닐 것이다. 뭐, 이제 와서 그 체계를 바꾸는 건 불가능하고 "자바 문자열 = 2바이트 단위"는 완전히 고정되고 정착했지만 말이다.

또한 GetStringChars는 GetStringUTFChars와 달리 굉장히 치명적으로 불편한 단점이 하나 있다. 바로.. 변환된 문자열이 NULL-terminated라는 보장이 없다는 것이다!
그래서 본인은 이 포인터를 사용할 때 메모리를 n+1글자만치 또 할당해서 null문자를 추가해 주는 매우 번거로운 두벌일을 하고, 아예 클래스를 이렇게 따로 만들어야 했다. 좀 개선의 여지가 없으려나 모르겠다.

class CJstrToString16 {
    JNIEnv *_ev;
    jstring _jstr;
    const jchar *_ret;
    char16_t *_arr;
public:
    CJstrToString16(JNIEnv *ev, jstring js): _ev(ev), _jstr(js) {
        jsize n = ev->GetStringLength(js);
        _ret = ev->GetStringChars(js, NULL);
        _arr = new char16_t[n+1];
        memcpy(_arr, _ret, n*sizeof(char16_t));
        _arr[n] = 0; //고작 요거 하나 때문에..
    }
    ~CJstrToString16() {
        ev->ReleaseStringChars(_jstr, _ret);
        delete[] _arr;
    }
    operator const char16_t*() const { return _arr; }
};

4. Visual C++의 STL

C++은 타 프로그래밍 언어들과 달리, 심지어 전신인 C와도 달리, 언어가 개발되고 나서 자신의 특성을 잘 살린 라이브러리가 언어 차원에서 붙박이로 곧장 제정되지 않았던 모양이다. 그래서 각 컴파일러들이 중구난방으로 파편화된 형태로 라이브러리를 제공해 오다가.. 표준화라는 게 1990년대 말이 돼서야 논의되기 시작했다.

템플릿이 추가되어 C++에서도 제네릭, 메타프로그래밍이라는 게 가능해진 뒤부터 말이다. 처음에는 자료구조 컨테이너 위주로 STL이라는 이름이 붙었다가 나중에는 그냥 C++ library가 된 걸로 본인은 알고 있다.

Windows용으로 가장 대중적인 C++ 컴파일러야 두 말할 나위 없이 MS Visual C++이다. 얘는 거의 20여 년 전 6.0 시절부터 P.J. Plauger라는 사람이 구현한 C++ 라이브러리를 제공해 왔다. C 라이브러리와 달리 C++ 라이브러리는 소스가 비교도 안 될 정도로 복잡하고 난해하다는 것(암호 같은 템플릿 인자들..=_=), 그리고 저렇게 마소 직원이 아닌 개인 이름이 붙어 있다는 게 인상적이었다. 2000년대 초까지만 해도 휴렛-패커드라는 회사명도 주석에 기재돼 있었다.

P.J. Plauger는 현재는 Dinkumware라고 C++ 라이브러리만 전문적으로 관리하고 라이선스 하는 회사를 설립해 있다. 나이도 생각보다 지긋한 듯..
그런데 이런 세계적인 제품에 들어가는 라이브러리가.. 성능이 의외로 시원찮은가 보다. Visual C++이 제공하는 컨테이너 클래스가 유난히도 느리다고 까이는 걸 여러 사이트에서 봐 왔다.

최근에는 본인 직장의 상사마저도 같은 말씀을 하시기에 "헐~!" 했다. 업무상 필요해서 string, set, map 등을 써서 수십, 수백 MB에 달하는 문자열을 분석하는 프로그램을 돌렸는데, 자료 용량이 커질수록 속도가 급격히 느려져서 자료구조를 직접 새로 짜야 할 판이라고 한다.

난 개인적으로는 C++ 라이브러리를 거의 사용하지 않고, 더구나 그걸로 그 정도까지 대용량 작업도 해 보지 않아서 잘 모르겠다. 그 날고 기는 전문가가 만든 코드에 설마 그런 결함이 있으려나? 아니면 컴파일러의 최적화 문제인지?

글쎄.. 이런 게 있을 수는 있다. MFC의 CString은 그냥 포인터와 크기가 동일하며 값으로 전할 때의 reference counting도 처리한다. 그러나 std::string은 자주 쓰이는 짧은 문자열을 번거로운 heap 메모리 할당 없이 빠르게 취급하기 위한 배열까지 내부에 포함하고 있다. 이런 특성을 모르고 std::string도 함수에다 매번 value로 전달하면 성능에 악영향을 줄 수밖에 없다.

그런 식으로 임시 객체가 쓸데없이 생겼다가 사라지는 구조적인 비효율이 C++ 라이브러리에 좀 있는 걸로 들었다. R-value 참조자 &&가 도입된 것도 vector의 내부 처리에서 그런 삽질을 예방하는 근거를 언어 차원에서 마련하기 위해서라지 않는가? 그리고 Visual C++이 그런 비효율을 보정하는 성능이 좀 시원찮다거나 한 것 같다. 전부 다 그냥 추측일 뿐이다.

그러고 보니 cout<<"Hello world"가 printf("Hello world")보다 코드 오버헤드가 작아지는 날이 과연 올지 모르겠다. 이것도 그냥 떡밥인 건지..?? =_=;;

Posted by 사무엘

2019/04/09 08:32 2019/04/09 08:32
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1606

Trackback URL : http://moogi.new21.org/tc/trackback/1606

Leave a comment

요즘 컴퓨터 프로그램들은 상업용이라 해도 과거처럼 디스켓이나 씨디가 담긴 패키지 박스의 형태로 배포되는 경우가 거의 없어졌다. 바이너리의 배포 자체는 인터넷으로 하며, 패키지가 있다 해도 그 안에는 시리얼 번호 쪽지 정도나 달랑 들어있다. 그 뒤, 사용자가 구매한 카피 개수만큼만 프로그램을 동시에 구동하고 있는지, 사용 기한이 경과하지 않았는지 같은 인증은 인터넷으로 진행한다.

정식 사용자가 확인되지 않은 프로그램은 일단 평가판 모드로 동작한다. 정품 인증이 안 됐고 평가판 기간도 경과했다면 그 다음부터는 기능 제한 모드로 동작한다. 문서나 데이터를 취급하는 업무용 프로그램이라면 이제 문서를 편집할 수 없고 일종의 viewer 형태로만 동작하게 된다.

과거에는 소프트웨어 개발사들이 사용자에게 자기 제품의 기능을 자랑하는 한편으로 정품 구매에 대한 동기를 부여하기 위해 셰어웨어, 평가판, 데모 같은 것을 따로 배포하곤 했다.
하지만 지금은 인터넷이 워낙 발달한 덕분에.. subset 구분 없이 전체 제품을 통째로 뿌린다. 그 뒤 제품을 구입하고 해금 비밀번호/일련번호를 받은 사용자에게만 전체 기능이 제공되도록 한다. 아래아한글, MS Office 같은 거대한 프로그램들도 이제는 다 이런 식으로 동작하고 있다.

과거에는 그런 일련번호를 수학 공식을 기반으로 생성하곤 했다. 하지만 오프라인 환경에서 소프트웨어적인 알고리즘에만 의존하는 copy-protection은 역공학을 통해 뚫릴 수 있다. 마치 주민 등록 번호 자동 생성기처럼 말이다. 어둠의 경로를 개척하는 사람들이 이만저만 똘똘한 게 아니기 때문이다.

하지만 인터넷이 소프트웨어의 배포와 불법 복제만 편하게 만든 건 아니며, 까놓고 말해 개발사가 사용자의 사용 패턴을 미주알고주알 파악하는 것도 더 용이하게 만들었다. (뭐, 서버 유지 비용은 부담해야 하지만..) 제아무리 클라이언트 프로그램을 복제하고 뿌려도 온라인 게임을 돈 안 내고 즐기는 건 불가능하며, 스타크래프트 불법 복제 립버전으로 배틀넷 접속은 할 수 없다. 또한, 주민 등록 번호는 생성하더라도 방대한 DB에 일일이 접속하는 실명 인증은 수학 공식만으로 뚫을 수 없지 않던가? 그런 식으로 창과 방패는 발전하는 것 같다.

물론 업무용 프로그램들은 온라인 게임과 달리 업데이트 체크나 인증을 할 때만 인터넷에 접속하지, 나머지 동작은 전부 어차피 오프라인에서 행해지는 게 대부분이다. 그런 부류라면 실행 파일을 분석해서 인증을 시도하는 부분만 변조하고 크랙해 버릴 수도 있다. 파일이 전부 암호화되지 않고 헤더에만 암호가 기록돼 있다면 그 부분만 건너뛰어 버리면 되듯이 말이다. 그렇기 때문에 오프라인에서의 소프트웨어 보안도 예나 지금이나 여전히 필요하다.

이렇게 소프트웨어의 정품 사용 여부를 파악하기 위해서 반드시 필요한 절차가 있다. 바로 프로그램이 돌아가고 있는 기기 내지 제품의 사용자를 중복 없이 유일하게 식별하는 것이다!
매번 변하는 난수 씨앗이야 그냥 현재 시각을 기반으로 생성한다고 하지만, 한 컴퓨터에만 고유하게 적용되는 시리얼 키 같은 걸 생성하려면 전세계에서 유일하고 불변하고 전무후무한 식별 번호가 하드웨어 차원에서 부여되어 있어야 한다.

하다못해 예전에 도스용 게임 중에서도 저장 기능이 없는 대신, 각 레벨별로 암호 코드가 부여되는 게 있었다. 그 코드값을 알면 나중에 상위 레벨에서부터 게임을 시작할 수 있다.
그런데 문제는 그 코드값이 컴퓨터마다 제각각으로 생성된다는 것.. 그러니 매 컴퓨터에서 게임을 새로 시작해서 상위 레벨에 직접 진입을 해야만 코드값을 알 수 있었다.

그리고 어떤 소프트웨어가 정품 인증을 받았다거나, 셰어웨어의 경우 등록판이 생성되었는데.. 그 인증 정보는 레지스트리나 파일 형태로 저장되곤 한다. 물론 꼼꼼하게 암호화해서 말이다.
그 인증 정보에는 당연히 특정 컴퓨터의 식별자도 들어있어야 할 것이다. 그래야 그 인증 정보만 다른 컴퓨터에다가 슬쩍 복사해서 집어넣더라도 명의가 도용되지 않을 테니 말이다.
이런 식으로 컴퓨터의 고유 식별자는 프로그램 개발자의 입장에서는 매우 다양하게 활용될 수 있다.

자동차에는 외부에 노출된 번호판과 별개로, 자동차 껍데기 자체를 식별하는 차대 번호라는 게 있어서 엔진룸이나 도어 한구석에서 확인할 수 있다.
노트북 PC는 시리얼 번호가 밑바닥에 적혀 있다. 그리고 스마트폰도 기기를 식별하는 고유 번호가 있는 건 마찬가지이다. 사용자를 식별하는 USIM과는 당연히 별개로 말이다.

그런데 PC는 컴퓨터를 유일하게 식별하는 깔끔한 단일 통합 메커니즘이 의외로 존재하지 않는다.
먼 옛날에 펜티엄 3~4 시절에는 CPU의 일련번호를 얻어 오는 명령이 있었던 듯하나.. 예제 코드가 어셈블리어여서 이식성이 전혀 없으며, 요즘 CPU에서는 통하지도 않는다고 한다.

컴퓨터를 식별하기 위해서 지금까지 제일 만만하게 쓰여 온 방법은 맥 어드레스(mac address)라는 48비트짜리 숫자이다. 요즘은 휴대전화 번호가 사람을 식별하는 준 주민 등록 번호나 마찬가지이지 않는가? 그것처럼 통신망에서의 주소는 기기 식별 용도로 나쁘지 않은 선택이다.
하지만 얘를 쓰기에는 요즘 컴퓨터 네트워크는 계층과 종류가 너무 다양해졌으며, 사용자가 값을 변조도 그리 어렵지 않게 할 수 있기 때문에 여러 모로 약발이 다했다.

하드웨어적인 방법에만 너무 의존하면.. 사용자가 램을 더 달거나 하드디스크를 교체한 것만으로 프로그램 정품 인증이 실패하는 불상사가 벌어진다. 도대체 한 컴퓨터의 정체성을 결정하는 것이 무엇인가 하는 본질적인 고민에 부딪히게 된다.
소프트웨어적인 방법으로는 HKEY_LOCAL_MACHINE 상에 있는 Windows의 product ID라든가 Machine GUID가 있는데.. 이것은 변조하기 쉽고 운영체제를 다시 설치하는 것만으로도 무력화될 수 있는 게 약점이다.

최근엔 본인도 이런 쪽으로 고민할 일이 좀 있었다. 그러다가 WMI(Windows Management Instrumentation)라는 DB인지 API인지.. 정체를 알 수 없는 방대한 물건을 최근에야 난생 처음으로 접했다. 여기에 시스템 정보와 관련된 것들이 다 있었다. 하드디스크의 시리얼 번호, 마더보드의 시리얼 번호, 뭐 별별 것까지 다.. 그야말로 끝판왕이었다.

그 중 Win32_ComputerSystemProduct라는 클래스에 있는 uuid 값이.. SMBIOS, 즉 펌웨어 레벨에서 새겨져 있는 불변 유일한 컴퓨터 식별자 역할을 얼추 하겠다는 결론을 내리게 됐다. 최소한 맥 어드레스보다는 더 믿을 만하지 않을까? 명령 프롬프트에서는 wmic csproduct get uuid라고 하면 얻을 수 있다.

WMI에 접근하는 건 C#에서는 꽤 간단하고 쉽게 할 수 있어 보이던데 C++에서는 COM을 초기화하고 온갖 복잡한 인터페이스를 몇 단계씩 생성해야만 가능했다. DB 아니랄까봐, COM으로도 모자라서 팔자에 없는 SQL 쿼리까지 날려야 되더라! SELECT * from Win32_ComputerSystemProduct 같은 식으로 말이다.
누가 클래스 라이브러리 하나 만들어 놓은 것조차 없는 듯... 저 GUID 하나만 달랑 얻어 오는 용도로 쓰기에는 낭비가 꽤 심해 보인다.

Windows와 달리 mac 계열은 하드웨어/소프트웨어가 워낙 딱딱 들어맞는 일체형이니 저 정도의 복잡한 고민은 필요 없을 듯하다. 시스템 정보를 보면 나오는 시리얼 번호와 하드웨어 UUID만으로 식별과 관련된 모든 고민이 끝이지 않을까?
게다가 gethostuuid라는 함수 한 방으로 그 값을 바로 구할 수 있었다.

이미 유비쿼터스니 사물 인터넷 IoT니 뭐니 하면서 운영체제 불문하고 수많은 기기들이 인터넷에 접속하고 있으며, 그에 맞춰서 주소 공간이 월등히 넓어진 ipv6도 서서히 보급되고 있다. ipv6는 주소 공간의 크기가 UUID의 그것과 동일한 128비트이다.

그러니 Windows/mac, 그리고 안드로이드/iOS를 불문하고 전세계 전무후무 유일불변이 보장되는 기기 식별자 같은 것도 제정되지 않을까 싶다. 이미 제정돼 있는데 본인이 아직 모르는 것일 수도 있겠지만, PC 한정으로는 내가 알기로 딱 떨어지는 답은 아직까지 없다. 심증에 속하는 여러 정황상의 단서들을 모아서 물증인 것처럼 편의상 활용하고 있을 뿐이다.

Posted by 사무엘

2019/04/06 08:35 2019/04/06 08:35
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1605

Trackback URL : http://moogi.new21.org/tc/trackback/1605

Leave a comment

* 2014년에 썼던 글을 보완하여 다시 올린다.

옛날에 도스 시절에는 일명 '외부 명령'이라 하여 별도의 프로그램 형태로 존재하는 명령들이 있었다. format.com, diskcopy.exe 같은 것들.
이것들은 자기가 소속된 도스 버전을 가려서 동작했다. 가령, MS 도스 5.0이 설치된 컴퓨터에다 도스 6.x에 존재하는 새로운 유틸리티를 복사해 와서 실행하면, 실행에 필요한 파일들이 다 있다 하더라도 '도스 버전이 다릅니다'라는 에러 메시지와 함께 프로그램이 그냥 실행되지 않았다. 이것은 운영체제의 버전을 가려 가며 실행하는 프로그램을 본인이 난생 처음으로 접한 사례였다.

Windows에도 자신의 버전을 알려 주는 API가 응당 존재한다. 하지만 이건 지금 구동 중인 운영체제가 무엇인지를 알려 주는 편의 기능을 구현할 때나 사용할 만한 기능이다. 일반적인 프로그램이라면 About 대화상자 같은 데서 말이다.
만약 프로그램이 운영체제의 버전을 가려 가며 실행해야 한다면, 단순히 운영체제의 버전을 갖고 판단하는 건 썩 좋은 방법이 아니다. 내가 실제로 사용하고자 하는 기능을 요청해 보고(CoCreateInstance, LoadLibrary/GetProcAddress 등), 그 요청의 성공 여부에 따라 실행 여부를 결정하는 게 바람직하다.

뭐, 지금은 아무 의미가 없는 예가 돼 버렸다만,
가령 내 프로그램이 유니코드 API를 사용하기 때문에 Windows 9x에서는 실행을 거부해야 한다고 치자.
그렇다면 CreateWindowExW건 RegisterClassW건 유니코드 API를 실제로 호출해 본 뒤, 그게 실패하고 GetLastError()==ERROR_CALL_NOT_IMPLEMENTED가 돌아올 때 실행을 거부하면 된다. 운영체제의 외형보다는 그 운영체제의 실제 실행 결과를 보고 판단하는 게 낫다는 게 바로 이런 의미이다.

그런 것도 다 필요 없고 운영체제의 버전 숫자를 정말로 정확하게 알아 와야 한다면,
그 경우를 위해 태초에 GetVersion()이라는 간단한 함수가 있었다. 얘는 버전과 관련된 여러가지 정보들을 비트 자릿수별로 묶은 32비트 정수를 되돌렸다.

그 정보의 의미를 C언어의 비트필드 구조체로 나타내 보면 대충 다음과 같다. 주석으로 표시된 숫자는 윈도 7 기준으로 반환되는 값들이다.
(최신 Windows 10 기준의 반환값을 소개하지 않은 이유는 후술하도록 하겠다)

union WINVERSION {
    DWORD dwValue;
    struct {
        UINT nMajorVer: 8; //6
        UINT nMinorVer: 8; //1
        UINT nBuildNumber: 15; //7601
        UINT bWin9xOrWin32s: 1; //0
    };
};

WINVERSION os;
os.dwValue = ::GetVersion();

이 함수는 아무 매개변수도 필요하지 않으며, 리턴값도 DWORD 달랑 하나이니 미치도록 가볍고 사용하기 편하다. Windows 9x와 NT 계열이 공존하던 옛날에, 지금 운영체제가 (1) NT 계열인지를 알고 싶다면 GetVersion()&0x80000000 (최상위 비트)만 하면 OK였다.
그 뒤, NT 3.x인지 4.0인지, 9x 계열의 경우 95인지 98인지 ME인지 같은 건 (2) major와 minor 번호를 보고 판별하면 됐다. (3) 빌드 번호는... 딱히 막 중요한 정보는 아닌 듯하다.

그러나 이 함수는 문제점과 한계도 보였다. 한눈에 봐도 각 비트로부터 의미 있는 정보를 추출하는 게 매우 지저분하고 번거로웠다. HIWORD, LOBYTE 삽질이 싫다면, 저런 비트필드 구조체는 프로그래머가 재량껏 알아서 만들어야 했으며, 응용 프로그램이 이 정보를 잘못 취급하는 경우도 많았다.

비교할 필요가 없는 필드까지 다 비교를 해 버리는 바람에, Windows 95 이상에서 모두 동작할 수 있는 프로그램이 Windows 95에서“만” 동작하게 고정돼 버리기도 했다. 혹은 Windows NT 4.0이 NT 3.51보다 낮은 버전으로 취급되는 촌극도 벌어졌다. (리틀 엔디언 기준으로 저 구조체를 보면, minor 버전이 major 버전보다 더 높은 자릿수에 놓여 있음)

더구나 운영체제의 정체성을 나타내는 정보는 단순히 버전 번호와 빌드 번호 이상으로 더욱 복잡해져 왔다. NT 계열의 경우 당장 서비스 팩이 있고, 이게 무슨 에디션인지도(홈? 서버? 워크스테이션? 등) 알 필요가 있는데 단순히 숫자 하나만 달랑 되돌리는 함수로는 그런 걸 알려 줄 수가 없었다.

이런 문제를 해결하기 위해 Windows 95 내지 NT 3.5에서는 OSVERSIONINFO라는 구조체를 인자로 받는 GetVersionEx라는 함수가 추가되었다. major, minor 버전 번호와 빌드 번호, 운영체제 계열이 모두 독립된 구조체 멤버로 독립하였으며, (4) 서비스 팩 내역도 완전한 문자열 형태로 되돌려 주니 버전 정보를 다루기가 편해졌다.

이 구조체는 맨 앞에 자신의 크기를 써 주게 돼 있으며, 덕분에 추후 확장이 가능한 형태이다.
Windows 2000부터는 OSVERSIONINFOEX 구조체가 추가됐다. 확장된 구조체는 서비스 팩의 번호조차도 major와 minor 꼴로 받을 수 있으며, (5) 같은 NT 계열 중에서도 클라이언트 라인과 서버 라인을 구분할 수 있다(wProductType==VER_NT_WORKSTATION / VER_NT_SERVER). Windows XP와 Server 2003은 버전 번호가 5.1과 5.2로 서로 달랐지만, 후대 버전부터는 버전 번호는 동일하고 이걸로 구분을 해야 한다. (Vista / Server 2008, 10 / Server 2016 같은..)

그리고 클라이언트 라인은 XP 이래로 오늘날의 10까지 (6) home과 pro 에디션 구분이 거의 관행이 돼 있는데.. 이건 wSuiteMask 멤버의 비트 플래그 VER_SUITE_PERSONAL (0x200)의 존재 여부로 판별 가능하다. 저 플래그가 존재하는 게 home 에디션이다.
VER_SUITE_* 다른 플래그들 중에는 Windows XP의 embedded 에디션, enterprise 에디션 같은 걸 나타내는 것들도 있으니 참고하면 된다.

요컨대 9x/NT 이후로도 클라이언트/서버, home/pro 같은 복잡한 구분이 계속 이어지는 것을 알 수 있다. 그래도 GetVersionEx 한 방이면 모든 정보를 얻을 수 있다.

이걸로 모든 이야기가 끝이 났으면 좋겠지만.. 아이고, 끝이 아니다. GetVersionEx 함수는 2010년대 이후로 마소의 정책상 사용이 더 권장되지 않는 deprecate 판정을 받고, 시간이 정지해 버렸다.
이 함수는 아무런 단서가 없는 환경에서는 Windows 8, 즉 버전 6.2보다 더 높은 값을 되돌리지 않는 샌드박스가 되었다. 실제로는 이 컴퓨터에 Windows 8.1이나 10이 돌아가고 있더라도 말이다. 이와 관련된 더 자세한 정보를 원한다면 다음 URL을 참고하시기 바란다.

이제 이 함수는 응용 프로그램에게 그 응용 프로그램보다 나중에 출시된 운영체제에 대한 정보는 주지 않기로 작정한 듯하다. GetVersionEx가 샌드박스 없이 실제 자기 버전을 되돌리는 조건은 다음과 같다.

  • 응용 프로그램의 manifest XML에(compatibility-application-supportedOS) 그 운영체제의 GUID가 등록되어 있다.
  • 혹은 응용 프로그램의 PE 헤더에 OS의 최소 요구 버전이 최신 운영체제의 버전으로 맞춰져 있다. Windows 8.1의 경우 6.3, Windows 10이라면 10.0이 되겠다.

운영체제와 함께 제공되는 메모장 같은 기본 프로그램들은 후자의 조치를 취한 상태이다. 이렇게 빌드된 프로그램에서는 GetVersionEx가 해당 버전을 정확하게 되돌린다. 하지만 이런 프로그램은 이전 버전 운영체제에서는 아예 전혀 동작하지 않으므로, 3rd-party 응용 프로그램이라면 이런 방법을 쓰기 곤란하다. 그러니 매니페스트 등록을 해야 한다.

물론 마소에서 2015년의 Windows 10부터는 기존 버전 번호 자체를 10.0으로 동결시켜 버리고 더 바꾸지 않기로 작정했다. 그러니 버전 번호 변경으로 인해 GUID를 또 등록하는 식의 혼란은 앞으로 더 없을 것이다.

운영체제의 버전의 절대값을 되돌리는 GetVersionEx 대신 마소에서 사용을 권장하는 함수는... 지금 운영체제의 버전이 응용 프로그램이 제시하는 버전보다 상대적으로 높은지 안 높은지 여부만을 되돌리는 VerifyVersionInfo 함수이다. 그리고 이걸 기반으로 IsWindows10OrGreater 같은 helper 함수들도 만들어져 있다. (VersionHelpers.h)

하지만 이 함수들도 내부적으로 GetVersionEx의 결과값을 기반으로 비교를 하는 것이기 때문에 앞서 언급한 샌드박스의 제약을 받는 건 마찬가지이다.

샌드박스 없이 운영체제의 정확한 버전을 얻어 오는 함수는 크게 두 군데에 있다.
먼저, 의외로 네트워크 API이다. 그렇다고 소켓 API 같은 건 아니고, Windows에서 독자적으로 제공하는 함수 중에 내 로컬 컴퓨터를 포함하여 원격 컴퓨터에 설치된 운영체제의 버전을 얻어 오는 함수가 있다. 대략 다음과 같이 코드를 작성하면 된다.

#include <LM.h>
#pragma comment(lib, "netapi32")

WKSTA_INFO_100 *p;
::NetWkstaGetInfo(NULL, 100, (LPBYTE *)&p);
printf("%d, %d\n", p->wki100_ver_major, p->wki100_ver_minor); //10, 0
::NetApiBufferFree(p);

저기 100은 수효를 나타내는 게 아니며 각각의 숫자들이 별개의 의미를 지님에도 불구하고, 상수 명칭이 존재하지 않아서 그냥 생으로 100을 넘겨 줘야 한다.
운영체제 버전 하나 좀 얻자고 웬 생뚱맞은 분야의 API를 써야 하는 것도 삽질스럽지만.. 저 함수를 통해서는 그냥 major와 minor 버전 번호만 얻을 수 있다. 서비스 팩이나 빌드 번호 같은 세부 정보는 얻을 수 없다.

저거 말고 다른 대안으로는.. ntdll.dll에 있는 native API인 RtlGetVersion을 써도 된다.
OSVERSIONINFO(EX)의 포인터를 받아들이고 정수값을 리턴하므로 prototype이 기존 GetVersionEx와 거의 동일하다.
단, native API 버전은 성공한 경우의 리턴값이 0이다. 리턴 타입이 BOOL이 아닌 셈이다.

얘는 Windows 8.1 내지 10 같은 요즘 운영체제에서는 잘 동작하는데, 과거의 Windows 2000에서는 GetVersionEx와 달리 서비스 팩 정보를 되돌리지 않았던 것으로 기억한다. 구형 OS에서는 오히려 기존 함수를 쓰는 게 더 낫다. 거 참..;;
Windows가 지난 20년 동안 운영체제의 버전과 제품 종류를 얻는 그 단순한 절차만 해도 얼마나 복잡하고 지저분해져 왔는지를 확인할 수 있다. 관련 여담을 몇 가지 더 남기는 것으로 글을 맺고자 한다.

  • OSVERSIONINFOEX는 C++ 상속 문법 같은 걸 이용해서 선언된 게 아닌 관계로, OSVERSIONINFO와는 언어 차원에서 아무런 연결 고리가 없다. GetVersionEx에다가 전달할 때는 OSVERSIONINFO*로 reinterpret_cast를 해 줘야 된다.
  • 과거 Windows XP에는 media center 에디션 내지 태블릿 PC 에디션 같은 바리에이션이 있었는데.. 이거 여부를 얻는 건 GetVersionEx가 아니라 GetSystemMetric라는 다소 생뚱맞은 함수에 있었다. SM_MEDIACENTER, SM_TABLETPC처럼 말이다 .
  • 끝으로, Windows 10부터는 (7) 릴리스 연-월을 나타내는 4자리 숫자가 사실상 버전 번호가 됐으니 이걸 표시해 줘야 할 것이다. 그런데 이건.. 본인이 아는 방법은 그냥 무식한 레지스트리 조회가 유일하며, 공식적인 API가 따로 있지 않다.;;;

Posted by 사무엘

2019/03/14 08:36 2019/03/14 08:36
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1596

Trackback URL : http://moogi.new21.org/tc/trackback/1596

Leave a comment

오늘은 Windows라는 운영체제가 GUI 프로그래밍 용도로 제공하는 공용 컨트롤들 중의 하나인 리스트뷰(List-view) 컨트롤에 대해 자세히 알아보도록 하겠다. (이름에서 '뷰'는 종종 생략되기도 함)
결론부터 말하자면 얘는 정말 세심하게 설계된 다재다능한 요물이다. 동일한 규격을 가진 다수의 아이템들, 특히 그림과 글자가 같이 가미된 아이템을 표시하는 모든 방식과 가능성을 고려해서 만들어졌다. 그래서 정말 많은 기능들을 제공한다.

리스트뷰가 기존의 재래식 초간단 리스트박스와 다른 점은 다음과 같다.

  • 리스트뷰는 글자뿐만 아니라 곁들여진 그림도 태생적으로 같이 처리 가능하다. 리스트박스에서는 그림과 글자를 같이 표시하기 위해서 얄짤없이 owner-draw로 가야 했다.
  • 마우스의 동작이 다르다. 리스트박스는 내부를 왼쪽 버튼으로 아이템을 선택해서 드래그 하면 선택막대가 자동으로 쭉 바뀌며 스크롤도 된다. 하지만 리스트뷰는 그렇지 않다.
  • 키보드의 동작도 다르다. 아이템을 복수 선택할 때 리스트뷰는 Ctrl+화살표를 눌러서 포커스만 이동시키고 Ctrl+Space로 선택을 하지만 리스트박스는 Shift+F8과 space 같은 다른 글쇠를 사용한다. 리스트뷰는 F2를 눌러서 아이템의 이름을 바꾸는 기능도 있지만 리스트박스는 그렇지 않다.

아울러, 리스트뷰가 같이 추가된 공용 컨트롤인 트리뷰(Tree-view) 컨트롤과 다른 점은 다음과 같다.

  • 트리는 아이템 하나를 HTREEITEM이라는 별도의 자료형으로 식별하지만, 리스트는 그냥 인덱스 번호이다. 트리는 노드 포인터 기반의 이산적인 컨테이너를 쓰지만, 리스트는 내부적으로 배열과 유사한 컨테이너를 쓰는 듯하다.
  • 리스트는 아이템의 복수 선택이 가능하지만 트리는 그렇지 않다.
  • 트리는 리스트와 같은 다양한 view 모드가 존재하지 않는다.
  • 아이템의 텍스트를 진하게 표시하는 state 플래그가 트리에는 있지만 리스트에는 없다.

리스트박스와 위상이 비슷한 자매 컨트롤(?)은 콤보박스이다. 하지만 리스트뷰와 위상이 비슷한 자매 컨트롤은 트리뷰라고 할 수 있다.
왼쪽에 트리뷰, 오른쪽에 리스트뷰를 배치한 프로그램으로는 탐색기, 레지스트리 편집기, 시스템 정보 등 의외로 꽤 많다. 왼쪽에서 카테고리를 선택하면 오른쪽에서 세부 정보가 표시되는 것이다. 오죽했으면 Visual C++의 MFC 프로젝트 마법사에도 요런 형태의 프로그램을 만드는 템플릿이 제공될 정도이다.

옛날에는 리스트박스를 서브클래싱 해서 drag & drop을 구현하고, owner-draw와 item data를 이용해서 얼추 트리 계층 구조라든가 check list를 구현하고, 파일이나 디렉터리나 드라이브 목록을 채워 주는 리스트를 만드는 등.. 별별 짓을 다 했다. 그리고 Visual Basic 부류의 RAD 툴들은 그걸 미리 구현해 놓은 리스트를 컴포넌트 형태로 제공했었다. 하지만 리스트뷰와 트리뷰 공용 컨트롤이 등장하면서 리스트박스의 역할이 상당수 분담되었다.

Windows 탐색기의 보기 메뉴에서 보는 바와 같이 리스트뷰 컨트롤에는 다양한 보기 모드가 있다.

(1) 큰 아이콘
아이콘이 중심이고 이를 설명하는 주 텍스트가 아이콘의 하단 중앙에 찍힌다. 이걸로 끝. 아이콘의 크기는 무엇이 되어도 상관없지만 보통은 표준 아이콘 크기인 32*32 또는 그보다 약간 더 큰 48*48이 쓰인다.
탐색기에서 확대 배율 조정이 되는 대부분의 모드들은 이 모드에 속한다. 아이콘의 크기만 바꾸는 거니까.. (보통 아이콘, 큰 아이콘, 아주 큰 아이콘..) 또한 당장 바탕 화면에 표시된 아이콘들도 다 리스트뷰의 이 모드인 것을 알 수 있다.

사용자 삽입 이미지

(2) 작은 아이콘
글자의 크기와 대등한 크기인 작은 아이콘이 쓰이며, 아이콘의 아래가 아니라 오른쪽에 주 텍스트가 나란히 찍힌다.

사용자 삽입 이미지

(3) 목록
아이템 하나가 표시된 모습이 작은 아이콘 모드와 완전히 동일하다. 그렇기 때문에 '작은 아이콘'과 차이가 무엇인지 언뜻 봐서는 구분하기 어렵다. 하지만 작은 아이콘(+ 큰 아이콘도 포함)에서는, 아이템을 드래그 해서 화면의 아무 위치로나 옮길 수가 있는 반면, 목록 모드는 그렇지 않다. i째 아이템은 현재의 스크롤 위치 기준으로 반드시 그에 상응하는 위치에 있어야 하며, 아무 위치로나 옮길 수 없다.

사용자 삽입 이미지

(4) 자세히(일명 report view)
한 줄에 아이템이 오로지 하나만 찍힌다. 작은 아이콘, 주 텍스트, 그 다음으로 n개의 부 텍스트가 마치 표처럼 일목요연하게 표시된다. 즉, 이 모드는 부 텍스트를 표 형태로 모두 볼 수 있는 유일한 모드이며, 상단에 헤더 컨트롤이 등장해서 쓰이는 유일한 모드이기도 하다.

사용자 삽입 이미지

사실, 헤더 컨트롤만 별도로 따로 생성할 수도 있다. 얘만으로도 각종 메시지 스펙이 공개돼 있는 별개의 공용 컨트롤이기 때문이다.
하지만 우리가 아주 특수한 사연이 있어서 리스트뷰 컨트롤 같은 거창한 물건을 직접 자체 구현이라도 하지 않는 한, 헤더만 끄집어내서 사용할 일은 별로 없을 것 같다.

지금까지 소개한 4종류의 모드를 정리하자면, 아이콘 모드들은 align을 어찌 하느냐에 따라서 상하와 좌우 스크롤바를 모두 볼 수 있고, '목록' 모드는 좌우 스크롤바만 볼 수 있다.
'자세히' 모드는 개수가 초과될 때는 상하 스크롤바이고, 아이템을 표시하는 폭이 초과됐을 때만 좌우 스크롤바를 볼 수 있다.

그리고 아이콘 모드는 기존 리스트박스에는 전혀 없던 새로운 기능이며, 기존 리스트박스와 가장 비슷한 모드는 '자세히' 내지 '목록' 모드라는 것을 알 수 있다. 이 두 모드에서는 아이콘은 필수가 아닌 그냥 선택, 옵션이다. 기존 리스트박스처럼 그림 없이 글자를 출력하는 용도로만 써도 된다.

이들에 비해 '작은 아이콘' 모드는 정체성이 불분명해서 사실 잘 쓰이지 않는다. 아이콘을 강조하고 싶으면 '큰 아이콘'으로 가면 되고, 좀 더 예쁘게 일목요연하게 아이템들을 출력하려면 '목록'(간단히) 또는 '자세히'로 가면 되기 때문이다. 저 그림에서도 보다시피 작은 아이콘은 폭이 들쭉날쭉이어서 보기에도 별로 좋지 않다.

그래서 Windows XP에서는 제5의 새로운 모드가 추가됐다. 바로..

(5) 타일
큰 아이콘을 사용하는데, 주 텍스트는 아이콘의 아래가 아닌 오른쪽에 출력된다.
아이콘이 좀 큰 편이니 주 텍스트의 아래에도 여유 공간이 생기는데, 거기에는 부 텍스트 중에서 사용자가 지정한 것을 덤으로 출력해 준다.

사용자 삽입 이미지

이것도 굉장히 참신한 발상인 것 같다. 타일의 폭은 사용자가 임의로 지정 가능하다.
align은 아이콘 모드처럼 left와 top을 모두 지정 가능하다. 다만, 아이템들의 위치까지 아이콘 모드처럼 임의 지정 가능한지는 잘 모르겠다.

원래 리스트뷰 컨트롤의 보기 모드는 4종류이다 보니.. 윈도우 스타일에서 0부터 3까지 딱 2개의 최하위 비트를 사용하여 지정하게 돼 있었다.
컨트롤을 생성하고 아이템들을 잔뜩 추가한 뒤에도 모드를 변경할 수 있었다. SetWindowLongPtr을 이용해서 스타일 값을 변경하면 컨트롤이 이를 인식해서 모드를 변경했다.

그런데 제5의 모드는 이런 식으로 지정할 수 없게 됐다. 리스트뷰 컨트롤은 기능이 워낙 너무 많아서 스타일, 확장 스타일, 거기에다 자신만의 고유한 전용 확장 스타일까지(LVM_SETEXTENDEDLISTVIEWSTYLE) 비트 플래그들이 꽉 찼기 때문이다.
결국은 LVM_SETVIEW라고 보기 모드를 지정하는 전용 메시지가 추가됐다. 새로운 보기 모드를 겨우 하나 추가하기 위해서였다.

네이버나 다음의 블로그들만 들어가 봐도 제목 목록만 표시, 본문까지 약간 포함해서 타일 형태로 표시.. 처럼 적어도 두세 종류의 보기 모드가 있는 걸 알 수 있다. 리스트뷰도 그런 식으로 그림과 글자의 표시 비율, 아이템당 전체 크기 같은 다양한 변수를 이런 식으로 제어할 수 있다고 생각하면 된다.
아이콘이 들어갈 자리에 사람 얼굴이 들어가면 무슨 인사기록표나 선거 후보 목록을 출력할 수 있을 것이고, 한자가 들어가면 옥편· 자전 내용을 이런 식으로 출력할 수 있을 것이다.

그럼 이제부터는 리스트뷰 컨트롤의 주요 개념이나 기능에 대해서 분야별로 간단히 소개한 뒤 글을 맺도록 하겠다.

1. image list

리스트와 트리 컨트롤은 아이템들 옆에 출력할 다양한 종류의 아이콘 그림들을 한데 관리하기 위해서 무슨 HICON을 몇백 개 내부적으로 관리..하는 건 아니고 image list라는 자료 구조를 공통으로 사용한다. image list는 마치 애니메이션 프레임처럼 크기가 동일한 여러 그림들의 배열이라고 생각하면 되며, 아이콘 핸들도 물론 손쉽게 등록할 수 있다. 투명색은 이미지 내부의 특정 배경색 또는 별도의 마스크 비트맵 중 편한 것으로 지정 가능하다.

또한 트리에서는 작은 아이콘이라는 한 종류만 사용하지만, 리스트 컨트롤에서는 구조적으로 큰 아이콘, 작은 아이콘 두 종류를 나눠서 지정 가능하다.
그리고 한 아이템의 아이콘에 대해서 여러 종류의 이미지를 한데 겹쳐서(overlay) 지정할 수도 있다. 파일이라면 '바로가기'임을 나타내는 자그마한 화살표라든가, 버전 관리 시스템에서 Up-to-date, modified 같은 상태를 나타내는 자그마한 modifier 그림이 바로 아이콘 overlay를 이용해서 표시된 것이다.

2. 그룹 분류

Windows XP에서는 타일 모드에 이어 리스트뷰 컨트롤에 아주 획기적인 기능이 하나 추가됐는데, 바로 '그룹' 기능이다. 필요하다면 그룹 내지 카테고리라는 것을 등록해 놓은 뒤, 아이템들별로 소속 그룹을 지정하면 이것들이 그룹별로 분류되어 딱 일목요연하게 표시된다.

사용자 삽입 이미지

그룹이 처음으로 도입된 XP에서는 이것 말고 다른 기능은 없다. Vista에서는 그룹에 대해 [+], [-] 버튼을 눌러서 마치 트리 컨트롤처럼 collapse/expand이 되게 하는 기능이 추가되었다. 단, 응용 프로그램에서 그게 가능하도록 별도의 비트 플래그를 넣은 그룹에 대해서만 그렇게 동작한다.
그룹은 다른 보기 모드에서는 다 지원되고 '목록' 모드만 열외이다.

3. 수많은 기능과 복잡한 API

리스트뷰 컨트롤은 당장 마소에서도 적극 사용하고 있다 보니, 자기 필요에 따라서 이것저것 수많은 기능들이 추가돼 왔다.
특히 IE4 시절에는 Active 데스크톱이니 뭐니 하면서 뭐든지 웹페이지처럼 보이게 하는 게 유행이었다. 리스트뷰 컨트롤의 아이템을 클릭하는 것조차 밑줄 쳐진 링크를 클릭하는 것과 비슷하게 보이게 하는 옵션은.. 음~ 정말 비장함이 느껴진다.

리스트뷰는 기능이 너무 많고, 공용 컨트롤 특유의 그 조작감까지 더해져서 다루기가 귀찮고 까다롭다. 리스트박스처럼 간단하게 LB_ADDSTRING + "문자열" 한 방으로 아이템을 추가할 수 없다. 뭘 더하고 고치려면 기본적으로 LVITEM 구조체 선언하고 마스크 플래그 지정하고..

더구나 문자열 부분 멤버는 읽기 쓰기 겸용으로 모두 쓰인다. Set 용도로 읽기 전용 문자열 포인터를 집어넣으려 해도 부득이하게 PTSTR 멤버에다가 const_cast를 해 줘야 된다. PTSTR과 PCTSTR을 공용체로라도 좀 같이 넣어 주지 하는 아쉬운 생각이 든다.

그리고 아이템 drag & drop은 컨트롤에서 우리에게 이벤트만 날려 주고 그걸로 끝이다. 드래그용 이미지를 생성하고 마우스 포인터 모양을 바꾸고 실제로 drop 처리를 하는 것, 아이콘 모드의 경우 실제 위치를 변경하는 LVM_SETITEMPOSITION 요청 따위는... 머리부터 발끝까지 사용자가 전부 일일이 구현해야 한다. 이거 일이 여간 번거로운 게 아니다.

헤더 클릭 정렬도 마찬가지다. 컨트롤이 자동으로 해 주지 않는다. 클릭된 헤더에 대해서 오름차순/내림차순/무정렬 상태를 나타내는 ▲▼ 모양을 표시하는 것, 다른 헤더에 있던 마크는 제거하는 것까지 전부 미주알고주알 우리가 해 줘야 되며, 아이템 비교 함수도 우리가 공급해 줘야 한다.
좋게 말하면 customize의 폭이 큰 것이고 나쁘게 말하면.. 귀찮다. 물론 재래식 리스트박스는 한번 등록된 아이템은 텍스트를 고치거나 순서를 변경하는 기능 자체가 전무했으니 그것보다는 상황이 나아진 셈이다.

4. 시스템 색상 변경

어떤 윈도우가 WM_PAINT를 받아서 자기 내용을 그릴 때, 매번 GetSysColor나 GetSysColorBrush를 호출하고, 매번 색깔을 새로 지정하고 펜과 브러시를 새로 생성한다면.. 시스템 색상이 나중에 달라지더라도 별 상관 없다.
하지만 성능을 위해서 이런 GDI 개체를 보관해 놓는다거나, 특정 시스템 색상이 합성된 상태로 비트맵 같은 걸 저장하고 있다면(일종의 캐싱).. 그것들은 시스템 색상이 바뀌었을 때 갱신되어야 한다.

이 상태를 알리는 메시지가 바로 WM_SYSCOLORCHANGE이다. 이제는 macOS조차도 최신 10.14 '모하비'에서 Dark 테마가 추가되었으니 시스템 색상 변경과 비슷한 개념이 도입된 셈이다. Windows는 다른 색깔 테마들은 다 없어졌지만 고대비 블랙/화이트만이 특수한 용도로 남아 있다.

WM_SYSCOLORCHANGE는 top-level 윈도우들에게 전파된다. 차일드에 속하는 리스트뷰 컨트롤이 이 메시지를 직접 받지는 못한다. 아이콘을 사용하지 않을 때는 별 문제가 없는데, 아이콘을 사용하는 컨트롤에 대해서 이 메시지를 수동으로 전해 줘야 한다. 그리하지 않으면 화면 배경의 흑백이 바뀌어도 쟤는 그게 반영되지 않아서 색깔 배색이 어색해지더라.

색깔 변경 통지도 마치 클립보드의 내용 변경 통지처럼 원하는 윈도우가 신청하면 top/bottom 위상을 불문하고 직통으로 받을 수 있어야 하지 않나 싶다. 이렇게 부모 윈도우가 일일이 전해 줘야 하는 건 디자인상 문제가 있어 보인다.

5. Ctrl+휠 인식

리스트뷰 컨트롤 내부에서 마우스 휠이 굴러갔다면 그렇다면 창 내부를 스크롤 하면 된다. 즉, 자체적으로 처리하면 되고 굳이 부모 윈도우에게 알려 줄 필요가 없다.
하지만 Ctrl+휠은 화면 확대 배율을 변경하는 용도로 쓰이는 게 요즘 추세이다. 응용 프로그램마다 자기가 사용하는 리스트뷰에서 지원하고자 하는 모드가 다를 테니, 이를 운영체제에서 임의로 일괄적으로 자동 지원할 수는 없다.

결국 Ctrl+휠은 그냥 휠과는 달리 부모 윈도우로 통지해 주는 게 바람직해 보인다. 하지만 이와 관련된 event notification은 공식적으로 존재하지 않는다. 탐색기는 Ctrl+휠을 어떻게 구현했는지가 궁금해진다. 하긴, 탐색기는 리스트뷰 컨트롤도 워낙 많이 마개조했으니 윈도우 프로시저를 서브클래싱 해서 메시지 전체를 통째로 가로채 버렸다면 그 정도 구현쯤은 일도 아니긴 했을 것이다.

6. 체크리스트 모드에서의 버그

리스트뷰 컨트롤(그리고 트리 컨트롤도)에는 모든 항목들에 대해 체크박스를 넣는 옵션이 있다.
보통은 아이콘 자리에다가 체크박스 이미지를 집어넣는 꼼수를 동원해서 야메로 체크리스트 모드를 구현하는 편인데.. 이 옵션은 이미지와 별개로 체크박스를 또 표시해 준다.

이 기능은 공용 컨트롤이 처음 개발되던 때부터 있었던 건 아니고 Windows 98 + IE4 내지 5 타이밍 때 추가되었다. 이 기능이 처음부터 지원됐다면 리스트뷰 컨트롤의 Selection model이라는 속성 하에서 "단일 선택 / 체크박스 / 복수 선택" 중의 한 옵션으로 지원되는 게 바람직했을 것이다. 체크박스 모드에서 또 복수 선택을 사용할 일은 없을 테니까 말이다.

그리고 체크리스트 모드는 그 정의상 보기 모드들 중에서는 '목록' 모드와 가장 잘 어울리고 아니면 기껏해야 '자세히' 모드와도 추가로 어울린다. 큰 아이콘이 부각되는 모드와는 아무래도 영 어울리지 않는데, 그래도 원한다면 그 모드에서도 체크리스트를 사용할 수는 있다.
다만, 이 모드에서 키보드나 마우스로 체크 표시를 반복하면 선택막대가 갈수록 진해지는데.. 이건 명백히 버그로 보인다. 고전 테마나 XP~7 같은 구버전에서는 이런 현상이 없었고 Windows 8~10에서만 저런다!

사용자 삽입 이미지


Posted by 사무엘

2019/01/13 08:36 2019/01/13 08:36
, ,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1575

Trackback URL : http://moogi.new21.org/tc/trackback/1575

Comments List

  1. 사포 2019/01/14 11:16 # M/D Reply Permalink

    윈도우 API들을 보다보면 초대형 프로젝트치고 정말 잘 설계했다고 느껴지는 부분이 많이 있더라구요. GUI 쪽도 그런 것 같아요.

    1. 사무엘 2019/01/14 13:03 # M/D Permalink

      네, 동감합니다. 뭐, 일부 지저분한 레거시, 첫단추를 좀 잘못 끼운 듯한 API 구조도 없지는 않지만.. Windows가 역사가 엄청 길고 레거시 지원이 정말 빵빵한 걸 감안하면 그 정도도 감지덕지이죠.
      사포 님, 오랜만에 뵙네요. 반갑습니다. ^^;;

  2. 사포 2019/01/16 10:24 # M/D Reply Permalink

    ㅎㅎ 오랜만이에용 새해 복 많이 받으세요 ^-^

Leave a comment
« Previous : 1 : 2 : 3 : 4 : 5 : ... 25 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/08   »
        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:
1239369
Today:
101
Yesterday:
295