Keywordream은 PDA 3기에서 24.03.06 ~ 24.04.02 동안에 진행했던 금융 관련 프로젝트이다. Keywordream은 주식이 왜 오르고 내리는지 알고 싶어하는 주린이들에게 각 종목에 대한 정보와 뉴스를 통한 시세 변화에 대한 이해를 제공한다.
이 프로젝트에서 내가 맡았던 역할은 주식 종목에 대한 시세 실시간으로 가져오기와 키워드 추출을 위한 뉴스 크롤링이다.
실시간 주식 종목의 시세를 가져오기 위해서 한국투자증권 API을 활용했다. 이 과정에서 몇 가지 도전에 직면했고 그 내용은 아래와 같다.
처음에는 클라이언트(Front-end)와 한국투자증권을 직접 연결하여 실시간 시세를 받아오려고 시도했었다. 하지만 한국투자증권에서 발급받은 키와 연결할 수 있는 것은 하나 뿐이라는 제약사항이 존재했다. 다시 말해, A 클라이언트가 연결하고 있는 상황에서 B 클라이언트가 연결하려고 하면, 먼저 연결된 A 클라이언트만 실시간 시세를 받아올 수 있었고 B 클라이언트는 실시간 시세를 받아오지 못하는 문제가 발생한 것이었다.
이 문제를 해결하는 방법으로 생각해낸 것은 2가지였다.
이 2가지 해결책을 가지고 사용자 입장에서 생각했을 때, 두 번째 방법이 사용자가 사용하기에 더 편한 방법이라는 생각이 들어서 두 번째 방법으로 서비스 구축을 진행했다.

한국투자증권 API로 인해서 실시간 시세를 받아오기 위해서 자체 서버와 한국투자증권 사이에서 웹소켓이 연결되었고, 받아온 시세를 우리 서비스를 사용하고 있는 사용자들에게 보여주기 위해서 자체 서버와 클라이언트 사이에서 추가로 웹소켓이 연결되었다.
웹소켓을 연결하는 방법은 WebSocket과 socket.io가 존재했고 둘 중에서 선택해야만 했었다.
자체 서버와 한국투자증권 사이에서는 WebSocket을 통한 연결을 선택했다. 여기에서는 자체 서버에서 사용자들이 보고 있는 주식 종목을 취합해서 한국투자증권에게 그 주식 종목들을 보내서 실시간 시세를 받아오기만 하면 되기 때문에 WebSocket만으로 충분하다고 생각했다.
자체 서버와 클라이언트 사이에서는 socket.io을 통한 연결을 선택했다. socket.io을 활용한 가장 큰 이유는 Room 기능이다. 예를 들어, A 클라이언트에서 주식 종목 a, b, c를 보고 있다고 하고 B 클라이언트에서 주식 종목 a, d, e를 보고 있다고 가정하도록 하겠다. 그렇게 된다면 자체 서버에서 한국투자증권으로 보내야 되는 주식 종목은 a, b, c, d, e가 되고 다섯 가지의 주식 종목에 대한 실시간 시세를 받아오게 된다. 받아온 모든 실시간 시세를 모든 클라이언트에게 보내주는 것은 굉장히 비효율적이라는 생각이 들었다. 그래서 Room Id을 주식 종목 코드로 활용한 Room을 활용하여 한국투자증권에서 실시간으로 받아온 데이터(다섯 가지의 주식 종목의 실시간 시세)에서 주식 종목 코드을 뽑아내어 해당 Room에 그 데이터를 보내주었다.

