15분 지연 프로그램 만들기

         

개발을 하다보면 다양한 요건을 만나게 된다. 특~히 특~~~~히 금융IT는 각종 규제 때문에 아주아주 다양한 요건과 예외를 만나볼 수 있는 좋은 곳이다. 여러분 모두 금융으로 오세요!!!!

최근에 회사에서 해외주식 서비스를 도입하면서 해외주가 데이터를 피딩받고 있다. 해외 거래소에서 제공하는 데이터는 국내와 같은점도 있지만 다른점도 많다. 예를 들어보면 미국 거래소는 상한, 하한이 없다. 국내는 30%가 넘게 오르거나 내려가면 거래가 막히는데, 해외는 그런거 없다. 쭉쭉 올라가거나 쭉쭉 떨어진다. 역시 미쿸... 

또 특이한 점이 국내에서 해외주식 데이터를 실시간으로 받아보려면 돈을 내야한다. 주식회사가 돈을 내야된다는게 아니라 보는 사용자 일일이 돈을 내야한다. (?) 이유는 모른다. 국내 법이 그렇다고... 또 돈을 안내면 안보여주면 될것을 15분 전 시세를 보여줘야 한다. 누가 그렇게 만들었는지는 모르겠다. 정말 쓸대없는 짓이다. ㅋㅋㅋ

말로는 되게 간단한데, 돈을 낸 사람은 실시간 시세를 보여주고, 안낸사람은 15분 전시세를 보여준다. 하지만 시세만 그런게 아니라 모든 재무데이터도 다 15분 전 기준이어야한다. 현재가는 15분 전인데 다른 지표들은 실시간이면 이상하니까.. 가장 큰 문제는 실시간으로 데이터를 받는 부분이다. 현재 증권어플리케이션의 구조는 서버가 거래소로부터 데이터를 받았을 때, 주가가 변했다면 클라이언트로 변한 데이터를 전송해주는 방식이다. 즉 서버가 Send하고 클라이언트가 Receive한다. 만약 돈을 안낸 사용자라면 이 과정에서 15분의 딜레이가 필요하다. 서버는 거래소로부터 제시간에 시세를 받았지만, 그 데이터를 15분 후에 클라이언트로 보내야한다. 즉 15분 웨이트 해야한다. 하지만 데이터의 양이.... 종목이 미국만 8000개고 1초에 호가가 10개씩만 변한다고 해도 8만개의 데이터가 있고, 각 데이터단 100바이트만 잡아도...

작업을 가장 최소화 할 수 있는 방법은 거래소(or 피딩업체. 해외는 거래소에서 직접받는게 아니라 중간 업체를 통해 받고있음)에서 실시간 데이터와 15분 지연 데이터를 따로 받는 방법이다. 이렇게 되면 증권사는 별다른 작업없이 고객에게 실시간과 15분 지연데이터 둘다 줄 수 있다. 하지만 이렇게 하면 데이터 받는 회선이 2개 필요해서 돈이 두배로 든다. 과연 어떤 회사가 돈 두배를 더들여서 개발자의 편의를 봐줄까? 개발자 입장에서도 하나로 할 수 있는걸 굳이 2개로 한다는 것은 자존심이 허락안한다. (는 뻥 ^^)

그래서 이 문제를 해결하기위해 15분 지연된 데이터를 처리할 수 있는 15분 지연 프로그램을 만들기로 했다. 가장 먼저 한일은 데이터가 얼마나 들어오는지 계산하는 것이다. 확인하기 위해 퇴근하기전에 들어오는 데이터를 파일로 떨구는 소스를 만들어놓고 퇴근했다. (해외는 저녁 5시부터 시작하기 때문임.. 절대 퇴근전이 좋아서 한거 아님) 다음날 같은시간 파일의 크기를 보니 약 9GB. 24시간 9GB라. 하지만 몰리는 시간도 있고 거래가 없는 시간도 있기 때문에 9를 24로 나누어서 1시간의 데이터를 구하는 것은 잘못된 계산일 것이다. 이럴땐 대~~~충 1GB로 잡으면 1시간은 뻐기겠지. 심지어 15분 데이터만 저장하고 있으면 되기 때문에 더 충분할 꺼야.

그래서 일단 1GB크기의 공유 메모리를 만들었다. (참고로 작업환경은 Redhat linux C 임)

struct DELAY_HOGA{
    long head;
    long tail;
    OVRS_HOGA hoga[1000000];
};
shm_id = shmget(DELAY_KEY, (1 * 1024 * 1024 * 1024), IPC_CREAT | 0666);
shm_addr = (uchar *)shmat(shm_id, 0, 0);
dHoga = (DELAY_HOGA *) shm_addr;

공유메모리에 1기가를 할당하고, 1000000개의 데이터를 저장할 수 있는 구조체로 불러와서 컨트롤할 수 있게 되었다. head와 tail은 데이터의 처음과 끝을 나타내려고 만든 변수이다. 데이터를 쌓을때마다 tail을 증가시키고, 15분이 지난 시점부터 데이터를 송신하면 head를 하나씩 증가시킨다. 즉 공유메모리에 만들었지만 큐(queue)라고 생각하면 된다.

