
0. 학습 목표
→ 2강 연결 코드에 데이터베이스를 새로 만드는 기능을 버튼 하나로 추가합니다.
0.1 이번 글에서 다룰 내용
이번 글은 실습 중심 강의입니다.
2강에서 만든 연결 코드는 그대로 두고,
버튼 하나와 메서드 하나만 더해 CREATE DATABASE를 실행합니다.
기능을 추가하는 과정에서,
왜 데이터 값은 %s로 안전하게 넘길 수 있는데 데이터베이스 이름 같은 식별자는 그럴 수 없는지를 함께 짚습니다.
이 구분은 5강의 SQL Injection 방지와 곧장 이어집니다.
| 구분 | 내용 |
| 이해할 것 | DB 생성 시에는 database를 지정하지 않고 연결하는 이유, 식별자에 %s를 못 쓰는 이유 |
| 만들 것 | 입력한 이름으로 데이터베이스를 생성하는 버튼과 create_database 메서드 |
0.2 이번 강의 실습 내용
이번 강의에서 직접 해보는 것은 다음과 같습니다.
- 데이터베이스 생성 버튼을 창에 추가합니다.
create_database메서드로CREATE DATABASE를 실행합니다.- 데이터베이스 이름에 간단한 검증을 넣어 봅니다.
- 같은 이름으로 두 번 눌러 오류 메시지를 확인합니다.
1. 무엇을 추가하는가?
→ 2강 코드에서 이번에 바뀌는 부분만 먼저 확인합니다.
2강의 창 구조는 그대로 유지합니다.
이번에 더하는 것은
버튼 하나와
메서드 하나,
그리고 이 둘을 잇는 시그널·슬롯 한 줄뿐입니다.
실제 diff 코드를 보기 전에, 이번에 손대는 자리를 먼저 지도로 확인합니다.
class MySqlDemoWindow(QWidget):
def __init__(self):
... 입력 필드 (2강) ...
... 버튼 생성 ← ① 새 버튼 만들기
... main_layout 배치 ← ② addWidget 으로 버튼 넣기
... 시그널 연결 ← ③ clicked.connect 연결
def connect_to_server_or_db(self): # 2강 메서드 (그대로)
...
# ④ create_database 메서드를 여기(클래스 안)에 새로 추가
①~③은 __init__ 안에서 동시에 바뀌는 자리이고,
④는 클래스 안에 새로 추가되는 메서드입니다. 아래는 ①~③의 실제 코드입니다.
# ── ① 버튼 생성 부분 ──
self.btn_connect = QPushButton("연결 테스트") # (2강)
self.btn_create_db = QPushButton("데이터베이스 생성") # 추가
self.result_label = QLabel("연결 결과가 여기에 표시됩니다.") # (2강)
# ── ② main_layout 배치 부분 ──
main_layout = QVBoxLayout(self) # (2강)
main_layout.addLayout(form_layout) # (2강)
main_layout.addWidget(self.btn_connect) # (2강)
main_layout.addWidget(self.btn_create_db) # 추가 (연결 버튼 바로 아래)
main_layout.addWidget(self.result_label) # (2강)
# ── ③ 시그널 연결 부분 ──
self.btn_connect.clicked.connect(self.connect_to_server_or_db) # (2강)
self.btn_create_db.clicked.connect(self.create_database) # 추가
이렇게 이전 기능은 손대지 않고 새 기능만 얹는 방식으로 구현합니다.
같은 입력 필드(host·user·password·db)를 연결 테스트와 데이터베이스 생성이 함께 사용합니다.
2. DB 생성 흐름 이해하기
→ 왜 생성할 때는 database를 빼고 연결하는지, 어떤 순서로 실행되는지 정리합니다.
2.1 생성할 때는 database를 지정하지 않는다
2강 연결에서는 database를 지정할 수 있었습니다. 그런데 데이터베이스를 새로 만드는 시점에는 그 DB가 아직 존재하지 않습니다. 아직 없는 DB를 database로 지정해 연결하면 그 단계에서 실패합니다. 그래서 생성 시에는 서버까지만 연결하고, 그 서버 위에서 CREATE DATABASE를 실행합니다.
conn = mc.connect(host, user, password) # database 지정 없이 서버에만 연결
cursor.execute("CREATE DATABASE 이름") # 서버 위에 새 DB 생성
MySQL 클라이언트에서 CREATE DATABASE 뒤에 USE 이름으로 그 DB에 들어가는 것과 같은 맥락입니다. 만들기 전에는 들어갈 수 없으니, 만들 때만 서버 수준에서 실행하는 것입니다.
2.2 생성 흐름 4단계
2강의 연결 → 확인 → 종료 사이에 SQL 실행 단계가 하나 들어갑니다.
데이터 구조를 바꾸는 작업이므로 commit()이 필요하다는 점이 새롭습니다.
# 1. 연결 (database 없이 서버에만)
conn = mc.connect(host, user, password)
# 2. 확인
if conn.is_connected():
성공_처리()
# 3. SQL 실행
cursor = conn.cursor()
cursor.execute("CREATE DATABASE 이름")
conn.commit() # DB 구조 변경이므로 반드시 커밋
# 4. 종료
conn.close()
cursor는 SQL을 실행하는 도구입니다. 연결(conn)에서 커서를 만들고, 커서로 SQL을 실행한 뒤, 변경 사항을 commit()으로 확정하는 순서를 기억해 둡니다.
3. 식별자에는 왜 %s를 못 쓸까
→ 데이터 값과 식별자의 차이를 이해하고, DB 이름을 검증하는 방법을 정리합니다.
3.1 값과 식별자는 다르다
5강에서 배울 파라미터 바인딩(%s)은 데이터 값을 안전하게 넘기는 방법입니다.
예를 들어 사용자 이름이나 비밀번호는 값이므로 %s로 넘길 수 있습니다.
그런데 데이터베이스 이름, 테이블 이름, 컬럼 이름 같은 식별자는 값이 아니라 SQL 문장의 구조에 해당합니다.
| 구분 | 데이터 값 | 식별자 |
| 예 | 사용자 이름, 비밀번호, 검색어 | DB 이름, 테이블 이름, 컬럼 이름 |
%s 사용 |
가능 (권장) | 불가능 |
| 넘기는 방법 | 파라미터 바인딩으로 전달 | 문자열로 직접 조합 + 검증 필요 |
그래서 CREATE DATABASE %s는 동작하지 않습니다.
식별자는 %s로 넘길 수 없으므로 문자열로 직접 넣어야 하는데,
사용자 입력을 그대로 문자열에 넣으면 위험해집니다. 검증이 필요한 이유가 여기에 있습니다.
3.2 DB 이름 검증하기
식별자는 바인딩할 수 없으니, 대신 허용할 형태만 통과시키는 검증으로 방어합니다.
가장 간단한 방법은 파이썬의 isidentifier()로 영문·숫자·밑줄로만 이루어진 이름인지 확인하는 것입니다.
if not db_name.isidentifier():
QMessageBox.warning(self, "입력 오류", "데이터베이스 이름은 영문·숫자·밑줄만 사용할 수 있습니다.")
return
이렇게 하면 이름에 공백이나 특수문자, SQL 문장을 섞으려는 입력을 걸러낼 수 있습니다.
검증을 통과한 이름만 문자열에 넣어 CREATE DATABASE를 실행합니다.
✔ 참고: "값은 %s로 바인딩하고, 식별자는 검증한다"는 규칙은 이 시리즈 전체를 관통합니다. 5강에서 데이터 값에 %s를 쓰는 이유가 바로 이 규칙의 반대쪽 절반입니다.
4. 생성 기능 구현하기
→ create_database 메서드를 작성해 실제 생성 기능을 완성합니다.
클래스 안에 create_database 메서드를 추가합니다.
위치 지도의 ④번 자리, 즉 connect_to_server_or_db 메서드 아래입니다.
2강 연결 메서드와 같은 conn = None + try-except-finally 뼈대를 그대로 쓰되,
그 안에 커서 생성과 CREATE DATABASE 실행, 커밋이 들어갑니다.

