ㅇㅇㅇ

개발 2013. 10. 1. 21:49

http://www.ndepend.com/

http://www.jetbrains.com/profiler/

Posted by OOJJRS
,

Local, LocalLow, Roaming

개발 2013. 9. 22. 21:56

보통 임시 폴더로 알고 있지만 임시 폴더라고 하기엔 사용처나 개념이 약간 다르다. XP 시절에는 별달리 구분이 없이 Application Data 라는 폴더 하나만 있었지만 (부모 경로도 미묘하게 다르다) 비스타 이후 세분화되었다.


Local 과 Roaming 폴더는 윈도우의 제반 환경이 사용 시 서버가 필요한가(AD) 여부에 따라 역할이 구분된다고 한다. 폴더 이름대로, 어느 컴퓨터에서든 내 계정으로 로그인할 경우 이전에 사용했던 환경이 현재 컴퓨터에 내용 일치(동기화)가 필요하면 Roaming 아래에, 그런 게 아니면 Local 에 기록한다는 것이다. 만약 동기화가 필요 없는 커다란 데이터가 불필요하게 Roaming 아래를 사용하면, 그 계정은 아마 로그인할 때마다 많은 시간을 필요로 하게 될 것이다. 그래서 대부분은 사용 로깅이나 환경 설정 같은 기록들을 Roaming 아래에 사용한다고 한다.


LocalLow 는 권한 등급(Integrity Level, 권장 번역 단어가 없음. 이하 IL)이 낮을 때에도 사용할 수 있는 폴더라는 뜻이다. 보안 위협이 큰 익스플로러 같은 브라우저는 IL이 Low로 설정되어 있다. 이런 프로그램도 당연히 임시로 뭔가를 저장하거나 기록할 수 있는 위치가 필요한데, 그때 Local은 사용하지 못하니 LocalLow를 사용하게 된다. 반대로 말하자면 프로그램이 가질 수 있는 권한이 설정에 따라 매우 다양한데, 무조건 뭔가를 기록해야할 필요(로그, 덤프 등)가 있다면 LocalLow에 하면 된다는 뜻.


※ 참고자료

Low IL로 프로그램을 실행하는 방법

Posted by OOJJRS
,

CSIDL

개발 2013. 9. 22. 21:15

msdn 페이지 링크

http://msdn.microsoft.com/en-us/library/windows/desktop/bb762494%28v=vs.85%29.aspx


저 동네 주소가 변경되어도 상관없는 tinylink 주소 같은 건 어떻게 못 하려나 백방으로 찾아도 빡세다.


CLSID 와 비슷해보이지만 전혀 다른 녀석들이다.


Posted by OOJJRS
,

원문은 이쪽을 참고하기 바랍니다.

http://tortoisesvn.net/tsvn_1.8_releasenotes.html


메이저 버전이 올라갈 때마다 뭔가 적절한 기능이 추가되었다는 소리이므로 (1.7 버전으로 올라갈 때에는 .svn 폴더가 루트 쪽으로 통합된다는 내용이 가장 마음에 들었다) 이번에도 역시 기대를 갖고 보았다.

(물론 1.8 버전이 나온지는 좀 되었고, 안정화...를 기다린다는 명목으로 1.8.3 에 와서 좀 돌려보았다. 그런데 이번에는 마이너 버전이 1.7 시절에 비해 팍팍팍 올라간 기분.. 그냥 기분 탓)



다음은 개선된 기능의 목록


1. 블레임 UI 에서 각 리비전 별로 컬러가 추가됨

2. 파일 중 일부 내용만 커밋할 수 있음 (나이스!)

3. 거북이 merge 의 기능 향상

리본 UI 의 도입, go to line (항상 왜 없는지 의아했던), 등등

4. 저장소 브라우저 개선

드디어 한 svn 서버가 가진 모든 저장소를 루트로부터 찾아볼 수 있다. (나이스!)

뒤로 가기와 앞으로 가기 (나이스! 좀 더 브라우저스럽게 되었다)

5. Third Party Library 개선

외부에서 제작된 라이브러리를 svn 에 커밋했을 경우, 해당 라이브러리가 버전업이 되면 관리에 약간 부담을 갖게 되는데 대표적으로 파일이 삭제된 경우가 있다. 기존에는 파일의 추가나 변경은 그럭저럭 상관 없었지만 파일의 삭제는 캐치하지 못하고 계속 svn 에 등록된 채로 남아있었는데, 이제 1.8 버전부터는 간단한 마우스 우클릭 메뉴를 통해 그러한 녀석들도 한 방에 정리해줄 수 있게 되었다.

6. Progress Dialog

업데이트 등을 할 때 뜨는 진행 상태 다이얼로그가 좀 더 좋아졌다~

7. SubWCRev.exe 개선 (일종의 매크로 치환 동작을 하는 프로그램이다)

A. UTF-16 인코딩을 예쁘게 지원하게 되었다.

B. 작업소에 리비전을 박아넣을 때, 특히 리비전을 버전 정보로 사용할 때를 위한 새 옵션이 추가되었다. 기존에는 윈도우의 리소스가 16bit를 사용하기 때문에 65535 이상의 리비전 번호를 버전 정보로 사용할 수가 없었는데, 그래서 종전에는 16bit를 넘어가는 리비전 번호는 음수로 표기되는 골 때리는 일이 있었다.

C. SubWCRev 가 이제 등록되지 않은 (작업소 내의) 파일과 폴더 등도 찾아준다. 요 기능을 통해 커밋 때 빠뜨리고 올리지 않은 파일 같은 걸 찾아내서 담당자를 욕할 수 있다 에러를 콱 반환할 수 있다.

8. Custom 속성들

커스텀 속성을 사용하면 거북이svn을 좀 더 편리하게 사용할 수 있단다.

근데 이건 문서도 따로 있을 뿐더러 너무 길어서 해석은 생략 (죄송합니다) > 링크는 여기 <

9. 훅 스크립트를 속성 설정으로도 가능! (우와 씨앙 나이스!!!! 이번엔 네가 베스트)

기존에는 다이얼로그로만 설정이 가능해서 하나의 설정을 여러 컴퓨터에서 써야할 경우 (개인이 여러 플랫폼을 사용할 때 뿐 아니라 특히 대형 프로젝트에서 각자 설정할 때) 별도의 툴을 만들어 쓰거나 서비스 마인드를 함양하거나 포기했어야했는데 이제 속성으로 걸 수 있게 되었다. 이제 막 포맷하고 다시 깔아도 돼

이거 설정하면서 왜 서버에 한방에 설정하는 기능이 없냐고 욕을 바가지로 했었는데 이제 귀차니즘에서 해방된다!!



일단 무조건 버전업하고 보는 거다.


Posted by OOJJRS
,

설계의 정반합 1

개발 2013. 8. 31. 17:14

내가 개발 설계에서 항상 고민하는 점은 어떻게 해야 나중에 가서 이 작업이 고민거리로 발전하는 것을 막을 수 있을까 하는 점이다.


쥐뿔도 모르던 옛날부터 눈만 높아진 지금에 이르기까지 나를 개발자로 있게 하는 원동력은 욕망과 귀차니즘이었다.


컴퓨터로 하고 싶은 것들은 여럿 있는데 어찌나 사용하기 불편하고 귀찮은 게 많던지, 꽤나 세월이 흐른 지금도 나는 버튼 하나만 누르면 내가 원하는 기능이 딱 되는 그런 환경을 꿈꾼다.


모르긴 몰라도 수많은 설계 서적들에서 도달하고 싶은 경지는 결국 나와 같지 않을까 하고 상상한다.


하나의 마법과도 같은 단어, 혹은 함수 호출, 키워드, 정의를 통해 프로그램 하나가 턱 하고 튀어나오는 그런 세계.


수많은 개발자들이 일자리를 잃게 될지도 모르지만 어릴 때부터 항상 나는 귀차니즘에 휩싸여 살아왔던 듯 싶다.





다음과 같은 (Pseudo C++) 예제를 보자.


// 1단계 : 1에서 100까지 출력

printf("1\n");

printf("2\n");

...

printf("100\n");



근성을 발휘하여 100라인을 만들어보았다(상상 속에서). 그런데 조금만 공부해도 반복문이란 것을 배운다. 컴퓨터가 좀 더 우리의 삶을 윤택하게 해줄 수 있는, 가장 잘하는 일 아닌가?


// 2단계 : 반복문을 사용하여 1에서 100까지 출력

for(int i = 1; i <= 100; ++i)

printf("%d\n", i);


좀 더 재사용성을 높여서 함수로 만들 수 있을 것 같다.


// 3단계 : 1에서 100까지 출력하는 로직을 함수화

void func()

{

for(int i = 1; i <= 100; ++i)

printf("%d\n", i);

}


자, 이제 우리는 func() 함수만 호출하면 무조건 1에서 100까지 출력할 수 있는 기능을 갖게 되었다.


여기까지는 우리가 보통 어렵지 않게(그리고 순식간에) 도달하는 영역이다.




함수를 배우고 나서 조금 지나면 Parameter/Argument라는 것을 배운다. 갑자기 함수의 활용폭이 넓어진 것 같은 기분을 느끼며 시작값 1과 종료값 100을 외부에서 줘본다.