한국투자증권에서 발급받은 키로 실시간으로 가져올 수 있는 주식 종목은 40개까지였다. 즉, 클라이언트들이 40개보다 많은 주식 종목을 보게 된다면, 넘은 주식 종목에 대해서는 실시간 시세를 가져오지 못하는 문제점이 존재했다.
그리하여 일단은 자체 서버에 모든 사용자들이 보고 있는 주식 종목을 파악할 수 있는 Map 형식의 저장 공간을 만들었다. Map에서 key는 주식 종목 코드 = Room Id였고 value는 Set() 형태이고 element들은 socketId가 저장이 된다.
위의 이미지에서 각각의 socketId을 aSocketId bSocketId라고 가정해보고 자체 서버에 있는 Map을 그려 보면 다음과 같다.
| key | value |
|---|---|
| a | aSocketId, bSocketId |
| b | aSocketId |
| c | aSocketId |
| d | bSocketId |
| e | bSocketId |
사용자가 다른 페이지로 이동하게 된다면, 클라이언트와 자체 서버가 연결되는 socketId가 변경이 된다. 이 점을 활용하여 Map에서 기존 socketId들을 제거하고 새로운 socketId들을 Map에 추가했다. 예를 들어 A 클라이언트가 a, b, c를 보고 있던 도중에 b만 있는 페이지로 넘어오면 Map은 다음과 같이 변경된다.(새로운 socketId을 aNewSocketId로 가정)
| key | value |
|---|---|
| a | bSocketId |
| b | aNewSocketId |
| c | |
| d | bSocketId |
| e | bSocketId |
위와 같이 변경이 되고 아무런 value을 가지고 있지 않은 c 종목에 대한 실시간 시세는 더 이상 보내주지 말라고 한국투자증권에 요청한 후 Map에서 삭제가 일어나게 된다.
| key | value |
|---|---|
| a | bSocketId |
| b | aNewSocketId |
| d | bSocketId |
| e | bSocketId |
즉, 실시간으로 받아오는 주식 종목의 개수를 지속적으로 체크하고 조절함으로써 40개가 넘지 않도록 했다.
하지만 동시에 접속하는 사용자 수가 많아지면 40개가 넘을 수 있다고 생각했지만, 프로젝트 최종 시연에서 우리 서비스를 이용하는 고객들의 수를 생각했을 때, 동시에 보는 주식 종목의 개수가 40개는 넘지 않을 것이라고 판단했다.
뉴스 크롤링을 위해서 서버를 따로 구축했다. 서버를 따로 구축한 이유는 서비스 기획 상 사용자가 해당 주식 종목에 대하여 클릭하면 실시간으로 뉴스 크롤링을 해야 되는 상황이 발생하는데 만약 어떠한 이유로 인해서 크롤링에 에러가 발생하는 경우에 우리 서버까지 멈출 수 있기 때문이다.
크롤링을 위한 서버는 나와 다른 팀원(크롤링한 뉴스를 가지고 키워드를 뽑아내는 역할) 둘 다 익숙하면서도 쉽게 구축할 수 있는 python 기반인 flask을 활용하여 구축했다.
python을 통해서 크롤링을 진행할 때, 크게 3가지 방식이 존재했다.
제일 먼저 Scrapy을 통한 방식이 제일 먼저 제외되었다. 그 이유는 프로젝트 마감 날짜가 딱 정해져 있었기 때문에 한 번도 사용해 본 적 없는 Scrapy을 통한 방식을 사용할 경우 마감 날짜를 맞추는데 힘들 것이라고 판단했기 때문이다.
그 다음으로는 Selenium이 제외되었다. 실시간 크롤링을 목적으로 하고 있었기 때문에 Beautifulsoup보다는 상대적으로 느린 Selenium이 제외되었다. 더하여 단순히 뉴스 크롤링을 하는 것이기 때문에 javascript 렌더링 또한 필요하지 않았다.
서비스의 기획에 의하면 사용자가 주식 종목을 클릭하면, 실시간으로 뉴스 20개를 크롤링하기 때문에 속도가 되게 중요한 문제로 떠올랐다. 하지만 여러 사용자가 동일한 뉴스 기사를 요청할 경우 중복 크롤링이 발생하여 효율성 문제가 발생했다. 이 문제를 해결하기 위해서 뉴스 기사를 한 번 크롤링하면 관련 데이터를 DB에 저장하고, 다른 사용자의 요청으로 동일한 뉴스 기사를 가져와야 될 상황이 발생하면, 크롤링을 하지 않고 DB에서 가져오는 방식을 선택했다. 이렇게 함으로써 사용자가 종목을 클릭하고 관련 페이지로 들어갈 때까지 걸린 시간을 평균 1/3 가까이 줄일 수 있었다.
어떻게 하면 더 줄일 수 있을까... 고민하던 와중에 뉴스 table의 구조에서 문제점을 발견할 수 있었다. 뉴스 table은 PK로 자체 id로 되어 있었고, newsId(크롤링한 페이지에서 정해져 있는 것)는 다른 column에 저장되어 있었다. 이로 인해서 이미 크롤링한 뉴스인지 확인하기 위해서 newsId column을 풀 스캔을 해야만 하는 상황이 발생했다. 검색속도 향상을 위해서 newsId column에 index을 적용하는 방법도 있겠지만 여기에서는 자체 id을 삭제하고 newsId을 PK로 지정하였다. 이로 인해 추후 뉴스 table에 데이터가 많아질 경우 기존보다는 검색 속도가 빠를 것이라고 생각한다.
개발이 끝나고 AWS의 EC2에 배포를 진행하게 되었다. 다른 사소한 문제도 있었지만 크롤링에서 상당한 문제가 발생했다. 로컬 서버에서는 아무런 이상이 없던 크롤링이 배포 후에는 불규칙적으로 크롤링이 안되는 문제가 발생했다. 시간을 체크해 보니 크롤링이 안될 때마다 대략 30.07초 정도 지연이 발생했다. 원인을 파악해본 결과 크롤링을 진행한 홈페이지에서 막아버린 것으로 추축되었다.
이 문제를 해결하기 위해서, 찾아본 결과 2가지 방법을 찾을 수 있었다.
1번을 선택하기에는 구축하는데 시간이 많이 걸리고 남은 시간이 얼마 없었다. 시간을 쪼개서 구축한다고 해도 그 이후에 문제가 발생할 경우 감당이 힘들 것이라고 판단하여 2번을 선택했다. 크롤링 제한 시간을 1초로 설정하여 특정 뉴스에 대한 크롤링이 실패해도 서버가 느려지지 않고 계속 진행되도록 조치했다.
프로젝트를 진행하면서 여러 어려움이 있었지만, 그 때마다 포기하지 않고 끝까지 관련 문제를 해결했다. 이번 프로젝트를 통해서 책임감을 기를 수 있었다. 더하여 팀프로젝트를 진행하면서 branch를 나눠 작업하거나 팀원이 어떤 문제가 발생했을 때, 같이 그 문제를 해결하는 과정에서 제대로된 협업을 경험할 수 있었다.