> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MariaDB에서 MySQL로 마이그레이션하기

> MariaDB 데이터베이스를 MySQL로 마이그레이션하는 방법: JSON 열, 시퀀스, INET6, 외래 키 제약 조건 이름을 포함해 실제로 로드를 깨뜨리는 스키마와 덤프의 차이입니다.

> [card] [VillageSQL is a drop-in replacement for MySQL with extensions.](https://villagesql.com/docs/en/8.4/stable/install-server.md)
>
> All examples on this page work on VillageSQL. Install Now →

MariaDB와 MySQL은 같은 와이어 프로토콜과 대부분의 같은 SQL 표면을 공유하므로, 마이그레이션은 속기 쉽게 단순해 보입니다: 덤프한 다음 로드합니다. 실제로는 소수의 MariaDB 전용 타입, 객체, 덤프 동작이 평범한 로드를 중간에 멈추게 하고, 그중 하나는 오류를 전혀 내지 않고 열의 저장 타입을 바꿉니다. 이 가이드는 MariaDB 12.3.3에서 가져와 MySQL에 로드한 실제 덤프를 바탕으로, 실제로 무엇이 깨지고 어떻게 고치는지 다룹니다.

## 데이터 타입과 기본값의 차이

| MariaDB 기능 | MySQL 상황 | 비고 |
| :- | :- | :- |
| `JSON` 열 타입 | MariaDB 쪽에서는 `CHECK (json_valid(...))` 제약 조건이 붙은 `LONGTEXT` | MariaDB의 `JSON`은 네이티브 타입이 아니라 별칭입니다. MariaDB에서 `SHOW CREATE TABLE`은 실제 열을 `longtext ... CHECK (json_valid(...))`로 보여줍니다. MySQL에는 네이티브 `JSON` 타입이 있지만, 덤프가 문자 그대로의 열 정의를 담고 있으므로 MariaDB에서 이렇게 만든 열은 MySQL에 `JSON`이 아니라 `LONGTEXT`로 로드됩니다. |
| `INET6` 열 타입 | 지원되지 않음 | MySQL과 VillageSQL에는 `INET6` 타입이 없습니다. `INET6` 열이 있는 테이블을 로드하면 바로 실패합니다. 값을 `VARCHAR(45)`로 저장하거나 INET 확장을 사용하세요. |
| `SEQUENCE` 객체와 `NEXTVAL()` | 지원되지 않음 | MariaDB의 `CREATE SEQUENCE`와 `NEXTVAL()` 열 기본값에는 MySQL 대응물이 없습니다. 대신 `AUTO_INCREMENT`를 사용하세요. |
| `INSERT ... RETURNING` | 지원되지 않음 | MariaDB는 `INSERT`에 `RETURNING`을 받아들이지만 MySQL은 그렇지 않습니다. 대신 삽입 뒤에 `SELECT LAST_INSERT_ID()`를 사용하세요. |
| 기본 콜레이션 | `utf8mb4_general_ci`(MariaDB 12.3.3) 대 `utf8mb4_0900_ai_ci`(MySQL 8.4와 VillageSQL) | 두 서버에서 `SHOW VARIABLES LIKE 'collation_server'`로 확인했습니다. `mysqldump`가 콜레이션을 `CREATE TABLE`에 명시적으로 쓰므로, 덤프된 테이블은 만들어질 때의 콜레이션을 유지합니다. 기본값은 콜레이션을 지정하지 않고 대상에서 새로 만드는 테이블에만 영향을 줍니다. |
| `SHOW CREATE TABLE`의 정수 표시 너비 | 제거됨 | MariaDB는 `int(11)`과 `bigint(20)`을 보여줍니다. MySQL 8.4와 VillageSQL은 `int`와 `bigint`만 보여줍니다. 이것은 표면적인 차이이고 저장된 데이터에 영향을 주지 않지만, MariaDB와 MySQL의 `SHOW CREATE TABLE` 사이의 diff가 테이블이 변경되었다는 증거는 아니라는 뜻입니다. |

## MariaDB 전용 스키마 객체가 로드를 깨뜨립니다

`mysqldump`는 시퀀스 정의를 테이블보다 앞에 쓰므로, `CREATE SEQUENCE`를 사용하는 스키마는 MySQL에 로드할 때 어떤 테이블이 만들어지기도 전에 즉시 실패합니다. 시퀀스가 들어 있는 수정하지 않은 덤프를 로드하면 다음이 나옵니다. `at line N` 값은 여러분의 덤프에서 문장이 있는 위치를 따르므로 여기 보이는 숫자와 일치하지 않습니다:

```
ERROR 1064 (42000) at line 23: You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to use near
'SEQUENCE IF EXISTS `invoice_seq`' at line 1
```

덤프에서 `DROP SEQUENCE` / `CREATE SEQUENCE` / `DO SETVAL(...)` 블록을 제거하고, `DEFAULT nextval(...)`로 읽히는 열 기본값을 평범한 `AUTO_INCREMENT` 열이나 애플리케이션이 할당하는 값으로 바꾸세요.

`INET6`로 타입이 지정된 열은 같은 로드의 더 뒤에서, 로더가 그것을 선언한 테이블에 도달하면 같은 방식으로 실패합니다. 이것은 이 가이드에서 어느 서버에 로드하느냐에 따라 문구가 달라지는 유일한 오류입니다. VillageSQL은 그 위치에서 사용자 정의 타입 이름을 받아들이는 문법을 가지고 있어 해석하지 못한 이름을 보고하므로, 타입 이름을 밝혀 줍니다:

```
ERROR 1064 (42000) at line 26: Expected a type or a custom type instead of "inet6"
near 'inet6 DEFAULT NULL, `created_at` timestamp NULL DEFAULT current_timestamp(), '
at line 5
```

순정 MySQL 8.4는 대신 일반적인 구문 오류로 같은 문장을 거부합니다:

```
ERROR 1064 (42000) at line 26: You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the right syntax to use
near 'inet6 DEFAULT NULL, `created_at` timestamp NULL DEFAULT current_timestamp(), '
at line 5
```

둘 다 `ERROR 1064`이고 둘 다 문제의 줄을 지정하므로, 해결 방법은 어느 쪽이든 같습니다. 로드하기 전에 열 타입을 `VARCHAR(45)`(텍스트 형식의 IPv6 주소에 충분한 길이)로 바꾸거나, 대상에 확장이 설치되어 있다면 확장이 제공하는 INET 타입으로 바꾸세요.

## 외래 키 제약 조건 이름이 테이블 사이에서 충돌합니다

MariaDB의 `mysqldump`는 이름 없는 외래 키 제약 조건에 평범한 정수로 자동으로 이름을 붙이고, 모든 테이블마다 `1`부터 다시 시작합니다. 같은 스키마의 두 테이블이 모두 문자 그대로 `1`이라는 이름의 제약 조건을 가질 수 있습니다:

```sql theme={null}
CREATE TABLE `order_items` (
  ...
  CONSTRAINT `1` FOREIGN KEY (`order_id`) REFERENCES `orders` (`id`),
  CONSTRAINT `2` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
) ENGINE=InnoDB ...;

CREATE TABLE `orders` (
  ...
  CONSTRAINT `1` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`)
) ENGINE=InnoDB ...;
```

MySQL은 외래 키 제약 조건 이름이 테이블 안에서만이 아니라 스키마 전체에서 고유할 것을 요구합니다. 두 번째 `CONSTRAINT `1\`\`을 로드하면 다음과 같이 실패합니다:

```
ERROR 1826 (HY000) at line 96: Duplicate foreign key constraint name '1'
```

이것은 원래 테이블이 MariaDB의 자동 이름 지정에 의존했을 때(`CREATE TABLE`에 `CONSTRAINT name`이 주어지지 않았을 때)만 일어납니다. 로드하기 전에 충돌하는 각 제약 조건을 고유한 이름, 예를 들어 `order_items_ibfk_1`과 `orders_ibfk_1`로 바꾸세요.

## JSON 열은 오류 없이 다른 타입으로 로드됩니다

위의 세 가지 문제와 달리, `JSON` 열은 오류도 경고도 없이 MySQL에 로드됩니다. MariaDB `JSON` 열의 덤프된 `CREATE TABLE`은 이렇게 생겼습니다:

```sql theme={null}
CREATE TABLE `products` (
  ...
  `attributes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
    DEFAULT NULL CHECK (json_valid(`attributes`))
) ENGINE=InnoDB ...;
```

MySQL은 `CHECK` 제약 조건과 `JSON_VALID()` 함수를 지원하므로, 이 문장은 쓰인 그대로 실행됩니다. 열은 MySQL 쪽에서 MySQL의 네이티브 `JSON` 타입이 아니라 `CHECK` 제약 조건이 붙은 `LONGTEXT`가 됩니다. `JSON_EXTRACT()`와 `JSON_LENGTH()`로 다시 읽으면 원본 데이터와 동일한 결과를 주므로 잃는 것은 없지만, 열의 선언된 타입은 새 MySQL 스키마가 사용할 것과 다릅니다. 대상에서 네이티브 `JSON` 타입을 원한다면, 로드한 뒤 열을 변경하세요:

```sql theme={null}
ALTER TABLE products MODIFY attributes JSON;
```

이렇게 하면 원래의 `CHECK (json_valid(...))` 제약 조건이 열에 붙은 채로 남습니다. 덤프는 그 제약 조건에 이름을 붙이지 않으므로, `CREATE TABLE`이 실행될 때 서버가 `products_chk_1` 같은 이름을 붙였고, `ALTER` 전에 이미 거기 있습니다. MySQL은 이미 타입 수준에서 잘못된 JSON을 거부하므로 네이티브 `JSON` 열에서는 무해하지만, LONGTEXT 정의에서 남은 쓸모없는 짐입니다. 깨끗한 스키마를 원한다면 제거하세요:

```sql theme={null}
ALTER TABLE products DROP CHECK products_chk_1;
```

## 인증의 차이

MariaDB와 MySQL 클라이언트는 같은 와이어 프로토콜을 사용하며, 한쪽에서 다른 쪽으로의 평범한 비밀번호 인증 연결은 동작합니다. 넘어가지 않는 것은 계정 정의 자체입니다. `mysqldump`는 기본적으로 `mysql.user` 테이블을 덤프하지 않으므로, 계정과 권한은 대상에서 직접 다시 만들어야 하고, 그렇게 하는 구문이 다릅니다.

비밀번호와 함께 만든 MariaDB 계정은 이렇게 보입니다:

```sql theme={null}
-- MariaDB
SHOW CREATE USER 'appuser'@'%';
-- CREATE USER `appuser`@`%` IDENTIFIED BY PASSWORD '*E6867A710B62E46C7948C1936692C0E9EAB160C5'
```

기본 `caching_sha2_password` 플러그인을 사용하는 MySQL이나 VillageSQL에서 같은 문장은 대신 이렇게 보입니다:

```sql theme={null}
-- MySQL / VillageSQL
SHOW CREATE USER 'appuser'@'%';
-- CREATE USER `appuser`@`%` IDENTIFIED WITH 'caching_sha2_password' AS '...'
--   REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK ...
```

MariaDB의 `SHOW CREATE USER` 문장을 MySQL에 복사하지 마세요. 대상에서 `CREATE USER ... IDENTIFIED BY '<password>'`로 계정을 새로 만들고 서버가 자체 기본 플러그인을 고르게 한 다음, `GRANT` 문을 다시 실행하세요.

방금 설정한 MariaDB 설치(예를 들어 새 Homebrew 설치)를 대상으로 마이그레이션을 테스트하고 있고, 비밀번호가 맞는데도 비밀번호가 있는 이름 있는 계정이 "Access denied"로 거부된다면, 먼저 익명 계정을 확인하세요:

```sql theme={null}
SELECT user, host FROM mysql.user WHERE user = '';
```

보안 설정이 되지 않은 MariaDB 설치는 `''@'localhost'`와 `''@'<hostname>'`을 남겨둘 수 있습니다. 호스트 일치가 사용자 이름 일치보다 더 구체적이므로, `localhost`로 해석되는 연결은 이름 있는 계정보다 먼저 익명 계정과 일치하고, 익명 계정은 비밀번호를 기대하지 않으므로 거부됩니다. 이것은 소스 쪽 MariaDB 설치의 문제이지 데이터를 따라 MySQL로 넘어오는 것은 아니지만, 익명 계정을 삭제할 때까지 소스 쪽 테스트를 혼란스럽게 만들 수 있습니다.

## 마이그레이션 접근법

1. **`mysqldump`로 스키마와 데이터를 덤프하세요**(MariaDB 호스트에서 이것은 보통 MariaDB 자체의 `mariadb-dump`에 대한 심볼릭 링크입니다. `mysqldump --version`을 실행해 어느 것인지 확인하세요).
2. **로드를 시도하기 전에 덤프에서 `CREATE SEQUENCE`, `INET6`, `RETURNING`을 검색하세요.** 셋 중 어느 것도 로드가 이미 중간까지 진행되기 전에는 유용한 오류를 내지 않습니다.
3. **먼저 스크래치 데이터베이스에 로드를 시도하세요.** `CREATE SEQUENCE`나 `INET6` 실패는 그 문장에서 전체 스크립트를 중단시키므로, 파일에서 앞서 정의된 테이블은 이미 로드되고 뒤에 정의된 테이블은 로드되지 않습니다. 반쯤 로드된 데이터베이스를 땜질하지 말고, 덤프를 고쳐서 새 데이터베이스에 다시 로드하세요.
4. **소스 스키마가 이름 없는 외래 키에 의존했다면 외래 키 제약 조건 이름 충돌을 확인하세요.** `grep -o 'CONSTRAINT \`\[0-9]\*\`' dump.sql\`이 그것들을 찾습니다.
5. **계정과 권한을 직접 다시 만드세요.** `mysqldump`는 그것들을 옮기지 않으며, `CREATE USER` / `SHOW CREATE USER` 구문은 두 서버 사이에 다릅니다.
6. **행 수를 검증하고 JSON 열을 표본 검사하세요.** `JSON` 열은 조용히 `LONGTEXT`로 로드됩니다. 네이티브 `JSON` 열로 변환할지 결정하기 전에 `JSON_EXTRACT()`로 데이터가 왕복되는지 확인하세요.

## 자주 묻는 질문

#### 같은 버전 계열의 MySQL 대상에 MariaDB `mysqldump` 파일을 수정해야 하나요?

스키마가 시퀀스, `INET6`, `RETURNING`, 이름 없는 외래 키를 사용하지 않는다면, 같은 덤프가 변경 없이 로드되는 경우가 많습니다. 스키마가 "그냥 테이블과 데이터"라는 이유로 성공할 것이라고 가정하지 말고, 먼저 스크래치 데이터베이스를 대상으로 로드를 테스트하세요.

#### 네이티브 MySQL `JSON` 열이 MariaDB 덤프를 통해 올바르게 왕복하나요?

값은 올바르게 왕복합니다. 열 타입은 그렇지 않습니다: MariaDB는 `JSON` 열을 `CHECK` 제약 조건이 붙은 `LONGTEXT`로 덤프하고, 나중에 명시적으로 `ALTER TABLE ... MODIFY`로 열을 `JSON`으로 바꾸지 않는 한 정확히 그것이 MySQL에 로드됩니다.

#### MySQL 클라이언트로 MariaDB 서버에 연결하거나 그 반대로 할 수 있나요?

예, 일반적인 비밀번호 인증 연결에서는 그렇습니다. 둘 다 같은 와이어 프로토콜을 사용하고, 한쪽 라이브러리로 빌드된 클라이언트는 계정이 비밀번호와 함께 존재하기만 하면 다른 쪽 서버의 계정에 인증할 수 있습니다. 둘이 갈라지는 곳은 연결이 아니라 각 서버가 받아들이는 SQL입니다.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| `ERROR 1064 ... near 'SEQUENCE IF EXISTS ...'` | `CREATE SEQUENCE`/`DROP SEQUENCE`/`DO SETVAL(...)` 문과 `DEFAULT nextval(...)` 열 기본값을 제거하고, 대신 `AUTO_INCREMENT`를 사용 |
| `inet6`를 언급하는 `ERROR 1064`(VillageSQL은 `Expected a type or a custom type instead of "inet6"`라고 말하고, 순정 MySQL은 일반적인 구문 오류를 냄) | 로드하기 전에 열을 `VARCHAR(45)`나 지원되는 INET 타입으로 변경 |
| `ERROR 1064 ... near 'RETURNING id'` | 문장에서 `RETURNING`을 제거하고 뒤에 `SELECT LAST_INSERT_ID()`를 사용 |
| `ERROR 1826 (HY000) ... Duplicate foreign key constraint name '1'` | 덤프에서 충돌하는 `CONSTRAINT` 이름을 스키마 전체에서 각각 고유하도록 변경 |
| `JSON` 열이 오류 없이 로드되지만 대상 스키마에 `LONGTEXT`로 표시됨 | 정상: MariaDB의 `JSON`은 `CHECK` 제약 조건이 붙은 `LONGTEXT`임. 대상에서 네이티브 타입을 원하면 `ALTER TABLE ... MODIFY <col> JSON`을 실행 |
| 비밀번호가 맞을 텐데도 이름 있는 계정에 "Access denied" | MariaDB 소스의 `mysql.user`에서 연결을 가로채고 있을 수 있는 익명 계정(`user = ''`)을 확인 |

## 함께 보기

* [PostgreSQL에서 MySQL로 마이그레이션하기](https://villagesql.com/docs/ko/guides/postgres-to-mysql.md) — PostgreSQL 소스를 위한 대응 가이드
* [MySQL의 스키마 마이그레이션](https://villagesql.com/docs/ko/guides/schema-migrations.md) — 마이그레이션에 필요한 DDL 변경 실행하기


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.