상세 컨텐츠

본문 제목

SRT 예약하기 프로젝트 - 02

개발 공부 (토이 프로젝트)

by letprogramming 2024. 1. 16. 01:44

본문

반응형

https://github.com/kminito/srt_reservation

 

GitHub - kminito/srt_reservation

Contribute to kminito/srt_reservation development by creating an account on GitHub.

github.com

위 프로젝트를 보고 많이 배울 수 있었고 정말 유용하게 사용할 수 있었다.

위 프로젝트를 어떻게 하면 더 편리하고 유용하게 사용할 수 있을 지 생각하다가 개선할 점은 아래와 같이 정리했다.

 

1. 실행 방법 간편화

2. 예약 완료 여부 알림

 

1. 실행 방법 간편화

기존 python 프로그램 실행 시 인자로 아래 필드들을 작성해야 한다.

사용자 ID, 사용자 Password, 출발역, 도착역, 조회일자, 조회시간, 열차 수, 예약 대기 여부

 

물론 미리 복사해놓고 필요에 따라 수정하여 실행하면 되지만

커맨드 사용에 익숙하지 않은 사람들은 어려울 수 있다고 생각했다.

 

또한 커맨드를 사용하기 위해서는 항상 컴퓨터로 접속하여 직접 실행을 해야한다.

기차를 예약해야 하는 상황이 빈번한 경우 불편할 수 있다.

 

2. 예약 완료 여부 알림

SRT 열차를 예약하면 10분 내에 결제를 진행해야 한다. 10분이 초과하면 자동으로 취소가 된다.

현재는 python print로 터미널에 print만 되고 있으므로, 수시로 SRT 앱을 확인하거나 터미널을 확인하고 있어야만 예약완료 여부를 알 수 있다.

 

그래서 내가 생각한 개선점은 discord 라이브러리 추가였다.

discord는 보편적인 채팅 앱으로 데스크톱와 스마트폰 모두 이용 가능하며,

게임을 할 때 보이스챗으로 유명하다.

주목할 점은 discord에서 개발자 API를 제공하고 있다.

 

API를 제공한다는 의미는 python 코드 내에서 discord 앱으로 채팅을 보내거나 여러가지 동작을 할 수 있다는 것이다.

www.discord.com  

 

Discord | Your Place to Talk and Hang Out

Discord is the easiest way to talk over voice, video, and text. Talk, chat, hang out, and stay close with your friends and communities.

discord.com

https://discord.com/developers/applications

 

Discord Developer Portal — API Docs for Bots and Developers

Integrate your service with Discord — whether it's a bot or a game or whatever your wildest imagination can come up with.

discord.com

 

discord API를 이용하여 수정을 진행했다.

 

1. 예약 완료 여부 알림

먼저 예약 완료 여부는 단순히 discord에 메시지를 보내는 것으로 해결했다.

python 내에서 discord 채널로 메시지를 보내는 방법은 아래와 같다.

 

a. webhook을 연결하여 특정 채널 webhook URL에 메시지 전송

b. bot을 생성하여 채널에 메시지 전송

 

가장 먼저 사용한 방법은 a 였다.

discord 채널을 생성한 후에 webhook 연동을 하면 URL이 생성된다.

python의 requests.post()를 이용하여 이 URL에 data 필드를 담아 전송하면 그대로 해당 채널에 메시지가 전송된다.

 

이를 이용해 예약 완료 외에도 현재 진행되고 있는 예약 정보를 최초에 출력하거나,

새로고침 횟수가 일정 횟수가 되었을 때 메시지를 전송하여 프로그램이 실행되고 있음을 사용자에게 알려줬다.

 

1. PC discord 에서 webhook 추가

2. webhook URL 복사

3. 해당 URL 로 HTTP POST request 전송

 

단순히 아래와 같이 전송하고자 하는 메시지 string값과 URL을 requests.post를 이용하여 전송하면

webhook이 연결되어 있는 나의 discord 채널에 메시지가 전송되는 매우 간단한 구조이다.

now = datetime.now()
message = {"content": f"[{now.strftime('%Y-%m-%d %H:%M:%S')}] {str(msg)}"}
requests.post(self.webhook_url, data=message)
print(message)

 

a의 장점은 특정한 패키지 import 없이 단순 http request를 통해 메시지를 전송할 수 있다는 점이었다.