// 4단계 : Parameter를 통해 함수의 활용폭을 넓힘

void func(int begin, int end)

{

for(int i = begin; i <= end; ++i)

printf("%d\n", i);

}


어이쿠, 갑자기 1과 100이라는 숫자가 보이지 않으니 이 함수가 어떤 값을 처리할 수 있는지 명확하게 보이지 않아 조금 불안해졌다(의미 응집성 하락). 하지만 이 작업 하나로 지금 1에서 50까지 출력해야할 곳에서도, 나중에 927에서 23598까지 출력해야할 곳이 생겨도 모두 처리할 수 있게 되어 행복도가 증가했다(활용도 증가). 이 함수는 이제 뭐든지 다 할 것 같다. 그래서 약 5000군데에서 호출하여 사용했다.




이 단계까지는 연습용 코드 몇 개만 해봐도 그렇게 어렵지 않게 겪어볼 수 있다(5000곳에서 호출하는 정도는 아니겠지만). 그런데 아무래도 여러 곳에서 사용하다보니 점점 함수의 견고함이 증명되어 신뢰도가 올라간다. 따라서 이 함수에 기능을 추가하면, 적어도 기존에 돌아가던 기능은 잘 돌아가고 추가 동작도 매끄럽게 될 것 같아서 다음과 같은 요구사항도 끼워넣게 되었다.


// 5단계 : begin ~ end 사이에서 2단위로 출력

void func(int begin, int end, bool jumpTwoStep)

{

if(jumpTwoStep)

{

for(int i = begin; i <= end; i += 2)

printf("%d\n", i);

}

else

{

for(int i = begin; i <= end; ++i)

printf("%d\n", i);

}

}


아뿔싸, Parameter 하나 추가했다고 5000군데에서 호출하던 곳에서 빌드 에러가 나서 동료/친구/나 스스로에게 욕을 왕창 얻어먹고 나서 5000곳을 변경할 자신은 없으니 황급히 Default-Argument 를 끼워넣는다.


// 5.5단계 : 기존 호출되던 함수와의 호환성 보장

void func(int begin, int end, bool jumpTwoStep = false)

{

if(jumpTwoStep)

{

for(int i = begin; i <= end; i += 2)

printf("%d\n", i);

}

else

{

for(int i = begin; i <= end; ++i)

printf("%d\n", i);

}

}


그런데 내 작업을 우연히 지나가다 본 선배/파트장/팀장/라이벌/리드개발자가 이 함수는 확장성이 없다며 변경할 것을 권고했다. 하지만 아무리 생각해도 나는 단순한 작업 대신 복잡도를 올리고 앞으로 더 쓰일지 확신도 없는 코드를 힘들여 생산해내는 것에 회의적이다. 하지만 그들의 부드러운 말투가 험악한 욕설과 구타로 변하기 직전 뜻을 꺾고 그들의 조언을 받아들였다.


// 6단계 : 확장성을 위해 명료함을 희생함. 또는 복잡도를 높여 미래를 대비함

void func(int begin, int end, int step = 1)

{

for(int i = begin; i <= end; i += step)

printf("%d\n", i);

}


만들어두고 보니 이전 코드가 시작과 종료값만 처리했지, step은 처리해두지 않았다는 설계적 구멍이 있었음을 역설한다. 그리고 그것 때문에 내가 고생했다며 억울함을 역설하면서, 한편으로는 이 견고하고 확장성 있던 함수의 약점을 내가 찾아 보완했다는 것에 기쁨과 자부심을 느끼며 업무를 종료하고 그날의 턴을 마친다(음?).




설계 시에 항상 화두에 오르는 것 중 하나는 명료함 vs 확장성이 아닐까 한다. 이제 와서 다시 1~3단계의 코드를 보고 6단계의 코드와 비교해보자. 애초에 우리는 1에서 100까지 출력하기 위해 작업을 시작했다. 하지만 6단계의 함수로는 대충 무엇을 하는지는 파악했어도 정확하게 어떤 출력값이 나오는지는 저 함수를 호출하는 코드를 포함하여 전체를 다 뒤져보거나, 실행해보기 전에는 알 수 없게 되었다. 데이터(1, 100, 1)와 로직(반복문)이 분리됨으로써 활용도가 높아지고 확장성을 가지게 되었지만, 프로그램의 정확한 동작을 분석하기 위해서는 좀 더 넓은 영역의 코드를 볼 필요가 생겼다는 뜻이다.


만약 나중에 가서 func() 함수의 step 기본값을 1에서 2로 변경해야할 일이 생긴다면, 그 사람에겐 5000군데 이상의 의존성을 지닌 코드를 모두 조사하고 분석해보고 나서야 문제 없이 작업을 완료할 수 있을 것이다. 더 슬픈 사실은 그렇게 시간을 들이고 나서 "이 작업은 진행할 수 없습니다" 라고 이야기할 지도 모른다는 것.

Posted by OOJJRS
,

가끔 vs를 사용하다보면 vs의 버그라든지 버그라든지 버그 같은 것을 만나서


신기한 상황에 봉착하곤 하는데 대개는 삽질을 여러 시간 한 뒤에 알아차리게 되므로


굉장히 우울한 경우가 대부분이다.


이번에도 역시 마찬가지로, 특정 파일이 계속 encoding 이 뻑나는 상황이 나오기에


나는 해당 텍스트 파일이 미쳤거나 모종의 파일 BOM 트릭 같은 것에 걸린 줄 알았다.


그러니까, 다음과 같은 상황인데





// Note : 호환 이라는 구문이 잘 보이는가?


하지만 닫았다 열면,



인코딩이 깨진다.


삽질을 한 줄로 요약하여 결론을 내자면,


다음과 같은 새 프로젝트에서도



그냥 호환 << 이라고 주석을 달아버리니 깨진다.


두 글자들이 다 깨지는 것도 아니고, // 호환 이라고 해도 깨진다.


이러다가 깨지지 않는 글자로 인해 인코딩이 정상적으로 먹으면, 그 뒤로는 호환 이라고 써도 깨지지 않는다.


내 진짜 억울해서 팔딱팔딱 뛸 일이다만... (내 30분...)


방심하다가 가끔 당한다.

Posted by OOJJRS
,

설계 시 위험 신호

개발 2013. 2. 12. 17:56

아키텍처를 현재 조직구조에 맞추도록 한다.

DB가 추가된 이유가 DB팀이 일거리를 찾기 위해서 라는 식은 곤란하다.


25개 이상의 최상위 아키텍처 컴포넌트가 존재하는 건 너무 복잡하다.


한 개의 요구사항이 설계의 전체 방향을 이끌어나간다.

이 요구사항이 없어지면 그 아키텍처는 다른 이슈를 감당하기엔 지나치게 다른 길을 걸어왔다.

하나의 요구사항에 집중하면, 다른 이슈들을 건성으로 다루는 경향이 있기 때문이다.


아키텍처가 운영체제의 특정 버전에 의존적이다.

생각보다 이런 경우가 잦다.


표준 컴포넌트가 사용될 수도 있는 곳에 독점적인 컴포넌트를 사용한다.


하드웨어 단위 구분과 동일하게 컴포넌트를 나눈다.

시스템이 진화하여 하드웨어가 변경되면, 넌 x 된다. 소프트웨어 조직을 하드웨어에 끼워맞추지 말라.


신뢰성 확보라는 명분 하에 필요하지 않은 중복이 있다.

DB 이중화, 2개의 시작 루틴 따위는 시스템을 필요 이상으로 복잡하게 한다.


설계가 예외상황 중심적이다.

핵심 공통성이 아닌 기능의 확장성만을 강조한다.


개발조직에서 시스템 아키텍트를 식별할 수 없다.


아키텍트 또는 프로젝트 관리자가 아키텍처의 이해 관계자를 식별하는데 어려움이 있다.

관계도가 명확하지 않다는 것은 고려 없이 만들었을 가능성이 높다.


개발자가 두 컴포넌트의 상호작용을 코딩하는데 있어서 지나치게 많은 선택권을 갖는다.

아키텍처가 과다하게 선택권을 제공하거나 해당 이슈를 간과하도록 설계됐다면, 아직 아키텍처를 정의하는 중이다.


아키텍트에게 아키텍처 문서 작성을 요청하면, 클래스 다이어그램만 만들고 다른 것은 만들지 않는다(못 한다).


아키텍트에게 아키텍처 문서를 요청하면, 누구도 보지 못한 도구에서 자동으로 생성된 다량의 문서더미를 제시한다.

자동으로 생성이라도 되면 다행이다. 너무 복잡해서 생성하다가 프로그램이 터지는 프로젝트를 나는 알고 있다.


전달된 문서가 예전 내용이라면 당연히 최신 내용으로 관리되지 않고 있다.


설계자 또는 개발자에게 아키텍처를 설명하도록 요청하면, 못 하거나 상이한 아키텍처에 대해서 설명하고 있다.



- 소프트웨어 아키텍처 평가 286p



굵은 글씨는 내 경험 상 깊이 느꼈던 부분들이다.


