[토이 프로젝트] 자동 갱신 게임 명부 제작(8) - 코드 확장하기

chaejm55·2022년 10월 26일
1

0. 이번 단계 진행 아이디어

template method 패턴을 활용해 다른 게임 데이터 갱신에도 사용가능 하도록 코드를 확장해보자!😀

  • .gg 계열의 게임 전적 사이트의 구조가 비슷해 기존 코드를 활용해보려합니다.

1. template method 패턴 적용 및 리팩터링

다른 게임의 정보 갱신 또한 시트에서 닉네임을 가져와 selenium driver로 갱신 버튼을 누르는 동일한 로직입니다. 따라서 template method 패턴을 활용해 추후 다른 게임 정보 갱신에도 확장성을 갖도록 코드를 리팩터링했습니다.

먼저 각 게임마다 갱신에 필요한 데이터를 따로 저장해두겠습니다.

crawl_metadata.py


base_url = {"maple": 'https://maple.gg/u/'}

button_xpath = {"maple": '//*[@id="btn-sync"]'}

button_str = {"maple": "최신정보"}
  • 딕셔너리 형태로 {"게임이름" : "필요한 값"} 형태로 따로 저장했습니다.

template method 패턴을 적용할 파이썬 파일을 만들어 새로 코드를 작성했습니다. 메서드의 자세한 구현은 기존 포스팅에서 작성한 내용과 대부분 중복되므로 생략하겠습니다.

아래 UML처럼 구현했습니다.


game_record_crawl.py

import crawl_metadata as metadata  # 게임 별 갱신에 필요한 데이터
...


# abstract class
class GameRecordCrawler(metaclass=ABCMeta):
    def __init__(self, json_file_path, sheet_url, worksheet_name, col, start):
        self.driver = None  # selenium driver
        self.character_list = []  # 갱신 할 닉네임 리스트
        self.success_cnt = 0  # 최종 성공 캐릭터 개수 카운트
        self.fail_cnt = 0  # 총 실패 횟수 카운트
        self.retry_cnt = 0  # 재갱신 시도 횟수 카운트
        self.total_cnt = 0  # 총 캐릭터 카운트
        self.MAX_RETRY = 5  # 실패 alert 발생 시 재 시도할 최대 횟수
        self.TIMEOUT = 180  # explicitly wait 최대 시간
        self.json_file_path = json_file_path  # credential 파일 경로
        self.sheet_url = sheet_url  # 명부를 가져올 스프레드시트 url
        self.worksheet_name = worksheet_name  # 명부 워크 시트 이름
        self.col = col  # 닉네임 있는 열
        self.start = start  # 닉네임 데이터가 시작 되는 행
        self.game_name = ""  # 게임 이름

    def crawl(self):  # template method
        # 1. 캐릭터 데이터 리스트 받아오기
        self.character_list = self.get_sheet_data(self.json_file_path, self.sheet_url, self.worksheet_name,
                                                  self.col, self.start)
        # 2. selenium driver 생성
        self.driver = self.create_driver()
        # 3. 전적 정보 갱신
        self.refresh_record(metadata.base_url[self.game_name], metadata.button_xpath[self.game_name],
                            metadata.button_str[self.game_name])
        # 4. 갱신 완료 후 selenium driver 종료
        self.driver.close()

    # 공통 메서드1 - selenium driver 생성(생략)
    def create_driver(self):
    	...
        return driver

    # 공통 메서드2 - 명부 시트에서 닉네임 받아오기(생략)
    def get_sheet_data(self, json_file_path, sheet_url, worksheet_name, col, start):
        ...
        return character_list

    # 필수 구현 메서드 - 게임 전적 갱신
    @abstractmethod
    def refresh_record(self, base_url, button_xpath, button_str):  # 게임별로 전적 갱신 하기
        pass


# concrete class
class MapleRecordCrawler(GameRecordCrawler):
    def __init__(self, json_file_path, sheet_url, worksheet_name, col, start):
        super().__init__(json_file_path, sheet_url, worksheet_name, col, start)
        self.game_name = "maple"  # 게임 이름 초기화
	
    # 추상 함수 구현
    def refresh_record(self, base_url, button_xpath, button_str):
        retry = 0  # 재시도 횟수 카운트
        while len(self.character_list) > 0 and retry < self.MAX_RETRY:
            ...
            retry += 1


