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

1. elseif 키워드

프로그래밍 언어에 따라서는 else if를 한데 묶은 축약형인 elseif 또는 elif 키워드를 별도로 제공하는 경우가 있다.
베이직이나 파이썬, 그리고 프로그래밍 요소 중에 없는 게 없는 백과사전형 언어인 Ada에는 저게 있다.

하지만 파스칼, C/C++이나 그 파생형 언어들은 전통적으로 그게 없다. 굳이 그걸 또 제공할 필요 없이 기존 if/else만으로도 동일한 표현력과 계산 능력 자체는 낼 수 있으며,
또한 더 큰 이유로는, 이들 언어는 안 그래도 공백이나 줄바꿈에 구애를 받지 않는 freeform 문법이기 때문이다. 필요하다면 어차피 else if를 한 줄에 나란히 연달아 써도 elseif와 얼추 비슷한 비주얼을 만들 수 있다. (컴파일러의 구문 분석 스택은 복잡해지겠지만..) 베이직과 파이썬은 그렇지 않다.

elseif 축약형은 else 절에서 실행되는 구문이 다음 if 절에 '완전히' 포함되어 있을 때 유용하다.
원래는 else 다음에 소스 코드의 들여쓰기가 한 단계 증가해야 하지만 그렇게 하기는 귀찮고..
수평적인 들여쓰기 단계에서 여러 개의 if를 대등한 위상에서 마치 switch-case처럼 늘어놓고 싶을 때 elseif가 쓰인다.

이런 점에서 보면 elseif 축약은 if-else에 대해서 tail-cut recursion을 한 것과 개념적으로 유사하다.
함수 재귀호출 뒤에 또 다른 추가적인 계산이 없다면, 그런 단순 재귀호출 정도는 스택을 사용하지 않는(= 한 단계 깊이 들어가는) 단순 반복문으로 바꾸는 것 말이다.

사실 C/C++은 elseif 축약이라는 개념은 언어 자체엔 없고 전처리기에만 #elif라는 형태로 있다.
전처리기는 알다시피 freeform 문법이 아니기 때문에 elif 없이 else와 if를 동시에 표현하려면 얄짤없이 줄 수가 둘로 늘어나야 하니,
문법을 최대한 간단하게 만들고 싶어서 부득이 그런 지시자를 넣은 것 같다.

2. NULL 포인터와 0

하루는 통상적으로 사용하던 #define NULL을 0에서 nullptr로 바꾸고 날개셋 코드를 리빌드해 봤다. 그랬더니.. 생각지 못했던 곳에서 엽기적인 컴파일 에러가 떴다.

아니 내가 머리에 총 맞았었나.. 왜 bool 변수에다가 NULL을 대입할 생각을 했지? =_=;;
HRESULT 리턴값에다가 S_OK 대신에 return NULL을 해 놓은 건 도대체 뭔 조화냐.
그리고 그 정도는 애교고.. obj=NULL이 원래는 컴파일 에러가 났어야 했는데 잘못된 코드를 생성하며 지나쳐 버리는 경우가 있었다. 포인터를 별도의 클래스로 리팩터링하는 과정에서 실수가 들어간 것이다.

그 클래스가 정수 하나를 인자로 받는 생성자가 있기 때문에 obj=Class(0)으로 자동으로 처리되고 넘어갔는데, 그 클래스는 독자적인 메모리 할당이 있으면서 대입 연산자 같은 것도 별도로 존재하지 않았다.
이런 일을 막으려고 C++엔 나중에 생성자에 explicit이라는 속성을 지정하는 키워드가 추가되었지만 그걸 사용하지 않는 레거시 코드를 어찌할 수는 없는 노릇이고..

아무튼 언어에서 type-safety를 강화하는 게 이렇게 중요하다는 걸 알 수 있었다.
Windows 플랫폼 헤더 include에서 NULL의 definition이 nullptr로 바뀌는 날이 언제쯤 올까? 옛날에 16비트에서 32비트로 넘어갈 때는 핸들 타입에 대한 type-safety를 강화하면서 STRICT 상수가 도입된 적이 있었는데.

NULL은 C 시절에 (void *)0, 초창기 C++에서는 타입 오버로딩 때문에 불가피하게 그냥 0이다가 이제는 nullptr로 가장 안전하게 변모했다.
개인적으론, PSTR ptr = false; 도 컴파일러 차원에서 안 되게 좀 막았으면 좋겠으나.. 포인터에 0상수 대입은 뭐 어찌할 수 없는가 보다.

3. 자바의 문자열

자바(Java)로 코딩을 하다 보면 나처럼 C++ 사고방식에 머리가 완전히 굳은 사람의 관점에서 봤을 때 궁금하거나 불편하다고 느껴지는 점이 종종 발견된다.
int 같은 기본 자료형이 아니면 나머지는 모조리 클래스이다 보니 한 함수에서 데이터 참조용으로나 잠깐 사용하고 마는 int - string 쌍 같은 것도 못 만드는지? 그런 것도 죄다 새 클래스로 만들어서 new로 할당해야 하는지?

그리고 기본 자료형은 값으로만 전달할 수 있으니 int의 swap 함수조차 만들 수 없는 건 너무 불편하지 않은지?
인클루드가 없는데 자신 외의 다른 클래스에 존재하는 public static final int값이 switch case 상수로 들어오는 게 가능한지? 등등..

이와 관련되어 문자열은 역시 자바 언어에서 좀 어정쩡한 위치를 차지하며 특이하게 취급되는 물건이다.
얘는 일단 태생은 기본 자료형이 아닌 객체/클래스에 더 가깝다. 그래서 타입의 이름도 소문자가 아닌 대문자 S로 시작하며, 이 개체는 가리키는 게 없는 null 상태가 존재할 수 있다.

그러나 얘는 문자열 상수의 대입을 위해서 매번 new를 해 줘야 하는 건 또 아니다. 이건 예외적으로 취급되는 듯하다.
그럼 그냥 String a; 라고만 하면 얘는 길이가 0인 빈 문자열인가(""), 아니면 null인가? 그리고 지역 변수일 때와 클래스 멤버 변수일 때에도 그 정책이 동일한가? 뭐 직접 회사에서 프로그램을 짜 본 경험으로는 전자인 것 같긴 하다.

단, 자바의 문자열을 다룰 때는 주의해야 할 점이 있다. 자바 프로그래머라면 이미 잘 숙지하고 계시겠지만, 문자열의 값 비교를 ==로 해서는 안 된다는 것이다. equals라는 메소드를 써야 한다.
==를 쓰면? C/C++식으로 얘기하자면 문자열이 들어있는 메모리 포인터끼리의 비교가 돼 버린다. 애초에 포인터의 사용을 기피하고 다른 걸로 대체하는 컨셉의 언어에서, 이런 동작은 99% 이상의 경우는 프로그래머가 의도하는 결과가 아닐 것이다.

C++에서야 문자열 클래스에 == 연산자가 오버로딩되지 않은 경우가 없을 테니 언어가 왜 저렇게 만들어졌는지 이해하기 어렵겠지만.. 자바는 연산자 오버로딩이란 게 없는 언어이며 String은 앞서 말했듯이 기본 자료형과 클래스 사이의 어중간한 위치를 차지하는 물건이기 때문에 이런 디자인의 차이가 발생한 듯하다. 자바는 안 그래도 걸핏하면 클래스 새로 만들고 get/set 등 다 메소드로 구문을 표현해야 하는 언어이니까.
오죽했으면 본인은 회사에서 자바 코드를 다루면서도 문자열 비교를 실수로 ==로 잘못 해서 발생한 버그를 발견하고 잡은 적도 있었다.

그나저나 유사 언어(?)인 스칼라, 자바스크립트 같은 언어들은 ==로 바로 문자열 비교가 가능했던 걸로 기억한다.

4. true iterator

파일을 열어서 거기에 있는 문자열을 한 줄씩 얻어 오는 함수(A), 그리고 각 문자열에 대해 출력을 하든 변형을 하든 일괄적인 다른 처리를 하는 함수(B)를 완전히 분리해서 별도로 작성했다고 치자. 혹은 한 디렉터리에 파일들을 서브디렉터리까지 빠짐없이 쭉 조회하는 함수(A)와, 그 찾은 파일에 대해서 삭제나 개명 같은 처리를 하는 함수(B) 구도로 생각할 수도 있다.
그런데 이 둘을 연계시켜서 같이 동작하게 하려면 어떻게 하는 게 좋을까?

이럴 때 흔히 떠올릴 수 있는 방법은,
A 함수에다가 B 함수까지 인자로 줘서 호출을 한 뒤, A의 내부 처리 loop에서 B에 넘겨줄 데이터가 준비될 때마다 B를 callback으로 호출하는 것이다. B는 간단한 일반 함수 + context 데이터 형태가 될 수도 있고, 아니면 가상 함수를 포함한 인터페이스 포인터가 될 수도 있다.

데이터 순회를 하는 A 자체도 파일을 열고 닫거나 내부적으로 재귀호출을 하는 등 state가 존재하기 때문에 매번 함수 실행을 시켰다가 종료하기가 곤란한 경우, 상식적으로 A를 먼저 실행시킨 뒤에 A가 계속 실행되고 있는 중(= 상태도 계속 유지되고)에 그 내부에서 B를 호출하는 게 바람직한 게 사실이다.
물론, 반복문 loop을 B에다가 두고, 반대로 B에서 A를 callback 형태로 호출하는 것도 불가능한 건 아니다. 그런데 프로그래밍 언어에 따라서는 이런 B 중심적인 사고방식의 구현을 위해 좀 더 획기적인 기능을 제공하는 물건도 있다.

def func():
    for i in [1,5,3]:
        yield i

a=func()
print a.next()
print a.next()
print a.next() # 예상하셨겠지만 1, 5, 3 순서대로 출력

파이썬에는 함수에 return 말고 yield 문이 있다. 그러면 얘는 함수 실행이 중단되고 리턴값이 지정되기는 하는데..
다음에 그 함수를 실행하면(정확히는 next() 메소드 호출 때) 처음부터 다시 실행되는 게 아니라, 예전에 마지막으로 yield를 했던 곳 다음부터 계속 실행된다. 예전의 그 함수 호출 상태가 보존되어 있다는 뜻이다.

난 이걸 처음 보고서 옛날에 GWBASIC에 있던 READ, DATA, RESTORE 문과 비슷한 건가 싶었는데.. 저건 당연히 GWBASIC을 아득히 초월하는 고차원적인 기능이다. C++이었다면 별도의 클래스에다가 1, 5, 3 static 배열, 그리고 현재 어디까지 순회했는지를 가리키는 상태 인덱스 정도를 일일이 구현해야 했을 텐데 저 iterator는 그런 수고를 덜어 준다.

단순히 배열이 아니라 binary tree의 원소들을 prefix, infix, postfix 방식으로 순회한다고 생각해 보자.
순회하는 함수 내부에서 다른 콜백 함수를 호출하는 게 아니라 매번 원소를 발견할 때마다 리턴값을 되돌리는 형태라면..
구현하기가 굉장히 까다로울 것이다. 스택 메모리를 별도로 할당한 뒤에 재귀호출을 비재귀 형태로 일일이 구현해 주거나, 아니면 각 노드에다가 부모 노드의 포인터를 일일이 갖춰 줘야 할 것이다.

C++의 map 자료형도 내부적으로는 RB-tree 같은 자가균형 dynamic set 자료구조를 사용하는데, 이런 iterator의 구현을 위해서 편의상 각 노드에 부모 노드 포인터를 갖고 있는 걸로 본인은 알고 있다. RB-tree는 내부적으로 로직이 굉장히 복잡하고 까다로운 자료구조이긴 하지만, 그래도 부모 노드 없이도 구현이 불가능한 건 아닌데 말이다.
안 그랬으면 iterator가 자체적으로 스택을 멤버 변수로 갖거나, 최소한 메모리 할당· 해제를 위해 생성자나 소멸자까지 갖춰야 하는 복잡한 class가 돼야 했을 것이다. 어떤 경우든 포인터 하나와 비슷한 급인 lightweight 핸들이 될 수는 없다.

