# MySQL

MySQL General Query Log로 모든 쿼리를 남기면 어떻게 될까?

@Hong2026-09-29

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
MySQL에서 General Query Log로 모든 쿼리를 남기면 무엇이 기록되고 무엇이 문제인가?
답
General log는 클라이언트가 스스로 보낸 쿼리까지 연결 ID별로 남기지만, 같은 조회 100개를 요청마다 새 연결로 보내면 407줄이 쌓이고 개인정보도 그대로 남는다.
조건
2026-09-29, MySQL 8.4.11(mysql:8.4 이미지) 기본 설정, 호스트의 mysql 클라이언트 8.4

환경

측정일
2026년 9월 29일
MySQL
8.4.11(mysql:8.4 이미지), 기본 설정
클라이언트
호스트에 Homebrew로 설치한 mysql-client@8.4의 mysql
실행
Docker Compose, 호스트 포트 3341
목차
  1. 실습 준비
  2. 실습 1: 켜고 첫 기록 읽기
  3. 실습 2: 민감한 값이 그대로 남습니다
  4. 실습 3: 연결이 잦을수록 로그가 빨리 늘어납니다
  5. 정리

이번 시간에는 MySQL의 General Query Log를 직접 켜 보면서, 켜고 끄는 방법과 기록을 읽는 법, 그리고 운영 서버에서 계속 켜 두면 안 되는 이유를 확인하며 배워가는 시간입니다.

애플리케이션이 보냈다고 생각하는 SQL과 MySQL이 실제로 받은 SQL이 다를 때가 있어요. ORM이 쿼리를 더 만들었을 수도 있고, 재시도 로직이 같은 문장을 한 번 더 보냈을 수도 있죠.

이럴 때 서버 쪽에서 요청을 그대로 받아 적는 로그가 General Query Log(이하 General log)예요.

실습 준비

이 글의 명령은 모두 실습 코드의 mysql-general-query-log/ 디렉터리에서 실행합니다.

  • compose.yaml: MySQL 8.4 컨테이너
  • setup.sql: 주문·회원 테이블과 애플리케이션 계정 app을 만드는 SQL

먼저 Docker로 MySQL 8.4를 띄울게요. compose.yaml은 이렇게 생겼어요.

services:
  mysql:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: root
    ports:
      - "3341:3306"

setup.sql은 주문 테이블 shop.orders(id, user_id, amount, 주문 2건)와 회원 테이블 shop.members(id, name, rrn, phone, 빈 테이블)를 만들어요. 그리고 애플리케이션이 쓸 app 계정(비밀번호 app1234, shop.*에 SELECT·INSERT·UPDATE 권한)도 함께 만듭니다.

다음으로 명령을 짧게 쓸 수 있도록 셸 함수 세 개를 만들어 놓을게요.

sql은 컨테이너 안에서 root로 접속하는 함수예요. app은 호스트에서 TCP로 접속해 애플리케이션 역할을 한다고 보시면 됩니다. 그래서 호스트에는 Homebrew로 설치한 mysql-client@8.4의 mysql이 PATH에 있어야 해요. glog는 General log 파일의 마지막 몇 줄을 보여 줍니다.

MYSQL_PWD 환경 변수로 비밀번호를 넘기는 방식은 실습에서만 써 주세요. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

docker compose up -d

sql() { docker compose exec -T -e MYSQL_PWD=root mysql mysql -uroot "$@"; }

app() { MYSQL_PWD=app1234 mysql -h127.0.0.1 -P3341 -uapp "$@"; }

glog() { docker compose exec mysql tail -n "$1" /var/lib/mysql/general.log; }

until sql -e "select 1" >/dev/null 2>&1; do sleep 2; done
sql < setup.sql

실습 1: 켜고 첫 기록 읽기

General log를 켜고 끄는 스위치는 general_log이고, 기본값은 OFF입니다. 서버를 다시 시작하지 않고 바꿀 수 있어요.

