Excel 파일 처리

2008/10/01 17:42 from 분류없음
대부분의 회사가 다 비슷하겠지만, 기획팀에서 데이터를 Excel로 관리한다. 전 회사에서 이 파일들을 "텍스트 (탭으로 분리)" 형식으로 저장을 해서 썼는데, 좀 더 복잡한 형태를 지원하는 포멧이 필요하게 됐다. 그래서 오늘 Excel 데이터 추출 프로그램을 만드는 준비를 좀 했고, 그 과정을 기록한다.

1. 오피스 2007의 경우에 .NET 프로그래밍 지원이 기본 설치에서 빠져 있다. 일단 이걸 추가해야 관련 어셈블리들이 설치가 된다.


2. 어떤 언어를 사용하든 큰 차이는 없겠지만, 난 C#을 이용했다. 일단 프로젝트를 생성하자. VS Tools for Office가 아니면 오피스 관련 프로젝트가 없으므로 일반 프로젝트로 생성해야한다.


3. 솔루션 탐색기에서 참조에 새 항목을 추가한다. 정확한 이름은 Microsoft Excel 12.0 Object Library인데, 설치된 오피스 버전에 따라 숫자는 다르리라 생각된다.


4. 열심히 삽질을 하면서 코딩을 해본다.


5. 주의 사항 몇가지.
- Open의 첫번째 파라미터인 파일 이름은 Full path여야 한다. 계속 예외를 내서 무슨 일인가 한참을 고민했다.
- 실제로 내가 원하는건 cell[1, 10].value 이런 식의 접근인데, range를 써야해서 상당히 복잡하다. 일단 원하는 바를 이루기는 했는데 이 부분은 개선이 필요할듯(혹 이와 관련된 내용을 아시는 분은 좀 알려주세요~)

6. 자 이제 상상력을 총동원해서 목표를 이루자!!
Posted by 조성경 트랙백 0 : 댓글 1

IOCP 구조(2)

2008/09/26 11:30 from Programming
IOCP 쓰레드 개수
에 이어 다음 생각을 정리해보자.

워커 쓰레드가 1개로 고정된다면 전역 버퍼의 존재가 필요없거나 병목지점으로 변하게 된다. 모든 쓰레드가 하나의 전역 버퍼에 물려 있다는건 좋은 생각이 아니므로 전역 버퍼를 안쓰도록 방향을 바꾸자.

그러나 IOCP에서 어떤 패킷을 받아서 누군가에게 전달하는 구조를 생각한다면 전형적인 소비자-생산자 구조이므로 중간에 버퍼가 필요하다.

워커 쓰레드가 1개로 고정되기때문에 다른 쓰레드의 버퍼로 데이터를 전달할때 블러킹이 되면 곤란하므로 버퍼를 만들때 신중한 구현이 필요하다. 처음 이 그림을 그렸을때는 SLIST_ENTRY 같은 lock-free 자료형이나 직접 구현을 생각해서 그림이 저 모양인데, 그리고 나서 생각해보니 각각에 IOCP를 사용하면 더 간단한 구현이 가능할 것 같다. 이건 어느정도 간단한 구현을 해봐야 제대로 된 구조를 잡을 수 있을 듯.

목표는 당연히 최고의 성능이며 부가적으로 락을 최소화하는 것이다. 그동안 일했던 경험에 의하면 락 전략을 세운 사람은 언제나 제대로 락을 걸지만 다른 사람들은 항상 삽질을 거듭했다는 것이다. 락 자체를 최소화함으로써 그러한 문제를 해결하고 싶다.

ps. 중간에 얘기한 것처럼 그림의 오른편에 있는 각각의 쓰레드들을 IOCP를 사용해 구현한다면 IO와 관련된 Worker Thread를 하나만 유지해야할 필요성은 없어 보인다.
Posted by 조성경 트랙백 0 : 댓글 0

IOCP 쓰레드 개수

2008/09/25 12:10 from 분류없음

왼쪽 그림이 IOCP를 사용하는 서버의 일반적인 구조라고 생각된다.

이 그림에서 끊임없는 화제거리를 제공했던 주제는 worker thread의 N을 얼마로 해야하나 였고, 심심할때 가끔 나오는 주제는 worker thread가 얼마나 많은 일을 해야하나였다.

쓰레드 개수야 아직도 정답이 없고 worker thread가 하는 일은 시대에 따라 조금씩 변해왔다.

내가 처음 게임회사에 일을 시작했을때 대세는 worker thread에서 가능한 적은 일을 하는 것이었다. 그당시에 얻을 수 있는 거의 모든 소스(책, 해외 웹사이트 등등)에서 그렇게 하는게 정답이라고 말을 했고 주변에서도 그렇게 일을 했다.