개인적으로는 지난 여름에 <날개셋> 한글 입력기 7.5에 들어가는 새로운 한글 입력 순서 재연 알고리즘을 구현할 때 비슷한 레벨의 iterator를 비재귀적으로 구현한 적이 있는지라, yield문의 의미가 더욱 절실히 와 닿는다.

Posted by 사무엘

2015/02/25 08:38 2015/02/25 08:38
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1066

1. 오픈소스

잘 알다시피 C/C++은 메모리 할당이나 문자열 등, 바이너리 차원에서 뭔가 언어나 구현체가 buliding block을 규정해 놓은 게 없다시피하며, 그나마 표준이 나온 것도 강력한 구속력을 갖고 있지는 못하다. 그러니 이 지저분함을 참다 못해서 COM 같은 바이너리 규격이 나오고 닷넷 같은 완전히 새로운 프레임워크도 나왔다.

아니면 일각에서는 소프트웨어 컴포넌트를 재배포할 때, 빌드된 라이브리러리를 주는 게 아니라 난독화 처리만 한 뒤 소스 코드를 통째로 넘겨주면서 빌드는 이 코드를 쓰는 쪽에서 자기 입맛대로 알아서 하라는 극단적인 조치를 취하기도 한다. 차라리 오픈소스 진영이 이런 점에서는 융통성이 더 있는 셈이다.
하지만 어지간한 컴덕력을 갖추지 못한 사람은.. 복잡한 빌드 시스템/configuration들을 이해할 수 없어서 소스 코드까지 통째로 줬는데도 줘도 못 먹는 촌극이 벌어진다.

이런 라이브러리 내지 유닛, 패키지는 기계어 코드로든 다른 바이트 코드로든 소스 코드가 바이너리 형태로 용이하게 재사용 가능한 형태로 가공되어 있는 파일이다.
그런데 실행문이 들어있는 소스 코드가 반드시 그대로 노출돼야만 하는 분야도 있다.

크게 두 갈래인데, 하나는 C++의 템플릿 라이브러리이고, 다른 하나는 웹 프로그래밍 언어 중에서도 전적으로 클라이언트 사이드에서 돌아가는 자바스크립트이다.
동작하는 환경 내지 타겟은 둘이 서로 완전히 극과 극으로 다르지만, 전자는 컴파일 때 최적화 스케일의 유연성 때문에, 그리고 후자는 선천적으로 기계 독립적이고 극도로 유연해야만 하는 웹의 특성상 오픈소스가 강제되어 있다.

자바스크립트는 비록 전통적인 기계어 EXE를 만드는 데 쓰이는 언어는 아니지만 그렇다고 해서 만만하게 볼 물건이 절대로 아니다. 람다, 클로저 등 어지간한 최신 프로그래밍 언어에 있는 기능은 다 있으며, 플래시에 하드웨어 가속 3D 그래픽까지 다 지원 가능한 경지에 도달한 지가 오래다.
또한 웹에서의 영향력이 워낙 막강하다 보니 전세계의 소프트웨어 업체들이 눈에 불을 켜고 실행 성능을 필사적으로 끌어올려 놓았다. 비록 컴파일을 통한 보안 유지는 안 되지만, 어느 수준 이상의 코드 난독화 기능도 당연히 있다.

뭐, C++ 표준 템플릿 라이브러리도 헤더 파일을 열어 보면, 남이 못 알아보게 하려고 코드를 일부러 저렇게 짰나 싶은 생각이 든다. 온갖 주석이 곁들여져서 알아보기 쉽게 널널하게 작성된 C 라이브러리의 소스들과는 형태가 달라도 너무 다르다..

C++ 템플릿에 대해서 한 마디 더 첨언하자면.. 제한적으로나마 함수나 몸체를 일일이 인클루드해서 노출하지 않아도 되는 방법이 있긴 하다.
몸체를 한 cpp(= 번역 단위)에다가만 구현해 놓은 뒤, 거기에다가 소스 코드 전체를 통틀어 그 템플릿이 인자가 주어져서 쓰이는 모든 형태를 명시만 해 주면 된다.

template Sometype<char>;
template Sometype<wchar_t>;

템플릿 함수에 대해서 template<> 이렇게 시작하는 특정 타입 전용 케이스를 만드는 것과 비슷해 보이는데..
위와 같은 식으로 써 주면, 해당 코드가 컴파일될 때 이 템플릿이 저런 인자로 실현되었을 때의 대응 코드가 모두 생성되고, 이게 다른 오브젝트 파일들이 링크될 때 같이 연결되게 된다. 이런 문법이 있다는 것을 15년 동안 C++ 프로그래밍을 하면서 처음 알았다.

물론 저것 말고 다른 임의의 새로운 타입으로 템플릿을 사용하고 싶다면 그렇게 템플릿을 사용하는 번역 단위에서 또 다시 템플릿의 선언부와 몸체를 싹 읽어들여서 분석을 해야 한다.
아마 과거의 export 키워드가.. 저런 템플릿 인자의 사용 형태를 자동으로 파악하는 걸 의도하지 않았나 싶은데 그래도 세상에 쉬운 일이란 없었던 듯하다.

2. 웹 프로그래밍의 성격

HTML, CSS, 자바스크립트 삼신기는 마치 웹 프로그래밍계에서의 삼권 분립이기라도 한 것 같다. 아무래도 당장 화면에 표시되는 핵심 컨텐츠가 HTML이니 요게 행정부에 대응하는 듯하며, HTML을 표시할 규격을 정하는 CSS는 사법부에 가깝다. 끝으로, 인터랙티브한 동작을 결정하는 자바스크립트는 입법부 정도?
물론 HTM 파일 하나에다가 스타일과 자바스크립트 코드를 다 우겨 넣었다면 그건 뭐 “짐이 곧 국가다, 법이다” 식으로 코드를 작성한 형태일 것이다.

예로부터 본인이 느끼기에 웹 프로그래밍은 뭔가 시대의 최첨단을 달리는 것 같고 간지와 뽀대가 나고 실행 결과가 사용자에게 가장 직접적으로 드러나 보이는 신기한 영역인 것 같았다. 하지만 (1) 코드와 데이터, 클라이언트와 서버, 코딩과 디자인의 역할 구분이 영 모호하며, 컴퓨터의 성능을 100% 뽑아내는 듯한 전문적이고 하드코어한 느낌이 안 들어서 마음에 안 들었다. 가령, 도대체 어디서는 java이고 어디서는 jsp이고 어디서는 js인지?

(2) 또한 이 바닥은 작성한 소스 코드가 제대로 보호되지 못한다. 서버 사이드에서만 돌아가는 PHP 같은 건 클라이언트에게는 노출이 안 되겠지만 그것도 서버 개발자들끼리는 결국 오픈소스 형태로 공유될 수밖에 없으니 말이다. 옛날에 제로보드의 소스가 그랬듯이.

끝으로, (3) 특정 CPU 아키텍처나 플랫폼에 구애되는 게 없다 보니 기반이 너무 붕 뜨는 느낌이고, 브라우저마다 기능이 제각각으로 달라지는 거 호환 맞추는 노가다가 필요한 것도 싫었다.
뭐, IE와 넷스케이프가 경쟁하고 IE6이 세계를 사실상 평정했던 먼 옛날에는 그랬고 지금은 이 문제는 많이 해소됐다. 바야흐로 2015년, HTML5 표준안까지 다 완성된 지경이니, 웹 프로그래밍도 이제 충분히 성숙했고 기반이 탄탄히 잡혔다. 격세지감이다. ActiveX도 점점 퇴출되는 중이다.

2004년에 IE6에 대한 대항마로 파이어폭스 0.8이 혜성처럼 등장했고, 2008년엔 구글 크롬이 속도 하나로 세계를 평정해서 IE의 독점 체계를 완전히 견제해 냈다. 지금은 크롬이 속도는 괜찮은 반면, 메모리 사용량이 너무할 정도로 많아서 파이어폭스가 다시 반사 이득을 보는 구도이다. 오페라는 Windows에서는 영 좀 마이너한 콩라인 브라우저가 아닌가 모르겠다.
그리고 무슨 브라우저든지 버전업 숫자 증가폭이 굉장히 커졌으며, 탭 브라우징에  메뉴와 제목 표시줄을 숨겨 놓는 인터페이스가 필수 유행이 돼 있다.

3. 보안 문제

세월이 흐르면서 웹 프로그래밍 환경이 좋아지고 있는 건 사실이지만, 보안 때문에 예전엔 바로 할 수 있었던 일을 지금은 못 하고 뭘 허가를 얻고 번거로운 절차를 거쳐야 하는 건 다소 불편한 점이다.
특히 내가 느끼는 게 뭐냐 하면, 한 HTML 파일에서 자신과 다른 도메인에 있는 CSS나 JS 같은 걸 덥석 인클루드 하는 걸 브라우저가 굉장히 싫어하게 됐다는 점이다. 이런 걸 이용한 보안 취약점 공격이 지금까지 많았는가 보다.

"이 사이트에는 안전한 컨텐츠와 위험한 컨텐츠가 같이 섞여 있습니다. 위험한 것도 모두 표시하시겠습니까?"라는 메시지가 바로 이런 상황에서 뜬다.
IE의 경우 예전에 잘 표시되던 사이트가 갑자기 표시되지 않을 때, 권한 취득을 위해 레지스트리에다 자기 프로그램 이름이나 사이트를 등록하는 등 조치를 취해야 했다.
구글 크롬은 발생 조건이 IE와 동일하지는 않지만, 자체 판단하기에 악성 코드의 실행을 유도하는 걸로 의심되는 지시문이 HTML 소스에 있는 경우, 화면 전체가 위험 경고 질문 화면으로 바뀐다.

최근에는 크롬과 IE에서는 멀쩡하게 보이는 웹 페이지가 파이어폭스에서만 제대로 표시되지 않는 문제가 있어서 회사 업무 차원에서 사이트 디버깅을 한 적이 있었다. 요즘 세상이 무슨 세상인데 웹 표준이나 렌더링 엔진의 버그 때문일 리는 없고, 파이어폭스가 자바스크립트 엔진으로 하여금 외부 도메인로부터 인클루드된 CSS 속성에 접근하는 걸 허용하지 않아서 발생한 문제였다.

4. 파일 관리가 되는 게시판

본인도 여느 프로그래머와 마찬가지로 다니는 회사에서 요즘 모바일에 웹까지 별별 걸 다 손대며 지냈다. 하긴, 공학 박사라 해도 취업 후에는 돈 되는 분야, 뜨는 분야를 따라 자기 주전공 연구 분야가 아닌 것도 손대 봐야 할 텐데 하물며 그보다 급이 낮은 단순 엔지니어들은 말이 필요하지 않을 것이다.

요즘은 게시판이나 블로그 엔진을 만들려면 단순무식한 텍스트 기본 폼이 아니라 위지윅 웹 에디터가 필수이다. ckeditor 컴포넌트에다가 이미지 업로드 기능을 연결해 넣을 일이 있었는데 이것도 여간 골치아픈 일이 아니라는 걸 작업을 하면 할수록 깨닫게 됐다.
손이 정말 많이 간다. 하지만 그걸 일일이 하지 않으면 이미지는 단순 외부 링크밖에 못 넣는 반쪽짜리가 된다.

이미지 파일이 하나 HTTP 규격대로 업로드되어 왔으면 서버 측에서는(PHP든 JSP든 무엇이든) 파일 크기가 적당한지(개별 파일 크기와 지금까지 업로드된 파일의 전체 크기 모두) 체크하여 적당하다면 이름을 중복 없는 랜덤 이름으로 바꿔서 서버에 저장한다. 이름에 한글이 들어간 파일이라고 업로드나 로딩이 제대로 안 되는 일이 없어야 하니까.

그 뒤에 그 그림을 불러올 수 있는 URL을 에디터 컴포넌트에다가 알려 준다. 이것도 간단하게 만들자면 그냥 서버의 특정 디렉터리를 그대로 노출하는 식으로 만들면 되겠지만 보안상 위험하니 가능한 한 제3의 장소에서 파일을 되돌리는 서버 프로그램 URL을 주는 게 안전하다.