# ④ connect_to_server_or_db 메서드 아래에 새로 추가
def create_database(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
# 데이터베이스를 만들려면 이름이 반드시 필요하다.
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "호스트, 사용자, 데이터베이스 이름을 모두 입력해야 합니다.")
return
# 식별자는 %s로 바인딩할 수 없으므로, 허용 문자만 통과시키는 검증으로 방어한다.
if not db_name.isidentifier():
QMessageBox.warning(self, "입력 오류", "데이터베이스 이름은 영문·숫자·밑줄만 사용할 수 있습니다.")
return
conn = None
try:
# 아직 db_name이 없을 수 있으므로 database 없이 서버에만 연결한다.
conn = mc.connect(host=host, user=user, password=password)
cursor = conn.cursor()
# 검증을 통과한 이름만 문자열로 넣어 실행한다.
cursor.execute(f"CREATE DATABASE {db_name}")
conn.commit()
msg = f"{db_name} 데이터베이스가 생성되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "생성 완료", msg)
except mc.Error as e:
# 이미 같은 이름의 DB가 있거나 권한이 없을 때 예외가 발생한다.
self.result_label.setText("데이터베이스 생성에 실패했습니다.")
QMessageBox.critical(self, "생성 오류", f"데이터베이스 생성에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
입력 검증이 두 단계인 점에 주목합니다. 먼저 비어 있는지를 보고, 그다음 형태가 올바른지(isidentifier())를 봅니다. 두 검증을 모두 통과한 이름만 SQL에 들어갑니다.
▶ 지금 해보세요
- 1에서 본 ①~③ 위치에 버튼과 시그널·슬롯 한 줄을 __init__에 추가합니다.
- ④ 위치에
create_database메서드를 작성합니다. - 연결 메서드와 뼈대(
conn = None+try-except-finally)가 같은지 비교해 봅니다.
5. 실행하고 결과 확인하기
→ 데이터베이스를 만들고, 같은 이름을 다시 만들 때의 오류까지 확인합니다.
main.py를 실행하고 데이터베이스 이름에 pyqtdb를 입력한 뒤 데이터베이스 생성 버튼을 누릅니다.
성공하면 생성 완료 메시지가 뜹니다. 이 pyqtdb는 이후 강의에서 계속 사용하는 실습용 데이터베이스입니다.

같은 이름으로 버튼을 한 번 더 누르면 이번에는 오류 팝업이 뜹니다.
이미 존재하는 데이터베이스를 다시 만들 수 없기 때문입니다.

이건 코드가 틀린 게 아니라 정상적인 예외 상황이며,
2강에서 만든 예외 처리 구조가 프로그램을 멈추지 않고 오류를 안내하는 모습을 직접 확인할 수 있습니다.
💡 팁: 이미 있으면 오류 대신 조용히 넘어가게 하려면 CREATE DATABASE IF NOT EXISTS {db_name}를 쓸 수 있습니다. 다만 이번 강의에서는 예외 처리가 동작하는 모습을 보기 위해 IF NOT EXISTS 없이 진행합니다.
▶ 지금 해보세요
- 데이터베이스 이름에
pyqtdb를 입력하고 생성 버튼을 눌러 성공 메시지를 확인합니다. - 같은 이름으로 다시 눌러 오류 팝업이 뜨는지 확인합니다.
- 이름에 공백이나 특수문자(예:
my db!)를 넣어, 검증 경고가 먼저 뜨는지 확인합니다. - MySQL 클라이언트에서
SHOW DATABASES;로pyqtdb가 목록에 있는지 확인합니다.
6. 이번 강의 완성본
→ 2~3강까지 이어 붙인 전체 코드를 한 번에 확인합니다.
본문에서는 변경분만 다뤘습니다.
아래는 위치를 대조하거나 코드가 어긋났을 때 기준으로 삼을 mainwindow.py 전체입니다.
main.py는 2강에서 작성한 것과 동일하므로 여기서는 mainwindow.py만 싣습니다.
import sys
from PySide6.QtWidgets import (
QApplication, QWidget, QLabel, QLineEdit,
QPushButton, QVBoxLayout, QFormLayout, QMessageBox,
)
import mysql.connector as mc
class MySqlDemoWindow(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("MySQL 연결 설정")
self.resize(420, 280)
# 접속 정보 입력 필드
self.host_edit = QLineEdit()
self.user_edit = QLineEdit()
self.password_edit = QLineEdit()
self.password_edit.setEchoMode(QLineEdit.Password)
self.db_edit = QLineEdit()
self.host_edit.setText("localhost")
self.user_edit.setText("root")
form_layout = QFormLayout()
form_layout.addRow("호스트", self.host_edit)
form_layout.addRow("사용자", self.user_edit)
form_layout.addRow("비밀번호", self.password_edit)
form_layout.addRow("데이터베이스 이름", self.db_edit)
self.btn_connect = QPushButton("연결 테스트")
self.btn_create_db = QPushButton("데이터베이스 생성")
self.result_label = QLabel("연결 결과가 여기에 표시됩니다.")
main_layout = QVBoxLayout(self)
main_layout.addLayout(form_layout)
main_layout.addWidget(self.btn_connect)
main_layout.addWidget(self.btn_create_db)
main_layout.addWidget(self.result_label)
self.btn_connect.clicked.connect(self.connect_to_server_or_db)
self.btn_create_db.clicked.connect(self.create_database)
def connect_to_server_or_db(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
if not host or not user:
QMessageBox.warning(self, "입력 오류", "호스트와 사용자 이름은 반드시 입력해야 합니다.")
return
conn = None
try:
if db_name:
conn = mc.connect(host=host, user=user, password=password, database=db_name)
else:
conn = mc.connect(host=host, user=user, password=password)
if conn.is_connected():
if db_name:
msg = f"MySQL 서버에 연결되었습니다.\n사용 중인 데이터베이스: {db_name}"
else:
msg = "MySQL 서버에 연결되었습니다.\n데이터베이스 이름은 지정되지 않았습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "연결 성공", msg)
except mc.Error as e:
self.result_label.setText("연결 중 오류가 발생했습니다.")
QMessageBox.critical(self, "연결 오류", f"MySQL 연결에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
def create_database(self):
host = self.host_edit.text().strip()
user = self.user_edit.text().strip()
password = self.password_edit.text()
db_name = self.db_edit.text().strip()
if not host or not user or not db_name:
QMessageBox.warning(self, "입력 오류", "호스트, 사용자, 데이터베이스 이름을 모두 입력해야 합니다.")
return
if not db_name.isidentifier():
QMessageBox.warning(self, "입력 오류", "데이터베이스 이름은 영문·숫자·밑줄만 사용할 수 있습니다.")
return
conn = None
try:
conn = mc.connect(host=host, user=user, password=password)
cursor = conn.cursor()
cursor.execute(f"CREATE DATABASE {db_name}")
conn.commit()
msg = f"{db_name} 데이터베이스가 생성되었습니다."
self.result_label.setText(msg)
QMessageBox.information(self, "생성 완료", msg)
except mc.Error as e:
self.result_label.setText("데이터베이스 생성에 실패했습니다.")
QMessageBox.critical(self, "생성 오류", f"데이터베이스 생성에 실패했습니다.\n오류 내용: {e}")
finally:
if conn and conn.is_connected():
conn.close()
→ 다음 강의 (4강): 방금 만든 pyqtdb 안에 users 테이블을 만듭니다. 이번에는 database를 지정해 연결하고, 동작 기록을 파일로 남기는 로그 기능도 함께 붙입니다.