그리 멀지도 않은 과거 그러니까 90년대로 돌아가보면 - 우리 모두가 스티븐스님의 책을 보면서 터미널에 다닥다닥 붙어 유닉스에서 네트워크 프로그램을 배우던 바로 그때 - 프로그램은 당연히 여러개의 프로세스로 이루어져 있었다. 포크(fork)를 통해서 새로운 프로세스를 만들어 일을 처리하는건 당연하면서도 훌륭한 방법이었으니까.

실제로 99년에 참가했던 산학협력 프로젝트에 교수님이 제안한 구조는 천개가 넘는 프로세스로 구성이 되어 있었고 그 결과물을 이상하게 생각하는 사람은 없었다. 그러나 시간이 흐르면서 21세기가 되자 분위기가 반전됐다. 과도한 context switching(쓰레드간의 작업 전환. 프로세스는 1개 이상의 쓰레드로 구성되므로 프로세스간의 전환도 결국 쓰레드 전환이다)이 인류의 적이 되어 버린 것이다.

적절한 쓰레드(프로세스) 개수를 유지해 쓰레드간의 전환없이 계속해서 일을 하는 프로그램을 만들어내는게 성능 문제의 한 주제가 돼버린 것이다. 이러한 추세에 탄력을 받았는지 어쩐지는 잘 모르겠지만 이즈음 슬슬 worker thread에서 하는 일도 변하게 된다. 버퍼에 단순히 데이터를 전달하는 입장에서 점점 더 많은 일을 하게 된 것이다. Worker thread가 IO처리만하고 잠드는 비용을 무시할 수 없었고 코어(그때는 프로세서였지)의 수도 늘어나면서 더 잘짜여진 쓰레드 전략이 필요해졌기 때문이다.

이 즈음의 가장 큰 이슈는 저 그림에서 Buffer(s)를 어떻게 처리하냐 하는 문제였던 것 같다(이 문제는 지금도 골치거리중 하나) 잘 만들어도 결국 버퍼에서 병목현상을 일으키게 되기 때문이다. 그때부터 지금까지도 서버 구조를 설계하면서 가장 신경 쓰는 부분중에 하나가 저 버퍼를 어떻게 처리하는가 하는 점이다. 일을 할때마다 조금씩 다른 구조로 설계를 했고 많은 변화가 있었던 부분이기도 하지만, 큰 그림은 저 위의 그림과 크게 다르지 않았다.

오늘 아침에 양치질을 하다가(드디어 이 글에서 진짜하고픈 말 등장이다) 문뜩 생각이 났는데 worker thread를 여러개 유지해야할 필요성이 있는가 하는 것이다(양치질 하면서 이런 생각을 왜 하는지는 묻지 말아주3). Worker thread가 여러개 필요한 가장 큰 이유는 worker thread가 블럭됐을 때 보다 효율적으로 일을 처리하기 위함이다. 그러나 내가 그동안 만들었거나 다른 사람들이 만들어 놓은 게임 서버의 구현을 생각해 볼때 worker thread가 블럭되는 경우는 사실상 없었다. 있다면 다수의 worker thread들이 저 buffer에 접근하면서 락을 걸어 서로를 괴롭히고 있을때 였다.
 
이렇게 생각해보자. 저 그림에서 worker thread를 1로 만들면 쓰레드간의 락이 일단 사라진다. 전체적으로 간결한 락 전략을 수립할 수 있고 쓰레드 개수 감소로 불필요한 문 전환도 근본적으로 생기지 않는다. 이러한 생각은 한 개의 worker thread를 사용해도 IO 성능에 문제가 생기지 않는다는 가정이 필요하다. IO의 성능에 차이가 없고 worker thread에서 살짝 다른 일을 하면서도 반응 속도에 영향이 없다면 훨씬 나은 구조가 될 수 있다. 물론 이렇게 되면 버퍼 뒤편에 있는 쓰레드들의 구성에도 변화를 줘야한다. 여러 쓰레드가 버퍼에 접근하면서 락을 사용하게 만들면 worker thread의 개수를 줄이면서 얻는 이익이 많이 날아가 버리니 말이다.

일이 좀 밀려 있기는 하지만 worker thread를 1개만 사용하는 구조를 테스트해봐야 겠다.
Posted by 조성경 트랙백 1 : 댓글 0
작업중인 코드를 보면 다음과 같은 클래스가 하나 있다.

분석이고 뭐고 할것도 없는, winsock dll을 로드/언로드하는 역할을 하는 간단한 클래스다. 이와 비슷한 기능을 하는 경우 별 생각없이 이런식으로 만들 곤 했는데, 최근들어 이렇게 하는게 무슨 소용이 있을까 하는 생각이 든다.