이제 데이터가 들어오면 이 메모리에 차곡차곡 쌓아보자. 물론 잘 쌓이는지 확인해보는 모니터링 프로그램도 하나 있으면 좋을 것 같다.

 

생각보다 빨리 들어온다.

카운트만 로그로 찍었는데 생각보다 많은 양의 데이터가 밀려들어오고있다.. 크흐.. 카운트는 잘 증가하고 있고, 아마 백만까지 가면 다시 1로 돌아갈 것이다... 음? 처리를 안해놨구나.. 다시 1로 돌아가게 하는 방법은 count에 나머지 연산자를 걸어주면 된다. n % 1,000,000 이렇게. 하지만 일단 100만으로 고정된게 아니기 때문에 나중에 추가하고, 지금은 데이터가 들어오는 형식 그대로 메모리에 쌓고있는데 이럴 필요는 없을 것 같다. 실제로 클라이언트로 전송하는 형태로 구조체를 바꿔주면 더 심플해 진다.

이런 느낌이다.

이제 메모리에 쌓인 데이터를 시간을 비교해서 지금보다 15분 전이면 처리하는 로직을 만들어야 한다. 그러기 위해선 일단 시간을 구해야 한다. 보통은 HH:MM:SS형태의 시간을 HHMMSS 이렇게 Int형 변수에 담아서 표현하는데, 이럴 경우 시차를 계산할 때 오류가 난다. 예를 들어 151000 에서 15분을 빼면 145500이 맞지만 Int형 변수이기 때문에 149500이 된다. 따라서 시, 분을 모드 초로 환산해서 하는 방법도 있지만, 나는 약간 편법으로 아래와 같은 방법을 사용했다.

if( (now_time - hoga_time) % 4000 > 1500)
{
    proc(&dhoga->hoga[dhoga->tail]);
    dhoga->tail++;
}

hoga_time은 메모리에 저장되어있는 가장 오래된 데이터의 시간이다. 현제시간과 저장된 데이터의 시간이 1500이상 차이난다면 실행하면 된다. %4000은 만약 HH부분이 1이상 차이날경우를 대비한 방어코드다. 위와같이 현재 시간이 151000이고 메모리 시간이 145500이라면 둘의 차이는 실제로는 1500 이지만 계산하면 5500이 나오게 된다. 이떄 % 4000을 이용함으로써 정상적인 값인 1500이 나오게 된다.

처리 프로세스 로그

실제로 처리되는 로그를 찍어보았다. 헤드는 현재 들어오고있는 데이터의 Index로 실시간으로 계속 증가한다. 데이터의 끝번호라고 생각하면 된다. 테일은 현재 프로그램이 읽고있는 메모리상의 위치이다. 맨 마지막을 보면 헤드가 420909이고 테일이 409752인걸 보니 15분 사이에 데이터가 약 10000개 정도 있나보다. 홍콩 거래소 하나로만 테스트를 진행했는데.. 다른 거래소까지 하면 *N을 해야할듯 하다.

아 그리고 처음에는 1000000개로 구조체를 만들었는데, 데이터를 줄이고나서 5000000개로 늘렸다. 그리고 HEAD와 TAIL을 계산할 때 아래와 같은 로직을 추가한다.

dhoga->head = (dhoga->head + 1) % 5000000;
dhoga->tail = (dhoga->tail + 1) % 5000000;

이렇게 하면 5000000이 넘으면 다시 0으로 초기화 된다. 이론적으로 원형 큐(circle queue)를 생성한 것이다. HEAD는 데이터가 들어오면서 계속해서 증가할 것이고, TAIL은 데이터를 처리하면서 HEAD를 따라갈 것이다. 기본적인 원형 큐와 다른점은, TAIL과 HEAD를 비교하지 않기 때문에 데이터의 순서가 꼬일수 있다는 점이다. 데이터를 처리하기 전에 시간을 비교해서 처리하기는 하지만 늦은시간이 앞에 위치한다면, 뒤에 처리해야 하는 데이터들이 제시간에 처리 안되고 엉뚱한 시간에 처리되는 경우가 발생할 수 있다. 따라서 데이터를 쌓을때 (Head를 증가시킬때) 바로 이전 데이터와 비교해서 이후 데이터만 받을 수 있는 방어코드를 추가하면 보다 깔끔하게 처리할 수 있다.

 

- 추가 개선할 점 

 메모리 생성하는 프로그램과 수신, 처리 프로그램 분리. 메모리 생성은 서버 기동시에 실행되게 하고, 수신과 처리 프로그램은 메모리 attach코드만 넣는다. 생성코드가 같이 있으면 프로세스가 재실행될때마다 메모리가 초기화 될 것이다.

 처리 후 메모리 초기화. 처리단에서 데이터를 처리한다음 처리된 데이터를 메모리에 남기느냐, 지우느냐 선택할 수 있다. 지운다면 한번 처리된 데이터가 중복으로 처리되는 것을 막을 수 있다. 

댓글(0)

Designed by JB FACTORY