그러나 콜백을 받을 수 없는 단점이 존재했다. 물론 나의 역량이 부족한 것일 수 있지만 콜백을 받기위해서 bot 도입을 했다.

 

그러나 이후에 2. 실행 방법 간편화를 진행하면서 b 방법을 도입하게 되었다.

이유는 2. 실행 방법 간편화에서 설명하도록 하겠다.

 

2. 실행 방법 간편화

실행 방법은 단계를 거쳐 간편하게 진행했다.

간편하다기 보다는 기능을 추가했다. 왜냐하면 수정한 방식에 장단점이 있기 때문이다.

 

기존의 방법으로 진행하면 실행 시에 터미널로 정보를 전달해줘야 하고,

일회성이라는 단점이 있었다. 그러나 실행이 단순하고 오류의 가능성이 적었다.

또한 성능적으로도 버벅임이 최종 결과물보다는 좋은 것으로 보였다.

 

하지만 위에서 말했듯이 터미널을 통한 실행은 예약이 빈번한 나에게 적합하지 않다고 생각했다.

 

그래서 생각해낸 방법은 discord에서 채팅 형식으로 원하는 정보를 입력하여 예약을 진행하게 하는 방식이였다.

이를 구현하기 위해서는 discord 채널에 메시지가 전송되는 타이밍을 나의 python 코드에서 캐치할 수 있어야 했다.

즉 콜백이 발생해야 했다. 현재 webhook을 이용한 구현은 discord 메시지를 단방향 (client -> server)로 전송할 수 밖에 없다.

server <-> client 통신을 위해 discord의 bot을 생성하여 채널에 추가했다.

이 bot은 사용자처럼 채널에 들어가있다. 이 bot을 python에서 client로 받아서 콜백을 받게 할 수 있었다.

 

이를 위해 discord.py라는 패키지를 import하여 사용했다.

https://discordpy.readthedocs.io/en/stable/index.html

 

Welcome to discord.py

 

discordpy.readthedocs.io

 

discord.py

먼저 아래 discord 패키지를 pip를 이용해 설치해준다.

pip3 install discord

or

pip install discord

 

아래는 예시 코드이다.

 

1. import discord로 discord 패키지를 import해준다.

2. intents는 하나의 세션이라고 생각하면 될 것 같다. 기본 intents를 생성하여 등록해준다.

3. client를 생성하여 discord 서버랑 통신할 수 있다.

4. 내 discord의 채널의 고유 ID를 get_channel을 통해 가져올 수 있다.

5. get_channel까지 되었다면 이제 해당 채널의 메시지를 전송, 콜백이 가능해진다.

import discord

intents=discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)

srt_channel = client.get_channel(_cfg['SRT_CHANNEL'])
ktx_channel = client.get_channel(_cfg['KTX_CHANNEL'])

@client.event
async def on_ready():
    print("discord on_ready called..")
    print(f"USER: {client.user.name}")
    print(f"USER: {client.user.id}")
    send_message("======================================")
    send_message("서버가 시작되었습니다")
    send_message("'예약하기'를 입력해주세요🐻‍❄️")
    
@client.event
async def on_message(message):
    
    print("discord on_message called..")
    print(message.content)

 

 

discord 채널에 메시지가 전송되면 on_message 라는 메소드가 호출되었다.

이 on_message 메소드 내에서 특정 단어를 필터링하는 방법으로 예약 시 필요한 정보를 받았다.

 

on_message 함수는 해당 채널에 메시지가 오기만 하면 호출된다.

즉, 사용자와 대화를 통해 예약 정보를 받기 위해서는 현재 상태를 알아야했다.

완벽한 방법은 아니지만 step을 임의로 설정했다.

 

현재 어떤 입력 조건까지 입력받았는 지를 알아야 했기 때문에, step을 설정하여 현재 상태를 알게했고,

각 단계마다 입력 변수에 대한 validation을 진행하여 오류를 최소화했다.

물론 위 방법이 완벽하진 않다. 단어로 필터링을 했기 때문에, 해당 채널 대화중에 단어가 끼면 프로그램이 실행되고 잘못된 값이 입력될 수 있다.

그러나 매크로 사용이라는 특수한 상황에 맞게 아래 사항에 더 초점을 맞추고 진행했다.