기록할 파일 경로(general_log_file)를 알아보기 쉽게 바꾸고 General log를 켠 다음, 애플리케이션 계정으로 주문을 하나 조회해 볼게요.

sql -e "SET GLOBAL general_log_file = '/var/lib/mysql/general.log'; SET GLOBAL general_log = ON;"
app shop -e "SELECT id, amount FROM orders WHERE user_id = 1;"
+----+--------+
| id | amount |
+----+--------+
|  1 |   1000 |
+----+--------+

그다음 General log 파일을 열어 보세요.

docker compose exec mysql cat /var/lib/mysql/general.log
/usr/sbin/mysqld, Version: 8.4.11 (MySQL Community Server - GPL). started with:
Tcp port: 3306  Unix socket: /var/run/mysqld/mysqld.sock
Time                 Id Command    Argument
2026-09-29T07:38:32.742363Z	   11 Quit	
2026-09-29T07:38:33.005910Z	   12 Connect	app@192.168.65.1 on shop using SSL/TLS
2026-09-29T07:38:33.006925Z	   12 Query	select @@version_comment limit 1
2026-09-29T07:38:33.008590Z	   12 Query	SELECT id, amount FROM orders WHERE user_id = 1
2026-09-29T07:38:33.009715Z	   12 Quit	

첫 세 줄은 파일을 열 때 남기는 머리글이고, 그 아래가 실제 기록이에요. 열은 네 개입니다.

  • Time: 서버가 요청을 받은 시간이에요. UTC로 남습니다.
  • Id: 연결 ID예요. 한 연결에서 보낸 요청은 모두 같은 ID를 가집니다. 번호는 서버가 연결마다 차례로 매겨서, 따라 하시면 다른 숫자가 나올 수 있어요.
  • Command: 요청 종류예요. Connect는 연결, Query는 SQL 문장, Quit은 연결 종료입니다.
  • Argument: 누가 어디서 어떤 방식으로 접속했는지, 또는 받은 SQL 문장이 들어가요.

11번 연결의 Quit은 General log를 켠 root 세션이 끝난 기록이에요. 12번이 방금 조회한 app 연결입니다.

여기서 눈여겨볼 건 직접 입력하지 않은 select @@version_comment limit 1이 끼어 있다는 점이에요. mysql 클라이언트가 접속하자마자 서버 버전 설명을 가져오려고 보낸 쿼리입니다.

드라이버나 ORM도 이런 설정 확인용 쿼리를 알아서 보내는 경우가 많아요. 애플리케이션 코드에는 보이지 않아도 General log에는 이렇게 남습니다.

실습 2: 민감한 값이 그대로 남습니다

이번에는 계정을 하나 만들면서 회원 정보도 하나 넣어 볼게요. 주민등록번호와 전화번호는 가짜 값이에요.

sql -e "CREATE USER 'report'@'%' IDENTIFIED BY 'Rep0rt!pw'; INSERT INTO shop.members VALUES (1, 'kim', '900101-1234567', '010-1234-5678');"
glog 5
2026-09-29T07:39:34.619330Z	   31 Connect	root@localhost on  using Socket
2026-09-29T07:39:34.619572Z	   31 Query	select @@version_comment limit 1
2026-09-29T07:39:34.619927Z	   31 Query	CREATE USER 'report'@'%' IDENTIFIED BY <secret>
2026-09-29T07:39:34.629858Z	   31 Query	INSERT INTO shop.members VALUES (1, 'kim', '900101-1234567', '010-1234-5678')
2026-09-29T07:39:34.633357Z	   31 Quit	

출력을 보면 CREATE USER의 비밀번호는 <secret>으로 바뀌어 남았어요. MySQL은 CREATE USER, ALTER USER, SET PASSWORD처럼 평문 비밀번호를 받는 문장을 General log에 쓰기 전에 다시 쓰거든요.

