DB 를 MySQL 5.7 에서 8.0 으로 올리게 됐다. 대상은 같은 DB 를 쓰는 서비스 4개.
업그레이드 준비라고 하면 보통 “8.0 에서 에러 나는 쿼리 찾기”를 떠올린다. 해 보니 그건 쉬운 쪽이었다. 에러는 테스트하면 바로 보인다. 무서운 건 에러 없이 결과만 달라지는 쿼리였다.
먼저 범위를 잡았다
| 대상 | 개수 |
|---|---|
| MyBatis 매퍼 파일 | 109 |
| JPA 어노테이션 (엔티티 · 네이티브 쿼리) | 374 |
| DDL 파일 | 16 |
| DB 서버 변수 실측 | 11종 |
쿼리는 정적 스캔으로 훑고, 서버 변수는 실제 DEV 서버에서 값을 뽑았다. 둘 다 해야 했다. 코드만 보면 서버가 어떤 sql_mode 로 돌고 있는지를 모른다.
1. 바로 터지는 것
GROUP BY … DESC
5.7 까지는 GROUP BY 에 ASC / DESC 를 붙일 수 있었다. 8.0 에서는 문법 에러다.
-- 5.7: 됨 / 8.0: 문법 에러
SELECT ym, COUNT(*) FROM monthly_stat GROUP BY ym DESC;
-- 고친 것
SELECT ym, COUNT(*) FROM monthly_stat GROUP BY ym ORDER BY ym DESC;
서버 설정 세 가지
sql_mode에서ONLY_FULL_GROUP_BY를 뺀 커스텀 설정 — 8.0 기본값으로 올라가면 SELECT 에 집계 안 된 컬럼이 있는 쿼리가ERROR 1055로 깨진다.NO_AUTO_CREATE_USER— 8.0 에서 없어진 모드다. 설정 파일에 남아 있으면 서버가 아예 안 뜬다.lower_case_table_names— 8.0 은 데이터 디렉터리를 처음 만들 때만 정할 수 있다. 나중에 바꿀 수 없다.
설정은 DB 담당자에게 이관안 5종으로 정리해 넘겼다. 코드에서 할 일은 ONLY_FULL_GROUP_BY 에 걸리는 쿼리를 미리 고치는 것이었다.
2. 조용히 틀어지는 것 — 진짜 문제
암묵적 정렬이 사라진다
5.7 은 GROUP BY 결과를 그룹 키 순서로 정렬해서 돌려줬다. 8.0 은 안 한다.
SELECT DATE_FORMAT(created_at, '%Y-%m') AS ym, COUNT(*)
FROM study_log
GROUP BY ym;
-- 5.7: 2026-01, 2026-02, 2026-03 … 순서로 나옴
-- 8.0: 순서 보장 없음
에러는 안 난다. 테스트도 통과한다. 그런데 월별 차트의 막대 순서가 뒤섞인다. ORDER BY 가 빠진 쿼리 2곳을 찾아 넣었다.
정렬 없는 페이징
SELECT * FROM content WHERE subject = ? LIMIT 20 OFFSET 40;
ORDER BY 없는 LIMIT 은 어느 버전에서든 순서를 보장하지 않는다. 지금까지는 실행 계획이 우연히 같아서 같은 순서가 나왔을 뿐이다. 엔진이 바뀌면 같은 행이 두 페이지에 나오거나, 어떤 행은 영영 안 나올 수 있다. 3곳에 정렬 기준(PK 포함)을 넣었다.
덤: 인덱스를 못 타던 날짜 조건
-- 문자열 컬럼을 함수로 감싸서 인덱스를 못 탐
WHERE DATE(plan_date) = '2026-09-22'
-- 범위 조건으로
WHERE plan_date >= '2026-09-22' AND plan_date < '2026-09-23'
EXPLAIN 기준 index → range, 스캔 행이 21.3만 → 10.6만 으로 절반이 됐다.
검증: 8.0 을 기다리지 않고 8.0 처럼 돌려 보기
5.7 세션에서 모드만 8.0 기본값처럼 켜고 돌렸다.
SET SESSION sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';
- 고치기 전:
ERROR 1055 - 고친 뒤: 통과
정적 스캔을 믿으면 안 되는 순간
스캔 도구가 “최고 위험” 으로 표시한 곳이 4개 있었다. 쿼리 안에서 사용자 변수(@var := …)에 값을 넣고 같은 쿼리에서 다시 읽는 패턴인데, 8.0 에서 동작이 달라질 수 있다는 경고였다.
바로 고치지 않고 세 가지를 확인했다.
- 데이터 — 그 변수가 실제로 결과를 바꾸는 데이터가 있는가
- 호출 로그 — 그 쿼리가 지금도 불리는가
- 소비 측 — 결과를 받는 쪽이 그 값을 쓰는가
네 곳 모두 수정할 필요가 없었다. 반대로, “하드코딩 값”이라고 잡힌 곳을 스캔 결과대로 고쳤다면 어떤 연령의 콘텐츠가 144건에서 2건으로 줄어 버릴 뻔한 오탐도 있었다. 실제 데이터로 돌려 보고 나서야 알았다.
덤 2: 몇 달째 조용히 실패하던 UPDATE
점검하다가 월간 리포트를 갱신하는 UPDATE 하나가 7개월째 실패하고 있던 걸 발견했다. 없는 컬럼을 참조하고 있었다. 실패해도 화면에 바로 티가 나지 않는 작업이라 오래 묻혀 있었다.
같은 유형이 더 있는지 보려고 네이티브 SQL 에서 참조하는 (테이블, 컬럼) 175쌍을 information_schema.COLUMNS 와 대조했다. 0건이었다.
SELECT TABLE_NAME, COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND (TABLE_NAME, COLUMN_NAME) IN ( … );
배운 것
- 업그레이드 준비의 핵심은 “에러 나는 쿼리”가 아니라 “결과가 바뀌는 쿼리”다. 에러는 테스트가 잡아 주지만, 순서가 바뀐 차트는 사용자가 잡는다.
- 5.7 의 GROUP BY 정렬, ORDER BY 없는 LIMIT — 지금까지 “우연히” 맞던 것들이 8.0 에서 깨진다.
- 서버 설정은 코드가 아니라 서버에서 실측하자.
NO_AUTO_CREATE_USER하나로 DB 가 안 뜰 수 있다. - 정적 스캔 결과는 후보 목록이지 할 일 목록이 아니다. 데이터 · 호출 · 소비 측으로 확인하고 고쳐야 오탐 수정이 장애가 되지 않는다.