# concrete class - 추후 기능 구현
class LolRecordCrawler(GameRecordCrawler):
    def __init__(self, json_file_path, sheet_url, worksheet_name, col, start):
        super().__init__(json_file_path, sheet_url, worksheet_name, col, start)
        self.game_name = "lol"  # 게임 이름 초기화

    def refresh_record(self, base_url, button_xpath, button_str):
        pass

  • MAX_RETRY: 홈페이지 점검 등 계속 되는 실패로 인한 무한 루프에 빠지지 않도록 최대 재시도 횟수를 5회로 제한했습니다.

  • crawl() : template method로 필요한 메서드들을 순서대로 호출해 subclass에서 동일한 구조의 로직을 사용하도록 해줍니다.

  • create_driver() : 공통 메서드로 selenium 드라이버를 생성합니다. 기존 main에 있었던 코드에서 driver 생성 부분을 빼내어 작성했습니다.

  • get_sheet_data() : 공통 메서드로 구글 스프레드 시트에서 닉네임을 가져옵니다. 기존 main에 있었던 코드에서 시트 데이터를 가져오는 부분을 빼내어 작성했습니다.

  • refresh_record() : 추상 메서드로 각 subclass에서 꼭 구현해야합니다. 세부적인 부분은 게임 마다 조금 다르기 때문에 subclass에서 각자 구현합니다.

  • MapleRecordCrawler: 추상 클래스를 구현한 subclass입니다. game_name을 "maple"로 초기화 하고 기존 포스팅에서 사용한 로직으로 refresh_record()를 구현하여 전적 정보 갱신을 합니다.

  • LolRecordCrawler: 추상 클래스를 일부 구현한 subclass입니다. game_name을 "lol"로 초기화 하고 추후 refresh_record()를 구현하면 쉽게 다른 게임 정보 갱신에 코드가 확장됩니다.


기존 코드가 있던 파일은 간단히 template method를 통해 실행하는 것으로 대체했습니다.


refresh_character.py

import game_record_crawl  # template method 구현 

def main():

    maple_crawler = game_record_crawl.MapleRecordCrawler("'JSON 파일 경로.json', 
    '스프레드시트 url', '명부', 1, 2)  # MapleRecordCrawler 객체 생성
    maple_crawler.crawl()  # template method로 갱신 수행


if __name__ == '__main__':
    main()

2. dotenv로 정보 감추기

json 파일 경로, 스프레드 시트 url 등의 민감한 정보는 감추는게 좋습니다. 특히 그런 정보를 노출한 채로 코드를 github 등에 업로드 한다면 위험할 수 있습니다.

여기서는 dotenv를 통해 .env에 민감한 정보들을 저장하고 코드에서 불러오는 방식을 사용하겠습니다. .env 파일은 절대 github 등에 업로드 해서는 안 됩니다.

먼저 dotenv 라이브러리를 설치하겠습니다.
$pip install python-dotenv

코드가 있는 경로에 .env를 작성하겠습니다. 윈도우의 경우 notepad++ 등을 통해 작성할 수 있습니다.


.env

JSON_FILE_NAME=json 파일경로
SPREADSHEET_URL=스프레드시트 url
WORKSHEET_NAME=워크시트이름

dotenv를 사용해 refresh_character.py 코드를 수정해보겠습니다.


refresh_character.py

import game_record_crawl  # factory method 파일 
import os
from dotenv import load_dotenv  # dotenv 사용

load_dotenv()

def main():
    JSON = os.getenv('JSON_FILE_NAME')
    SPREADSHEET_URL = os.getenv('SPREADSHEET_URL')
    WORKSHEET_NAME = os.getenv('WORKSHEET_NAME')

    crawler = game_record_crawl.CrawlRecord()
    crawler.crawl("maple", JSON, SPREADSHEET_URL, WORKSHEET_NAME, 1, 4)


if __name__ == '__main__':
    main()

민감한 정보 노출이 없도록 코드가 잘 수정 되었습니다.

3. 마치며

자동 갱신 게임 명부 만들기 토이 프로젝트 시리즈가 끝났습니다!🎉👏 2년 동안 구글 스프레드 시트 내장함수부터 시작해서 파이썬 코드 사용, 리팩터링, 타 게임 갱신 확장성 확보까지 했던 기록들을 약간 오랜기간에 걸쳐 포스팅했습니다.

간단히 자동 갱신 게임 명부를 만들어 보고 싶다!라는 생각에서 시작해서 여기까지 오게 되었습니다. 작은 프로그램이지만 2년 동안 실행하며 여러 오류를 대처하느라 이것저것 찾아보며 오류를 수정했습니다. 또, 코드를 좀 더 간결하게 수정해가면서 메서드 나누기, 모듈화, 디자인 패턴 적용 등의 고민을 했고 이런 경험들을 기록하여 나중에 도움이 되고자 이 시리즈를 포스팅했습니다.

이 토이 프로젝트를 하면서 아무리 혼자하는 작은 서비스라도 코드를 잘 나누지 않고 주먹구구식으로 짠다면 추후 유지보수가 정말 힘들다는 것을 알게 되었습니다. 간단한 서비스를 짜더라도 나중에 환경 변화 대비를 위해 꼭 코드를 잘 나눠 짜고 확장을 대비해 디자인 패턴을 적극 활용해야 함을 느꼈습니다. 또, 포스팅 하지는 않았지만 aws 등 클라우드 서비스를 활용하고 인스턴스 스케쥴링을 통해 비용을 절감하는 방법 또한 배웠습니다.

이 작은 프로젝트를 진행하며 얻은 바를 바탕으로 앞으로의 큰 서비스 유지보수 및 설계를 할 때 큰 도움이 될거라 생각합니다.

긴 포스팅 함께해주셔서 감사합니다. 🙇 🙋‍♂️

4. Reference

템플릿 메소드(Template Method)

python에서 .env 설정 파일 사용하기

profile
여러가지를 시도하는 학생입니다

0개의 댓글