하지만 이 보호는 비밀번호 문법에만 적용돼요. 일반 컬럼에 넣은 주민등록번호와 전화번호는 그대로 남았습니다.

그러니 회원 가입, 결제, 본인 인증처럼 개인정보를 다루는 쿼리가 오가는 서버라면, General log 파일 자체가 개인정보 파일이 된다고 보시면 됩니다.

실습 3: 연결이 잦을수록 로그가 빨리 늘어납니다

예를 들어서 우리가 같은 조회 100개를 보낸다고 해볼게요. 한 연결에서 모두 보내는 경우와, 요청마다 새로 연결하는 경우를 서로 다른 파일에 기록해서 비교해 보겠습니다.

sql -e "SET GLOBAL general_log_file = '/var/lib/mysql/pooled.log';"
for i in $(seq 1 100); do echo "SELECT amount FROM shop.orders WHERE id = 1;"; done | app > /dev/null
sql -e "SET GLOBAL general_log_file = '/var/lib/mysql/per-request.log';"
for i in $(seq 1 100); do app -e "SELECT amount FROM shop.orders WHERE id = 1;" > /dev/null; done
sql -e "SET GLOBAL general_log_file = '/var/lib/mysql/general.log';"
docker compose exec mysql sh -c "cd /var/lib/mysql && wc -l pooled.log per-request.log && grep -c 'SELECT amount' pooled.log per-request.log"
  110 pooled.log
  407 per-request.log
  517 total
pooled.log:100
per-request.log:100

출력을 보면 두 파일 모두 조회는 100개인데, 요청마다 연결한 쪽은 줄 수가 네 배 가까이 돼요.

왜 그럴까요? 실습 1에서 본 것처럼 요청마다 Connect, 클라이언트가 보내는 설정 확인 쿼리, 실제 쿼리, Quit이 한 묶음으로 남기 때문이에요.

실제 서비스라면 커넥션 풀이 연결을 재사용하죠. 하지만 드라이버가 연결을 빌려줄 때마다 상태 확인 쿼리를 보내는 설정도 흔해요. 오래 켜 두기 전에 1분만 켜서 얼마나 쌓이는지 먼저 재 보시면 안전해요.

확인이 끝나면 바로 꺼 주세요.

sql -e "SET GLOBAL general_log = OFF;"
sql -t -e "SHOW VARIABLES LIKE 'general_log';"
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| general_log   | OFF   |
+---------------+-------+

SET GLOBAL로 켠 설정은 서버를 다시 시작하면 기본값인 OFF로 돌아가요. 반면 SET PERSIST로 켜면 재시작 뒤에도 계속 켜진 채로 남습니다. 그러니 조사용으로는 SET GLOBAL을 써 주세요.

정리

범위: MySQL 8.4.11(mysql:8.4 이미지) 기본 설정, 호스트의 mysql 클라이언트 8.4.

  • 조회 하나를 보낸 app 연결에는 조회 앞에 클라이언트가 스스로 보낸 select @@version_comment limit 1이 같은 연결 ID로 남았어요. 애플리케이션 코드에 없는 쿼리를 찾을 때는 연결 ID별로 묶어 읽어 보세요.
  • CREATE USER의 비밀번호는 <secret>으로 가려졌지만, 일반 컬럼에 넣은 주민등록번호와 전화번호는 그대로 남았습니다. 로그 파일은 개인정보 자료로 관리해 주세요.
  • 같은 조회 100개가 한 연결에서는 110줄, 요청마다 새로 연결하면 407줄을 남겼습니다. 오래 켜 두기 전에 짧게 켜서 쌓이는 양부터 재 보세요.
  • 이 글에 싣지 않은 추가 측정에서는 잠금을 기다린 UPDATE가 뒤에 보낸 SELECT보다 먼저 기록됐지만 약 2.7초 늦게 끝났습니다. General log의 줄 순서로 실행 순서를 판단하지 마세요.
내 링크 만들기