대신증권 CYBOS PLUS 시작하기 (9) - WithEvents 알아보기
학습 목표
- win32com에서 제공하는 WithEvents 이해하기
- 이벤트 처리기에 처리할 이벤트 알려주기
win32com에서 제공하는 WithEvents 이해하기
마이크로소프트 개발 도구 사이트에 보면 Visual Basic 내에서 사용되는 `WithEvents` 모듈에 대한 간략한 설명이 있다. 아래의 사진을 보면 '선언된 멤버 변수가 이벤트를 발생시킬 수 있는 클래스의 인스턴스를 참조하도록 지정'한다고 나와 있다. 하지만 그 내용을 자세히 살펴보기 보다는 `WithEvents`가 어떠한 방식으로 동작하는지를 이해하는 편이 코드를 구현하는 데에 조금 더 큰 도움이 될 것이다. 잠깐 멈춰서 `WithEvents`란 무엇인지에 대해 정의를 내려보자면 '이벤트가 발생했을 때 그 이벤트를 처리하는 클래스를 지정하는 이벤트 처리기를 지정하는 것'이라고 할 수 있다. `WithEvents` 함수의 코드 구조는 우리가 기존에 생성했던 COM 연결 인스턴스(`self.cybos` 또는 `self.stockmst` 등)를 첫 번째 인자로 전달해주고, 그 인스턴스에서 이벤트가 발생했을 때 그 이벤트를 처리할 클래스(이벤트 처리기)를 두 번째 인자로 전달해주면 된다.
실제로 대신증권에서 이벤트를 발생시키는 DsCbo1.StockMst 모듈을 예로 들어 살펴보자. 이 모듈은 이전에 BsCbo1.py 파일 내부에서 `class StockMst:` 클래스 내부의 초기화 함수에서 `self.stockmst`라는 이름으로 하여 인스턴스를 생성해두었었다. 그리고 대신증권 홈페이지에서도 어렵지 않게 확인할 수 있듯이, 이 모듈은 `Received`라는 이벤트를 발생시키는데, 우리는 앞서 살펴봤듯이 ①기존에 생성했던 COM 연결 인스턴스(`self.stockmst`)와 ②이벤트가 발생했을 때 그 이벤트를 처리할 클래스(가칭 `event_handler`)를 `WithEvents`의 인자로 전달해주어야 한다. 그 구조를 요약하면 아래와 같다.
내용만 들었을 땐 어려워보일 수도 있지만, 위의 코드 구조를 간단하게 설명하자면 다음과 같이 요약할 수 있다.
- `self.stockmst` 인스턴스에서 이벤트가 발생하면 `event_handler` 클래스를 실행시켜서 그 이벤트를 처리해라.
그럼 이제 이 내용을 토대로 아래의 코드를 참고해보자.
※ Line: 7, 24~26
## DsCbo1.py ##
import win32com.client
class StockMst:
def __init__(self):
self.stockmst = win32com.client.Dispatch("DsCbo1.StockMst") ## COM 연결
self.handler = win32com.client.WithEvents(self.stockmst, Handles=event_handler)
def _StockMst(self, item_code):
"""주식종목현재가 반환"""
self.stockmst.SetInputValue(0, item_code)
self.stockmst.BlockRequest()
item_code = self.stockmst.GetHeaderValue(0)
item_name = self.stockmst.GetHeaderValue(1)
y_close = self.stockmst.GetHeaderValue(10)
now = self.stockmst.GetHeaderValue(11)
open = self.stockmst.GetHeaderValue(13)
high = self.stockmst.GetHeaderValue(14)
low = self.stockmst.GetHeaderValue(15)
print(f"[{item_code}] {item_name}")
print(f" 전일종가:{y_close}, 현재가:{now} ")
print(f" 시가:{open}, 고가:{high}, 저가:{low}")
class event_handler:
def __init__(self):
pass
이벤트 처리기에 처리할 이벤트 알려주기
우리가 지금까지 구현한 코드를 요약하자면 'self.stockmst 인스턴스에서 이벤트가 발생하면 event_handler, 니가 처리해.' 정도이다. 근데 여기서 `class event_handler`는 알잘딱깐센 사람같은 존재가 아니기 때문에 우리가 '앞으로는 너가 이벤트를 처리하라.'는 명령만 수행할 뿐 정확히 "어떠한 이벤트를 어떻게 처리해야 하는지"에 대해서 지정해주지 않으면 이 아이는 아무것도 하지 못한다. 그렇다면 우리는 어떠한 이벤트를 어떻게 처리해야 할지 어떻게 알려줄 수 있을까 ?
아예 프로그램을 처음부터 만들어버리는 거라면 우리가 직접 이벤트를 생성해주어야 했었겠지만, COM 방식(컴파일러 방식)은 기본적으로 다른 언어로 프로그래밍되어 있는 프로그램을 다른 언어에서 사용하는 방식이라는 내용으로 미루어 보나 대신증권 홈페이지에서 제공되는 여러 문서들을 보나 특정 모듈에서 제공되는 이벤트 목록은 이미 지정되어 있기 때문에 먼저 ①어떠한 이벤트가 있는지에 대해서는 크게 고민하지 않아도 된다. 그렇다고 해서 ②그 이벤트를 어떻게 처리해야 하는지가 어려운 것도 아니다. 프로그램은 이미 수신받을 수 있는 데이터의 범위가 사전에 설정되어 있고 그 내용 역시 우리가 바꿀 수 없기 때문이다. 다시 말해, 정해져 있는 프로그램의 기능 내에서 어떠한 데이터를 가져다가 어떻게 활용할지는 전적으로 우리의 선택이며 우리가 가져올 수 없는 데이터는 (그 모듈 안에서는) 무슨 짓을 해도 가져올 수 없기 때문에 크게 알아보거나 찾아볼 필요가 없다.
일단 다른 부분들은 다 차치하고, "DsCbo1.StockMst" 모듈은 `Received`라는 이벤트를 발생시키는 모듈이므로 코드를 짜서 이 이벤트를 처리해보도록 하자. 근데 코드를 구축하기 전에 (아마도 수많은 분들이 애를 먹었겠지만) 어이없게도, 대신증권 도움말은 현재 업데이트가 되어 있지 않다. 왜냐하면 수신받을 함수의 이름으로 `def Received(self):`를 사용했는데 아무런 반응이 없길래, 혹시나 싶어서 `def OnReceived(self):`를 사용했더니 이벤트가 정상적으로 호출되고 수신된 데이터도 정상적으로 수신되고 출력됐기 때문이다. 참 프로그램을 사용하는 입장에서는 불친절한 것 같다.
즉, 대신증권에서는 위의 도식에 따른 흐름으로 동작하긴 하지만 "DsCbo1.StockMst" 모듈에서 서버와의 통신 결과로 발생시키는 이벤트는 `Received`가 아닌 `OnReceived` 이벤트를 발생시킨다는 것이다. 하지만 정확히 어떠한 방식으로 동작하는지에 대한 설명을 잠시 보류해두기로 하고, `class StockMst` 내에서 `def _StockMst(self, item_code):` 함수를 호출했을 때 그 결과로 발생하는 일은 어떠한 일들이 있는지 살펴보도록 하자.
※ [수정 파일: DsCbo1.py] Line: 12, 26~28
## Boss.py ##
import win32com.client
from pywinauto import application
from COM import CpSysDib
from COM import CpUtil
from COM import CpTrade
from COM import DsCbo1
import time
class cybos:
def __init__(self):
self.cybos = CpUtil.CpCybos()
self.stockchart = CpUtil.CpStockCode()
self.trade = CpTrade.CpTdUtil()
self.codemgr = CpUtil.CpCodeMgr()
self.stockmst = DsCbo1.StockMst()
self.stockchart = CpSysDib.StockChart()
self._open_cybosplus()
self.stockmst._StockMst("A005930")
def _open_cybosplus(self):
if self.cybos._IsConnect() == True: ## 연결되어 있는 경우
print(f"로그인되어 있습니다.")
elif self.cybos._IsConnect() == False:
print(f"로그인되어 있지 않습니다. 로그인을 시도합니다.")
login()
while True:
if self.cybos._IsConnect() == False:
time.sleep(1)
elif self.cybos._IsConnect() == True:
print(f"로그인되었습니다. 반복문에서 나갑니다.")
break
class login:
"""CYBOS PLUS 실행 함수"""
def __init__(self):
self.Id = "NHK4900"
self.Password = "A9985623"
self.Cert = "P13691369*"
self.run()
def run(self):
app = application.Application()
executable_path = "C:\\DAISHIN\\STARTER\\ncStarter.exe"
args = f" /prj:cp /id:{self.Id} /pwd:{self.Password} /pwdcert:{self.Cert} /autostart"
app.start(f"{executable_path}{args}")
if __name__ == "__main__":
cybos()
## DsCbo1.py ##
import win32com.client
class StockMst:
def __init__(self):
self.stockmst = win32com.client.Dispatch("DsCbo1.StockMst") ## COM 연결
self.handler = win32com.client.WithEvents(self.stockmst, Handles=event_handler)
def _StockMst(self, item_code):
"""주식종목현재가 반환"""
self.stockmst.SetInputValue(0, item_code)
self.handler.set_instance(self.stockmst)
self.stockmst.BlockRequest()
item_code = self.stockmst.GetHeaderValue(0)
item_name = self.stockmst.GetHeaderValue(1)
y_close = self.stockmst.GetHeaderValue(10)
now = self.stockmst.GetHeaderValue(11)
open = self.stockmst.GetHeaderValue(13)
high = self.stockmst.GetHeaderValue(14)
low = self.stockmst.GetHeaderValue(15)
print(f"[{item_code}] {item_name}")
print(f" 전일종가:{y_close}, 현재가:{now} ")
print(f" 시가:{open}, 고가:{high}, 저가:{low}")
class event_handler:
def set_instance(self, disp):
self.disp = disp
print(f"self.client:{self.disp}")
▶ 실행 결과 확인하기
[통신결과:1] 서버와의 연결에 성공했습니다.
로그인되어 있습니다.
self.client:<win32com.gen_py.Cpdib 1.0 Type Library.IDib instance at 0x152025456>
[A005930] 삼성전자
전일종가:77200, 현재가:80200
시가:79400, 고가:80200, 저가:78700
DsCbo1.py 파일 내에서 Line: 7 부분의 코드를 보면 `WithEvents` 함수의 disp 인자로 `self.stockmst`를 전달해주고 있고, 이벤트 발생 시 호출할 user_event_class 인자로 `event_handler`를 전달해주고 있다. 그리고 이 내용을 `self.handler`라는 인스턴스를 생성한 후에 Line: 12 부분의 코드에서 `self.handler.set_instance(self.stockmst)`라는 구문을 통해 이벤트 처리기 클래스의 인자로 `self.stockmst`를 전달해주었다. 이 인자는 추후에 대신증권 서버로부터 `OnReceived` 이벤트가 발생했을 때 `GetHeaderValue()` 함수와 함께 데이터를 수신받기 위해 필요한 변수이다. 이 내용이 이해가 가지 않는다면 Line: 14~20 구간의 코드를 보면 어렵지 않게 이해할 수 있다. `GetHeaderValue()` 함수의 앞에는 `self.stockmst`가 위치해 있는데, 이를 `OnReceived` 함수 내부에서도 `GetHeaderValue()` 함수를 통해 데이터를 가져오기 위해서도 필요한 인스턴스이기 때문에 event_handler 클래스의 인자로 전달해주는 것이다.
여기까지의 내용이 이해가 가지 않는다면, `def _StockMst(self, item_code):` 함수 내부에 있는 부분인 Line: 14~20 부분의 코드들을 class event_handler 클래스 내부에 작성해둔 `def OnReceived(self):` 함수로 옮겨적는 절차를 수행해보면 어렵지 않게 이해할 수 있다. 즉, `def _StockMst(self, item_code):` 함수 내부에서는 데이터를 수신받는 `GetHeaderValue()` 함수를 모두 삭제한 후에 class event_handler 내부에 `def OnReceived(self):` 함수를 새롭게 생성한 후 해당 함수 내에서 데이터를 가져오는 함수를 통해 데이터를 수신받는 것이다.
## 변경 전 _StockMst
def _StockMst(self, item_code):
"""주식종목현재가 반환"""
self.stockmst.SetInputValue(0, item_code)
self.handler.set_instance(self.stockmst)
self.stockmst.BlockRequest()
item_code = self.stockmst.GetHeaderValue(0)
item_name = self.stockmst.GetHeaderValue(1)
y_close = self.stockmst.GetHeaderValue(10)
now = self.stockmst.GetHeaderValue(11)
open = self.stockmst.GetHeaderValue(13)
high = self.stockmst.GetHeaderValue(14)
low = self.stockmst.GetHeaderValue(15)
print(f"[{item_code}] {item_name}")
print(f" 전일종가:{y_close}, 현재가:{now} ")
print(f" 시가:{open}, 고가:{high}, 저가:{low}")
## 변경 후 _StockMst
def _StockMst(self, item_code):
"""주식종목현재가 반환"""
self.stockmst.SetInputValue(0, item_code)
self.handler.set_instance(self.stockmst)
self.stockmst.BlockRequest()
## 변경 전 class event_handler
class event_handler:
def set_instance(self, disp):
self.disp = disp
print(f"self.client:{self.disp}")
## 변경 후 class event_handler
class event_handler:
def set_instance(self, disp):
self.disp = disp
print(f"self.client:{self.disp}")
def OnReceived(self):
print("Raised Received Event")
item_code = self.disp.GetHeaderValue(0)
item_name = self.disp.GetHeaderValue(1)
y_close = self.disp.GetHeaderValue(10)
now = self.disp.GetHeaderValue(11)
open = self.disp.GetHeaderValue(13)
high = self.disp.GetHeaderValue(14)
low = self.disp.GetHeaderValue(15)
print(f"[{item_code}] {item_name}")
print(f" 전일종가:{y_close}, 현재가:{now} ")
print(f" 시가:{open}, 고가:{high}, 저가:{low}")
해당 내용이 반영된 후 DsCbo1.py 파일 내부의 코드는 아래와 같다.
## DsCbo1.py ##
import win32com.client
class StockMst:
def __init__(self):
self.stockmst = win32com.client.Dispatch("DsCbo1.StockMst") ## COM 연결
self.handler = win32com.client.WithEvents(self.stockmst, event_handler)
def _StockMst(self, item_code):
"""주식종목현재가 반환"""
self.stockmst.SetInputValue(0, item_code)
self.handler.set_instance(self.stockmst)
self.stockmst.BlockRequest()
class event_handler:
def set_instance(self, disp):
self.disp = disp
print(f"self.client:{self.disp}")
def OnReceived(self):
print("Raised Received Event")
item_code = self.disp.GetHeaderValue(0)
item_name = self.disp.GetHeaderValue(1)
y_close = self.disp.GetHeaderValue(10)
now = self.disp.GetHeaderValue(11)
open = self.disp.GetHeaderValue(13)
high = self.disp.GetHeaderValue(14)
low = self.disp.GetHeaderValue(15)
print(f"[{item_code}] {item_name}")
print(f" 전일종가:{y_close}, 현재가:{now} ")
print(f" 시가:{open}, 고가:{high}, 저가:{low}")
▶ 실행 결과 확인하기
[통신결과:1] 서버와의 연결에 성공했습니다.
로그인되어 있습니다.
self.client:<win32com.gen_py.CpDib 1.0 Type Library.IDib instance at 0x162647024>
[A005930] 삼성전자
전일종가:77200, 현재가:80200
시가:79400, 고가:80200, 저가:78700
이제 이후의 함수나 이벤트들은 모두 이와 같은 방식에 따라 반복적인 코드를 구현하여 동작하게 될 것이므로 아직까지 이해가 가지 않은 부분이 있다면 이전 게시글부터 다시 한 번 읽어보면서 확실하게 이해하고 넘어가는 게 좋을 것 같다.
'AUTO TRADE > [대신증권] CYBOS PLUS' 카테고리의 다른 글
대신증권 CYBOS PLUS 프로그램 구현 (2) - 자동로그인 구현하기 ② (0) | 2024.08.18 |
---|---|
대신증권 CYBOS PLUS 프로그램 구현 (1) - 자동 로그인 기능 구현하기 ① (0) | 2024.08.18 |
대신증권 CYBOS PLUS 시작하기 (8) - 모듈별로 클래스 생성하기 (0) | 2024.08.08 |
대신증권 CYBOS PLUS 시작하기 (7) - 차트조회 클래스 생성하기 (0) | 2024.08.04 |
대신증권 CYBOS PLUS 시작하기 (6) - 분봉차트 데이터 조회하기 (0) | 2024.08.04 |
소중한 공감 감사합니다