AD JB Toolbox 01

MariaDB 강좌 / Python에서 MariaDB 연결하고 매개변수 쿼리 사용하기

Python에서 MariaDB를 사용할 때는 드라이버로 연결한 뒤 SQL 문과 사용자 값을 별도 인수로 전달해야 합니다. 매개변수 쿼리는 입력을 SQL 구문으로 이어 붙이지 않게 하며, 함께 최소 권한 계정과 명시적인 COMMIT·ROLLBACK을 사용해야 데이터 변경을 통제할 수 있습니다.

실습 환경과 데이터베이스 계정 준비하기

이 예제는 Python 3, PyMySQL 1.1.2, 로컬 TCP로 연결하는 MariaDB를 사용합니다. 핵심 동작은 Python 3.12·MariaDB 12.3.3에서 확인했습니다. PyMySQL은 Python에서 MariaDB와 통신할 수 있는 드라이버이며, 여기서 사용하는 값 자리표시는 %s입니다. 다른 드라이버의 물음표 구문과 혼용하지 마세요.

관리자 연결에서 아래 SQL을 실행합니다. 공개 예제의 Replace_With_Your_Own_Lab_Password는 실제로 사용할 암호가 아니므로 자신만의 실습용 암호로 바꾸세요. 사용자 생성은 첫 실행용이며 같은 계정이 이미 있다면 계정과 권한을 확인하고 중복 생성하지 않습니다. 생성과 실행의 책임은 사용자 권한 관리에서 구분할 수 있습니다.