1. 사용자는 사용법을 숙지하고 있다.

2. 프로그램의 잘못된 입력에 대해 인지할 수 있다.

3. 매크로가 잘못되었다고 해도 사용자의 채팅에 따라 다시 시작할 수있다.

4. 우선 기능 구현이 먼저다.

 

async-await

on_message 호출 시 어려웠던 점은 async-await 처리였다.

if 문을 이용해 특정 상태일 때를 조건으로 활용하면 원하는 방향으로 프로그램이 수행될 것 같았지만,

실제로는 콜백이 비동기로 발생하기 때문에, await 처리가 필요했다.

각 단계 설정, discord 메시지 전송 시에 await을 설정하여 원하는 동작 수행 후에 다음 단계로 넘어가게 했다.

 

예시)

아래 코드는 on_message 함수의 일부이다.

즉 메시지가 오면 아래 if 문의 조건에 따라 수행되게 된다.

처음에는 초기조건에 의해 "예약하기" && "wait" if 문으로 잘 진입될 것이다.

여기까지는 async-await 문제와 상관없다.

 

await 없이 message.channel.send()와 set_step을 호출하고나서

이후에 다시 on_message가 호출된다면 일반적인 생각으로는 elif 조건을 탈 것으로 예상된다.

코드 순서상으로는 현재 current_step이 "init"이 되었고, station만 station_list에 있다면 수행되어야 한다.

 

그러나 아래와 같이 await을 붙이지 않는다면,

message.channel.send()호출과 set_step은 수행이 예측 불가하다.

step이 바뀌지 않아 초기 또는 오류 메시지로 처리 될 수 있고,

입력 완료 메시지가 "입력하세요" 메시지보다 먼저 전송되거나 엉뚱한 메시지가 전송되기도한다.

 

물론 현재 status 처리, 즉 step 처리를 string으로 하는 것은 좋은 방법은 아닌 것으로 보이지만,

이와 별개로 async-await 비동기 처리는 중요한 개념으로 보인다.

특히 사용자와 인터랙션 하는 프로그램인 경우에는 더욱 입력 처리에 안정성을 추가해야할 것 같다. 

if message.content == "예약하기" and current_step == "wait":
    login_id = login_id
    login_psw = login_psw
    await message.channel.send("🚉출발역을 입력하세요")
    await set_step("init")
elif (current_step == "init" or current_step == "dpt_stn") and (message.content in station_list):
    if current_step == "init":
        for station in station_list:
            if message.content == station:
                dpt_stn = station
                await message.channel.send(f"🚉출발역 입력 완료")
                await message.channel.send(f"'{dpt_stn}역'")

 

추가적으로 await이 추가되면서 당연히 메시지 전송과 프로그램의 전체적인 응답 속도가 떨어졌다.

이는 다음 개선 사항으로 생각된다.

 

1. 필요한 정보만 discord 전송 (필요없는 메시지 삭제, 메시지 content 가다듬기)

2. 필요한 부분만 await 처리

 

fork()

 

모든 입력 정보를 받은 후에는 기존의 SRT 예약 객체를 생성하여 run()을 호출하는 식으로 진행했다.

기존의 프로그램은 SRT 객체를 생성하면서 프로그램이 실행되기 때문에,

프로그램의 어떠한 부분이 먼저 수행될 지를 정리하는 것도 필요했다.

 

수정된 프로그램에서는 discord client의 run()이 먼저 수행되어 discord 메시지를 계속 수신한다.

on_message가 호출되면서 정보가 완성되면 마지막에 SRT run()이 수행된다.

 

이 때 발생한 문제는 discord client가 run되고 on_message가 호출된 시점에 SRT 객체의 run()이 호출되면서

루프에 빠진다는 것이였다.

 

SRT run()은 예약되기 전까지는 반복해서 동작을 수행하고 멈추지 않기 때문에 discord client 입장에서는

pending 상태에 빠지게 된다.

discord 입장에서는 에러 발생이고, 프로그램 전체적으로 보았을 때도 예약이 되지 않으면 discord 메시지 콜백을 사용할 수 없는 것은

만약 예약 정보를 잘못입력하면 강제로 종료하는 방법으로 프로그램을 재실행해야했다.

 

이를 해결하기 위해서 os.fork()를 이용했다.