위지윅 에디터에서는 임의의 개수의 파일이 업로드될 수 있기 때문에 글에 얽힌 첨부 파일들을 따로 디렉터리나 DB 형태로 관리해서 글이 삭제될 때 같이 지워지게 해야 한다.
사실, 이쪽으로 조금만 더 신경 쓰면 글별로 아예 첨부 파일 관리자라도 간단한 형태로 만들어야 하게 된다. 우와..;;

그리고 골때리는 건, 아직 작성 중이고 정식으로 등록하기 전의 임시 상태인 글에 첨부된 그림들을 처리하는 방식이다.
일단은 그림들이 임시 폴더에다가 올라가고 주소도 임시 폴더 기준이지만 글이 정식으로 등록됐다면 글 중에 삽입된 이미지들의 주소를 수동으로 바꿔야 하고 파일도 옮겨야 한다.
또한 그 상태로 글이 더 등록되지 않고 사용자가 back을 눌렀다면, 서버에 올라왔던 임시 파일들도 나중에 지워 줘야 한다. 이런 것까지 도대체 어떻게 다 구현하지?

이건 일게 위지윅 에디터 컴포넌트가 감당할 수 있는 수준이 아니기 때문에 그걸 블로그 엔진이나 게시판에다 붙여 쓰는 웹 프로그래머가 자기 서버의 사정에 맞게 세팅을 해야 한다.
겨우 이미지 업로드 기능 하나만 달랑 구현하는 테크닉을 소개한 블로그만으로는 정보가 너무 부족했다.
Windows에서 공용 컨트롤에다 드래그 드롭을 처음부터 직접 구현하는 것만큼이나 손이 많이 갔다. 나 같은 이 바닥 초짜로서는 그저 경악스러울 뿐.

프로그램의 완성도를 더 높이려면, 사용자가 곱게 이미지 파일만 올리는 게 아니라 php나 html 같은 보안상 위험한 파일을 올리는 건 아닌지 감시해야 한다. 첨부 파일 정도가 아니라 위지윅 웹 에디터 자체도 위험하다고 그런다. HTML이 근본적으로 문서와 코드가 뒤섞인 형태이다 보니 정말 매크로가 잔뜩 든 Office 문서처럼 취급되는가 보다.
아무튼, 나모 웹에디터와 제로보드가 뜨던 시절에 비해 요즘 웹은 너무 방대하고 복잡하다.

Posted by 사무엘

2015/02/02 08:39 2015/02/02 08:39
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1057

요런 개념을 표로 일목요연하게 한 번쯤 정리할 필요를 예전부터 느꼈던지라, 잠시 짬을 내어 만들었다. C++ 프로그래머라면 고개가 절로 끄덕여질 내용 되겠다. 

