프로그래밍/Python

[python] 각종 옵션/설정 정보를 JSON으로 다루기

채윤아빠 2023. 9. 4. 14:07
728x90
반응형

개요

윈도우 환경에서 작업을 할 적에는 *.ini 형식의 파일로 옵션/설정 정보를 관리하였고, 관련 API 등이 편하게 되어 있어서 이용에 전혀 불편함이 없었는데, 파이썬의 configParse를 이용하려니 만족스럽지가 못하였습니다.

그러던 차에 JSON 형식의 파일이 파이썬의 dict 형과 1:1 대응이 된다는 점에서 착안하여 다음과 같이 옵션/설정 정보를 관리하였더니 INI 형식보다 더 유연하고 사용하기가 좋았습니다.

본 글에서는 파이썬에서 각종 옵션/설정 정보를 JSON으로 다루는 방법을 정리해 보겠습니다.


키 이름 상수 정의하기

자바스크립트에서 JSON 데이터를 다루거나, 파이썬의 dict 형 자료를 이용할 때, 꼭 문제가 되는 것이 키 이름에서 오타가 나서 알 수 없는 곳에서 헤맸던 경험이 종종 있었습니다.

그래서 번거롭더라도 키 이름을 다음과 같이 아예 상수로 정의하여 이용하였더니, 코드 작성 시 오타가 나면 바로 알려 주어서 키 이름을 꼭 상수로 정의하고 있습니다.

JSON_TC_NAME: Final                = 'test_center'

JKEY_TOP_LEFT: Final            = 'top_left' # 윈도우 = 좌상 좌표
JKEY_USE_PORTS: Final            = 'use_ports'
JKEY_IS_REAL: Final                = 'is_real'
JKEY_LOG_LEVEL: Final            = 'log_level'
JKEY_MAIN_TAB_INDEX: Final        = 'main_tab_index'
JKEY_TITLE: Final                = 'title'
JKEY_MODEL: Final                = 'model'
JKEY_HOST: Final                = 'host'
JKEY_PORT: Final                = 'port'

위와 같이 키 이름을 상수로 정의하면, 실제 키 이름을 언제든지 일괄 변경할 수 있고, 상수 이름도 "Rename" 등으로 전체 코드에서 일괄 변경할 수 있는 등 리팩토링할 때 아주 유용합니다.


기본 설정값 정의하기

키 이름을 상수로 정의한 이후에 실제 프로그램의 기본 설정 정보는 다음과 같이 상수형 dict로 정의하여 이용합니다.

INI 형식에서는 아래와 같은 구조 형식으로 정의하기가 어렵지만, dict에서는 클래스 자체가 포함될 수도 있고, 정의한 것 그대로 읽고 쓸 수 있다는 장점이 있습니다.

# 기본 GUI 설정 정보
DEF_GUI_CONF: Final = {
    JKEY_TOP_LEFT: [0, 0]
    , JKEY_USE_PORTS: 1
    , JKEY_IS_REAL: 1
    , JKEY_LOG_LEVEL: 10
    , JKEY_MAIN_TAB_INDEX: 0
    , JSON_TC_NAME:{
        JKEY_TITLE:"시험치구"
        , JKEY_MODEL:"TC"
        , JKEY_HOST:"192.168.0.171"
        , JKEY_PORT:50000
    }
}

기본 설정 정보도 상수로 정의한 이유는 말 그대로 기본값으로 설정 정보 파일이 사라지거나 했을 경우에도 오류없이 프로그램이 동작할 수 있도록 기본 설정정보를 정의해 놓은 것이며, 만약 설정 파일이 사라진 경우에는 기본 설정정보로 설정파일을 작성하게 됩니다.


설정 정보를 다루는 함수 작성 예시

다음 함수는 dict 데이터를 JSON 파일로 저장하는 함수 예시입니다.

