Home [DB] Python MySQL 2013 오류 해결하기
Post
Cancel

[DB] Python MySQL 2013 오류 해결하기

개요

안녕하세요.

이번 글에서는 Python을 이용한 MySQL 연결 중 2013 오류를 해결하는 과정을 살펴보겠습니다.


들어가기 앞서

1. ORM

가장 먼저 알아볼 개념은 ORM입니다.

ORM은 Object-relational mapping의 줄임말로 관계형 데이터베이스와 객체지향언어 사이에서 데이터를 변환하는 프로그래밍 기술입니다.

예를 들어 다음과 같은 테이블을 가정해봅시다.

  • products

    idnameprice
    1apple2
    2book14
    3chicken6

products에서 가격이 5 이상인 품목을 가져오고 싶다면 다음과 같은 SQL 쿼리가 필요합니다.

SELECT name, price FROM products WHERE price >= 5;

하지만 ORM을 적용한다면 동일한 동작을 다음과 같이 수행할 수 있습니다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    price = Column(Integer)

# 해당하는 데이터베이스의 엔진 생성
engine = create_engine('sqlite:///sqlalchemy_example.db')

# 엔진을 이용해 세션 생성
Session = sessionmaker(bind=engine)
session = Session()

# 데이터베이스에 쿼리 수행
products = session.query(Product).filter(Product.price >= 5).all()
for product in products:
    print(f"Name:: {product.name} - Price::{product.price}")

ORM은 크게 다음 2가지 특징을 보입니다.

  1. 테이블의 메타데이터와 일치하는 클래스 정의
  2. 해당 클래스의 객체를 이용해 테이블 조작

ORM의 장점과 단점을 살펴보며 다음 주제로 넘어가겠습니다.

  • 장점
    1. 개발 효율 증가: 유사한 코드들의 반복을 보지 않을 수 있습니다. 예를 들어 5개의 서로 다른 SELECT문과 5개의 서로 다른 메서드 중 어느 것이 효율적일지는 바로 알 수 있습니다.
    2. 수많은 SQL을 배우지 않아도 됨: 표준 ANSI SQL을 제외하면 수많은 SQL이 존재합니다. ORM은 이런 문제에서 자유롭습니다!
  • 단점
    1. 러닝 커브: 이미 SQL을 사용 중이라면 새로운 개념인 ORM의 학습이 필요합니다. 러닝 커브는 필연적입니다.
    2. SQL대비 성능 저하: 특정 객체를 SQL로 변환하는 작업으로 인해 상대적으로 느립니다. 복잡하지 않은 CRUD 작업의 경우 SQL과 큰 차이가 없지만 복잡하고 대규모 데이터를 처리할수록 차이가 벌어집니다.

2. SQLAlchemy

두번째로 살펴볼 내용은 SQLAlchemy입니다.

SQLAlchemy는 파이썬과 DB에서 동작하는 SQL 툴킷 겸 ORM입니다.

공식 문서에서는 SQLAlchemy를 3개의 파트(ORM, Core, Third party 또는 DBAPI)로 구분하는데 이 중 사용자가 접근하는 파트는 ORM과 Core입니다.

SQLAlchemy 구조 [그림01] SQLAlchemy 구조

ORM 파트는 직전에 다룬 ORM을 구현한 파트입니다.

예시로 작성된 코드를 살펴보면 SQL 쿼리가 아닌 클래스를 통한 DB 접근을 보여줍니다.

Core 파트는 ORM 파트와 Third party 파트와 사이에 존재하며 DB와의 동작을 추상화한 단계입니다.

engine, session, connection으로 DB와 연결하며 rollback, commit 등을 통해 다양한 동작을 수행합니다.

또한 ORM 외에도 일반적인 SQL의 실행 역시 가능합니다.

3. MySQL Connection

마지막으로 MySQL의 Connection입니다.

MySQL은 크게 클라이언트와 연결된 foreground thread와 내부 엔진에서 동작하는 background thread로 나눠집니다.

여기서 1개의 커넥션은 1개의 foreground thread로로 관리합니다.

MySQL은 foreground thread의 상태를 확인하면서 연결을 종료할지 결정하는데요.

다음은 연결 종료 여부와 관련된 MySQL 설정의 예시입니다.

  • wait_timeout: 활동이 없는 커넥션을 기다리는 시간. 기본값은 28800 (8hours)
  • interactive_timeout: 활동 중인 커넥션을 기다리는 시간. 기본값은 28800 (8hours)