서버에서 저 코드를 사용한다면 반드시 IOCP 서버와 붙여서 사용한다(다른 방법도 있겠지만, 열에 아홉은 IO model로 IOCP를 쓴다). 다음과 같은 형식이 더 낫지 않을까?

자잘한 클래스들이 많아지면 파일의 개수가 늘어나 솔루션이 버벅거리고 컴파일이 오래 걸리는 물리적 단점 외에도 코드의 흐름이 불분명해진다는 문제도 생긴다.

위의 두 코드 조각을 보면 Init()에 WSAStartup()이 있는 경우는 Init()의 코드 흐름 속에 분명하게 DLL을 로드하는 부분이 들어가지만 UsingWinsock이라는 클래스를 만든 경우에는 이 놈이 어디선가 됐다는 가정을 하게 될 뿐이다. 사실 어디서 언제 되는지는 알 방법이 없다(대략 위치와 시점의 짐작은 가능하다).

그렇다고 저 UsingWinsock이 지 혼자 뭔가를 할 수 있는 그런것도 아니다. 이런 생각을 가지고 보니까 많은 부분이 마음에 안든다. 과연 저걸 나눈 목적을 나는 이룬걸까?라는 질문에 "응"하고 즉시 대답 할 수 없는 부분이 많다. 그냥 파블로프의 개마냥 본능적으로 나누고 쪼갠 것들이 많았다.

현대의 프로그램을 보면 비슷한 목적을 이루는 방법이 몇 가지 있다. 함수, 파일, 네임 스페이스, 클래스까지. 좀 더 일반적인 이름으로는 모듈화 정도가 될 수 있을듯 하다. 물론 성격이 서로 약간씩 다르긴하지만 이들을 통해 비슷한 결과를 만들어 낼 수 있다.

너무 자잘한 네임스페이스, 너무 잘게 쪼갠 함수, 너무 세분화된 클래스, 몇 줄되지도 않는 파일들. 나는 왜 이런 짓들을 무의식 중에 하고 있는 걸까.

디비쪽을 보면 정규화라는 것이 있다. 제거하고 쪼개고 또 제거하고 다시 쪼갠다. 그렇다고 계속해서 정규화를 하는 것은 아니다. 반정규화라는 걸 하기도 한다(내가 학교 다닐때는 역정규화라고 배웠는데 요즘에는 반정규화라고 하데). 성능을 위해 다시 합치는 행동을 말한다. 코딩중에는 이런 반정규화 과정을 적용하는 건 불가능한 걸까.

뭐든 지나치면 모자르니만 못하다는게 일반적으로 통하는 얘기다. 아직 어디서 멈춰야할지 모르는 상태가 맞다. 그러나 앞으로는 쪼개고 나누기 전에 꼭 다시 한 번 생각하고 싶다.

ps. 이런 생각을 주변에 얘기했더니 다들 한 바탕 설교를 늘어 놓는다. 모듈화의 필요성부터 시작해서 훌륭한 클래스의 조건들까지 별 소릴 다 들었다. 직접 말하지는 않았지만 속으로 이런 생각을 했다. "나도 그렇게 생각할 때가 있었어."
Posted by 조성경 트랙백 0 : 댓글 0
What every programmer should know about memory
Posted by 조성경 트랙백 0 : 댓글 0

대문자를 소문자로 변환하는 방법은 대단할게 없다. 다 알고 있는 다음 두 가지중 하나를 상황에 맞게 사용하면 된다.

int tolower(int c);
char* _strlwr(char* str);

MS Visual C++의 경우에는 2005 버전으로 넘어오면서 strlwr라는 이름이 _strlwr로 수정되었다. MSDN만을 읽어보면 _strlwr가 ISO C++을 준수한다고 하는데 자세한 사정은 잘 모르겠다. ANSI C의 경우에는 strlwr, _strlwr가 모두 존재하지 않는다. C++ 확장에만 들어 있다.

영문 환경이라면 저 두 함수는 아주 멋지게 작동을 하고, 어디 잘못될 곳도 없다. Microsoft는 저 두 함수를 다국어 환경에 맞추기 위해서

_totlower, _mbctolower, towlower
_tcslwr, _mbslwr, _wcslwr

를 추가했다(Secure 버전은 별도). TCHAR형을 사용하면 빌드 환경에 따라 문자 데이터 형을 바꿔주니 무난하게 사용을 할 수 있다. 당신이 영어 사용권에 살고 있다면 알고 있어야할 지식은 여기까지다. 여기까지만 알고 있어도 세상살이가 그다지 고달프지 않다.