fork는 운영체제에서 중요하게 배우는 개념으로 프로세스를 생성하는 메소드이다.

 

python과 관계없이 UNIX 체제에서 사용 가능한 개념이므로, 모든 언어에서 사용 가능한 개념이다.

fork()를 호출하면 해당 시점에 자식 프로세스가 생성된다.

부모와 자식 프로세스는 pid가 다르기 때문에 다른 프로세스이다.

따라서 다른 동작을 수행하게 할 수 있다.

 

pid를 조건으로 활용하여 부모 프로세스와 자식 프로세스의 동작을 분기할 수 있다.

 

fork() 호출 시 return 값이 0이면 자식프로세스이다.

fork() 호출 시 return 값이 0이 아닌 값이면 부모 프로세스이다.

 

import os

def parent_process():
    print("This is the parent process.")
    print("Parent PID:", os.getpid())

def child_process():
    print("This is the child process.")
    print("Child PID:", os.getpid())

child_pid = os.fork()

if child_pid == 0:
    child_process()
else:
    parent_process()

 

예를 들어,

위 코드에서 os.fork()를 수행하는 시점까지는 부모 프로세스만 존재한다. 일반적으로 우리가 생각하는 코드 수행 순서이다 (위 -> 아래)

os.fork()가 수행되고 child_pid에 return 값이 저장된다.

밑에 if 문으로 가는 프로세스는 부모 프로세스, 자식 프로세스 2개의 프로세스이다.

위 문장이 좀 이해가 안될 수 있다고 생각한다.

처음에는 위 개념이 이해가 어렵다. 왜냐하면 일반적으로 지금까지 코드는 하나의 프로세스가 지나가면서 수행했고,

반복문이 아닌 이상 반복해서 수행하지 않는다.

그러나 fork()가 호출되는 순간, 운영체제가 또 다른 프로세스 하나를 생성한다고 생각하면 된다.

그러면 개발자 입장에서는 지금 이 코드를 수행하는 프로세스가 부모인 지 자식인 지 확인할 수 있는 방법이 없다.

그래서 두 프로세스를 구분하기 위해서 child_pid 값이 0인 지 검사한다.

 

0이면 자식 프로세스이고, 0이 아니면 부모 프로세스이다.

이는 자식 프로세스에게 일을 시킬 수 있음을 의미한다.

즉, 프로세스를 생성하여 별도의 일을 시키고, 부모 프로세스에서는 또 다른 일을 할 수 있는 것이다.

if - else 문을 통해서 자식과 부모 프로세스의 영역을 나눈다.

child_process는 if 문 내에서 주어진 일을 수행하고 종료한다.

parent_process는 자식 프로세스가 주어진 일을 수행할 때까지 대기한다.

자식 프로세스가 모든 일을 끝냈을 때 비로소 부모 프로세스도 else가 수행되고 종료한다.

 

돌아와서, 입력 정보를 모두 받은 상태에서 SRT 객체의 run()을 수행할 때,

프로세스를 생성해서 별도로 동작하게 해도 문제가 없을 것으로 생각했다.

왜냐하면 이미 사용자에게 입력 정보도 모두 받았고, 해야할 일은 SRT 예약하는 로직을 돌리기만 하는 것이기 때문이다.

프로세스를 fork()로 생성하여 SRT.run()을 수행하게 했다.

 

부모 프로세스는 SRT.run()에서 자유로워지고 client.run() 상태에서 on_message를 받을 수 있다.

fork로 분리를 하면 만약에 SRT 예약 프로세스에서 문제가 생겨도 부모 프로세스에는 영향이 없으니, 프로그램의 안전성도 더 높다고 생각했다.

 

이 방법의 전제조건은 discord client의 run()이 실행되고 있어야 한다는 것이다. 즉 로컬에서 서버를 돌려야 한다는 뜻이다.

최종적으로는 클라우드 서버에 코드를 실행시켜서 항상 수행되고 있다면 컴퓨터에서 수행하지 않고 예매를 할 수 있을 것이다.

 

 

 

반응형

'개발 공부 (토이 프로젝트)' 카테고리의 다른 글

SRT 예약하기 프로젝트 - 01  (0) 2024.01.16
SRT 예약하기 프로젝트 - 00  (0) 2024.01.16

관련글 더보기