본문 바로가기
데이터베이스

Flyway MySQL 프로시저 syntax error — SQL이 아니라 파서가 문제였다

by 꼼냥냥 2026. 9. 9.
728x90

Flyway로 MySQL 저장 프로시저를 넣었더니 syntax error near '' at line 80으로 배포가 멈췄다. 80번 줄은 멀쩡했고, 그 줄만 떼어 DB에서 직접 실행하면 잘 만들어졌다. 원인은 SQL이 아니라 Flyway 8.0의 MySQL 파서였다. END REPEAT를 프로시저의 끝으로 잘못 읽고 그 앞에서 문장을 잘라 버린다. 에러는 DB가 내지만 원인은 파서에 있다. 그래서 SQL을 아무리 봐도 안 보인다.

어떤 구조에서 나나

스키마 변경을 Flyway로 관리한다. 앱이 뜰 때 미적용 마이그레이션을 순서대로 자동 실행하고, 이력은 FLYWAY_SCHEMA_HISTORY에 남는다. 별도 CLI를 돌리지 않는다.

테이블만 다룰 때는 문제가 없었다. 저장 프로시저와 이벤트를 넣으면서 겪은 것들이다.

집계용 프로시저를 R__ 파일로 넣고 배포했더니 이렇게 죽었다.

syntax error near '' at line 80

프로시저 안에 이런 구조가 있었다.

CREATE PROCEDURE SP_EXAMPLE()
BEGIN
    ...
    REPEAT
        ...
        IF ... THEN
            ...
        END IF;          -- ← 여기 세미콜론
    UNTIL done END REPEAT;
END;

파서가 END REPEAT를 프로시저의 끝으로 잘못 읽는다. 그래서 그 앞의 ;에서 문장을 끊어 버리고, 잘린 조각을 DB에 보낸다. DB는 당연히 문법 오류를 낸다.

해결 — 커서와 루프를 안 쓴다

프로시저를 평평하게 다시 썼다. REPEAT·WHILE·커서를 전부 없애고 DELETE + INSERT ... SELECT 두 문장으로 바꿨다.

CREATE PROCEDURE SP_EXAMPLE()
BEGIN
    DECLARE v_base VARCHAR(8);
    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
    SET v_base = DATE_FORMAT(ADDDATE(NOW(), INTERVAL -1 DAY), '%Y%m%d');

    DELETE FROM SUMMARY_TABLE;

    INSERT INTO SUMMARY_TABLE (...)
    SELECT ... FROM ( ... UNION ALL ... ) V
     GROUP BY ...;
END;

점포별로 끊어 커밋하려고 루프를 넣었던 것인데, 1편의 READ COMMITTED로 원본 락이 사라지니 나눌 이유 자체가 없어졌다.

지금은 파일 맨 위에 이유를 박아 뒀다.

-- 커서·REPEAT·WHILE 은 쓰지 않는다. Flyway 8.0 의 MySQL 파서가 END REPEAT 를
-- 프로시저 끝으로 잘못 읽어 생성이 깨진다.

주석이 없으면 다음 사람이 똑같이 루프를 넣는다. 파서 문제라 코드만 봐서는 알 수 없다.

실패한 이력 행을 지워야 다시 시도된다

실패하면 FLYWAY_SCHEMA_HISTORY에 SUCCESS = 0 행이 남는다. 파일을 고쳐도 그 행이 있으면 validate가 막는다. 그 행을 지우고 다시 배포해야 한다.

CREATE INDEX IF NOT EXISTS가 없다

이건 구조 검사로 안 잡히고 실제 DB에 두 번 돌려야 발견된다.

우리 규칙은 신규 테이블 묶음을 한 파일에 몰아 넣는 것이다. MySQL은 DDL 롤백이 안 되니 파일 중간에서 실패하면 부분 적용이 남는다. 그래서 모든 DDL을 멱등하게 쓴다.

CREATE TABLE IF NOT EXISTS FOO ( ... );