문제는 아시아권 특히 일본어가 사람을 괴롭힌다. 일본 문자인 "ダ"를 보자. 유니코드가 아닌 Shift-JIS 코드를 사용할때 저 문자의 코드값은 0x83(-125), 0x5F(95)의 두 바이트가 된다. 아래의 코드 조각을 보자.

사용자 삽입 이미지

















그림에서 보듯이 _mbslwr를 거치면서 lwr의 값이 ""로 바뀌게 되고, oops를 출력하게 된다. 이는 한글 윈도우에서 실행한 결과이기 때문이며, 같은 바이너리를 일본어 윈도우에서 실행한다면 same을 출력한다(실제로 그렇다).

여기까지는 좋다. 테스트 결과에 의해 일본어 윈도우에서 정상적으로 작동할 것이라는 가정을 가지고 일을 하면 되니까. 며칠전 이상한 버그가 보고 돼 조사를 하고 있었는데, 결론은 저 _mbslwr가 이상한 행동을 한다는 것이다. 제대로 된 개발을 한다면 언어별로 윈도우를 설치하고 다 테스트를 해봐야겠지만, 실제는 그렇지 못하고 저 위의 코드는 그 나라에서는 정상적으로 작동한다고 가정을 할 수밖에 없다(변명일지도...).

코드를 최대한 손대지 않고 수정하기 위해서 로켈등 관련부분을 다양하게 적용해봤지만 아무런 소용이 없었다. 결국 문자열을 유니코드로 수정->변환->다시 DBCS로 수정해 저장하는 이상한 짓을 하고 말았다. 당면한 문제는 일단 수습을 했지만, 내가 모르는 무엇인가가 작용을 하고 있다는 사실이 대단히 기분 나쁘다.

준비중인 차기작은 모두 유니코드를 사용하기때문에 이런 문제를 겪지는 않겠지만, 당장에 널려 있는 코드들도 문제고 저안에서 무슨 일이 일어나고 있는지 모르고 있다는 사실이 더 큰 문제다. 그러나 일본어 윈도우를 설치하기는 싫다 -_-;;

ps. 참고로 환경은 VS2005(sp1 미적용), windows xp, windows 2003이다.

Posted by 조성경 트랙백 0 : 댓글 0

art.oriented에 놀러 갔다가 답글을 달았는데, 별 생각 없이 "상호작용하는 함수에서 빠져나올때 자신이 사용한 동기화 객체를 초기화하는 부분이 따라오는게 맞겠죠."라는 말을 썼다.

언젠가 당한적이 있으니 저런 소리를 했을텐데하고 생각을 해보니 꽤나 오래전에 삽질했던 기억이 난다. 5~6년은 된 일인듯. 그때 상황이 정확히 기억날리는 당연히 없고, 대략 비슷하게 만들면 다음과 같다.

// Example
class CCriticalSection
{
public:
     CCriticalSection() { InitializeCriticalSection(&m_cs); }
    ~CCriticalSection() { DeleteCriticalSection(&m_cs); }
    void Enter() { EnterCriticalSection(&m_cs); }
    void Leave() { LeaveCriticalSection(&m_cs); }

private:
    CRITICAL_SECTION m_cs;
};

unsigned __stdcall ThreadFunc(void* param)
{
    CCriticalSection* pcs = static_cast<CCriticalSection*>(param);
    pcs->Enter();
    return 0;
}

int _tmain(int argc, _TCHAR* argv[])
{
    CCriticalSection* pcs = new CCriticalSection;
    
    pcs->Enter();

    uintptr_t handle = _beginthreadex(NULL, 0, ThreadFunc,
(void*)pcs, 0, NULL); Sleep(100); delete pcs;
// lock 상태에서 그냥 삭제 WaitForSingleObject((HANDLE)handle, INFINITE); printf("Thread terminated."); return 0; }


대부분은 그냥 그런 코드고  실제 문제가 됐던 부분은 주석이 달린 저 부분이다. CRITICAL_SECTION이 lock된 상태에서 개체를 삭제해버린 저 문제 말이다. CCriticalSection의 소멸자가 호출돼 CRITICAL_SECTION을 해제하기때문에 리소스 릭은 없지만 ThreadFunc을 빠져 나올 방법은 없다(데드락).

삭제라는 복잡한 방법을 썼지만 delete pcs의 위치에서 CRITICAL_SECTION을 해제(DeleteCriticalSection())해도 결과는 같다(사실 코드도 거의 같다).

교훈은 하나다. 동기화 오브젝트를 삭제(해제)해야 한다면 현재 상태를 반드시 확인하고 꼭 풀어 줘야한다. 아니면 좋지 않다.

ps. 동기화 객체를 삭제하면 락을 다 해제하고 자폭하는게 직관적이라고 생각하는 사람은 나뿐일까...

Posted by 조성경 트랙백 0 : 댓글 0