토큰 type 앞 type 뒤 value 앞 양 value 사이 value 뒤
&   참조자형 명시 address-of (L-value만) 비트 AND  
&&   R-value 참조자형 명시   논리 AND  
*   포인터형 명시 배열 및 포인터 역참조 곱셈  
±     양/음 부호 덧셈/뺄셈  
괄호() (1) 형변환(typecast)
(2) 타입 선언자 나열 순서 조절
함수형 명시 연산 순서 조절   함수 호출
대괄호[]   배열형 명시     배열 참조
부등호<>   템플릿 인자 명시   비교 템플릿 함수 인자
콤마,       (1) 콤마 연산
(2) 함수 호출/템플릿 인자 구분
(3) 변수 선언 구분
 

  • 괄호는 영어에서 접속사도 되고 지시형용사/지시대명사, 관계대명사까지 다 되는 that만큼이나 정말 다재다능한 물건이다.
  • 베이직은 이례적으로 =, ()가 쓰임이 중첩되어 있다. =가 대입과 동등 연산을 모두 담당하며, ()가 함수 호출과 배열 첨자를 모두 담당한다. 함수 호출은 문법적으로 매우 제한된 문맥에서만 허용되니, C/C++같은 함수 포인터가 존재하지 않는다면 () 중첩이 아주 불가능하지는 않은 듯하다.
  • C/C++은 @ $ ` 기호를 전혀 사용하지 않는다. 예전에 베이직은 각종 기괴한 기호들을 이용하여 변수의 자료형을 표현하곤 했다. A$는 문자열, A%는 정수, A#은 실수 같은 식이다.
  • 파스칼은 포인터형을 선언하는 토큰이 ^인데, C/C++와는 달리, 포인터형을 나타낼 때와 포인터를 역참조할 때 토큰이 등장하는 위치가 서로 다르다. ^Type 그리고 Value^ 이런 식.
  • int *a와 int a[5]배열에 대해서는 똑같이 *a를 쓸 수 있지만, 잘 알다시피 배열의 역참조와 포인터의 역참조는 개념적으로 다르다. C/C++을 처음 공부하는 초보자가 굉장히 혼동할 수도 있을 듯하다.
  • 포인터는 다중 포인터가 존재할 수 있고 역참조도 여러 단계를 연달아 할 수 있다. 그러니 *가 여러 개 연달아 올 수 있다. 그 반면, 참조자는 구조적으로 딱 한 번만 참조/역참조가 가능하게 만들어진 포인터의 축소판이다. 그렇기 때문에 &&에다가 별도로 R-value 참조자라는 물건을 집어넣을 수도 있다. 이걸 생각해 낸 고안자는 정말 천재다.
  • 일반적으로 &는 address-of 연산자이며 R-value를 상대로는 적용이 되지 않는다.
    그러나 일부 값은 L-value가 아님에도 불구하고 & 연산자의 적용이 가능하며, 심지어 a와 &a가 동일하게 취급되는 것도 있는데, 바로 static 배열과 일반 함수이다.
    기본적으로 포인터의 성격을 갖추고 있는지라 &를 안 해도 기본적으로 자신의 주소가 되돌아오고, &를 붙여도 무방하다는 오묘한 특징이 있다.
  • 한편, C/C++에서 배열은 고유한 자료형임에도 불구하고 함수의 인자로 전달되거나 리턴값이 될 때는 그냥 포인터든 배열의 포인터든 포인터의 형태로만 전달된다. 배열 그 자체가 전달되지는 못한다.
    배열을 생으로 함수를 통해 주고 받으려면 구조체에다가 배열을 집어넣어야 한다.

Posted by 사무엘

2015/01/02 19:39 2015/01/02 19:39
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1046

PL(프로그래밍 언어)계에서 함수형 프로그래밍 언어는
자동차 엔진으로 치면 로터리 엔진, 발전소 업계로 치면 핵융합 발전 같은 뭔가 이상은 높지만 현실은 아직 좀 시궁창인 그런 떡밥스러운 영역으로 간주되는 것 같다.

전산학을 전공해서 PL 수업을 들은 분이라면 이미 잘 아시겠지만, 프로그래밍 언어란 크게 절차형과 선언형으로 나눌 수 있다.
절차형은 튜링 기계라는 컴퓨터의 특성을 그대로 반영하여 메모리로부터 값을 읽은 뒤 연산을 수행해서 값을 변경하고, 메모리 위치도 바꾸는 절차를 순차적으로 일일이.. 즉 HOW 위주로 기술하는 언어이다. 정보 올림피아드 경시부가 다루는 것은 응당 절차형 프로그래밍 언어를 활용하여 프로그램을 작성해서 문제를 해결하는 능력이다.

(덧붙이자면 튜링 기계에다가 데이터뿐만 아니라 코드, 즉 상태 변환 로직까지 동일한 메모리에다 올려서 해석하는 계산 모델이 바로 오늘날 컴퓨터의 근간이 된 프로그램 내장형, 즉 폰 노이만 모델이다. 자동차 엔진로 치면 정말 외연 기관에서 흡입-압축-폭발-배기 4행정 내연 기관으로 변모한 수준의 발전이 아닌가 싶다.)

한편, 선언형은 우리가 원하는 솔루션의 정의 내지 조건이 이러하다.. 라고만 써 주는 형태의 WHAT 지향형 언어이다. 그러면 컴퓨터가 알아서 문제를 풀어 낸다.
따지고 보면 데이터베이스 질의어인 SQL은 DLL, EXE 같은 실행 파일을 만드는 용도의 언어가 아닐 뿐이지 아주 대표적인 선언형 언어이다. 복잡한 DB에서는 질의어를 만드는 것도 굉장히 복잡한 일이 되며, 두 DB간의 JOIN은 어떻게 시키고 어느 구문부터 풀이해서 최적의 성능으로 질의를 수행할지 결정하는 것도 아주 어려운 축에 든다. 이런 거 성능 소요 시간을 몇 % 단축시키는 알고리즘을 개발해 내면, DB를 연구하는 전산학과 대학원 연구실에서는 그게 곧바로 논문감이 된다.

흔히 인공지능 문제 풀이형 언어로 알려져 있는 프롤로그도 선언형 언어이다. 이건 진짜 여러 변수들을 선언한 뒤 변수들간의 인과관계를 쭈욱 나열해 주면 이를 바탕으로 언어 런타임이 문제의 답을 찾아 낸다.

까놓고 말해 절차형 프로그래밍 언어로 "아인슈타인의 퍼즐" 같은 걸 풀려면, 프로그래머가 재귀호출에 각종 백트래킹 알고리즘을 직접 구사해야 하니 앞에서 말했던 정보 올림피아드 경시부 급의 기술이 필요하다. 그러나 프롤로그에서는 "영국인.집 = 빨강, 스웨덴.애완동물 = 개" 이런 식으로 단서만 주어진 규칙대로 쓴 뒤 쿼리를 날리면 금붕어를 기르는 사람의 국적을 구할 수 있다.

아마 네모네모 로직이나 스도쿠 같은 것도 해답이 갖춰야 할 조건을 명시하는 것만으로 바로 풀 수 있지 싶다. 단서들을 바탕으로 뺑뺑이를 돌리는 추론 과정은 언어 런타임 내지 엔진이 해 준다.
대학 학부 시절, OR개론 수업 때 잠시 접했던 선형계획법 문제 풀이 프로그램인 k-opt도 역시 지정된 문법에 따라 변수와 부등식들을 써 놓고 최소화/최대화 조건을 명시하면 프로그램이 해를 찾아 주니.. 일종의 선언형 프로그래밍 언어 런타임에 속한다고 할 수 있겠다.

그러니, 절차형 언어의 컴파일러는 최적화를 하는 게 기계어 코드 생성이나 멀티코어 병렬화 같은 아주 미시적인 것과 관계가 있는 반면, 선언형 언어의 수행 방식을 최적화하는 것은.. 거시적인 알고리즘 전략까지 결정해야 하니 더욱 까다로울 것이다. 미시적인 건 해당 언어 엔진이 아주 정교하게 구현되어 있지 않은 이상 신경 쓰기 힘들다.

앞서 언급한 SQL이나 프롤로그는 선언형 프로그래밍 언어 중에서 일종의 '논리 지향'인 물건들이다. 그런데 선언형의 하위 범주로는 '함수 지향', 함수형 프로그래밍 언어라는 게 있다. 이게 절차형보다는 좀 더 수학자스러운 형태로 컴퓨터를 부려먹는 방법을 기술하는 방법론이라고 한다. (함수형이 여느 절차형 프로그래밍과 계산 능력이 동등하다는 것은 튜링 기계와 람다 대수가 동치라는 것이 증명됨으로써 알려져 있다.)

순수한 함수형 프로그래밍 언어에서는 지저분한 대입 연산이 없고 한번 생성된 값은 변경 없이 계속 그 값으로 남아 있다. 새로운 값이 계속 생성될 뿐이다. 사실 문자열을 이런 사고방식으로 처리하는 라이브러리나 언어, 프레임워크에서는 이미 있는 문자열을 변경하는 게 굉장히 어렵기도 하다. Windows RT의 String 클래스도 그랬던 걸로 기억..

함수형 언어에서는 대입이 없으니 응당 뺑뺑이 loop도 있을 수 없다. loop을 대신하는 것은 재귀호출이다! loop조차도 기존 값을 계속 바꾸는 게 아니라 새로운 값을 자꾸 만들어 내는 방식으로 구현된다는 뜻이다.
처음에 해답의 범위가 0부터 100 사이에 있었다면 그 다음 턴에는 0부터 50 (log n 시간 복잡도), 혹은 0부터 99로 자가호출이 이뤄지고, 이것이 문제가 완전히 해결될 때까지 반복된다. 왜냐하면 이 문제의 솔루션이 바로 그런 형태로 귀납적으로 정의돼 있기 때문이다. 팩토리얼이든, 두 수의 최대공약수이든, 정렬이든 다른 무엇이든.

이 패러다임에서는 함수가 다른 여느 데이터와 완전히 동일한 수준으로 다른 함수의 인자가 될 수 있고, 특히 이름 없이 함수의 몸체만을 여느 값처럼 달랑 전해 줄 수 있고, 다른 함수로부터 합성되고 유도된 새로운 함수가 함수의 리턴값이 될 수 있다. 새로운 함수가 동작하는 데 필요한 주변 문맥은 클로저라는 물건이 알아서 다 처리해 준다.
C/C++의 함수 포인터에 머리가 굳은 프로그래머라면 calling convension은 무엇인지, this 포인터가 포함된 멤버 함수인지, pointer-to-member라면 다중 상속으로 인한 부가 오버헤드는 없는지 같은 디테일 때문에 머리가 복잡해질 것이다.

함수형 언어에서 if문은 응당 자기 자신도 조건이 만족하는 쪽의 값을 되돌리는 함수 형태이다.
그러나 if는 조건이 만족하는 쪽만 '계산'이 행해질 터이니 if(a) b; else c; 를 나타내는 if(a, b, c)는 통상적인 함수 호출 func(a, b, c)와 의미상으로 완전히 동일할 수는 없다. 예약어로 따로 해석되고 취급을 받아야 할 듯하다.

물론 이런 함수형 프로그래밍 언어가 구현되기 위해서는 현실에서 컴파일러가 최적화해 줘야 하는 것, 그리고 언어 런타임이 해 줘야 하는 오버헤드가 적지 않다. 끝없이 새로운 값을 생성해 내더라도 이제 참조가 끝나서 더 쓰이지 않는 값은 GC가 알아서 제거해 줘야 하고, 재귀호출, 특히 tail recursion 정도는 알아서 메모리 복잡도를 O(n) 급으로 늘리지 않는 일반적인 loop처럼 돌아가게 컴파일러나 런타임이 최적화를 해 줘야 한다. 함수를 값처럼 부드럽게 다루는 것도 기술적으로는 단순 함수 포인터 이상의 추상화 계층이 필요하며, 말처럼 쉬운 일이 아니다.

예를 들어.. X라는 함수가 있는데.. 얘는 a라는 인자를 받고는,
b라는 인자를 받아서 a에다가 b를 더한 값을 되돌리는 Y라는 함수를 되돌린다고 치자.
결국 Y는 X라는 함수가 호출될 때 전달되었던 매개변수 내지 그때 생성된 X 내부의 지역 변수에 의존하여 동작하는데..
나중에 Y가 단독으로 호출될 때는 X라는 함수는 실행이 끝나고 그 문맥이 존재하지 않는다. 이를 어찌하리?
이런 딜레마를 피하기 위해 C/C++ 언어는 애초에 함수 안에 함수를 만드는 걸 지원하지 않는 쪽으로 설계되었으며, C++의 functor 같은 것도 전부 자기가 자체적으로 데이터 멤버를 갖고 있는 객체 형태로 만들어지게 된 것이다.

또한, 아무리 대입이 사이드 이펙트가 남는 지저분하고 기피되어야 하는 연산이고.. 다른 모든 연산을 loop 대신 재귀호출로 때운다 해도.. 당장 외부 파일/키보드로부터의 input은.. 대입 연산 없이는 감당이 도저히 불가능하다. 그리고
return t1.len() > t2.len() ? t1: t2
처럼 그 재귀호출의 결과값을 취사 선택하는 간단한 판단을 위해서라도 임시 변수에 대입하는 것 정도는 근본적으로 전혀 없을 수가 없다.
어디 그 뿐이랴. 대용량의 단일 데이터를 대상으로 여러 함수들이 포인터만 주고받으며 동작하다 보면, 한 함수가 자기 argument 안에 입출력 대상인 모든 데이터를 집어넣는 것은 무리가 있다.

허나 함수형 프로그래밍이 성능면에서 불리한 요소만 있는 건 아니다. 대입으로 인한 side effect 같은 게 없으니 소스 코드의 정적 분석은 더 용이할 것이고 병렬화 등 입맛에 맞는 최적화에도 더 유리할 것이다. 애초에 선언형 프로그래밍 언어는 구체적인 실행 방식은 그런 똑똑한 컴파일러나 언어 엔진에게 맡기고 있으니까.
이러니 PL 분야를 연구하는 전산학자나 수학 덕후들이 함수형 프로그래밍 언어에 더욱 열광하는 듯하다. 저런 패러다임이 응집도· 결합도 같은 소프트웨어 공학적인 측면에서 더 깔끔한 코드를 만드는 데 도움이 되는 것은 덤이다.

대학교 전산학과에서는 함수형 프로그래밍 언어로 보통 Scheme을 실습하는 편이다. 본인도 먼 옛날 학부 시절에 '프로그래밍의 이해(PP)'라는 과목을 통해 그 물건을 접했으며, 그걸로 무슨 다항식의 곱셈을 하는 프로그램도 숙제로 만들고 여러 덕질을 했었다. 함수형 언어의 진짜 본좌라고 일컬어지는 Haskell 같은 건 난 모름.;;

여담이지만 지금 생각해 보니, 온갖 복잡한 괄호가 배배 꼬여 있는 Scheme 코드는.. 언어학에서 문장 구문 분석을 괄호로 표현해 놓은 것과 사뭇 닮았다는 생각이 들었다. (S (NP .. ) (VP ...)) 이러는 식.
Schme에서는 S 대신에 define, lambda, if 따위가 있을 것이다.

물론 그때는 본인은 <날개셋> 한글 입력기 개발에 도움이 안 되는 건 진짜 생까고 무시하던 시절이어서 그 코스의 의미를 제대로 이해를 못 했다. 왜 괜히 계산 과정을 이 따위로 어색하게 표기를 하는지..??
그건 사칙연산 같은 기초적인 연산자조차도 통상적인 표기법이나 우선순위를 깡그리 무시하고 정말로 오로지 함수 위주로.. 프로그래밍이, 아니 계산(computing)이라는 작업 자체를 몽땅 주어진 규칙대로 피연산자들을 처리해서 reduce하는 과정이라고 극도로 추상화한 귀결일 것이다. 일종의 발상의 전환인 것이다. car, cdr 명령이 튜링 기계로 치면 메모리 셀을 이동하고 값을 읽는 동작에 해당할 것이다.

단, Scheme도 마냥 순수 함수형 언어이기만 한 것은 아니다. 필요한 경우 대입 연산이 있을 수 있고 일부 절차형 패러다임 구문을 집어넣을 수도 있다. 마치 C#에서 부분적으로 unsafe, unmanaged 코드를 집어넣듯이 말이다.
그리고 반대로 C++ 역시, 기본적으로 객체지향 패러다임을 주류로 내세운 절차형 언어이지만 최근에는 함수형 프로그래밍 패러다임도 받아들여서 람다 함수를 기존 함수 포인터나 functor의 대용으로 쓸 수 있게 되었듯이.. 요즘 언어들의 대세는 자기 정체성은 유지하면서 다른 패러다임에서도 유용한 건 적극 받아들이는 것인 듯하다.

과연 함수형 프로그래밍 언어가 그저 대학교 과목에서나 잠깐 접하고 마는 떡밥 내지 PL 분야의 연구자들만 쓰는 도구 수준을 넘어.. 현업에서 적극 즐겨 쓰이는 날이 올지 모르겠다. 지금 현업에서 전혀 안 쓰인다는 말은 아니지만 아직까지는 수학 덕후, 컴덕후들의 전유물이라는 인상이 강한 편이니 말이다. 그래도 한 가지 확실한 건, 함수형 프로그래밍 패러다임을 실현해도 될 정도로 요즘 컴터 환경이 좋아지자, 각종 언어들에도 이 패러다임이 적극 많이 도입되고 이게 유행을 타고 있다는 사실이다.

여담으로, 람다 대수를 고안한 앨론조 처치는 family name이 어째 '교회'다..;; 독실한 신자 가문이기라도 했나 싶은 잡생각이 든다.

그리고 궁금한 게 있는데.. 이름 없는 함수에서 재귀호출을 해야 할 때 함수 자기 자신을 가리키는 this, self 같은 키워드는 없는가 싶다.
이 의문은 C++에서 람다 함수가 추가되었을 때부터 여러 프로그래머들에 의해 제기되어 왔다. 하지만 뾰족한 해결책은 없으며, std::function에다가 자신을 저장한 뒤, 그 변수명을 캡처로 도로 넘겨 줘야만 재귀호출이 가능하다. Scheme 역시 일단 def로 자기 이름을 지은 뒤, 그 이름을 호출해 줘야 된다.

재귀호출을 그렇게도 좋아하는 함수형 언어가

[](int x) { return x<=1 ? 1: this_func_itself(x)*(x-1); }

개념적으로 this_func_itself에 해당하는 키워드 같은 건 정말 없는 건지 신기한 노릇이 아닐 수 없다.

Posted by 사무엘

2014/12/28 08:39 2014/12/28 08:39
, , ,
Response
No Trackback , 9 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1044

오늘은 오랜만에 옛날 GWBASIC 추억 얘기를 또 늘어놓아 보겠다. 예전에 했을 법도 해 보이는데 여러 키워드로 검색을 해 보니 안 한 것 같다. 베이직 얘기를 전문적으로 하는 건 한 2년 만의 일이다.

GWBASIC은 초딩이었던 본인을 프로그래밍의 세계로 이끈 추억의 장난감이다.
본인은 어릴 때부터 컴퓨터가 다른 전자 기기와는 뭔가 차원이 다른 대단한 물건이라는 걸 실감했다.
텔레비전은 오로지 일방적으로 전달만 하는 물건인 반면, 컴퓨터는 내가 직접 명령을 내려서 모니터에 찍히는 글자의 색깔을 바꿀 수 있고, 내가 원하는 화면을 구성할 수 있고, 그림도 그릴 수 있고 소리도 내고 파일로부터 정보를 읽고 쓰면서 뭔가 '나만의 능동적인 세계'를 표현할 수 있기 때문이다.

본인은 딱히 머리가 빨리빨리 잘 돌아간다거나 수학 덕후 최적화 덕후 기질이 있지는 않았다. 단지, 새로운 세계를 표현하는 것 자체에 집착했다. 그래서 정보 올림피아드도 경시에서는 영 재미를 못 보고 그 대신 공모 부문에서 다 입상했다.

GWBASIC은 결과를 즉시 확인할 수 있는 대화식 구조라는 게 굉장히 인상적이다. 행번호에 GOTO문 남발은 굉장히 기괴하고 거추장스러운 개념이긴 하지만, 행번호가 없는 명령은 곧장 실행되고, 행번호가 붙은 명령은 메모리에 등록되어서 나중에 행번호 순으로 한꺼번에 실행된다는 그런 발상은... 참 아무나 할 수 있는 게 아닌 듯하다. RUN, MERGE, DELETE, CHAIN처럼 기억된 프로그램 자체를 확장하거나 바꾸는 명령이 있다는 것도 기괴하고 말이다.

GWBASIC에는 프로그램을 불러오거나 저장하는 명령으로 LOAD, SAVE가 있다. 그런데 GWBASIC은 좀 특이한 게, 여느 프로그래밍 툴처럼 소스 코드를 plain text로 저장하는 게 아니라 내부 바이너리 바이트코드로 저장하는 게 기본 옵션이다. 바이트코드는 같은 소스를 저장했을 때 plain text보다 크기가 작고, 로딩/저장 속도도 더 빠르다는 이점이 있다.

세월이 워낙 많이 흘렀기 때문에 지금은 그 바이트코드의 포맷이 다 알려져서 인터넷에 굴러다닌다. 포맷이 정식으로 공개된 건지 아니면 해커들이 리버스 엔지니어링을 해서 알아낸 건지는 잘 모르겠다.
다른 프로그램에서도 소스 코드를 볼 수 있게 저장하려면 SAVE"파일이름", A라고 뒤에 A를 덧붙여야 한다.

그리고 한편으로 P라는 옵션이 있다. P 옵션은 A와는 반대로 소스 코드를 내부 바이너리 코드로 저장하되 그걸 XOR 기반의 간단한 암호화까지 해서 저장한다.
P 옵션으로 저장된 소스는 불러와서 실행은 가능하지만, LIST로 내용을 열람하거나 코드를 수정할 수 없다. 따라서 비록 GWBASIC에 소스 코드를 EXE로 컴파일하는 기능은 없지만, 다 만든 프로그램을 남에게 인계할 때는 P 옵션으로 저장된 프로그램 파일을 전해 주면 소스 코드 유출을 막을 수 있다. 이론적으로는 말이다.

그러나 그 시절의 GWBASIC에 무슨 전문적인 코드 암호화나 난독화 기능이 있었던 것도 아닌데, 겨우 그 정도의 허접한 보호 기능은 당연히 뚫리고도 남았다.
P 옵션의 암호화 방식도 다 알려져 있고, GWBASIC의 버그를 이용하여 보호 기능 자체를 뚫어 버리는 방법도 존재한다. 이것은 아주 오래 전부터 베이직 프로그래머들 사이에 나돌던 공공연한 비밀 테크닉이었다.

자, 0xFF 문자 2개로 구성된 2바이트짜리 파일(가칭 UN.BAS)을 만든다. 간단하지만 키보드로 바로 입력할 수 없는 문자이긴 한데.. 헥사 에디터를 쓰든지 아니면 GWBASIC 자체를 이용해서 이런 파일을 생성하는 프로그램을 짜서 돌려도 된다.

그 뒤, P 옵션이 붙은 임의의 소스를 LOAD한 뒤, 그 상태에서 UN.BAS를 뒤이어 LOAD하고 나면..
기존 소스의 프로텍션이 풀리고 LIST 열람이 가능해지는 걸 볼 수 있다. 마법과 같은 일이 벌어진다.

사용자 삽입 이미지

그 뿐만이 아니다.
NEW를 입력해서 기억되어 있던 소스를 다 지운 뒤에도 UN.BAS를 LOAD하면.. 방금 지워졌던 소스가 되살아난다.

사용자 삽입 이미지

이게 도대체 어째서 가능할까? (UN.BAS는 0xFF 0xFF일 뿐, 저 소스 코드 자체가 들어있는 거 절대 아님.. -_-)
사실, GWBASIC은 내부적으로 기억하고 있는 바이트코드를 디코딩해서 LIST로 출력하고 소스 코드를 고치는 것을 허용할지 여부를 간단한 boolean 변수 하나로만 판단하는 듯하다. 저장할 때 XOR 인코딩 여부 역시 그 변수로 판단하며, 불러올 때의 XOR 디코딩 여부는 파일 앞부분에 있는 헤더로 판단한다.
그러니, 그 메모리 주소의 값만 바꿔 버리면 프로텍션을 곧바로 풀 수 있다. GWBASIC의 보안 체계는 근본적으로 허술했던 것이다.

그리고 GWBASIC의 고유 파일 포맷에 따르면, 프로텍션이 걸리지 않은 파일은 0xFF로 시작하고, 걸린 파일은 0xFE로 시작한다.
그러므로 0xFF 0xFF 2바이트짜리 파일은 GWBASIC으로 하여금 프로텍션 플래그는 해제하지만 그 뒤에 거의 즉시 파일이 끝나 버리기 때문에 메모리 상의 다른 소스 코드는 건드리지 않는 역할을 하는 듯하다.

원래는 그렇게 파일이 갑작스럽게 끝났을 때의 처리를 GWBASIC이 깔끔하게 해야 하지만 그렇지 못하기 때문에 졸지에 프로텍션만 풀어 버리는 게 가능한 듯하다.
NEW를 한 것까지 어떻게 undo를 하는지까지는 잘 모르겠지만 말이다.

아무튼, 이런 게 오늘날의 소프트웨어 보안 용어로 치자면 일종의 버그이고 exploit이다.
만약 GWBASIC이 Windows, Office, Visual Studio처럼 오늘날까지 살아 있는 제품이고 GWBASIC이 세계 기업들의 돈줄을 좌지우지하는 솔루션이었다면 이건 뭐 당장 긴급 업데이트/패치감이 됐을 것이다. 회사의 자산인 소스 코드가 간단한 해킹으로 죄다 유출되게 생겼으니 말이다.

업데이트 명분으로 맨날 귀가 따갑도록 나오는 “악의적으로 조작된 파일을 열 경우 임의의 코드가 실행... 까지는 아니어도 뭐가 어찌될 수 있는 보안 취약점이 Microsoft 모 제품에서 발견되었습니다” 문구가 가리키는 게 바로 이런 거다.
자그맣게 조작된 파일이 GWBASIC의 저장 프로텍션을 풀어 버리니, 이 꼼수가 보안의 관점에서 오늘날 시사하는 바가 크다고 생각되어서 문득 글을 써 보았다.

옛날에, 286 XT/AT를 갖고 '교육용 컴퓨터' 이러던 시절에는 단색 그래픽 모드에서 동작하는 여러 '교육용 소프트웨어'들도 있었다. CAI라고 들어 보셨는가?
'약수와 배수', '컴퓨터 개론' 같은 타이틀이 있었는데, 개중에는 정말 놀랍게도 GWBASIC으로 개발된 것도 있었다.
물론 런타임인 GWBASIC.EXE와 소스 코드들은 다 파일 이름과 확장자를 교묘하게 바꿨고, 실행은 CAI.BAT라는 파일로 했다.

소스 코드를 열어 보니 당연히 프로텍션이 걸려 있었다. 그러나 본인은 저 테크닉을 이용하여 프로텍션을 풀고 코드를 열람해 보기도 했다. 분량이 상당히 방대했으며 지금 다시 봤으면 여러 재미있는 아이디어들을 발견했을 법도 해 보지만, 본인은 그 당시엔 프로그래밍 실력의 부족으로 인해 그다지 충분한 재미를 못 봤다.

그 느리고 허접한 GWBASIC으로 자체 한글 출력과 그것도 모자라서 입력까지 구현했는데 과연 어떻게 구현했을지가 궁금해지지 않는가?

GWBASIC의 후신인 QBasic이야 고릴라 내지 NIBBLES 같은 예제 게임 프로그램이 MS-DOS 5.0에 같이 곁들여 제공되기도 했다.
순수 GWBASIC으로 근성으로 만들어진 프로그램은? 먼 옛날에 무슨 허접한 자동차 경주 게임 같은 걸 본 게 마지막이다. 각 스테이지의 이름은 태양계의 행성 이름이었는데... 기억하는 분이 있으려나 모르겠다.

상용 제품인 QuickBasic도 GWBASIC의 전통을 이어받아 소스 코드를 자기 고유 포맷으로 저장하는 기능이 있었다. 물론 GWBASIC과 호환되는 포맷은 아니었다. 그리고 축소판인 QBasic은 그런 기능이 없다.
지금은 '큐베'라고 하면 음악 DAW 프로그램인 큐베이스(Cubase)가 먼저 떠오르는 세상이 됐으니 이것도 격세지감이다.

* 그리고 GWBASIC과 관련된 추가 여담.
IBM PC(=도스)용으로 이식된 GWBASIC이야 기본 프롬프트가 Ok이지만, 더 구닥다리 8비트 롬 베이직 같은 걸 보면 프롬프트가 READY인 경우가 있다. 빌 게이츠 아저씨가 GWBASIC을 최초로 만들 때 원래 의도했던 메시지는 ready였다고 한다.
그 사고방식은 오늘날 같은 Windows+GUI 시대에까지도 남아 있다. Excel이나 Visual Studio, 심지어 MFC 기본 어플이.. 내부적으로 더 처리할 메시지가 없이 사용자의 입력만 기다리는 idle 상태로 진입했을 때 아래의 상태 표시줄에 나타나는 메시지는 바로 Ready이며, 우리말로는 그냥 '준비'이다.

그랬는데 ready가 ok로 바뀐 이유는.. 메모리를 단 3 바이트라도 더 확보하기 위해서였다고 한다. 뿌우우... 묵념.

단, Ok도 다 대문자 OK도 아니고, 대문자 O에 소문자 k로 정해진 이유는 본인으로서는 지금도 알 길이 없다.

Posted by 사무엘

2014/10/15 08:23 2014/10/15 08:23
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1018

C/C++에서 구조체나 클래스는 통상적으로 global scope에서 선언되거나 기껏해야 다른 클래스 내지 namespace의 내부에서 선언된다. 즉, 어차피 비실행문들만 있는 곳에서 선언되는 편이다.
그러나 실행문으로 이뤄져 있는 함수 안에서 이들을 새로 선언하는 것도 문법적으로 가능하다. 다시 말해, 변수를 선언하는 것뿐만 아니라 그 변수들의 type을 결정하는 구조체나 클래스를 즉석에서 선언해 쓰는 것도 가능하다는 뜻이다.

다른 곳에서 두고두고 재사용하는 구조체가 아니라 함수 한 곳에서 튜플 같은 형태로 잠깐만 사용하고 마는 구조체라면, 이런 식으로 함수 안에서 간단히 선언해서 사용하면 좋다. 하긴, 그러고 보니 struct, class, union, enum뿐만 아니라 typedef도 실행문과 비실행문 문맥에서 모두 사용 가능한 물건이다.

함수 안에서 클래스 같은 걸 따로 선언하는 건 C#에서는 가능하지 않으며 C++만의 전유물인 듯하다.
이렇게 함수에서 선언된 자료형은 유효 범위도 마치 지역변수처럼 그 함수 안으로 완전 local하게 한정된다. 그래서 각종 IDE 같은 데에 명칭이 뜨지도 않는다. 지금부터는 이와 관련해서 좀 더 기괴한 이야기들을 풀어 보겠다.

1. 무명 자료형

C/C++에는 '이름 없는' 구조체/클래스/공용체 따위의 개체가 있을 수 있다. '이름 없는' 함수 그 자체만의 선언은 지원되지 않아서 함수형 프로그래밍 패러다임이 도입된 C++0x 이후에서야 람다와 함께 등장한 반면, 이름 없는 복합 자료형이라는 개념은 있었던 것이다.

class {
public:
    int x,y,z;
} obj;

C#이나 자바 스타일이라면 상상도 할 수 없는 일이겠지만, C/C++은 자료형의 선언과 해당 자료형에 속하는 변수의 선언을 동시에 할 수 있다. class OBJ { ... } a,b,c; 도 OBJ a,b,c;나 심지어 int a,b,c;와 개념적으로 같다.
class, struct, enum, union 등을 선언한 뒤에는 닫는 중괄호 다음에 세미콜론을 반드시 붙여야 하는 이유가 바로 이 때문이다.

그런데 이름 없는 자료형은 자료형의 선언과 함께 변수 선언도 같이 해 주는 게 선택이 아닌 '필수'라는 차이가 있다. 그도 그럴 것이, 얘는 이름이 없는 일회용 자료형인 고로 그 자료형을 선언하는 구문이 끝난 뒤에는 그걸 지칭할 방법이 없기 때문이다. 변수가 단 하나라도 같이 선언돼 있어야 나중에 C++11의 auto 라도 써서 그것과 동일한 자료형의 변수를 추가로 만들 수 있을 것이다.

이런 무명 자료형이라는 개념은 대개 한 자료구조 내부에서 구조체와 공용체를 섞어 가며 쓸 때 유용하지만, 그렇잖아도 일회용 성격이 강한 local 자료형에서도 더욱 의미가 있다. 굳이 이름을 생각할 필요 없이 내가 생각하는 복합 자료형을 간단하게 만들어서 쓰게 해 주기 때문이다.
물론 local뿐만 아니라 global scope에서도 무명 자료형을 얼마든지 선언해서 쓸 수 있다. C/C++의 오묘한 면모 중 하나이다.

2. 함수 안에 함수

C/C++은 복합 자료형은 앞서 살펴보았듯이 무명으로 선언할 수 있고, 그 안에 또 다른 복합 자료형을 nested된 형태로 선언하고 집어넣을 수 있다. 그러나 실행되는 코드의 집합인 함수를 그렇게 일종의 값처럼 자유자재로 다룰 수 있지는 않았다.

함수 자체를 다른 함수에다가 전달하는 것은 그나마 함수 포인터가 있으니 불가능하지는 않지만, 그건 자료형, 함수명 등에 대한 작명이 필요하며 기계 중심적이고 융통성이 부족했다. 또한 함수 안에다가 또 일회용으로 간단히 쓰고 마는 함수를 잠깐 선언하는 것도 가능치 않아서 global/class scope 차원에서의 선언이 필요했다. 남는 건 매크로 함수밖에 없지만 이게 얼마나 구조적으로 허접한 물건인지는 역시 설명이 필요하지 않는 수준이고.

void func()
{
    void simple_func(int x) { }

    simple_func(0);
    simple_func(1);
}

nested function은 C와 파스칼의 큰 차이 중 하나이기도 했다. 파스칼은 지원하지만 C/C++ 계열은 지원하지 않았기 때문이다. 마치 가변 길이 배열만큼이나 언어 차원에서 결코 지원되지 않을 금기 봉인 사항이기라도 한가 궁금하다. 다만, 옛날에 gcc던가 극소수 C 컴파일러에서 확장 옵션을 통해서 nested function을 지원하는 걸 본 것 같다.

물론, 중첩 함수를 써서 할 수 있는 일은 중첩 함수라는 개념이 없이도 완전히 똑같이 할 수 있기 때문에 상호 등가 교환이 가능하다. 마치 클래스에서 public과 private 구분을 해 주든, 아니면 전부 싸잡아 public인 struct로 코드를 작성하든.. 이것은 코드의 유지 관리의 편의성 내지 정보 은닉하고만 관계가 있지 프로그래밍 언어의 구조적인 계산 능력과는 무관한 것하고 같은 맥락이다. 그래서 C/C++은 nested 함수라는 개념을 도입하지 않은 듯하다. 정수 타입에 subrange 같은 개념도 없을 정도이니 뭐~

지금이야 람다 덕분에 함수 안에 함수의 선언이 사실상 가능해졌다. 캡처 같은 새로운 개념도 같이 도입됐다. 하지만 이건 일반적인 함수와 개념적으로 같은 물건은 아니다.
C++에서는 (1) 중첩 namespace 안에 들어있는 함수가 얼추 비슷한 개념일 수 있으며, 이것 말고도 좀 더 직접적으로 함수 안에 함수를 만드는 것이 편법 우회 경로로 가능하다. (2) 바로 함수 안에 클래스를 선언하고 멤버 함수를 정의하는 것이다. 이런 식으로.

int main(int argc, char* argv[])
{
    class A {
    public:
        static void Func() { puts("function inside function"); }
    };
    A::Func();
    return 0;
}

특히 static 함수는 this 포인터를 사용하지도 않으니 진짜로 일반 함수와 다를 바가 없다.
함수 안에다 구조체를 정의하는 것으로도 모자라서 완전한 형태의 클래스를 정의하고 멤버 함수를 정의하는 것까지도 가능하다니 놀랍지 않은지?

단, 이런 지역 클래스에서 멤버 함수를 선언할 때는 논리적으로 당연한 제약이 하나 걸린다. 함수의 몸체는 반드시 그 클래스 안에서 저렇게 정의되어야 한다. 안 그러면 아까 무명 자료형에서 변수 선언을 바로 안 해 줄 때처럼 경고가 뜬다.

비주얼 C++의 경우 일단 C4822 경고만 뜨고 그걸 실제로 호출까지 한 경우 링크 에러가 났지만, 요즘은 그 즉시 C3640 에러도 같이 나오는 듯. 링크 에러가 더 친절하게 컴파일 에러로 바뀌었다.
클래스의 밖인 그 함수 몸체 안에서 또 void A::Func() { } 이런 식으로 함수 몸체를 따로 정의하는 건 문법적으로 허용되지 않기 때문이다.

또한, 이런 이유로 인해, 지역 클래스는 static 멤버 함수는 가질 수 있는 반면 static 멤버 변수(=데이터)는 가질 수 없다.
그건 함수 안의 일반 static 변수와 같은 취급을 받으려나 궁금했는데, 만들어 보니 그건 언어 문법 차원에서 허용되지 않으며 곧바로 컴파일 에러가 난다. static const도 허용되지 않는다.

그러고 보니 이름 없는 클래스도 static 멤버 변수를 사실상 가질 수 없을 듯하다. 사실, 이름 없는 클래스에다가 그런 것까지 바라는 것 자체가 변태 도둑놈 심보이긴 하다. ㅎㅎ
멤버 함수야 몸체를 클래스의 선언부 안에다 강제로 집어넣는 식으로 정의할 수 있지만 static 변수는 결국 클래스 밖에서 따로 정의를 해야 하는데, 클래스 이름이 없으니 정의를 할 수 없어서 링크 에러가 나기 때문이다.
이거 정말 복잡한 문제다. C++이 C#/Java하고는 다른 독특 기괴한 면모가 이런 데서 또 발견된다.

Posted by 사무엘

2014/07/25 08:32 2014/07/25 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/988

하루는 본인은 회사 업무를 위해 인터넷에 굴러다니는 어느 암호화 알고리즘 소스를 프로젝트에다 붙여 쓴 적이 있었다.
그런데 곧장 문제가 발생했다. 본인이 맡은 부분은 Windows용 클라이언트인데, 같은 소스를 사용하는 다른 플랫폼 클라이언트 내지 서버와 교신이 제대로 되지 않고 있었다.

결국은 문제의 코드를 별도의 콘솔 프로그램 프로젝트로 떼어서 따로 돌려 보니, 문제의 원인은 그 암호화 알고리즘에 있음이 밝혀졌다. 같은 소스를 빌드해서 돌렸는데 결과가 서로 차이가 나는 것이었다.
게다가 Visual C++로 빌드하는 같은 Windows용 프로그램도, 알고 보니 debug 빌드는 결과가 옳게 나오는데 release 빌드만이 문제가 있었다!

debug와 release가 서로 다르게 동작하는 프로그램은 십중팔구가 멀티스레드 race condition 아니면 단순 초기화되지 않은 변수 때문이다. 물론 이 코드는 스레드를 따로 만들지는 않으니 의심 부분은 응당 후자. 이거 또 남이 짜 놓은 복잡한 코드에서 꼭꼭 짱박혀 있는 버그 찾느라 무진장 고생하겠다는 생각과 함께 몇 시간 동안 디버깅을 진행했다.

release 모드로 빌드된 프로그램은 함수 인라이닝과 각종 최적화 때문에 debug 빌드처럼 한 라인씩 엄밀하게 step in이 되지 않으며 변수값 조회도 안 되는 경우가 종종 있다. 그러니 도대체 언제부터 두 빌드의 변수값이 달라지는지 printf 신공을 펼치면서 꽤 어렵게 문제 원인을 추적해야 했다.

문제의 범위는 많이 좁혀졌다. stack이나 heap 메모리를 초기화하지 않고 쓴 경우는 눈을 씻고 찾아도 없었다. 마치 난수 씨앗처럼 초기의 동일한 input으로부터 일련의 output들이 계산을 통해 파생되는데, 언제부턴가 두 빌드가 생성해 내는 변수값이 미묘하게 서로 달라지는 게 보였다. 저 동일한 input 말고 계산에 영향을 끼치는 요소는 정말 없는데? 왜 값이 달라지지..?

그리고 결국은 설마 하던 녀석이 사람을 잡았다는 걸 알게 됐다. 문제의 함수는 바로.. 이것이었다!

unsigned long Rol(unsigned long x, long y)
{
    if (y % 32 == 0) {return x;}
    else {return ((x << y)^(x >> -y));}
}

저 간단한 함수의 실행 결과가 release 빌드와 debug 빌드가 서로 달랐다. 비주얼 C++ 2012, 2010, 2003 전부 공통으로.
암호화 알고리즘에서 절대 빠지지 않는 그 이름도 유명한 비트 회전(bit rotation)을 구현한 함수인데..
비트를 음수 shift하는 연산은 좀 생소해 보였다.

본인은 15년 가까이 C/C++ 프로그래밍을 해 오면서 지금까지 막연히 A<<-B = A>>B, A>>-B = A<<B이지 않으려나 생각해 왔다.
그런데 실상은 전혀 그렇지 않았다.
컴퓨터의 구조적인 특성상 나눗셈에서 피연산자의 부호에 음수가 섞이면 몫과 나머지의 부호가 수학에서 생각하는 직관적인 형태로 구해지지 않는다는 건 어렴풋이 알고 있었다만, 비트 shift에도 그런 특성이 있구나.

음수 shift의 결과는 언어 스펙 차원에서 undefined인 모양이다. 진짜 말 그대로 A=A++처럼 '그때 그때 달라요'인 듯.
중의적인 코드를 컴파일러마다 제멋대로 번역하는 것 자체를 모조리 막을 수는 없겠지만, 그건 최소한 '이식성'에 문제가 생길 수 있다고 경고라도 띄워야 하지 않나 싶다.

실제로 위의 함수를 실행하면

Rol(0xBE9F8300, 1);
Rol(0xEC6BFC33, 1);
Rol(0xFC58371A, 1);

의 함수값은 release 빌드에서는 각각 0x7D3F0600, 0xD8D7F866, 0xF8B06E34이 나온다.
그러나 debug 빌드에서는 0x7D3F0601, 0xD8D7F867, 0xF8B06E35가 나오며, 이게 맞는 값이다. release는 무슨 이유에서인지 최하 자리 1비트를 누락하고 있었던 것이다. 그러니 이후의 암호화 결과가 몽땅 틀어지는 건 당연지사.
설상가상으로 xcode에서는 더 이상한 결과가 나왔던 걸로 기억한다.

유명 암호화 라이브러리가 왜 저렇게 이식성 없는 연산을 썼는지 난 잘 모르겠다. 음수 shift의 결과가 어떻게 나올 것을 기대한 건지?
저 문제를 우회하느라 지금까지 머리로만 알고만 있었지 실무에서 쓸 일이 전혀 없으리라 생각했던 테크닉을 쓰게 됐다.
소스 코드의 특정 구간에 한하여 최적화를 잠시 끄는 #pragma optimize("", off) 되시겠다.

bit rotation은 bit shift에다가 한쪽 끝에 있는 비트들을 따로 반대편 끝에다 shift시켜서 얹어 준다는 차이만이 있을 뿐이다. 32비트 부호 없는 정수 기준으로, 작은 자리수가 큰 자리로 이동하는 왼쪽(<<) rotation을 나보고 구현하라면 이렇게 짜겠다.

UINT Rol2(UINT x, int y)
{
    return (x<<y)|(x>>(32-y));
}

32라는 숫자가 보기 싫으면 sizeof 등을 써서 다른 방식으로 바꾸면 되고.
그리고 이렇게만 짜도 컴파일러는 이 연산 전체의 의미를 알아보고 당연히 rol이라는 '비트 왼쪽 회전'이라는 '한 인스트럭션'으로 최적화해서 번역해 준다. bit shift인 shl, shr만큼이나 rotation도 굉장히 기계 친화적인 동작이며, 전용 명령이 있는 것이다. 하지만 정작 저 공개 라이브러리 함수는 Visual C++ 컴파일러가 rol이라고 최적화하지 않는다.

아마 -n shift는.. 전체 비트수에 대한 보수(32-n)만치 shift하는 것과 같다고 전제를 한 듯하다.
그리고 or 대신 xor을 쓴 것은 그게 컴퓨터 구조 차원에서 기계어 코드 길이가 더 짧거나 속도가 조금이라도 더 빨라서 그런 듯하다. 필요하다면 x=0조차도 x^=x로 표현하는 게 컴퓨터 세계이니 말이다.

결국은 음수 처리까지 정확하게 해서 shift든 rotation이든 -n만치 하는 건 반대편으로 n만치 하는 것과 같은 게 보장되는 함수를 만들려면..
if문을 써서 처리를 완전히 따로 하고 <<, >> 자체에는 어떤 경우든 음수 shift가 존재하지 않게 하는 게 이식성 면에서 가장 좋은 해결책으로 보인다. 흥미진진한 경험을 한 날이었다.

Posted by 사무엘

2014/06/15 08:36 2014/06/15 08:36
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/974

#define의 대체제

확실히 #define은 다른 걸로 대체 가능할 때는 가능한 한 안 쓰는 게 좋을 것 같다.
C++은 용도별로 다음과 같은 다양한 대체제를 제공한다.

1. 매크로 함수의 대체제: 인라인 함수로 대체 가능하며, 템플릿까지 동원하면 매크로 함수 만만찮은 유연한 메타프로그래밍이 가능하다.
또한 한 함수 안에서만 지엽적으로 반복되는 루틴을 정리하려면 C++0x부터는 람다 함수를 쓸 수도 있다.

2. 매크로 상수의 대체제: 정수의 경우 enum을 쓰면 같은 성격의 여러 심벌들을 한데 묶어 놓을 수도 있어서 좋다.
그리고 문자열은 그냥 const char/WCHAR 형태의 전역/클래스 static 변수로 처리함. 선언과 정의가 따로 존재해야 해서 불편할 수 있으나, 이것은 선언부에다 값을 다 집어넣고 확장 문법인 __declspec(selectany) extern const 를 지정해서 해결할 수도 있다.

아무 통제도 없이 너무 일방적으로 효력이 나타나는 #define보다는 저런 대체제들이 type-safety와 엄격한 scope 검증이 보장되기 때문에 "훨씬 더" 깔끔하다. 가능한 한 전처리기보다는 컴파일러에게 일을 맡기는 게 바람직하다.
내가 만든 명칭이 매크로로 이미 존재하여 딴 걸로 치환되고 있는 줄도 모르고 컴파일러가 자꾸 이상한 난독증을 보이며 에러를 뱉는 것 때문에 빡친 경험이 있는 사람.. 주변에 의외로 많다. ㅎㅎ

단, 그럼에도 불구하고 대체제가 존재하지 않아서 #define을 불가피하게 써야만 하는 경우는 아마도 다음과 같을 것이다.

1. #if #elif #endif 같은 조건부 컴파일 변수 지정

2. 함수 형태를 갖추기조차 민망할 정도로 너무 간단한 로직. 디버그 빌드에서도 독립된 함수 호출이 아니라 언제나 인라이닝이 반드시 보장되기를 바라는 부분

3. 호출하는 함수나 지정하는 변수 이름을 말 그대로 간단히 치환만 시키기를 원하는 경우

4. 대체제의 문법적 한도를 넘는 과격한 구문 치환을 해야 하는 경우. 특히 #나 ## 같은 연산자를 동원해서 완전히 새로운 토큰을 만들어 내야 할 때

5. __LINE__, __FILE__, __TIME__ 같은 빌드/디버그 정보를 그때 그때 삽입하고 싶을 때

6. 정수와는 달리 부동소숫점과 문자열은 여전히 #define이 유용한 경우가 있다.
부동소숫점은 enum이 지원되지 않고 static const 멤버도 클래스 선언부에서 바로 값 지정이 되지 않기 때문이다. (이걸 지원하는 컴파일러도 있긴 하나, 일단은 비표준임)
문자열은 매크로 상수의 경우, concatenate(연결)되는 문자열의 일부가 되는 게 가능하다. const 상수는 그렇지 않다.

#include와 #define이 너무 지저분하고 컴파일 시간을 증가시키는 요인이라며 없애자니.. 위와 같은 용도까지 부정하는 건 현실적으로 무리이긴 하다.

여담으로..
근래엔 남이 만든 코드를 읽다가 IID_PPV_ARGS라는 매크로를 보고 감탄하여 내가 짠 기존 코드에다가도 다 리팩터링을 해서 적용해 놨다.

CoCreateInstance와 IUknown::QueryInterface 때 꼴도 보기 싫던 void ** 형변환을 없애 주는 매우 편리하고 유용한 물건이다. COM이 등장한 건 무려 20년이 넘었고 C++에 템플릿이 추가된 것도 만만찮게 오래 됐을 텐데 이 매크로는 무려 Windows 7의 플랫폼 SDK에서야 정식 등장했다는 게 놀랍다.
매개변수 2개를 하나로 줄이는 역할까지 하니 이 정도라면 컴파일러가 아니라 전처리기 매크로밖에 선택의 여지가 없긴 하다.

Posted by 사무엘

2014/04/01 19:20 2014/04/01 19:20
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/947

3. 더 기괴하고 잉여력마저 의심되는 오버로딩

(1) 비트 연산자도 아니고 논리 연산자 && || !는 오버로딩할 일이 거의 없으며, 각종 C++ 디자인 패턴 책에서도 오버로딩을 권하지 않는 물건들이다. 그 연산자를 건드릴 게 아니라 개체를 건드리는 게 순리이다. 논리 연산자들이 취급할 수 있는 정수나 boolean 값으로 형변환하는 연산자를 제공하는 게 이치에 맞다.

굳이 논리 연산자를 오버로딩해 버리면, 일단 언어가 원래 제공하는 단축연산 기능이 사라지게 된다. 즉, A && B에서 A가 이미 false이면 B의 값은 아예 계산하지 않고 함수를 호출하지도 않는 것 말이다. 오버로딩된 함수는 논리 연산자라도 언제나 A와 B의 값을 먼저 계산한 뒤에 실행된다.

(2) 어떤 개체가 메모리에 차지하는 주소를 얻어 오는 기능은 그 어떤 타입이나 클래스에 대해서도 절대불변으로 동작해야 하는 기능이지 않은지? 마치 개체의 고정된 크기를 얻어 오는 sizeof 연산자처럼 말이다.
그럼에도 불구하고 C++의 단항 & (address-of) 연산자는 오버로딩 가능하다!

class Foo {
    //...
public
    int operator&() { return 0; }
};

void Bar(Foo *p)
{
    //...
}

이렇게 선언하거나 더 얄밉게 연산자 함수를 아예 private로 감춰 버리고 나면,
지역변수나 클래스/구조체의 멤버로 직접 선언된 Foo 개체는 Bar라는 함수에다 넘겨주는 게 불가능해진다. =_=;;;

Foo a; Bar(&a);

이런 테크닉이 무슨 필요나 의의가 있는지는 난 잘 모르겠다.

전통적인 &는 변수의 주소라는 R-value만 되돌리는 연산자인데, 이를 오버로딩하면 &는 참조자 같은 걸 되돌릴 경우 L-value를 되돌리는 것도 가능해진다. 따라서 그 값에 대해서 또 주소를 얻는 &를 적용하는 게 덩달아 가능해진다.
그러나 이 경우 &&를 연달아 쓸 수는 없으며, & &a 같은 식으로 토큰을 분리해 줘야 한다. 예전에 중첩 템플릿 인자를 닫을 때 > 사이에 공백을 넣어 줬던 것처럼 말이다.

(3) 게다가, 우선순위가 가장 낮으며 그저 여러 연산자들을 한데 나열하는 역할만을 하는 콤마 연산자도 오버로딩 가능하다! (,는 오버로딩 없이도 원래 아무 피연산자에 그 어떤 타입이 와도 무조건 괜찮은 유일한 이항 연산자임)
콤마는 함수 인자 구분용으로도 쓰인다는 특성상, 이 연산자는 가변 인자 함수 호출을 흉내 내는 용도로 쓰일 수 있을 것 같다. list, 3, 2, 1, 8, 4; 이라고 써 주고 list.add(3); list.add(2); ... 같은 효과를 낼 수도 있다는 뜻이다. 하지만 이걸 남발하는 건 좀 사악한 짓인 듯.

(4) 기괴한 오버로딩의 진정한 종결자로 내가 최후까지 남겨 둔 건 바로 ->* (pointer-to-member) 이다. 얘는 유사품인 ->하고는 오버로딩을 하는 방식이 사뭇 다르다!
-> 연산자가 아무 인자가 없는 멤버 함수인 반면, ->*는 단 하나의 인자를 받는다. 그 인자는 아무 타입이나 될 수 있으며, ->* 연산자 함수 자체도 다양한 타입으로 오버로딩될 수 있다. 가령,

POINT& operator->*(int x) { return m_pt[x]; }

이렇게 오버로딩이 된 클래스가 있다면

(obj->*0).x = 100;

이런 식으로 활용이 가능하다. 0이 연산자 함수의 인자로 전달된다. 0뿐만이 아니라 당연히 int 변수 n 같은 것도 줄 수 있다. .이나 -> 연산자 다음에는 구조체/클래스의 멤버가 뒤따라야 하는 반면, .*이나 ->* 연산자 다음에는 임의의 타입에 속하는 value가 올 수 있는 구조인 것이다. ->는 가리키는 대상 포인터이지만 .*는 대상으로부터 얻을 오프셋 자체가 고정이 아니라 동적이며, ->*는 대상과 오프셋이 모두 동적임을 뜻한다.

struct A { int x,y; };

struct B { A m_Obj; };

이렇게 A를 멤버로 갖는 B라는 클래스가 있다고 치자.
클래스의 멤버 포인터는 클래스에 종속적이다.
그러므로 클래스 B에 대해서 A에 소속된 멤버 포인터를 적용하고 싶다면 ->* 연산자를 오버로딩하여 다음과 같은 연산자 함수를 써 주면 된다.

int& operator->*(int A::*t) { return m_Obj.*t; }

그러면

B bar;
int A::*temp = &A::x;

bar->*temp = 100;
bar.m_Obj.*temp = 100;

위의 두 구문은

bar.m_Obj.x = 100;

과 동일한 의미를 지니게 된다. 실무에서 이걸 오버로딩할 일이 있을지는 잘 모르겠지만..;;
멤버 변수가 저렇고, 멤버 함수의 포인터에 대해서는 머리가 터질 것 같아서 생략하련다.
C++의 세계가 더욱 심오하게 느껴지지 않는가?

4. C++ 연산자 오버로딩의 한계

(1) 당연한 말이지만 원래 C++ 언어에 없는 새로운 토큰을 만들어 낼 수는 없다. 가령, @, ** 같은 듣보잡 기호를 연산자로 정의할 수는 없다. 특히 *는 포인터의 연쇄 역참조용으로도 쓰이기 때문에 ** 같은 건 C++에서 절대로 토큰으로 쓰일 수 없는 문자열이다.

(2) .(구조체 멤버 참조) .*(멤버 포인터) ::(scope) ?:(조건 판단. 유일한 삼항 연산자) sizeof 연산자는 의미가 완전히 고정되어 있으며 재정의할 수 없다.

(3) C/C++이 원래 정의하고 있는 연산자의 우선순위와 피연산자 결합 방향을 변경할 수는 없다. 그리고 built-in type에 대해 이미 정의되어 있는 연산의 의미를 재정의할 수도 없다.

이런 모든 구체적인 디테일들을 다 명시해야 한다면 C++의 참고용 매뉴얼은 정말 상상을 초월하게 두꺼울 수밖에 없겠다는 게 실감이 간다.

Posted by 사무엘

2013/07/17 08:36 2013/07/17 08:36
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/856

오늘은 C++에서 기초적이면서도 아주 심오하고 기괴한 문법 중 하나인 연산자 오버로딩에 대해서 좀 살펴보도록 하자.
정수, 포인터 같은 기본 자료형뿐만 아니라 사용자가 만든 임의의 개체도 연산자를 통해 조작할 수 있으면 천편일률적인 함수 호출 형태보다 코드가 더 깔끔하고 보기 좋아지는 효과가 있다. 물론, 아무 논리적 개연성 없이 오버로딩을 남발하면 코드를 알아보기 힘들어지겠지만 말이다.

C++은 연산자 오버로딩을 위해 operator라는 키워드를 제공한다. 그리고 개체에 대한 연산자 적용은 연산자 함수를 호출하는 것과 문법상 동치이다(syntactic sugar). 가령, a+b는 a.operator +(b) 또는 ::operator +(a,b)와 동치라는 뜻이다. 연산자 함수는 표준 규격이 없이 C++ 컴파일러마다 제각각으로 제정한 방식으로 심벌 명칭이 인코딩된다.

앞에서 예를 들었듯이 연산자 함수는 클래스에 소속될 수도 있고, 특정 클래스에 소속되지 않은 중립적인 전역 함수로 선언될 수도 있다. 클래스의 멤버 함수인 경우 this가 선행 피연산자에 포함된 것으로 간주되어 함수의 인자 개수가 하나 줄어든다. 연산자 함수는 명백한 특성상 default 인자를 갖는 게 허용되지 않는다.

한 클래스와 관련된 처리는 당연히 가능한 한 클래스의 내부에서 다 알아서 하는 게 좋다. 그러니 전역 함수 형태의 연산자 함수는 이항 연산자에서 우리 타입이 앞이 아니라 뒤에 나올 때 정도에나 필요하다. 가령, obj+1이 아니라 1+obj 같은 형태일 때 말이다. 그리고 이 전역 함수는 당연히 클래스의 내부에 접근이 가능해야 연산을 수행할 수 있을 것이므로 보통 friend 속성이 붙는다.

연산자가 클래스 함수와 전역 함수 형태로 모두 존재할 경우엔 모호성 에러가 나는 게 아니라 전역 함수가 우선적으로 선택되는 듯하다. a+b에 대해서 ::operator +(a,b)와 a.operator +(b)가 모두 존재하면 전자가 선택된다는 뜻. 언어의 설계가 원래 그렇게 된 건지는 잘 모르겠다.

본인은 연산자 오버로딩을 다음과 같은 양상으로 분류해 보았다.

1. 쉽고 직관적인 오버로딩

(1) 사칙연산, 대입, 비교, 형변환 정도가 여기에 속한다. 기본 중의 기본이다.
사칙연산을 재정의함으로써 C++에서도 문자열을 +를 써서 합치는 게 가능해졌다(동적 메모리 관리까지 하면서). 그리고 복소수, 유리수, 벡터 같은 복합적인 수를 표현하는 자료형을 만들 수 있게 되었다.

(2) 내부적으로 포인터 같은 외부 리소스를 참조하고 있기 때문에 단순 memcpy, memcmp 알고리즘으로는 대입이나 비교를 할 수 없는 클래스의 경우, 해당 연산자의 오버로딩이 필수이다. 이 역시 문자열 클래스가 좋은 예이다. 대입 연산자는 복사 생성자와도 비슷한 구석이 있다.
대입 연산자는 생성자와 역할이 비슷하다는 특성상, 연산자들 중 상속이 되지 않는다. 모든 클래스들은 상속에 의존하지 말고 자신만의 대입 연산자를 갖추고 있어야 한다는 뜻이다. 단, 순수 대입인 = 말고 +=, -= 같은 부류들은 상속됨.

(3) 비교 연산은 같은 클래스에 속하는 여러 원소들을 다른 컨테이너 클래스에다 집어넣어서 순서대로 나열해야 할 때 매우 요긴하게 쓰인다. 전통적인 C 스타일의 비교 함수보다 훨씬 더 직관적이다.

(4) 형변환 연산자야 상황에 따라 어떤 개체의 형태를 카멜레온처럼 바꿔 주고 개체의 사용 호환성을 높여 주는 기능이다. 포인터나 핸들 하나로만 달랑 표시되는 자료형에 대해서 자동으로 메모리 관리를 하고 여러 편의 기능을 수행하는 wrapper 클래스를 만든다면, 본디 자료형과 나 자신 사이의 호환성을 이런 형변환 연산자로 표현할 수 있다.
문자열 클래스는 const char/wchar_t * 같은 재래식 문자 포인터로 자신을 자동 변환하는 기능을 제공하며, MFC의 CWnd도 이런 맥락에서 HWND 형변환을 지원하는 것이다.

(5) 한편, 비트 연산자는 산술 연산보다야 쓰이는 빈도가 아무래도 낮다. 비트 벡터나 집합 같은 걸 표현하는 클래스를 만드는 게 아닌 이상 말이다.
단, 뭔가 직렬화 기능을 제공하는 스트림 개체들이 << >>를 오버로딩한 것은 굉장히 적절한 선택인 것 같다. MFC의 CArchive라든가 C++의 iostream 말이다. input, output이라는 방향성을 표현할 수 있고 또 비트 shift라는 개념 자체가 뭔가 '이동, 입출력'이라는 심상과 잘 맞아떨어지기 때문이다.

다른 연산자들보다 적당히 우선순위가 낮은 것도 장점. 하지만 산술 연산 말고 비교나 비트 연산자는 <<, >>보다도 우선순위가 낮으므로 사용할 때 유의할 필요가 있다.

2. 아까 것보다는 좀 더 생소하고 어려운 오버로딩

(1) 필요한 경우, operator new와 operator delete를 오버로딩할 수 있다. 물론, 객체를 생성하고 소멸하는 new/delete 본연의 기능을 완전히 뒤바꿀 수는 없지만, 이 연산자가 하는 일 중에 메모리를 할당하여 포인터를 되돌리는 기능 정도는 바꿔치기가 가능하다는 뜻이다. C/C++ 라이브러리가 기본 제공하는 heap이 아닌 특정 메모리 할당 함수를 써서 특정 메모리 위치에다가 객체를 배체하고 싶을 때 말이다.

특히 new/delete는 여러 개의 인자를 받을 수 있는 유일한 연산자 함수이다. size_t 형태로 정의되는 메모리 크기 말고 다른 인자도 받는 다양한 버전을 만들어서 new(arg) Object; 형태의 문법을 만들 수 있으며, 이때 arg는 Object의 생성자가 아니라 operator new라는 함수에다 전달된다. (delete 연산자는 어떤지 잘 모르겠다만..;;)

그리고 이놈은 전역 함수와 클래스 멤버 함수 모두 가능하다. 클래스 멤버 함수일 때는 굳이 static을 붙이지 않아도 이놈은 static이라고 간주된다. 당연히.. malloc/free 함수의 C++ 버전인데 this 포인터가 전혀 의미가 없는 문맥이기 때문이다..

사실, C++은 단일 개체를 할당하는 new/delete와 개체의 배열을 할당하는 new []/delete []도 구분되어 있다. 후자의 경우, 생성자나 소멸자 함수를 정확한 개수만치 호출해야 하기 때문에 배열 원소의 개수를 몰래 보관해 놓을 공간도 감안하여 메모리가 할당된다.
그러나 이것은 언제까지나 컴파일러가 알아서 계산을 하는 것이기 때문에 배열이냐 아니냐에 따라서 메모리 할당/해제 함수의 동작이 딱히 달라져야 할 것은 없다. 굳이 둘을 구분해야 할 필요가 있나 모르겠다.

(2) ++, --는 정수나 포인터 같은 아주 간단한 자료형과 어울리는 단항 연산자이다. 그런데 개체의 내부 상태를 한 단계 전진시킨다거나 할 때... 가령, 연결 리스트 같은 자료형에서 iterator를 다음 노드로 이동시키는 것을 표현할 때는 요런 물건이 유용하다. 난 파일 탐색 함수를 클래스로 만들면서 FindNextFile 함수 호출을 해당 클래스에 대한 ++ 연산으로 표현한 적이 있다. ㅎㅎ

++, --는 전위형(prefix ++a)과 후위형(postfix a++)이 둘 존재한다. 후위형 연산자 함수는 전위형 연산자 함수에 비해 잉여 인자 int 하나가 추가로 붙는다. 그리고 후위형 연산자는 자신의 상태는 바꾸면서 바뀌기 전의 자기 상태를 담고 있는 임시 객체를 되돌려야 하기 때문에 처리의 오버헤드가 전위형보다 더 크다. 아래의 코드를 참고하라.

A& A::operator++() //++a. a+=1과 동일하다.
{
    increment_myself();
    return *this;
}

A A::operator++(int) //a++.
{
    A temp(*this);
    increment_myself();
    return temp;
}

보다시피, 전위형과 후위형의 실제 동작 방식은 전적으로 관행에 따른 것이다. 전위형이 후위형처럼 동작하고 후위형이 전위형처럼 반대로 동작하게 하는 것도 프로그래머가 마음만 먹으면 엿장수 마음대로 얼마든지 가능하다.
정수의 경우 프로그래머가 for문 같은 데서 a++이라고 쓴다 해도 똑똑한 컴파일러가 굳이 임시 변수 안 만들고 ++a처럼 최적화를 할 수 있다. 그러나 사용자가 오버로딩한 연산자는 실제 용례가 어찌 될지 알 수 없으므로 컴파일러가 선뜻 최적화를 못 할 것이다.

일반적으로 a=a++의 실행 결과는 컴파일러의 구현 내지 최적화 방식에 따라 들쭉날쭉 달라지는 것으로 잘 알려져 있다. (당장 비주얼 C++와 xcode부터가 다르다!) 또한 a가 일반 정수일 때와, 클래스일 때도 동작이 서로 달라진다. 이식성은 완전히 안드로메다로 간다는 뜻 되겠다. 더구나 한 함수 호출의 인자 안에서 a와 a++이 동시에 존재하는 경우, 어떤 값이 들어가는지는 같은 컴파일러 안에서도 debug/release 빌드에 따라 차이가 생길 수 있으므로 이런 모호한 코드는 작성하지 않아야 한다.

(3) 포인터를 역참조하는 연산자인 * ->는 원래는 포인터가 아닌 일반 개체에 대해서 쓰일 수 없다. 그러나 C++은 이런 연산자를 오버로딩함으로써 인위적인 '포인터 역참조' 단계를 만들 수 있으며, 이로써 포인터가 아닌 개체를 마치 포인터처럼 취급할 수 있다. 형변환 연산자와 비슷한 역할을 하는 셈이다.

C++ STL의 iterator가 좋은 예이고, COM 포인터를 템플릿 형태로 감싸는 smart pointer도 .을 찍으면 자신의 고유한 함수를 호출하고, ->를 찍으면 자신이 갖고 있는 인터페이스의 함수를 곧장 호출하는 형태이다.
-> 연산자는 전역 함수가 아니라 인자가 없는 클래스 멤버 함수 형태로 딱 하나만 존재할 수 있다. 다음 토큰으로는 구조체나 클래스의 멤버 변수/함수가 와야 하기 때문에 리턴 타입은 구조체나 클래스의 포인터만이 가능하다.

(4) []도 재정의 가능하고 심지어 함수 호출을 나타내는 ()조차도 연산자의 일종으로서 재정의 가능하다!
[]의 경우 클래스가 포인터형으로의 형변환을 제공한다면 정수 인덱스의 참조는 컴파일러가 자동으로 알아서 그 포인터의 인덱스 참조로 처리한다. 그러나 [] 연산자 함수는 굳이 정수를 받지 않아도 되기 때문에 특히 hash 같은 자료구조를 만들 때 mapObject[ "H2O" ] = "산소"; 같은 구문도 가능해진다. 단, 진짜 고급 언어들처럼 인자를 여러 개 줄 수는 없다. map[2, 5] = 100; 처럼 할 수는 없다는 뜻.

()도 사정이 비슷한지라, 클래스가 함수 포인터로의 형변환을 제공한다면 굳이 () 연산자를 재정의하지 않아도 개체 다음에 곧장 ()를 붙이는 게 가능은 하다. 그러나 그 함수 포인터는 this 포인터가 없는 반면, () 연산자 함수는 this를 갖는 엄연한 멤버 함수이다.
함수 포인터로의 형변환 연산자 함수는 언어 문법 차원에서 토큰 배열의 한계 때문에 대상 타입을 typedef로 먼저 선언해서 한 단어로 만들어 놔야 한다.

Posted by 사무엘

2013/07/15 08:35 2013/07/15 08:35
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/854

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2021/10   »
          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:
1676160
Today:
76
Yesterday:
591