sungyup's.

PostgreSQL / Managing Database Design with Schema Migrations / 6.2 Schema vs Data Migrations

6.2Schema vs Data Migrations

데이터를 옮기는 올바른 절차

데이터 마이그레이션 개요

지난 포스팅에 이어, posts 테이블을 수정하는 상황을 가정해보자. 이 테이블은 id, url, lat, lng 컬럼을 가지고 있는 테이블로, lat과 lng를 loc이란 컬럼으로 묶어보도록 하자.

이 과정은 아래의 3단계를 거칠 것이라 예상할 수 있다.

  1. loc 컬럼 추가하기(구조 변경하기)
  2. lat, lng 컬럼의 데이터를 loc 컬럼으로 복사하기(데이터 옮기기)
  3. lat, lng 컬럼 제거하기(구조 변경하기)

여기서 2번째 단계인 데이터 옮기기는 구조 변경(스키마 마이그레이션)과 꽤 다르다.

이 두 가지를 한번에 하는 것은 간편할 것 같지만, 일반적으로는 바람직하지 못하다고 여겨진다. 왜냐하면 데이터 구조를 변경하는 것(컬럼을 추가하고 제거)은 순식간에 처리할 수 있지만, 일반적으로 굉장히 많이 저장되어 있는 데이터들을 옮기는 것은 시간을 꽤 소요하기 때문이다.

만약 이전에 본 Transaction이 설정되어 있다면, 데이터를 옮기는 중에 Transaction이 생긴다면 별도의 워크스페이스에서 데이터의 추가/제거가 이루어지기 때문에 처리가 끝나고 실제 데이터베이스로 옮겨갈 때 오류를 발생시킬 수도 있다.

따라서 데이터 마이그레이션은 아래의 순서로 이루어져야 한다.

  1. loc 컬럼 추가하기. null 값을 허용한다.
  2. lat, lng, 그리고 loc에 모두 값을 쓰는 코드를 만들어 배포하기
  3. lat, lng 컬럼의 데이터를 loc 컬럼으로 복사하기(데이터 옮기기)
  4. 코드를 loc에만 값을 쓰도록 업데이트하기
  5. lat, lng 컬럼 제거하기(구조 변경하기), loc 컬럼 null 값 금지하기

그러면 실제 예시로 데이터 마이그레이션을 진행해보자.

데이터 마이그레이션 예시

우선 posts 테이블을 만드는 마이그레이션 파일을 작성한다.

bash
npm run migrate create add posts table
javascript
exports.up = (pgm) => { pgm.sql(` CREATE TABLE posts ( id SERIAL PRIMARY KEY, url VARCHAR(300), lat NUMERIC, lng NUMERIC ); `); }; exports.down = (pgm) => { pgm.sql(` DROP TABLE posts; `); };

웹 서버 만들기

다음으로, 웹 서버를 만든다. 이번 예시에선 express.js로 앱을 만든다. 아래는 express로 만든 간단한 index.js다. PostgreSQL과 직접적인 관계는 없으니 이 코드는 사실 이해되지 않아도 문제없다.

javascript
const express = require("express"); const pg = require("pg"); const pool = new pg.Pool({ host: "localhost", port: 5432, database: "socialnetwork", user: "sungyupju", password: "", }); const app = express(); app.use(express.urlencoded({ extended: true })); app.get("/posts", async (req, res) => { const { rows } = await pool.query(` SELECT * FROM posts; `); res.send(` <table> <thead> <tr> <th>id</th> <th>lng</th> <th>lat</th> </tr> </thead> <tbody> ${rows .map( (row) => ` <tr> <td>${row.id}</td> <td>${row.lng}</td> <td>${row.lat}</td> </tr> ` ) .join("")} </tbody> </table> <form method="POST"> <h3>Create Post</h3> <div> <label>Lng</label> <input type="text" name="lng" /> </div> <div> <label>Lat</label> <input type="text" name="lat" /> </div> <button type="submit">Create Post</button> </form> `); }); app.post("/posts", async (req, res) => { const { lng, lat } = req.body; await pool.query( ` INSERT INTO posts (lng, lat) VALUES ($1, $2); `, [lat, lng] ); res.redirect("/posts"); }); app.listen(3005, () => { console.log("Listening on port 3005"); });

