[개발 일지] Mysql Bulk Insert

Rudy·2021년 11월 23일
4

오늘은 가볍게 CSV파일로 이루어진 데이터를 Mysql DB에 Bulk Insert하는 과정을 다뤄보겠습니다.

시작은...

현재 제가 당근마켓에서 백엔드 개발을 하고 있는 서비스 '당장모아'에서는 사용자들이 동네의 숨겨진 장소들을 새롭게 알아보고 쉽게 본인의 테마에 추가할 수 있도록 '둘러보기' 탭을 제공합니다.
둘러보기 탭에서 어떤 장소를 보여주면 좋을지 고민하던 저희 팀은 처음에는 단순히 당근마켓의 POI 서비스가 가지고 있는 동네 장소들 중 랜덤으로 N개를 보여주려고 했습니다.
하지만 생각보다 장소의 카테고리가 너무 다양했고, 매력적인 장소를 제공하자는 취지에 맞지 않는 데이터들이 너무 많이 내려왔습니다.
결국 저희는 두 번째 안으로 저희가 직접 매력적인 장소를 추가해보려고 했지만 이 또한 쉽지 않았습니다. 적어도 1000개 정도의 장소가 있어야 사용자들에게 매번 랜덤하게 새로운 장소들을 보여줄 수 있을 것이라는 계산이 나왔지만 어떤 기준으로, 어디까지 장소를 추가해야할지 감이 잘 오지 않았습니다.
그러던 중 저희 팀 디자이너 분께서 당근마켓의 '동네생활' 탭에서 유저들이 한 번이라도 언급한 장소라면 매력적이라고 생각할 수 있지 않을까라고 아이디어를 내주셨고 관련 부서에서 약 1000개 정도의 데이터를 CSV파일로 얻을 수 있었습니다.
처음에 데이터가 온 줄 모른채로 다른 작업에 집중하고 있던 저는 디자이너 분께서 제가 만든 추천 장소 추가 어드민으로 해당 CSV파일에 있는 장소명을 하나하나 추가하고 있는 것을 발견했고, 스크립트를 짜면 쉽게 DB에 넣을 수 있을 것 같다고 생각했습니다.
이왕 해보는거 단순히 반복문으로 1000개의 쿼리를 보내지말고 멘토님들이 한 번쯤 경험해보면 좋다고 말씀해주신 Bulk Insert를 시도해보기로 했습니다.

핵심

Bulk Insert의 핵심은 간단합니다.

INSERT INTO <테이블 이름> (칼럼1 이름, 칼럼2 이름) VALUES ?

다음 쿼리를 통해 ?에 넣고자 하는 값들을 배열로 넣어주기만 하면 됩니다.
Bulk Insert가 매번 쿼리를 날리는 것보다 속도가 빠른 이유는 한 번의 쿼리 전후로 이루어지는 작업들을 한 번만(혹은 줄여주기) 때문입니다.

잠깐... 상대는 마이크로서비스다

핵심은 간단하고 이제 ?에 넣어줄 배열만 만들면 되니 빠르게 node.js로 스크립트를 짰습니다.
그런데 다 짜고나서 생각해보니 중요한 점을 놓친 것을 알게 되었습니다.
저희 서비스의 추천장소 테이블에는 placeId 뿐만 아니라 지역별 조회를 위해 해당 POI의 regionId도 칼럼으로 가지고 있습니다.
그런데 이 placeId를 통해 한 번에 regionId를 가져올 수 있는 방법이 없었습니다. (서로 다른 부서에서 담당하고 있어 다른 서비스로 운영되기 때문에)
약 1000개의 데이터를 두 번의 HTTP 리퀘스트를 거쳐 가공해야했고 단순히 node.js로 반복문을 돌린다면 비동기처리로 인해 한 번에 1000번 이상의 리퀘스트가 두 개의 서버로 날아갈 것이 예상되었습니다.
거의 공격에 다름없는 리퀘스트가 예상된다고 두 부서 개발자분께 문의했더니 조금씩 나눠서 보내주실 것을 요청하셨습니다.

해결책

몇 번에 나눠 리퀘스트를 나눠야할지도 애매하고 Bulk로 데이터를 가져오는 API도 있었지만 몇 개까지 한 번에 받아올 수 있는지도 애매했던 저는 SetInterval함수를 이용해서 시간이 조금 걸리더라도 초당 1번씩 리퀘스트를 보내서 INSERT 문에 넣어줄 배열을 완성하고 한 번에 집어넣는 방식을 택하기로 결정했습니다.

코드

const mysql = require('mysql');
const path = require('path');
const fs = require('fs');
const axios = require('axios').default;

const conn = mysql.createConnection({
    host: '',
    port: 3306,
    database: '',
    user: '',
    password: '',
})

const sql = "INSERT INTO recommend_place (placeId, regionId) VALUES ?";
const values = [];

const file = fs.readFileSync('./place_list.csv', {encoding: 'utf-8'});
const rows = file.split("\n");
const result = [];

for (let rowIndex in rows) {
    if(rowIndex == 0) continue;
    const row = rows[rowIndex].split(",");
    const placeId = row[0];
    result.push(placeId)
}

index = 0;
const err_arr = [];
const interval = setInterval(function() {
    console.log(index);
    const id = result[index];
    if (id) {
        axios.get('' + id)
    .then(res => {
        const poi_id = res.data.id;
        const coordinates = res.data.coordinates;
        axios.get('', {
            params: {
                lat: coordinates.latitude,
                lng: coordinates.longitude
            },
            headers: {
                "X-Api-Key": ""
            }
        })
        .then(response => {
            const regionId = response.data.data.regions[0].id;
            const value = [poi_id, regionId];
            values.push(value);
            index++;
        })
        .catch(e => {
            console.log(e)
        })
}).catch(e => {
    console.log(e)
    err_arr.push(id);
    index++;
})
}
if (index === result.length){
    clearInterval(interval);
    conn.query(sql, [values], function(err) {
        console.log(err);
        conn.end();
    })
    console.log(err_arr);
}
}, 1000)

완성된 코드입니다.(빠르게 문제를 해결하기 위해 짠 코드라서 틀린 부분이나 왜 이걸 이렇게? 싶은 부분이 많을 수 있습니다. 댓글로 알려주시면 정말 감사하겠습니다.)

지금까지 빠르게 INSERT하기 위해 짰지만 마이크로서비스 환경에서 다른 서버에 부하를 주지 않기 위해 결국은 시간을 포기한 Bulk Insert 경험담이었습니다.
읽어주셔서 감사합니다.

profile
Run, as you always do

0개의 댓글