여기까지는 된다. 문제는 인덱스다. MySQL에는 CREATE INDEX IF NOT EXISTS가 없다. 분리해 두면 재실행 시 이렇게 죽는다.

ERROR 1061 (42000): Duplicate key name 'IDX_FOO_BAR'

멱등성이 깨진다. 한 파일에 몰아 넣은 전제가 무너진다.

그래서 신규 테이블의 인덱스는 CREATE TABLE 안에 인라인으로 넣는다.

CREATE TABLE IF NOT EXISTS USER_GRADE_TOTAL
(
    STORE_CODE VARCHAR(5)  NOT NULL,
    USER_ID     VARCHAR(45) NOT NULL,
    TOTAL_PAY   BIGINT      NOT NULL DEFAULT 0,
    PRIMARY KEY (STORE_CODE, USER_ID),
    KEY IDX_USER_GRADE_TOTAL_PAY (STORE_CODE, TOTAL_PAY)
);

기존 테이블에 인덱스를 추가하는 건 별개다. 그건 작은 마이그레이션으로 따로 뺀다.

그 밖에 걸렸던 것

함수·트리거에는 특성 선언이 필요하다

RETURNS 뒤, BEGIN 앞에 NO SQL이나 READS SQL DATA를 안 붙이면 이렇게 거부된다.

ERROR 1418: This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA
in its declaration and binary logging is enabled

바이너리 로깅이 켜진 서버에서만 나온다. 개발에서 통과하고 운영에서 터질 수 있다.

CREATE OR REPLACE가 없다

MySQL은 프로시저·함수에 CREATE OR REPLACE를 지원하지 않는다. R__ 파일은 항상 이 형태다.

DROP PROCEDURE IF EXISTS SP_EXAMPLE;
CREATE PROCEDURE SP_EXAMPLE() ...

버전 번호를 정수로 쓰지 않는다

V3, V4처럼 순번을 쓰면 브랜치가 갈릴 때 반드시 충돌한다. 두 사람이 각자 V5를 만든다. 타임스탬프로 바꿨다.

date +%Y%m%d%H%M%S      # → V20260826160001__create_user_grade_total.sql

성공한 V 파일은 절대 고치지 않는다

체크섬이 바뀌어 validate가 실패한다. 고칠 게 있으면 새 파일로 앞으로 고친다(forward-fix).

반대로 R__는 고치는 게 정상이다. 체크섬이 바뀌면 자동으로 다시 적용된다. 함수·프로시저·뷰는 새 파일을 만들지 말고 기존 R__를 수정한다.

파괴적 변경은 두 번에 나눈다

EC2를 순차 배포하므로 옛 코드가 새 스키마를 보는 구간이 반드시 생긴다. 컬럼을 지우거나 이름을 바꾸면 그 구간에서 옛 코드가 죽는다.

그래서 expand/contract로 나눈다.

  1. 추가 — 새 컬럼을 만든다. 옛 코드는 모르니 영향 없다
  2. 전 서버에 새 코드 배포
  3. 제거 — 다음 배포에서 옛 컬럼을 지운다

한 번에 하면 배포 중간에 장애가 난다.

정리

  • Flyway 8.0의 MySQL 파서는 REPEAT/WHILE/커서를 만나면 프로시저를 중간에서 자른다. 에러는 DB가 내지만 SQL은 멀쩡하다.
  • 실패하면 SUCCESS = 0 이력 행을 지워야 재시도된다.
  • CREATE INDEX IF NOT EXISTS가 없다. 신규 테이블 인덱스는 CREATE TABLE 안에 인라인으로.
  • 함수·트리거는 NO SQL / READS SQL DATA 선언이 필요하다. 바이너리 로깅이 켜진 서버에서만 터진다.
  • 버전은 타임스탬프. 정수 순번은 브랜치에서 충돌한다.
  • 성공한 V는 불변, R__는 고치는 게 정상.
  • 파괴적 변경은 expand/contract 두 단계로.

댓글