CREATE DATABASE IF NOT EXISTS appops_lab
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE appops_lab;
CREATE TABLE app_notes (
  note_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  note_name VARCHAR(80) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE USER 'appops_app'@'127.0.0.1'
  IDENTIFIED BY 'Replace_With_Your_Own_Lab_Password';
GRANT SELECT, INSERT ON appops_lab.app_notes TO 'appops_app'@'127.0.0.1';

계정의 호스트 부분 127.0.0.1은 로컬 네이티브 서버의 TCP 접속을 전제로 합니다. 컨테이너나 원격 서버에서는 서버가 보는 클라이언트 출발지가 달라질 수 있으므로 접속 계정의 호스트와 연결 경로를 확인해야 합니다. 단순히 모든 호스트에 관리자 권한을 허용하는 방식으로 해결하지 않습니다.

프로젝트 폴더와 실행 파일 구성하기

새 폴더를 만든 뒤 다음 명령을 실행합니다. 가상 환경을 활성화하는 대신 그 환경의 Python을 직접 지정하므로 PowerShell 실행 정책을 바꿀 필요가 없습니다.

python -m venv .venv
.venv\Scripts\python.exe -m pip install PyMySQL==1.1.2

위 명령은 Windows PowerShell 기준입니다. Linux·macOS에서는 같은 단계에서 .venv/bin/python을 사용합니다. 아래 전체 코드를 UTF-8 파일 notes_app.py로 저장하세요. 암호는 실행 때 입력받으며 코드나 SQL 문자열에 넣지 않습니다.

import argparse
import getpass
import os
import pymysql

def save_and_find(conn, name, sort_key="id"):
    if not name or len(name) > 80:
        raise ValueError("Name must contain 1 to 80 characters")
    columns = {"id": "note_id", "name": "note_name, note_id"}
    if sort_key not in columns:
        raise ValueError("Unknown sort key")
    with conn.cursor() as cur:
        cur.execute("INSERT INTO app_notes (note_name) VALUES (%s)", (name,))
        cur.execute(
            "SELECT note_id, note_name FROM app_notes "
            "WHERE note_name = %s ORDER BY " + columns[sort_key],
            (name,),
        )
        return cur.fetchall()

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--name", default="O'Reilly")
    parser.add_argument("--sort", choices=("id", "name"), default="id")
    args = parser.parse_args()
    password = getpass.getpass("Database password: ")
    conn = pymysql.connect(
        host=os.getenv("DB_HOST", "127.0.0.1"),
        port=int(os.getenv("DB_PORT", "3306")),
        user=os.getenv("DB_USER", "appops_app"),
        password=password,
        database="appops_lab",
        charset="utf8mb4", autocommit=False,
        connect_timeout=5, read_timeout=10, write_timeout=10,
    )
    try:
        rows = save_and_find(conn, args.name, args.sort)
        conn.commit()
        for note_id, note_name in rows:
            print(note_id, note_name)
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

if __name__ == "__main__":
    main()

연결 제한 시간과 읽기·쓰기 제한 시간을 지정하고 문자 집합을 utf8mb4로 맞췄습니다. 이 글은 PC의 로컬 실습 연결을 다룹니다. 다른 네트워크로 연결할 때는 서버 인증서를 검증하는 TLS 설정도 별도로 구성해야 하며 암호만 있다고 전송 경로가 보호되는 것은 아닙니다.

정상 이름과 따옴표가 있는 이름으로 실행하기

.venv\Scripts\python.exe notes_app.py --name "Notebook"
.venv\Scripts\python.exe notes_app.py --name "O'Reilly" --sort name

두 명령은 서로 다른 실행입니다. 매번 데이터베이스 암호를 입력하며 첫 실행은 Notebook, 두 번째 실행은 작은따옴표가 포함된 O'Reilly를 삽입하고 같은 이름의 행을 조회합니다. 새 테이블에서 한 번씩 실행하면 각각 그 이름의 한 행이 출력됩니다. 자동 증가 ID는 기존 데이터나 롤백 이력에 따라 달라질 수 있으므로 숫자 자체를 고정된 정답으로 삼지 않습니다.

같은 이름으로 다시 실행하면 새 행을 하나 더 저장하고 그 이름에 맞는 모든 행을 반환합니다. UNIQUE 제약으로 이름의 유일성을 강제한 예제가 아니기 때문입니다. 업무에서 중복을 막아야 한다면 중복의 기준과 데이터베이스 제약을 함께 설계해야 합니다.

값 바인딩과 문자열 조합의 차이 이해하기

핵심은 cur.execute(sql, (name,))처럼 SQL과 값의 튜플을 분리하는 것입니다. 원소가 하나인 튜플은 쉼표가 필요합니다. SQL 안의 %s에 따옴표를 직접 붙이거나 Python의 % 연산, f-string, 문자열 덧셈으로 사용자 값을 끼워 넣지 않습니다. PyMySQL 공식 예제도 SQL과 인수를 분리하여 전달합니다.

예를 들어 x' OR 1=1 -- 라는 이름을 함수에 전달해도 그 전체를 이름 값으로 저장하고 같은 문자열만 비교해야 합니다. 여기서 말하는 매개변수 쿼리는 드라이버 API 사용 방식입니다. 모든 드라이버가 반드시 서버의 prepared statement 프로토콜을 사용한다는 뜻은 아니며, 이 예제의 안전성을 설명하기 위해 그렇게 표현하지 않습니다.

열 이름과 ASC·DESC 같은 구문 요소는 일반 값 매개변수로 대체하지 않습니다. 예제의 ORDER BY는 사용자가 보낸 문자열을 직접 넣는 대신 개발자가 정의한 columns 사전에서만 선택합니다. 명령줄의 choices 검사에 더해 함수 안에서도 검증하여 다른 호출 경로에서 함수가 사용되어도 허용 목록을 벗어나지 않게 했습니다. Cursor.execute의 인수 규칙을 참고하세요.

확정·취소·권한을 함께 확인하기

autocommit=False이므로 함수가 성공했다고 자동 저장되는 것은 아닙니다. main은 삽입과 조회가 성공한 뒤 commit하고, 도중에 예외가 나면 rollback합니다. finally에서 연결을 닫아 성공·실패 어느 경로에서도 자원이 남지 않게 했습니다. 다른 연결에서 저장 여부를 확인할 때는 확정 시점과 트랜잭션의 경계를 구분하세요.

실습 계정에는 SELECT와 INSERT만 주었습니다. 프로그램이 테이블을 만들거나 삭제하지 않으므로 CREATE·DROP을 줄 필요가 없습니다. 계정 분리는 SQL 인젝션 예방을 대체하지 않지만, 입력 처리나 프로그램에 문제가 생겼을 때 가능한 작업 범위를 제한합니다. PyMySQL 연결 옵션에서 autocommit과 시간 제한을 확인할 수 있습니다.

자주 발생하는 오류와 실습 정리

증상 확인 방법
ModuleNotFoundError 설치에 쓴 .venv의 Python으로 실행했는지 확인
Access denied 계정·암호·호스트 부분과 서버가 보는 클라이언트 주소 확인
Connection refused 또는 시간 초과 서버 실행 여부, DB_HOST·DB_PORT, 방화벽과 연결 경로 확인
처리 후 데이터가 없음 예외로 rollback되었는지, 올바른 서버·스키마에서 commit된 결과를 조회했는지 확인
문자열에 따옴표가 있을 때만 오류 사용자 값을 SQL에 직접 조합하지 않았는지 execute 호출 확인

연결 대상이 기본값과 다르면 PowerShell에서 $env:DB_PORT = "3307"처럼 비밀이 아닌 연결 설정을 지정할 수 있습니다. 암호는 계속 입력창을 사용합니다. 사용을 마친 환경 변수는 Remove-Item Env:DB_PORT로 제거할 수 있습니다.

정리가 필요할 때는 프로그램을 종료하고 관리자가 이 실습 전용 계정과 테이블임을 확인한 뒤 아래 문장을 실행합니다. 업무 계정을 같은 이름으로 바꿔 삭제하지 마세요.

DROP USER 'appops_app'@'127.0.0.1';
DROP TABLE appops_lab.app_notes;
같은 카테고리의 다른 글
MariaDB 강좌 / 윈도 함수로 순번과 순위 계산하기

MariaDB 강좌 / 윈도 함수로 순번과 순위 계산하기

고객별 결제 금액을 집계한 뒤 ROW_NUMBER·RANK·DENSE_RANK의 동점 처리를 비교합니다. 윈도 정렬과 출력 정렬, 고객 행 단위와 바깥 조회의 순위 필터를 설명합니다.

MariaDB / 피벗 테이블 만드는 방법

MariaDB / 피벗 테이블 만드는 방법

피벗 테이블은 엑셀의 유용한 기능 중의 하나입니다. 예를 들어 아래의 왼쪽 데이터를 오른쪽 표처럼 만드는 것이 피벗 테이블입니다. 조건에 따라 그룹화시켜서 보기 좋게 데이터를 정리하는 것이라 보면 됩니다. MariaDB에서도 몇 가지 함수를 이용하여 피벗 테이블을 만들 수 있습니다.

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

MariaDB 강좌 / CASE로 조건별 값과 집계 만들기

CASE 조건의 순서와 ELSE를 상품 재고 상태로 실습합니다. SUM·COUNT 안의 조건부 집계, 0과 NULL의 차이, WHERE로 그룹을 없애는 경우와 0을 표시하는 경우를 설명합니다.

MariaDB 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

MariaDB 강좌 / 두 연결로 격리 수준·잠금·교착 상태 확인하기

별도 연결 두 개로 REPEATABLE READ·READ COMMITTED, 행 잠금 대기와 교착 상태를 비교합니다. 실행 순서, 예상 읽기 값, 실패 후 전체 트랜잭션 정리와 연결별 설정 복원을 안내합니다.

MariaDB 강좌 / UPDATE·DELETE를 실행하기 전 대상 행 확인하기

MariaDB 강좌 / UPDATE·DELETE를 실행하기 전 대상 행 확인하기

UPDATE·DELETE 전에 기본키와 현재값으로 대상 행을 확인하고, InnoDB 트랜잭션에서 수정·삭제 결과를 검증한 뒤 취소합니다. 영향받은 행 수와 ROLLBACK의 적용 한계도 설명합니다.

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

ALTER TABLE을 새 코드 열 추가·기존 행 채우기·제약 적용으로 나누어 실습합니다. NULL·중복 사전 검사, MODIFY 속성 보존, 실제 실패 입력과 잠금·복구 계획을 설명합니다.

MariaDB 강좌 / 숫자 계산과 타입 변환: ROUND·CAST

MariaDB 강좌 / 숫자 계산과 타입 변환: ROUND·CAST

ROUND와 CAST를 할인 금액·정확한 소수·숫자 문자열로 실습합니다. 반올림 시점, 앞자리 0 손실, 잘림 경고와 SQL 모드에 따른 저장 동작의 차이를 설명합니다.

MariaDB 강좌 / 조회 종합 실습: 월별 매출·고객별 구매·미판매 상품

MariaDB 강좌 / 조회 종합 실습: 월별 매출·고객별 구매·미판매 상품

월별 paid 매출, 미구매 포함 고객별 구매 금액, 미판매 상품 조회를 종합 실습합니다. 주문·상세 단위와 취소 정책을 정하고 세 결과의 전체 금액을 교차 검증합니다.

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

MariaDB 강좌 / NULL을 올바르게 비교하기: IS NULL·COALESCE

NULL·빈 문자열·0을 구분하고 IS NULL·IS NOT NULL과 COALESCE의 역할을 실습합니다. 일반 비교에서 빠지는 NULL 행, NULL-safe 비교와 NOT IN 목록의 함정을 설명합니다.

MariaDB 강좌 / 숫자 타입 선택하기: 정수·DECIMAL·부동소수점

MariaDB 강좌 / 숫자 타입 선택하기: 정수·DECIMAL·부동소수점

정수의 부호·범위, DECIMAL의 정밀도·반올림, FLOAT·DOUBLE의 근삿값 차이를 비교합니다. 같은 입력을 저장해 비교하고 범위 초과 오류와 경고를 확인하는 실습을 제공합니다.