6.2Schema vs Data Migrations
데이터를 옮기는 올바른 절차
데이터 마이그레이션 개요
지난 포스팅에 이어, posts
테이블을 수정하는 상황을 가정해보자. 이 테이블은 id
, url
, lat
, lng
컬럼을 가지고 있는 테이블로, lat과 lng를 loc
이란 컬럼으로 묶어보도록 하자.
이 과정은 아래의 3단계를 거칠 것이라 예상할 수 있다.
loc
컬럼 추가하기(구조 변경하기)lat
,lng
컬럼의 데이터를loc
컬럼으로 복사하기(데이터 옮기기)lat
,lng
컬럼 제거하기(구조 변경하기)
여기서 2번째 단계인 데이터 옮기기는 구조 변경(스키마 마이그레이션)과 꽤 다르다.
이 두 가지를 한번에 하는 것은 간편할 것 같지만, 일반적으로는 바람직하지 못하다고 여겨진다. 왜냐하면 데이터 구조를 변경하는 것(컬럼을 추가하고 제거)은 순식간에 처리할 수 있지만, 일반적으로 굉장히 많이 저장되어 있는 데이터들을 옮기는 것은 시간을 꽤 소요하기 때문이다.
만약 이전에 본 Transaction이 설정되어 있다면, 데이터를 옮기는 중에 Transaction이 생긴다면 별도의 워크스페이스에서 데이터의 추가/제거가 이루어지기 때문에 처리가 끝나고 실제 데이터베이스로 옮겨갈 때 오류를 발생시킬 수도 있다.
따라서 데이터 마이그레이션은 아래의 순서로 이루어져야 한다.
loc
컬럼 추가하기.null
값을 허용한다.lat
,lng
, 그리고loc
에 모두 값을 쓰는 코드를 만들어 배포하기lat
,lng
컬럼의 데이터를loc
컬럼으로 복사하기(데이터 옮기기)- 코드를
loc
에만 값을 쓰도록 업데이트하기 lat
,lng
컬럼 제거하기(구조 변경하기),loc
컬럼null
값 금지하기
그러면 실제 예시로 데이터 마이그레이션을 진행해보자.
데이터 마이그레이션 예시
우선 posts
테이블을 만드는 마이그레이션 파일을 작성한다.
bashnpm run migrate create add posts table
javascriptexports.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과 직접적인 관계는 없으니 이 코드는 사실 이해되지 않아도 문제없다.
javascriptconst 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 컬럼 추가하기
bashnpm run migrate create add loc to posts
javascriptexports.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/lng
및 loc
에 모두 데이터 쓰기
index.js
코드의 post
부분을 아래와 같이 수정한다.
javascriptapp.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
파일을 추가하고 코드를 작성한다.
javascriptconst 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));
이후 해당 코드를 실행한다.
bashcd migrations/data node 01-lng-lat-to-loc.js
앱 서버 업데이트하기
index.js
의 lat
, lng
관련 코드를 삭제한다. 물론, req에서는 lat과 lng를 받으므로 해당 데이터를 받지만 데이터를 추가할 때 loc
에 추가하는 식으로 바꾸는 것이다.
javascriptapp.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
명령어로 컬럼을 제거하는 파일을 만든다.
javascriptexports.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
명령어를 실행해 테이블 구조를 최종적으로 변경한다.