Posted by OOJJRS
,

http://codeka.com/blogs/index.php/2009/03/21/got-visual-studio-2008-professional-want

 

Posted by OOJJRS
,

http://veblush.blogspot.kr/2012/10/map-vs-unorderedmap-for-string-key.html


매우 정리가 잘 되어 있는 글입니다

Posted by OOJJRS
,

static lib 파일

개발 2012. 12. 30. 23:51

http://en.wikipedia.org/wiki/Library_(computing)


http://stackoverflow.com/questions/140061/when-to-use-dynamic-vs-static-libraries


http://msdn.microsoft.com/en-us/library/958x11bc.aspx


http://stackoverflow.com/questions/4546507/lib-linking-other-libs

Posted by OOJJRS
,

로 출발한 내용들


일단 기본 지식부터


SxS

http://en.wikipedia.org/wiki/Side-by-side_assembly



/MD, /MT 설정들

http://msdn.microsoft.com/en-us/library/2kzt1wy3(v=vs.100).aspx




제기한 주제와는 큰 관련 없지만 그럭저럭 /MD 와 /MT 설정에 대한 토론글

http://stackoverflow.com/questions/757418/should-i-compile-with-md-or-mt


역시 별 관련은 없지만 마침 나처럼 부스트 라이브러리를 빌드하는 사람의 글이 찾아져서

http://stackoverflow.com/questions/9527713/mixing-a-dll-boost-library-with-a-static-runtime-is-a-really-bad-idea




이건... 내 의문과 딱 떨어지는 누군가의 질문

http://social.msdn.microsoft.com/Forums/en-US/vcgeneral/thread/a98b46f1-2d26-45c5-89c5-30714b18596a/





전통적으로 인스톨러를 좋아하지 않는 나는 기본인 /MD 설정보다 /MT 를 즐겨쓴다.


Posted by OOJJRS
,

VisualC.PDF


매번 찾기 힘들어서 링크 2


http://msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx


http://msdn.microsoft.com/en-us/library/hh567368.aspx


우씨 근데 정작 찾는 페이지가 없네 어디갔지

Posted by OOJJRS
,

매번 찾기 힘들어서 링크


http://msdn.microsoft.com/ko-kr/library/b0084kay(v=vs.100).aspx

Posted by OOJJRS
,

프로젝트 규모가 조금 커지면 간혹 만나볼 수 있는 문제다.


주로 프로젝트 관리가 체계적이지 못하거나 잘못되어 발생하는 문제인데, 의외로 원인을 찾는데 시간이 조금 걸린다.


문제의 원인이 여럿이기 때문이지만, 크게 중요한 문제가 아니라서 생각되어 그냥 넘어가기도 한다.


하지만 개인적으로는 이 문제를 매 수행 시 개발자를 번거롭게 할 뿐 아니라 빌드의 정합성을 보장하지 못 하게 하는 문제로 판단한다.


지금까지 경험했던 원인은 다음과 같았는데,



1. 빌드 대상인 파일이 모종의 원인에 의하여 마지막 수정 시간이 현재 시간보다 꽤나 앞으로 설정된 경우

이 경우 obj 파일에 찍힌 timestamp 보다 cpp 파일의 수정 시간이 항상 크기 때문에 항상 빌드를 요청한다.

대개 시간과 얽힌 테스트를 할 때 시스템(OS) 시간을 조정하면서 작업을 하다보면 저도 모르게 발생함.


2. 실제 include 되어 사용하지만 프로젝트에는 포함되지 않은 파일이 있는 경우

대개 헤더파일의 경우가 많이 발생하는데 (유니티 빌드의 경우는 cpp도 발생함) 결국 timestamp 비교가

온전치 못해 발생한다.

또는 프로젝트에는 필터에 포함되어 있으나 실제 파일이 없는 경우도 동일하게 발생한다.


3. lib 파일의 timestamp 가 꼬여서 현재 시간보다 먼 미래로 가버렸을 때

꼬임의 이유야 몇 개 있겠지만 본질적으로 1번과 크게 다르지 않고 소스와는 달리 알아채기 어렵지 않다.

소스는 파일이 매우 많아서 찾기 어려울 수 있지만 lib 파일은 출력폴더의 특정 확장자만 뒤져보면 되기 때문



기억나지는 않지만 아마 몇 가지 사소한 원인이 더 있었다고 기억하는데, 오늘 새로운 원인을 접했다.


이것도 본질적으로는 그냥 프로젝트 관리 실수에다 timestamp 와 연관이 있는 문제여서 다른 원인들과 같은 범주다.



4. 프로젝트들의 PCH 파일명이 동일하여 뒷 프로젝트가 앞 프로젝트 PCH 를 덮어쓴 경우

이 원인을 찾기 위해 헤맸던 이유가, out of date 가 나는 양상이 기존 것들과 약간 차이가 있었기 때문인데,

clean and rebuild(build) => 성공

바로 리빌드 시 => 빌드 실패 (가장 이해하기 힘들었던 부분)

+ 빌드 => 리링킹(?)

+ 빌드 => 변경된 파일이 없으므로 빌드는 하지 않고 최후 lib 파일만 작성(timestamp 갱신)

+ 빌드 => 빌드 성공 (이후로는 빌드하지 않음)



웃기는 건 저 다섯 단계 사이에서 어느 시점에 실행하느냐에 따라 계속 out of date 가 나기도 하고


한 번만 더 빌드하기도 하고 한다는 점이다.


이런 랜덤한 양상을 겪고 나니 그 동안 희석되었던 PCH 에 대한 거부감이 다시 살짝 살아날 뻔했지만...


빌드 환경을 정리해주는 것으로 해결하였다.

Posted by OOJJRS
,

http://stackoverflow.com/questions/10604243/call-stack-corruption-in-win32-vs2010-null-dereference-but-not-in-x64



자세한 해석은 귀찮으니 패스


결국 심볼이 잘못 연결된 문제


혹시 잊어먹을까 싶어 남겨둔다

Posted by OOJJRS
,

파일, db, 네트워크 어떠한 기반 장치로부터인지에 관계 없이 데이터가 구축될 때 구조를 두 가지로 나눌 수 있을 것 같다.

(편의상 의미를 알아볼 수 있는 정도의 수도Pseudo 코드를 사용하였습니다. 그런 API 없는데요? 라고 하시면 곤란합니다 :-D)




1. 정형화Normalize된 데이터 구조라고 명명함. 혹은 목적형For Objective 스크립트라고 해도 되겠다(내 맘대로 명명했다).


이러한 데이터들은 코드에서 직접적인 행위를 통해 접근이 가능한 집단이다. 완전히 일반적인 언어를 만들고 있지 않는 한 데이터의 가공 및 처리 편의성을 위해 명확한 목적을 지닌 하드코딩 연결부가 존재하는데,



<server ip="192.168.0.1" port="15000" />

----------------------------------------------------------------

if(!LIB_NET::connect(xml["server"]["ip"], xml["server"]["port"]))

{

    cout << "접속 실패" << endl;

    return 1;

}



와 같은 예가 정형화된 데이터를 사용한 예다. 코드는 명확하게 xml 로 server 가 존재함(예외 처리가 추가될 수 있다고 하더라도)을 알고 있으며 "server" 라는 토큰은 명확히 어딘가로의 접속을 위해 사용되는 매개체가 된다. 조금 더 시간을 투자한다면 복잡한 일반화를 통해 코드에서 server 를 직접적으로 활용하여 connect 를 호출하지 않고 스크립트에 코드를 두며 해당 parser 를 제작하는 식으로 처리할 수도 있지만, 웬만큼 크지 않은 프로젝트 대부분에서 이는 낭비에 속할 것 같다.



정형화된 데이터는 개발자 입장에서 읽고 쓸 때 명쾌하며 빠른 개발 속도도 보장한다.


빠른 대신 독립성의 이점은 별로 없다. 개발자가 모두 관리한다고 가정했을 때 이런 어중간한 데이터의 분리보다는 차라리 코드로 한 몸에 있는 것이 훨씬 낫다.


즉, 장단점을 하나로 정리하자면,


독립성, 관리비용 : 동적 데이터 구조 > 목적형 구조 > 코드


위와 같은 어중간한 방법이 가장 좋은 효율을 내는 경우가 아이템 1,000개를 만들 때 제작자와 개발자가 다른 경우다. 이때 목적형 스크립트는 서로 간에 의사소통 비용을 적절히 조율할 수 있고 요구사항/제공사항을 명확하게 나열할 수 있어 데이터의 1차 관리로 많이 활용되는 것으로 보인다.


<name>    아이템 이름

<stat>      아이템을 착용했을 때 증가하는 스탯 정의부

<desc>    아이템 설명


와 같은 명세서가 있다고 할 때 제3작업자인 스크립터는 쉽고 빠르게 단순반복 작업이 가능하다.


그러나 이 방식의 치명적인 단점이 있는데, 재활용하는 스크립트의 수가 적은 곳에 사용되면 1회용 코드가 난무하게 된다는 것이다. 위의 connect 에 사용된 server 정보와 같은 예다.




2. 동적Dynamic 데이터 구조