오류와 해결 과정

아래의 샘플 코드는 Github 레포(Github Link)에서 확인 가능합니다.

오류 상세

오류의 전문은 다음과 같습니다.

Error Code: 2013. Lost connection to MySQL server during query

단순히 해석하면 “쿼리 실행 중 MySQL 서버와의 connection이 끊겼다”는 내용입니다.

처음 위 오류가 발생한 파이썬 프로그램을 테스트했을 때는 정상 동작했습니다.

그래서 오류를 해결하기 위한 여정을 떠났습니다..!

오류 원인

이 오류를 구글링해보면 다양한 원인을 찾을 수 있습니다.

  • 클라이언트(대표적으로 MySQL Workbench) 측의 connection 관련 설정
  • 네트워크 연결 오류
  • MySQL 서버 측의 wait_timeout 초과로 인한 connection 해제

며칠의 삽질 끝에 발견한 오류의 원인은 "MySQL 측의 wait_timeout을 초과한 python의 connection 연결 종료"였습니다.

해당 오류를 재현한 다이어그램은 다음과 같습니다.

mysql 2013 error [그림02] 2013 오류 원인

해결 과정

오류를 파악했으니 해결할 차례입니다!

가장 먼저 공식 문서 중 disconnect를 다루는 문서의 내용을 시도했습니다.

해당 문서는 create_engine 메소드의 pool_pre_ping 옵션과 pool_recycle 옵션, @event.listens_for 데코레이터 적용 등 다양한 방법을 소개합니다.

하지만 제가 겪은 오류는 MySQL에서 연결을 해제하는 오류였기 때문에 위의 내용은 효과가 없었습니다.

그래서 try ~ catch 문을 통해 예외를 확인하고 연결을 재생생하는 방식으로 진행했습니다.

mysql 2013 error fix [그림03] 2013 오류 해결

다음 코드는 실제로 적용한 코드를 일부 변형하여 작성했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from typing import Dict

import sqlalchemy
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.exc import DBAPIError
from sqlalchemy.engine.result import Result


class SqlalchemyMysqlSession:
    __engine:sqlalchemy.Engine
    __session:Session
    __last_engine_option:Dict[str, object] = {}
    __last_session_option:Dict[str, object] = {}

    def create_engine(self, *args, **kwargs) -> None:
        self.__engine = sqlalchemy.create_engine(*args, **kwargs)
        self.__last_engine_option = {
            "args" : args,
            "kwargs": kwargs,
        }

    def create_session(self, *args, **kwargs) -> None:
        self.__session = sessionmaker(self.__engine, *args, *kwargs)()
        self.__last_session_option = {
            "args" : args,
            "kwargs": kwargs,
        }

    def execute(self, *args, **kwargs) -> Result:
        try:
            ret = self.__session.execute(*args, **kwargs)
        except DBAPIError as e:
            if e.orig.args[0] == 2013 and e.connection_invalidated:
                self.rollback()

                self.create_engine(*self.__last_engine_option.get("args", []), **self.__last_engine_option.get("kwargs", {}))
                self.create_session(*self.__last_session_option.get("args", []), **self.__last_session_option.get("kwargs", {}))
                
                ret = self.__session.execute(*args, **kwargs)
        finally:
            self.__session.commit()
            return ret

    def commit(self) -> None:
        self.__session.commit()

    def rollback(self) -> None:
        self.__session.rollback()

    def close(self) -> None:
        self.__session.close()

마무리하며

이번 글에서는 Python과 MySQL에서 발생한 2013 오류를 해결하는 과정을 살펴봤습니다.

이전까지는 DBMS라는 개념보다는 각 DB의 쿼리에 대해서만 고민했습니다.

하지만 업무를 진행하면서 DBMS는 Database Management System이라는 사실을 되새길 수 있었습니다.

비록 오류를 해결하느라 며칠에 걸친 삽질을 했지만 그 과정이 즐겁게 다가왔네요 :)

이 글이 조금이나마 도움이 되셨으면 합니다.

감사합니다. 😀


참고 문헌

This post is licensed under CC BY 4.0 by the author.

[회고] 2023년을 되돌아보며

[Jekyll] Chirpy 테마에 custom sidebar 적용하기

Comments powered by Disqus.