def save_json_conf(json_file_path: str, conf: dict) -> int:
    """ dict 형식의 설정 정보를 JSON 파일로 저장합니다.

    Args:
        json_file_path (str): 저장될 JSON 파일에 대한 경로 문자열 (파일명 포함)
        conf (dict): JSON 형식으로 저장할 설정 정보

    Returns:
        (int, error_message): 이상이 없다면 0, 오류가 발생한 경우에는 오류 코드 = 1
    """
    from json import dumps

    try:
        with open(json_file_path, mode = "wt", encoding = 'UTF-8') as f:
            json_str = dumps(conf, indent = 4, ensure_ascii = False)
            f.write(json_str)
            return (0, '')
    except Exception as e:
        return (1, f'save_json_conf error: {e}')

다음 함수는 JSON 파일로부터 dict 데이터를 읽어 들이는 예시입니다.

def load_json_conf(json_file_path):
    """ JSON 파일을 열어 dict 형태로 로딩합니다.

    Args:
        json_file_path (string): JSON 파일에 대한 경로 문자열 (파일명 포함)

    Returns:
        (dict, error_message): 이상이 없다면, JSON 객체를 반환하고 오류가 발생한 경우에는 error_message 항목에 오류 문자열을 반환합니다.
    """
    from json import loads
    from os import path

    if (not path.exists(json_file_path)):
        return (None, "load_json_conf error: FileNotFoundError: [Errno 2] No such file")

    with open(json_file_path, mode = "r", encoding = 'UTF-8') as f:
        json_str = f.read()
        try:
            json_data = loads(json_str)
            return (json_data, '')
        except Exception as e:
            return (None, f'load_json_conf error: {e}')

마지막으로 dict 자료에서 특정 키의 값을 가져올 때는 항상 None 여부를 확인해야 합니다.
특정 키에 대한 항목이 없을 경우, 지정한 기본값을 반환하는 함수 예시입니다.

def get_dict_value(target_dict, key, default_value = None):
    """ dict 객체 내에서 key에 해당하는 값을 반환합니다.
    key에 해당하는 값을 찾을 수 없을 경우에는 default_value를 반환합니다.

    Args:
        target_dict (dict): 입력 dict 객체
        key (string): key 문자열
        default_value (Any, optional): 기본값으로 반환할 값. Defaults to None.

    Returns:
        Any: dict 객체 내에서 key로 찾은 데이터 값. 없으면 default_value 값이 반환됩니다.
    """
    if (type(target_dict) is dict):
        value = target_dict.get(key)
        if (value == None):
            return default_value
        else:
            return value
    else:
        return default_value

사용 예

위에서 정의한 GUI에 대한 설정정보를 다음과 같이 활용할 수 있습니다.

import constants as const

    def initUI(self) -> None:
        conf_filename = const.CONF_FILE_FA50_GUI
        self._conf, error_message = const.load_json_conf(conf_filename)
        if (self._conf == None): # GUI 설정 정보 읽기 실패
            self._conf = const.DEF_GUI_CONF
            const.save_json_conf(conf_filename, const.DEF_GUI_CONF)
            self._logger.error(f'load_json_conf fail: {error_message}')
        left, top = const.get_dict_value(self._conf, const.JKEY_TOP_LEFT, [0, 0])
        self.move(left, top)
        self.tabWidget.setCurrentIndex(const.get_dict_value(self._conf, 'main_tab_index', 0))
        loadWidgetFromConf(self.gbTcInfo, self._conf[const.JSON_TC_NAME])
        self._is_real = 0 != const.get_dict_value(self._conf, const.JKEY_IS_REAL, 1) != 0
        log_level = const.get_dict_value(self._conf, const.JKEY_LOG_LEVEL, self._logger.level)
        self._logger.setLevel(log_level)


    def saveConf(self) -> None:
        """ GUI에 설정된 정보들을 저장합니다. """
        self._conf[const.JKEY_TOP_LEFT] = [self.x(), self.y()]
        saveConfFromWidget(self._conf[const.JSON_TC_NAME], self.gbTcInfo)
        conf_filename = const.CONF_FILE_GUI
        const.save_json_conf(conf_filename, self._conf)