위의 구조를 좀 더 발전시켰다, 라는 개념보다 반대의 구조를 찾는다고 하면 동적 데이터 구조다. 목적형 구조는 하드코딩을 발전시킨 개념으로 보이지 하드코딩과 반대 개념이라고 보여지진 않는다.


동적 데이터 구조는 말 그대로 언어의 처리 방식과 비슷하다. 데이터가 얼마나 들어올 지, 무슨 데이터가 들어올 지 정해져 있지 않기 때문에 정형화된 모양으로 읽어들일 수가 없다.



<color name="black" r=0 g=0 b=0 />

<color name="blue" r=0 g=0 b=255 />

<color name="yellow" r=0 g=255 b=255 />



위와 같은 데이터는 얼마나 중복으로 들어올 지 알 수 없다. 동적 데이터를 처리하는 구조가 더욱 복잡해지는 이유는, 무엇이 들어올 지도 잘 모르지만 "얼마나 중복이 생길 지"도 잘 모른다는 사실이 더욱 구조를 복잡하게 만든다. 단순히 위의 예를 처리하려고 해도 정형화된 아이템 구조에서는 필드 하나로 끝날 스크립트가 리스트 구조 등을 요구하게 된다.


동적 데이터 구조는 코드에서 명확하게 그 대상을 지칭할 수 없다는 문제를 항상 염두에 두어야할 것 같다. 그렇게 만들려고 한다면 못 할 것은 없겠지만, 스크립트로 만든 데이터들을 코드에서 임의대로 지칭하려면 고려해야할 것들이 꽤 많다. 생성 영역과 관리 영역domain 이 다르기 때문이다. 반면, 스크립트에서 만들었다면 스크립트에서 해당 구조를 지칭하는 것은 그리 어렵지 않다.



<text color="yellow">안녕하세요?</text>



만약 yellow 가 하나 더 추가되어 2개가 되었을 경우 이를 코드 영역에서 지칭하려면 수정 후 리빌드 과정이 필요하지만, 같은 스크립트 레이어에서 이를 지칭하기 위해선 그냥 식별자identifier 만 수정하면 된다. 이는 코드에서 만든 것을 스크립트에서 지칭하기 어려운 이유와도 일맥상통하는데, 결국 관리자가 다르다는 것이 이유인 것 같다.


그래도 사용을 하려고 들면 못 하진 않는다. 다만 다음과 같은 일이 생기면 껄끄러운 것은 나 뿐인가?



while( xml["color"].IsValid() )

colormap[xml["color"]["name"]] = RGB( xml["color"]["r"], xml["color"]["g"], xml["color"]["b"] );

------------------------------------------------------------------------------------------------

DrawLine(10, 10, 100, 100, colormap["black"]);



동적 스크립트로부터 얻은 데이터로부터 특정 키 값을 주어 라인을 그릴 때 색상 값을 취하고 있다. 아마 이 스크립트에는 다음과 같은 주석이 항상 따라다닐지도 모르겠다.



<color name="black" r=0 g=0 b=0 /> <!-- 코드에서 사용하고 있으니 변경 시에 개발자에게 문의하세요 -->



저런 경우가 많아지면 설계 상으로 꽤 곤란할 것 같은데, 나만 그렇게 느끼는 건지도 모르겠다.





위의 두 가지를 모두 지원하려니, 라이브러리가 DOM 형식과 반복자 형식을 항상 같이 지원해야 한다.


작금의 많은 XML 라이브러리들을 위시하여 스크립트 파서나 데이터 관리자 클래스들이 두 가지 모양을 모두 지원하는 이유에 대해 짧게 고찰해보았다.


DOM 형식이 코드도 보기 예뻐서 좋은데, 여러 모로 참 귀찮다. 으아아아아아!!!!!!!


결론은 한탄 뻘글

Posted by OOJJRS
,

Wrapper, Bridge, Mediator

개발 2012. 8. 24. 11:26

세 가지는 모두 비슷한 개념으로 여겨진다. 역할은 모두 인터페이스 교정에 사용되므로 공통점이 많은 이 세 가지 기법의 주요 차이점을 알아본다.


개인적으로 브릿지에 대한 오해가 약간 있었다.




Wrapper


Decorator 패턴의 주요 기법. 어떤 Component 의 기능을 그대로 놔두고 그 위에 기능을 얹는다(단순히 어댑터의 역할만 할 수도 있다).

어떤 Component 의 Wrapper Class 는 대상 Component 에 당연히 의존성Dependency이 강하다. 즉, Wrapper Class 만 따로 배포할 수는 없다는 뜻.




Bridge


Bridge 패턴으로도 사용됨. BUS, 공통 언어 같은 개념으로 보아도 무방할 듯. 대개의 경우 어댑터 역할을 할 때 설계 문서상으로는 암묵적으로 생략되는 경우가 많다고 함. 이는 임시적인 속성을 지니는 브릿지의 특성 상 그럴 듯하지만 구현과 설계의 괴리를 높이는 요소로 지적될 수 있을 듯.




Mediator


중재자 패턴은 링크 생략. 래퍼와 브릿지의 속성을 모두 지니고 있으며 두 패턴의 끝판왕 같은 기분으로 등장했다. 임시적인 녀석이 아니라 설계 단계에서도 명확하게 표현되는 특징적인 방식. 말하자면 서로 다른 라이브러리를 소통하게 하고 활용하고 꾸미기 위해 필수로 필요할 경우 해당 기능(래핑, 브릿지)을 모조리 모아 패키지로 구성했다고 생각할 수 있다.

게임의 비즈니스 로직에서 만약 중재자를 사용해야 한다면 엄청나게 슬픈 일이 될 것 같다. 엔진과 라이브러리 등의 밑단으로 잘 내려보자.

Posted by OOJJRS
,

결론부터 말하자면 그런 방법 없다.


#include 와 #pragma comment(lib, file) 은 동작 방식에 차이가 있다.

#include는 강력한 경로 조합 기능을 갖추고 있으나 #pragma 는 현재 폴더를 기준으로 하는 상대경로 조합 밖에 되지 않는다.

왜 그렇게 만들었는지 누가 MS에 물어보고 답을 얻으면 내게도 공유해주길 바란다.



따라서 다음과 같은 경우

C:\MyProject\MyProject.sln 파일을 빌드한다고 하고

C:\MyLib\lib\FileManager.h
C:\MyLib\lib\FileManager.lib
 
가 있고 MyProject.sln 파일은 C:\MyLib 를 include 및 lib 폴더 경로에 지정했다고 한다면


#include "lib/FileManager.h"

#pragma comment(lib, "lib/FileManager.lib")