터미널에 node index.js를 실행하고 브라우저에서 localhost:3005/ports로 들어가면 서버에서 lng, lat을 추가할 수 있다.

loc 컬럼 추가하기

bash
npm run migrate create add loc to posts
javascript
exports.up = (pgm) => { pgm.sql( ` ALTER TABLE posts ADD COLUMN loc POINT; ` ); }; exports.down = (pgm) => { pgm.sql( ` ALTER TABLE posts DROP COLUMN loc; ` ); };

이후 DATABASE_URL=postgres://USERNAME@localhost:5432/socialnetwork npm run migrate up 커맨드를 실행해 loc 컬럼을 추가한다.

서버 코드를 바꿔 lat/lngloc에 모두 데이터 쓰기

index.js 코드의 post 부분을 아래와 같이 수정한다.

javascript
app.post("/posts", async (req, res) => { const { lng, lat } = req.body; await pool.query( ` INSERT INTO posts (lng, lat, loc) VALUES ($1, $2, $3); `, [lat, lng, `(${lng}, ${lat})`] ); res.redirect("/posts"); });

이후 데이터를 추가해서 PGAdmin4에서 확인해보면, loc 컬럼에 추가한 데이터가 원하는 형식으로 들어간다.

데이터 마이그레이션하기

데이터를 옮길때, 한쪽에서 완전히 없어지고 다른쪽에 완전히 들어가야 하는 상황이므로 Transaction을 쓰는것이 좋다.

다만 주의해야할 것이 있다. 대량으로 데이터를 옮기는 도중, 실제 앱을 사용하고 있는 유저들이 값을 추가/업데이트한다면 대량 데이터 마이그레이션이 끝나야만 업데이트가 가능해진다. 즉, 실제 앱을 사용하는 유저들의 쿼리 처리가 우선 순위에서 밀린다.

migrations 폴더에, data 폴더를 추가하고 01-lng-lat-to-loc.js 파일을 추가하고 코드를 작성한다.

javascript
const pg = require("pg"); const pool = new pg.Pool({ host: "localhost", port: 5432, database: "socialnetwork", user: "sungyupju", password: "", }); pool .query( ` UPDATE posts SET loc = POINT(lng, lat) WHERE loc IS NULL; ` ) .then(() => { console.log("Update complete"); pool.end(); }) .catch((err) => console.error(err.message));

이후 해당 코드를 실행한다.

bash
cd migrations/data node 01-lng-lat-to-loc.js

앱 서버 업데이트하기

index.jslat, lng 관련 코드를 삭제한다. 물론, req에서는 lat과 lng를 받으므로 해당 데이터를 받지만 데이터를 추가할 때 loc에 추가하는 식으로 바꾸는 것이다.

javascript
app.get("/posts", async (req, res) => { const { rows } = await pool.query(` SELECT * FROM posts; `); res.send(` <table> <thead> <tr> <th>id</th> <th>lng</th> <th>lat</th> </tr> </thead> <tbody> ${rows .map( (row) => ` <tr> <td>${row.id}</td> <td>${row.loc.x}</td> <td>${row.loc.y}</td> </tr> ` ) .join("")} </tbody> </table> <form method="POST"> <h3>Create Post</h3> <div> <label>Lng</label> <input type="text" name="lng" /> </div> <div> <label>Lat</label> <input type="text" name="lat" /> </div> <button type="submit">Create Post</button> </form> `); }); app.post("/posts", async (req, res) => { const { lng, lat } = req.body; await pool.query( ` INSERT INTO posts (loc) VALUES ($1); `, [`(${lng}, ${lat})`] ); res.redirect("/posts"); });

lat, lng 컬럼 삭제하기

npm run migrate create drop lng and lat from posts 명령어로 컬럼을 제거하는 파일을 만든다.

javascript
exports.up = (pgm) => { pgm.sql(` ALTER TABLE posts DROP COLUMN lng, DROP COLUMN lat; `); }; exports.down = (pgm) => { pgm.sql(` ALTER TABLE posts ADD COLUMN lng numeric, ADD COLUMN lat numeric; `); };

이후, DATABASE_URL=postgres://USERNAME@localhost:5432/socialnetwork npm run migrate up 명령어를 실행해 테이블 구조를 최종적으로 변경한다.