는 빌드되지 않는다. #include "lib/FileManager.h" 는 잘 되지만 #pragma 가 파일을 찾을 수 없다며 링크 에러가 난다.
(여담이지만 #pragma 는 역슬래시 경로를 먹이기 위해선 \\ 가 두 개 필요하여 귀찮으므로 보통 슬래시 경로를 많이 사용한다)

이유는 #include 의 경우 lib/FileManager.h 를 찾기 위해 우리가 미리 설정해둔 include 폴더 경로를 모두 조합하여

결국 C:\MyLib\lib/FileManager.h 를 조합하여 찾아내지만

#pragma 의 경우는 그런거 다 안 되므로 다음과 같은 결과가 되어

C:\MyProject\lib/FileManager.lib

당연히 없다고 나오게 된다. 물론 저 위치에 복사해 넣으면 잘 되지만 우리가 원하는건 그런 게 아니잖아?



그냥 오늘도 새벽녘에 코딩하다가 상기의 예와 비슷한 처리를 해둔 코드가 눈에 보여 포스팅해보았다.



아차, 해결책을 공유하지 않았는데 참 웃기는 일이지만 다음과 같이 lib 경로에 포함시키고 코드를 수정하면 잘 된다.

C:\MyLib\lib

#pragma comment(lib, "FileManager.lib")


그냥 MS가 이상하게 만들었다고 생각되지 않는가?


Posted by OOJJRS
,
SomeHeader.h

#ifndef _SOME_HEADER_
#define _SOME_HEADER_

// 헤더 내용들이 많이 있다.

#endif


OtherHeader.h

#pragma once

// ... 뭐 있겠지



두 가지 방식의 차이점이라든지 비교우위 등을 알고 싶어하는 이들이 많다.

짤막하게 짚고 넘어가자면 전자(SomeHeader)는 언어 차원의 표준이며 컴파일러가 항상 지원해야하는 믿을 수 있는 기능이고

후자(OtherHeader)는 일부 성능 좋은(?) 컴파일러만 지원하는 변종 기능이다.

변종인 만큼 후자의 방식은 전처리기가 아예 파일도 열어보지 않고 컴파일을 건너뛰고,

전자는 일단 파일을 열어보고 건너뛴다는 차이점을 가지며, 변종이 조금 더 속도에서 우세하다고 한다.



맞는 얘기들이었지만 사실 이젠 좀 철 지난 이야기들이다.

실제로 #ifndef 든 #pragma 든 둘 다 전처리기(Preprocessor) 지시자이며 컴파일을 하기 전에 먼저 처리되는 것들이다.

#ifndef 와 #endif (#else 라든지 #elif 도 좋겠다) 사이에 수백만줄 정도가 있다면 #pragma once 와 차이가 좀 벌어질 수 있으나

일반적인 기준에서 잘 짜여진 헤더파일(그 자체로 문서로 여겨질 수 있는 요약본 형식의)의 크기 안에서는

그럭저럭 무난하게 무시해줄 수 있다.
(물론 그렇다고 해서 그 헤더 파일이 수천번 include 되어도 무난하다는 소리는 절대 아니다)


중요한 것은 정말로 파일 전체가(꼭 헤더에 한정되지 않는다) 동일 obj 에서 한 번만 로딩된다면 #pragma once 도 괜찮겠지만

다른 조건들이 포함되어 있다면 #ifndef 를 활용하는 것이다. #ifndef 의 원래 용도를 잘 생각해보자.
(혹자는 #pragma once 가 표준이 아님을 들어 #ifndef 를 권하고 있으나 사실 가장 널리 사용되는 상위 컴파일러들은 모두 지원하고 있으므로 딱히 문제가 되진 않는다)

만약 헤더 파일이 수천줄 쯤 된다면, 그리고 그 헤더 파일을 많은 곳에서 include 한다면 #ifndef 보다는 #pragma once 를

쓰는 것이 확실히 전처리 속도에 도움이 된다.


감이 잘 안 오는 사람들을 위해 아래와 같은 스샷을 마련했다


 

대충 파일 구조는 위의 것을 보면 쉽게 알 수 있을 것이고

컴파일 결과를 그냥 IDE에서 컴파일해버리면 알기 어려우니 직접 명령어를 조작하여 컴파일을 시도해보는 것이 좋다.

원래 영어 IDE 를 사용하지만 스샷을 위해 한글을 마련했다. 왕친절한 것 같다.


 

 
<아 이렇게 친절할 수가.jpg> 



result.cpp 의 내용을 보자


 

드문드문 보이는 빈 공백들은 바로 전처리기가 #ifndef ~ #endif 같은 조건에 걸려 삭제해버린 줄들이다.
(물론 쓸모 없는 전처리기 구문 자체도 지우기 때문에 원래 3번 라인 밑에 나와야 하는 #ifndef _IFNDEF_ 같은 구문이 모두 빈줄로 대체되어 있다)


other.cpp 의 경우를 보아야 #ifndef 와 #pragma once 의 차이점을 확실히 알 수 있다.

other.cpp 는 22줄에서 보다시피 처음으로 ifndef.h 파일을 포함하였는데 이후 middle.h 에서 다시 한 번 포함한다(36줄)

그러나 앞서 이미 포함되어 있었으므로 모두 공백으로 대체되었다.

그러나 30줄에서 보다시피 other.cpp 는 pragmaonce.h 파일도 먼저 포함하였는데 이후 44번째 줄에서 보다시피

middle.h 가 선언한 #include "pragmaonce.h" 명령은 공백으로 대체되어 아예 포함조차 시도하지 않았다.

이것을 통해 #ifndef 와 #pragma once 의 실제 움직임을 조금은 따라가 볼 수 있는가?



#line 지시문에 대해서는 관심있는 사람들에 한해 찾아보도록 남겨두겠다.



그런데 주의할 점이 있다. 여기서 1회 포함 및 생성이란 얘기는 "파일 단위"에서의 이야기다.

그리고 이것이야말로 Unity Build 라는 기법이 왜 필요한가의 이유가 된다.


내가 심심해서 main.cpp 와 other.cpp 두 개를 만들지는 않았을 것이다.

우리는 보통 #ifndef (또는 #pragma once) 를 사용하면 헤더 파일을 전체 프로젝트에서 한 번만 include 할 것이라고

착각할 수 있는데(또는 별 신경 안 쓰고 넘어갈 수 있는데) 위에서 보다시피, 사실이 아니다.

한 번만 include 하는 것은 모두 오브젝트 단위에서의 이야기다.

오브젝트가 다르면 제 아무리 이전에 8만줄짜리 파일을 포함하여 컴파일을 이미 진행했다 하더라도

컴파일러는 다시 컴파일을 진행해버린다는 이야기다.

이런 짓을 하지 않기 위해 Unity Build 라는 스킬이 등장한 것이고 이와 관련해서는 서툰 내 글솜씨보다

훨씬 잘 된 자료가 있으므로 그것을 공유하며 이 포스팅을 마칠까 한다


http://www.slideshare.net/devcatpublications/ndc2010-unity-build




 


Posted by OOJJRS
,


해당 방식은 내가 만든 것이 아니라 이미 숙련된 프로그래머라면 대부분 쓰고 있을(또 만들 수 있는) 방법이다.

그 동안 이 방식보다는 다른 방법을 이용했었는데, 이번에 라이브러리화시키면서 끝내 만들게 되었으므로 하는 김에

조금 더 시간을 투자하여 많은 분들(특히 프로그래머 지망생들)에게 소개하고자 한다.

이 글을 읽기 위해서는 꽤 폭넓은 기반 지식이 필요하다(깊을 필요는 없다).

가능한한 모든 연관된 것을 설명하겠지만 링크나 검색 등으로 때우는 경우도 있을 것이다.




switch(sz)
{
  case "Name":
    ...
    break;
  case "Age":
    ...
    break;
}


위와 같은 구문이 C나 C++ 상에서 돌아가지 않는다는 것은 이미 잘들 알고 있는 사실이다.
(당연하죠! ... 때문에 컴파일 오류가 나잖아요? 라고 반문하는 사람은 없길 바란다)


switch 명령은 case가 많을수록 이론상 if문과의 성능 차이가 기하급수로 벌어지게 되어 있다.

이론상인 이유는 if - else if 열거식의 경우에도 만약 비교하는 대상 중 어느 한쪽이 상수라면 현대의 똑똑한 컴파일러가

자동으로 점핑 테이블을 만들어 마치 switch - case처럼 동작하기 때문이다.

상수 값(예제) 호출될 주소 (상대값)
2 0x0000
4 0x0016
6 0x0048
8 0x0052

※ 상수 값에 따라 IP 는 마치 함수호출처럼 자신의 주소를 향해 펄쩍 뛰어간다. break 구문을 만나면 switch 가 끝나는 블럭으로 또 한 번 점프한다. break 나 continue 는 goto 구문의 다른 (특수한 용도로 한정된) 이름이다.




그런데 상수가 아닐 경우는 점핑 테이블 생성이 불가능해진다. 아니, 불가능하다기보다는 우리가 원하는 그 성능이 나올 수가 없다.

점핑 테이블을 쓰는 것은 시간이 오래 걸리는 테이블 생성 작업을 1회 한 후(2011. 05. 31. 수정되었음) 한 번의 비교로 원하는 곳으로 찾아가 작업하겠다는 성능을 기대하는 것인데

변수가 case에 들어가게 되면 일반적인 if - else if 로밖에 구현이 안되는 것이다.

EIP = EIP + jmptable[nIndex];


하지만 변수가 case 에 가능하다고 하면 대체 jmptable 은 얼마나 커져야할까. 게다가 변수가 상수가 아닐 경우에는

nIndex 값 자체가 산출되지 않아 저런 식으로는 불가능하고 연결리스트 따위를 동원해야하니 속도 성능 같은 건 나락으로

떨어지는 것이다.




그래서 보통 빨리 짜야하거나 몇 가지 안되는 케이스를 가진 문자열 비교는 고전적으로 if - else if 로 행해진다

if(strcmp(sz, "Name") == 0)
{
}
else if(strcmp(sz, "Age") == 0)
{
}
else ...

이런 연쇄 else if 구문을 볼 때마다 속이 메스껍고 피로함을 느낀다면, 반갑다. 나도 그렇다.

게다가 더 큰 문제는 else if 중첩에는 한계가 있다는 것이다(error code C1061 참고)
(물론 정확히는 else if 중첩만이 문제가 아니지만, 어쨌든 많이 쓸 수가 없다)


그래서 이것을 개선한다면, 전통적으로 map이나 hash_map 을 사용해왔다.


static map< string, int > m;
void Init()
{
  m["Name"] = ENUM_NAME;
  m["Age"] = ENUM_AGE;
}

void func()
{
  switch(m[sz])
  {
    case ENUM_NAME:
      ...
      break;
    case ENUM_AGE:
      ...
      break;
  }
}



오, 오오... 깔끔하게 스위치 전환에도 성공했고 성능상의 이점도 가져왔다.

아 그런데... 뭐 좀 부족함이 느껴지지 않는가?

저런 문자열 비교 루틴이 수십 군데서 사용된다고 생각해보자(물론 다들 문자열 목록이 다른).

그런 경우가 얼마나 있겠냐고? 단순히 스크립트 데이터를 해석하는 코드만 해도 저런 것들이 부지기수다.


그래, 난 근성가이야. 하면서 그 수십 군데의 루틴을 모두 저렇게 열거형 상수를 선언하고 맵으로 고친다...

못할 건 없다. 초기화 함수 하나 만들고 변수 하나 선언하고 열거형 쯤 만들면 되지.

하지만 저런 부분들은 노가다 작업이다. 아무나 해도 될 뿐더러 저런 작업을 하고 있으면 굉장한 손해를 본다.

성능상으로야 가장 퍼펙트하지만(map 대신 hash_map을 쓴다면, 비록 표준은 아니지만 더욱 성능으로는 우위에 설 수 있을 것이다)

코드의 유지보수 관리에는 개떡이 되는 것이다.

매번 문자열 토큰(대개 태그라고도 하는데) 하나 추가될 때마다 상수도 추가해야하고 초기화에 라인도 추가해야하고

뭔가 작업할 곳이 최소한 세 군데가 생기는 것이다(열거형 상수쪽, 초기화쪽, switch 안)



그래서 생각하기를, 그냥 case 하나 달랑 추가하면 그 모든 걸 자동으로 해주는 신통방통한 녀석 없을까? 다.

완전 베스트 아닌가? 소개한다.


아래 소개된 코드는 앞서 이야기한대로, 내가 최초 사용한 기법은 아니지만, 작성은 스스로 했다(내 입맛에 맞게).

하지만 이해하기 쉽도록 몇 부분을 고쳤으며 이대로 활용하기엔 조금 불안한 부분이 있음을 미리 고백한다.
(뭐, 실무용으론 조금 부족하지만 예제용으론 그냥 써도 된다)


#define STR_SWITCH_BEGIN(key) \
{ \
  static stdext::hash_map< string, int > s_hm; \
  static bool s_bInit = false; \
  bool bLoop = true; \
  while(bLoop) \
  { \
    int nKey = -1; \
    if(s_bInit) { nKey=s_hm[key]; bLoop = false; } \
    switch(nKey) \
    { \
      case -1: {

#define CASE(token)  } case __LINE__: if(!s_bInit) s_hm[token] = __LINE__; else {
#define DEFAULT()    } case 0: default: if(s_bInit) {

#define STR_SWITCH_END()   \
      }      \
    }       \
    if(!s_bInit) s_bInit=true; \
  }        \
}



실제 사용은 다음과 같이 한다

STR_SWITCH_BEGIN(sz)
{
  CASE("Name")
    ...
    break;
  CASE("Age")
    ...
    break;
}
STR_SWITCH_END()


자, 이제 우리 입맛에 맞는 것들이 준비되었다. 앞으로 case가 추가된다 하더라도 단지 저곳에만 작업해주면 된다.

그럼 나머지 부분들은 모두 자동화되어 처리된다.

그런데 여기서 그냥 납득하지 않고 의문을 갖는 분들이 있다. 현명한 자세다. 세상에 공짜는 없는고로, 저런 게 그냥 됐으면

진작에 C++ 에서 표준으로 제공했을 것이다.

map(또는 hash_map)을 사용한 switch 보다 손해보는 점은 case 구문마다 if가 들어간다는 점과 위에서 미리 사용한

변수들이 자칫 기존 코드와 겹칠 수 있다는 점, 마지막에 STR_SWITCH_END 가 들어가 혼동된다는 점 등이다.

하지만 이 정도는 감수할 수 있을 정도에 불과하며, 사후 코드 관리 측면에서는 월등한 이점을 가져다준다.



마지막으로, 코드만 봐도 쉽게 이해할 수 있겠지만 위의 것을 사용할 때 놓치기 쉬운 몇 가지 주의점에 대해 언급하고자 한다.




1. 최초 1회에 한하여 초기화 과정을 수행하는데, 따라서 처음 1번의 비교는 제법 시간이 걸린다(모든 요소에 대해 일단 다 돌고 나서 원래 비교하려고 했던 값을 비교하므로). 그러니 만약 1회 비교하고 끝나는 루틴 같은 곳에는 저거 쓰지 말기 바란다.


2. hash_map 은 현재 C++ 표준이 아니다. 물론 그럴 확률은 지극히 낮지만, 언제 가서 코드가 메롱해질지 장담할 수 없다. 불안하다면 그냥 map 을 써도 좋다. map 은 표준인 만큼 견고함에 있어서는 해시보다 월등하다. 물론 비교 성능은 해시보다 좀 떨어진다.
(그래서 나도 해시맵을 탐탁찮게 생각하는 사람 중 하나이며, 해싱이라는 스킬 자체가 과연 키 해싱에 대해 무결성이 가능한지에 대해서도 회의적이므로 늘 불안불안하다)


3. VS 상에서 컴파일할 경우 릴리즈모드는 상관없으나 디버그모드에서는 컴파일 옵션을 바꿔주지 않으면 컴파일이 되지 않는다. 저 부분이 무슨 역할을 하는지는 검색하면 금방 나오니 활용하길 바란다.


원래 디버그 모드는 Program Database for Edit & Continue 옵션으로 되어 있는데

이것을 Program Database 로 바꿔주어야 한다(릴리즈 모드 옵션이다)


__LINE__ 이라는 매크로 때문인데 이것이 /ZI 옵션 상에서는 상수가 아니라 변수로 제어되기 때문에

상수로 코드에 박히는 /Zi 옵션이 필요한 것이다.
(case 에는 상수만 쓸 수 있다는 것을 다시 한 번 상기하자)


4. 실제 돌려보면 알겠지만, case 부분에 변수가 사용 가능하다! 하지만 컴파일만 되는 것일 뿐, 수행 결과는 첫 1회 초기화 때의 값에 따라 switching 되어 나온다. 너무 들뜨지 않길 바란다. 왜 그런지는 소스를 보면 잘 알 수 있을 거라 생각된다.



5. 혹시나 해서 첨언한다. 위의 예제 코드와 내가 만든 코드 사이의 차이는 언어 코드(MBCS / UNICODE) 분석 부분과 여러가지 예외 / 에러 처리 등이다.




많은 이들에게 도움이 되었으면 한다.


Posted by OOJJRS
,
말은 거창하지만 실상은 단순하다.
 
많은 선배 프로그래머들이 충고하고 MEC++ 책(※ More Effective C++ 스캇 마이어스 저, 항목 5)에서 강조한 부분이 있기도 하지만 후배 프로그래머들에게 잘 와닿지도 않는 모호한 경고를 앵무새처럼 늘어놓고 싶은 생각은 없다.
 
나는 대원칙만 제대로 세워두었다면 암시적 변환의 실보다는 득을 더 많이 얻을 수 있다고 생각하는 편이다.


다음 코드를 보자.

struct SB_TYPE_RELS    // 인간관계를 나타내는 타입
{
private:
unsigned char value;
public:
operator char*()
{
static char* s_sz[] =
{
"일반",
"아군",
"적군",
};

return s_sz[value];
}
};

위의 타입은 실제로 어떤 ENUM 타입의 값을 갖고 있지만 그것을 텍스트로 출력할 필요가 있기 때문에

다음과 같이 사용된다.

SB_TYPE_RELS Rels = SB_RELS_일반;
cout << Rels << endl;

이 경우 Rels는 암시적으로 char* 로 변환이 가능하기 때문에 operator <<  함수를 따로 정의하지 않아도 컴파일러가 처리해주게 된다.


그런데 주의할 점이 있다.

다음과 같은 가변 인자에서
printf("관계 : %s", Rels);

우리 생각엔 Rels가 "일반"으로 바뀌어서 %s 자리에 쏙 들어가야할 것 같은데 실제 컴파일을 하면 에러가 난다.

이유는 printf 가 내부에서 만들어질 때 %s를 만나면 인자에서 주소를 대입하게 되는데 그 모습이 우리 상상과는 많이 다르기 때문이다.
 
요점만 얘기하자면 컴파일러는 Rels가 char* 로 변환되어야한다는 사실을 모른다. 그래서 끔찍한 메모리 포인터 에러를 뱉는다.



두번째 주의점은 암시적 변환의 경우 미리 정의만 되어 있다면 얼마든지 몇 번에 걸쳐 변환을 해주지만,

변환 생성자의 경우는 외부 타입 -> 현재 클래스의 타입으로 만들어내는 방식이기 때문에

컴파일러가 두 번 이상 암시적으로 변환을 해주진 않는다는 것이다.


꿍얼꿍얼... 뭔 소린지 모르겠으니 역시 예제 코드를 보자


class CString
{
public:
CString(const std::string& sz);          // STL 의 string 타입을 통해 CString 인스턴스를 생성한다

bool operator == (const CString& s);  // 두 문자열이 서로 같은지 비교해본다
};

void main()
{
    CString  s("Hello World");      // 문제 없다. "" 문자열은 string 으로 암시적으로 한차례 변환되어 들어간다
    if( s == "Hello" )                   // 에러! "Hello" 를 CString 으로 변환할 수가 없다
}


오우, 안되는게 당연하지! 라면서 명확해 보인다고? 당신은 앞으로 크게 될 가능성이 있다.

그럼 다음과 같은 경우는 어떨까?


class CString
{
public:
CString(const std::string& sz);          // STL 의 string 타입을 통해 CString 인스턴스를 생성한다
CString(const CString& o);               // 익숙한 복사 생성자가 추가되었다
};

void main()
{
CString k = "Hello";   // 초기화를 했는데
k = "World";             // 바꾸고 싶다. 근데 에러가 났어 읭!?
}


"헐, 이거 뭐야." 라는 반응이 나오는가 아니면 "당연히 안되지!" 라는 반응이 나오는가? "이게 뭥미" 라는 반응이라면, 당신 조금 곤란하다.

조금만 생각해보면 "World" 는 CString 타입으로 암시적 변환이 가능하므로("World" => string 타입 => CString)

CString("World") 로 변환한 다음 복사 생성자를 호출하면 될 것 같다.

하지만 컴파일러는 저런 연산 과정을 할 수 없다. 우리 눈에만 명확해보이는 것이다.

그 기저에는 복사 생성자가 변환 생성자로 취급되지 않는다는 원칙이 깔려있다는 사실을 명심하자.



6월 14일자로 이 뒷부분의 내용이 생각나서 다시 이어 써본다.

그렇다면 복사 생성자일 때만 저것이 에러가 나는가? 그렇지 않다. 일반적으로 2번 변환을 하게 되면

컴파일러는 변환 과정 중에 n X m 크기의 테이블을 만들어서 변환을 해야할텐데, 이것의 성능이 도저히 측정이 되지 않기 때문에

뺄 수밖에 없는 것이다.


예를 들어 위의 경우 "World" 가 string 이 되고 그것이 다시 CString(const string&) 이 된다는 사실을 찾으려면

1차 변환 대조표와 2차 변환 대조표를 늘어놓고 일일이 매칭을 시켜봐야하는데 이게 몇 개가 나올지 예측이 안되므로

2번 이상의 암시적 변환은 허용하지 않고 있다.
(그러고도 못 찾을 수 있으니 컴파일러 입장에서는 그냥 에러를 뱉는게 속편하지 않은가?)


Posted by OOJJRS
,

서적 Effective C++ 은 개발자에게 널리 알려진 좋은 책이다.

그 중 개정 3판의 20항목에 다음과 같은 이야기가 나온다.


"값에 의한 호출보다는 상수 객체 참조에 의한 호출이 대개의 경우 낫다"


물론 이 책을 이미 정독했거나 충분한 경험을 가진 사람은 왜 저기에 "대개"가 붙는지 알고 있으리라.

그 내용에 대해서는 바로 연이어 21항에서 설명하고 있다.




여기서 살펴볼 것은 20항에서 기본 타입과 STL 반복자, Functor 등에 대해 언급한 부분이다.

저자는 이 세 가지에 대해서는 값으로 넘기는 것이 참조 호출로 넘기는 것보다 낫다고 이야기하고 있다.

원문의 내용을 보지 않아 정확히 해석이 이루어진 것인지 알 수 없지만 일단 위의 내용을 수긍하고 가기 위해

예제를 만들어 테스트를 해보았다.


직접 돌려보기 귀찮은 사람은 결과 텍스트 파일만 보아도 좋다.
 
직접 돌려볼 사람은 디버그 코드가 잔뜩 삽입되는 디버그 모드로 컴파일해서 "이거 순 엉터리잖아!" 라고 하지 말고
 
릴리즈 모드로 최적화를 뺀 상태로 컴파일하기 바란다. 최적화 옵션을 주어 컴파일해버리면 컴파일러가 구조체에 대해서는

알아서 참조 호출을 해버리고 함수 내용이 몇줄 안되면 인라인으로 만들어버린다.

평소라면 똑똑하다며 좋아하겠지만, 이런 테스트로는 참 곤란한 녀석이다.
(VS2008의 컴파일러가 C/C++에 대해 그렇게 효율적인 코드를 생성하지 못한다는 속설에 대해서는 일단 잊자)




테스트는 기본 타입(int), 기본 생성자를 가진 구조체를 자료로 하는 STL 타입(vector 사용), 기본 생성자를 가진 구조체 타입, 사용자가 생성자를 정의한 구조체 타입으로 4개를 등장시켰고

좀 더 많은 대조군과 세밀한 조사가 필요하겠지만 원론적으로는 불필요할 것 같아 생략하였다.




결과 파일에 의하면 중간 중간 스위칭으로 인한 어마어마한 시간 손실을 제외한 유효한 호출값에 대해서

모든 경우에 값 호출보다 참조 호출이 속도에서 우위를 보인다. 저자의 이야기와는 살짝 다른데...?




이론적으로 파헤쳐보자.

분명한 사실은, C++ 에서는 기본 타입이라 하더라도 내부적으로 모두 객체 취급을 받으며 클래스처럼 동작한다.
(물론 진짜 클래스인 것은 아니다)

즉 int i; 라고 선언한다 해서 이것이 C에서처럼 단순하게 메모리에 4바이트 땡기는 것으로 끝나지 않는다는 소리다.

일반적인 구조체/클래스의 생성자 호출과 비슷한 과정을 거쳐 메모리가 초기화되며(0이 아닌 어떤 값, VS 컴파일러는 0xCCCCCCCC로 초기화한다) 서로 대입되는 과정에서는

C에서처럼 단순히 어셈블리 코드의 메모리 로드/스토어 과정으로 변환되는 것이 아닌 operator = 함수 호출이라든지

복사 생성자의 호출 등 <함수 오버헤드>적인 측면이 생긴다.

다만 일반적인 생성과 대입 측면에서 볼 때 최적화 옵션이 주어지면 C++의 기본 인자는 마치 C의 그것처럼 동작한다.

디버그 코드로는 엄청난 내용들이 int i = 3; 이라는 짧은 내용 안에도 미어터지게 들어가서 호출 자체 시간도 약 10배 정도 늘어날 뿐더러

값 호출과 참조 호출은 2배 가량의 격차를 보인다(기본 타입인데...). 하지만 최적화를 하면 걱정할 필요는 없다.
(여기서 우리는 C와 C++의 기본 타입에 대한 차이를 확실히 알 수 있다)

덕분에 기본 타입은 내부적으로 주어진 주소에 메모리 상의 값을 끌어다 집어넣는 1회 연산의 차이로 끝나기 때문에 둘의 속도는 비슷하다. 다음 어셈 코드를 보자.


void f1(int i)를 호출하기 직전 모함수에서의 i 값을 레지스터에 밀어넣는 어셈블리 코드

004401AB  mov         eax,dword ptr [i]     // eax 레지스터에 i 변수의 값을 밀어넣기
004401B1  push        eax                        // eax 레지스터를 스택에 밀어넣기

void f2(const int& i)를 호출하기 직전 ... 어쩌구

00440245  lea           eax,[i]                   // eax 레지스터 값을 i 변수의 주소값으로 만들기
0044024B  push        eax                       // eax 레지스터를 스택에 밀어넣기

※ mov는 제2연산항의 "값"을 레지스터에 밀어넣기 때문에 load 연산(즉, 메모리 접근)이 추가적으로 필요하지만 lea는 주소를 넘기므로 추가적인 메모리 접근이 필요 없어 훨씬 빠르다. 단, lea의 경우 주소값을 써야하기 때문에 레지 대 레지 값 복사는 불가능하다

여기서 볼 수 있듯이 참조 호출이 값에 의한 호출보다 더 빠르다는 것은 일단 여지없는 정론이다.



자, 그럼 STL을 보자. STL은 내부의 모든 존재들이 클래스로 이루어져있다. 반복자도 물론 예외는 아니다.

그렇다면 반복자의 경우도 당연히 값으로 넘겨서 생성/소멸 과정을 거치는 것보다 참조로 넘기는 것이 속도에서 훨씬

빠르리라는 것은 당연히 추측할 수 있고 실제 결과로도 그러하다. 제 아무리 STL이 속도 위주로 설계되었고 최적화되었다고 해도

붕어빵 틀에 재료를 넣으면 붕어빵이 나올 수밖에 없는 것처럼 속도면에서는 값 호출이 참조 호출보다 빠를 수가 없는 것이다.

그 뒤로 이어지는 구조체 호출은 따로 분석하지 않겠다. 워낙 당연하기 때문이다.

void f3(vector< SDATA >::iterator it)를 호출하기 직전 모습

004402EA  lea         eax,[ebp-2Ch]
004402ED  push      eax 
004402EE  call        std::_Vector_iterator<SDATA,std::allocator<SDATA> >::_Vector_iterator<SDATA,std::allocator<SDATA> > (43D789h)
004402F3  mov         dword ptr [ebp-220h],eax

(와, 끔찍해라. 중간의 저 call 명령이 야기할 로스를 생각하며 잠시 묵념. 게다가 STL은 단일 클래스가 아닌 상속의 꽃과 같은 구조들이므로 안에서도 여러 차례 콜이 이루어지고 있다. 묵념을 한 3회는 더 해야할 듯. 게다가 <당연히> 레지스터엔 들어갈 수 없으니 메모리에 살포시 올려두고 함수로 간 다음 다시 찾아와야 한다. 기본 타입 int 형이 자신의 값을 레지스터 eax에 집어넣고 홀가분하게 호출했던 것과 비교하면 극과 극인 셈)

void f4(const vector::iterator&) 를 호출하기 직전 모습

0044038F  lea          eax,[ebp-2Ch]
00440392  push        eax 

(차이가 느껴지시는가?)




자 그렇다면, 여기까지 와서 생각해보자. EC++ 의 저자는 헛소리를 한 것인가?
 
아니면 일부러 교묘하게 독자의 이해력과 집중도를 오판하도록 만든 것인가?
(책의 전반부에서는 분명 "속도"에 중점을 두어 이야기하고 있었는데 후반부에 정리할 때는 다른 이유 때문에 값 호출이 더 낫다고 주장한 것인가?)

그도 아니면 그냥 해석상의 오류인가?

아니면 정말 VS2008 컴파일러가 바보라서 저렇게밖에 안되는건가?(설마 이걸로 믿는 사람은 없다고 생각하겠다)



단호하게 주장하는데, 기본 타입이든 아니든, 위의 내용들은
 
최소한 속도 면에 있어서
값에 의한 호출을 쓸 바에는 상수 참조 호출을 사용하는 것이 훨씬 낫다는 것을 보여주고 있다.

(그리고 그것이 우리의 상식에 부합한다)



그리고 속도 면이 아니라 다른 이유 때문에 위의 세 가지(기본 타입, STL 반복자, Functor)에 대해서는 값 호출이 낫다고

주장한 거라면, 역시 의도를 오해하지 않도록 가필이 필요하다.

어쨌든 책 내용을 수정하셔야 할 듯...
Posted by OOJJRS
,

Linking Error에 대해서

개발 2009. 11. 24. 16:11
프로그래밍 이론의 가장 외곽이라고 해야할까 - 프로그래머가 당장은 몰라도 처음 배울 땐 크게 개의치 않아도 되는

개념 중 하나가 링킹 과정이다. 컴파일 과정은 사실 건너뛰기도 쉬운 것이 우리나라의 현실 같은데 기본기 장만에

그다지 도움이 되는 현실은 아니라고 하겠다.



어쨌든 그 때문인지 초보 프로그래머나 갓 입문한 사람들은 이 링킹 에러가 등장하면 강도 높은 당황에 직면한다.

많이 쓰이는 Visual Studio를 예로 들어보자(gcc에 대해 다루지 않는다고 너무 미워하지 말라).

(6.0 키 셋팅이라는 가정 하에) F4 단축키를 누르거나 에러 목록을 더블 클릭하면 대개 에러가 난 코드로 이동해준다.

아 근데 이놈의 링킹 에러는 에러라고 뜨면서 컴파일이 안되긴 하는데 어디서 에러가 난건지 도통 알려주질 않으니

고치긴 해야겠고 영어라 무슨 소린진 모르겠고(요즘엔 한글 번역이 되어 나오는 것 같아 그나마 다행스럽다?)

설혹 메시지를 알아보더라도 그 뒤로 솰라솰라 나오는 영문자들은 분명 익숙한 글자 - 내가 만든 함수명 - 들을 찾을 수는

있겠는데 내가 설정한 것들과는 완전 딴판이라 결국 알아볼 수가 없다.

이것을 어떻게 고쳐야하겠는고... 느느니 한숨이요 버리느니 성질이라.



링킹 에러를 접하면 일단 당황하지 않는 것이 중요하다. 컴파일 과정을 자세히 들여다보면 각 파일에 대한 컴파일을 끝내고

링킹 과정을 거친다는 것 정도는 알아낼 수 있다.


[컴파일 과정은 무사히 끝나고 링킹 과정에서 에러가 난 모습]


위와 같은 현상이 일어나는 이유는 간단하다. 컴파일러는 초반에 Func 함수의 원형을 보고
 
"아, 어딘가에 이 함수의 본체가 있겠구나" 라고 생각하고 컴파일을 오류 없이 처리 완료했는데,
 
링킹 과정에 들어와서 실제로 엮어보려고 하니 본체가 어디에도 없는
것이다. 그래서 링킹 에러를 뱉게 된다.

왜 이 에러가 컴파일 시기에 에러를 만들지 않고 링킹 때 만들어내는가 하면, Func라는 함수의 본체가 반드시 같은 파일 내에

소속된다는 보장이 없기 때문이다. 원형은 Test.cpp에 되어 있지만 그 함수의 본체는 Func.cpp에 있을지도 모르는 일이다.

때문에 링킹 과정은 여기저기 흩어져 있을 함수들을 찾아 연결시켜주게 되는데 그 과정에서 컴파일 내용과는 달리

찾을 수 없어서 링킹 에러가 난 것이다.


링킹 에러의 원인은 여러 가지가 존재하지만 근본은 이와 다르지 않다. 다만 저 치명적인 에러(fatal error)를 무서워하지 않고

대처하면 된다. void __cdecl Func(... 어쩌구 라고 나오는 건 그나마 가벼운 경우이며 좀 더 왕창 복잡하게 나오는 경우도

있으나 당황하지 말고 "아 내가 뭔가 함수 본체를 안 만들어주었구나" 라고 생각하고 차분히 찾아보자.



다음은 링킹 에러가 많이 나는 예시다. 초보 시절엔 제법 참고가 될 듯 싶다.

1. 함수 원형과 함수 본체의 형식이 다를 때(또는 본체가 없을 떄) - 즉 리턴 형식이나 인자 형식이 다를 때 툭툭 뱉어낸다. 본질적으로 모든 링킹 에러의 원인이다.

2. 어떤 함수를 실제로 호출하는 코드가 하나도 없다면, 원형만 있고 본체가 없다고 해서 링킹 에러가 나지는 않는다.

3. 다형성Overloading을 이용해서 함수를 여러 개 같은 이름으로 작성했을 때, 실제 호출한 형식에 일치하는 놈이 없을 때 - JAVA/C++ 같은 OOP 언어나 다형성 지원 언어에서 대개 일어나지만 본질은 1번과 같다. 잘 찾아서 수정해주도록 하자

4. 함수가 static으로 외부에 공개되지 않았는데 외부에서 호출해대려 할 때
함수의 본체나 원형에 static이 붙으면 그 함수는 해당 파일 내에서만 사용이 가능하다. 다른 파일에서 호출하려 해도 함수 자체가 외부로 유출이 안되기 때문에 없는 것으로 판단하고 에러를 뱉어낸다. 단 이 경우는 대개 컴파일 단계에서 에러가 잡히며,
드문 경우로 어떤 파일에서 void Func(); 라는 원형과는 달리 그 본체가 static void Func() {} 따위의 내용을 갖고 있다면
링킹 에러를 뱉는다. 이 역시 1번과 본질은 같다.

5. 사실 링킹 에러는 비단 함수에 국한되는 것은 아닌데, 변수의 경우 extern int k; 라는 것을 A.cpp에서 선언했는데 어디에도 int k; 라는 전역 변수가 없거나 static int k; 따위로 선언되어 있다면 마찬가지로 찾을 수 없다고 링킹 에러를 뱉게 된다.


기타 등등의 많은 경우가 생길 수 있는데, 결국 링킹 에러는 컴파일러가 찾으려는 놈을 못 찾아서 생기는 문제다.

너무 두려워만 말고 내가 직접 찾아주자!
Posted by OOJJRS
,
대개 COM 관련된 내용을 검색하면 그저 표준호출(__stdcall)을 사용하라고 써있을 뿐 그 이유에 대해서는 적혀있지 않다.
(마소의 MSDN에서도 찾아볼 수 없는 내용이라 그런지도 모른다 - 어쩌면 나만 못 찾은 것일지도)

안타까운 일이라고 생각된다.




단순하게 얘기하자면 COM 함수의 원형이 그렇게 만들어져 있으니 공부를 위해선 맞춰가는 수밖에 없지 않겠느냐고

할 수 있겠지만 맥빠지는 이야기다.

C의 기본 호출은 __cdecl이며 해당 함수를 호출하는 쪽(호출원)이 인자 크기만큼의 메모리를 회수한다(스택 이야기).

함수를 완전한 독립된 개체라기보다 하나의 강력한 반복문 정도로 취급하는 것이며 C가 탄생할 때의 배경과 어울린다.

반면 C++의 기본 호출은 여러 가지 이유에서 __stdcall이며 이는 함수 쪽에서 스스로 스택을 정리한다.

메모리 관리 면에서 보자면 독립성과 일종의 캡슐화를 갖춘 셈이고 진보된 방법임은 분명하다. 다만 이것은 메모리 측면의

이야기일 뿐이며 기법 면에서 보자면 가변 인수를 사용할 수 없는 등의 약점이 있다.



COM은 OOP의 개념으로 탄생한 기술이고 따라서 C 호출방식보다 표준호출을 선택한 것 같다.

가변 인자를 사용하지 않는다면, 미미하긴 하지만 C 호출방식보다는 표준호출이 어셈블리 코드에서 조금 더 유리하다.
(C 호출방식은 함수를 여러번 호출하면 그때마다 메모리 회수 코드를 작성해야하므로 그만큼 줄 수가 늘어난다)



COM 함수를 직접 재정의하는 경우가 아닌 이상 일반 함수들도 무조건 표준호출을 할 필요가 없음을 알리기 위해 몇 자 적어보았다.
Posted by OOJJRS
,