새소식

Welcome to the tech blog of Junior Backend Developer Myoungji Kim!

Development/Python

게시판 데이터 마이그레이션 with Python

  • -

🐳 들어가기 전

2023년 8월 초, 타 쇼핑몰 업체가 우리 업체의 솔루션으로 이전해왔다.

작업 규모가 컸기 때문에 데이터 이전을 위한 임시 TF 팀이 신설되었고, 이런저런 이유로 나는 중간에 합류하게 되었다.

마이그레이션 작업을 위한 로직은 파이썬으로 개발 중이었고 덩달아 나도 실무에서 처음 파이썬을 써보게 되었다.

메신저가 온 걸 모르고 있었는데, 무심코 고개를 돌리니까 TF팀 팀장님이 옆에 서계셨다 (?ㅋㅋㅋ)

"파이썬 처음인거 알고 있는데, 어려우면 GPT가 해줄거에요~" 라는 말에 빵터지면서 🤣 본격적으로 업무를 시작했다

 

  게시판

나는 게시판 이전을 담당했다.

우리 파트에서 내가 주로 담당한 도메인은 상품이었는데, 게시판이라 조금 낯설었지만

게시판 관련 이슈를 몇번 처리한 적은 있어서 크게 부담되지는 않았다.

 

✅ 게시판 데이터 마이그레이션 작업 과정

🔅 테이블 분석하기

가장 먼저 스키마 분석이 필요했다. 

이전 전 DB, 이전 후 DB의 테이블들을 모두 분석해야했기에 막막했는데,

일단 PK, FK를 파악하기 위해 간단한 ERD부터 그려보기로 했다.

좌: 이전 전 DB / 우: 이전 후 DB (블러 처리O)

이해를 위해 같은 역할의 컬럼은 모두 동일한 색으로 칠해주었고

이 작업 덕분에 어떤 테이블의 데이터를 어떻게 매핑해서 이전해줄지 감이 오기 시작했다.

 

🔅 데이터 매핑하기

이전 전 DB의 특정 컬럼의 데이터를, 이전 후 DB의 어느 테이블의 어떤 컬럼으로 저장해줄지 분석이 필요했다.

처음에는 관련 컬럼이 너무 많다고 생각해서 일일히 매핑해서 작성해주는게 과연 효율적일까..? 라고 생각했는데

우리가 AI를 사용하는게 아니기 때문에(?) 결국 이 방법이 제일 안전했다.

예시를 위해 가상으로 만든 매핑 표

이전 후 DB 테이블을 기준으로 이전 전 DB의 어떤 테이블의 어떤 컬럼 값을 매핑해줄 것인지를 모두 분석했다

결과적으로 총 5개의 테이블에 데이터 이전이 필요했다.

 

🔅 개발하기

분석이 끝났으니 이제 본격적으로 개발을 시작할 차례

처음에 머릿 속으로 그린 큰 Flow는 아래 이미지와 같았다.

파이썬으로는 이전 전 DB에 연결해서 필요한 데이터를 가져오고

데이터 매핑한 array를 생성해서 INSERT 쿼리를 작성하는 것까지 개발하는 것으로 계획을 세웠다.

 

한 로직 내에서 DB Connection을 2번 발생시키며 오랜 시간 대기하는 것보다

차라리 당장 필요한 sql 파일을 전체적으로 먼저 추출하고,

추후 mysql에 접속해서 쿼리를 한번에 날리는 것이 더 좋겠다고 판단되었다

실제 PR에 올렸던 로직 Flow

생각보다 로직이 길어져서 PR 올릴 때 한번 Flow를 정리해봤다

 

STEP 01. 이전 전 DB에서 필요한 전체 데이터 가져오기

개발 중인 레포의 목적이 "데이터 이전"인 만큼 이전 전 DB와 통신하여 SELECT로 필요한 데이터를 모두 가져오는 작업이 필요했고,

그만큼 SELECT 쿼리가 많이 필요했으며 카테고리별로 쿼리를 관리하는 작업도 필요했다.

 

현 레포에서는 쿼리들을 TOML로 관리했다.

파이썬이 처음이라,, TOML도 처음 봐서 위 포스팅으로 간단하게 이해했다.

# board.toml
[board_secret]
query="""
SELECT {column} 
FROM table_name 
WHERE isSecret = '{isSecret}'
"""
column=["no", "subject", "body", "isSecret", "writeDate"]

[board_secret.params]
isSecret="y"

한 테이블 당 쿼리 하나에 대한 정보를 담을 수 있도록 하였고, 필요한 경우 파라미터용 테이블을 하나 더 추가하였다.

# 가상으로 작성한 예시 코드입니다
example_toml = get_toml("board.toml")
board_secret_datas = fetch_all("board_secret")

이제 필요한 곳에서 toml 파일을 불러와 실행하고자 하는 테이블명을 파라미터로 넘겨 데이터를 가져오면 된다.

(get_toml(), fetch_all()는 목적에 맞게 직접 구현된 함수입니다)

 

STEP 02. 데이터 매핑하기

이전 전 DB에서 필요한 데이터를 가져왔으니, row 별로 이전 후 DB의 테이블에 맞게 매핑을 해주어야 한다.

# 함수명, 변수명 모두 가상으로 작성했습니다
def mapping_board_to_new_board(board_data):
	mapping_result = {
    	"no": board_data["no"],
        "title": board_data["subject"],
        "contents": board_data["body"],
        "secretFl": board_data["isSecret"],
        "reg_date": board_data["write_date"],
    }
    return mapping_result

매핑 함수는 위와 같이 딕셔너리 타입의 값이 return 될 수 있도록 구성하였다.

 

STEP 03. INSERT 쿼리 만들기

# Insert 쿼리 생성
def get_insert_query(table_name, insert_row):
    keys = ", ".join(insert_row.keys())
    values = ", ".join([to_string(item) for item in insert_row.values()])
    query = f"""INSERT INTO {table_name} ({keys}) VALUES ({values});\n"""
    return query

 

이후 매핑된 결과로 INSERT 쿼리를 생성해준다.

INSERT 쿼리 생성 다음 스텝으로 고민했던 방향은 2가지였다.

1) 이전 후 DB에서 바로 쿼리 실행
2) sql 파일로 작성

나는 2) sql 파일로 작성 을 선택했다.

한번의 로직에서 이전 전 DB 커넥션을 열었다가 끊고,

이전 후 DB 커넥션을 다시 여는 것이 부담이 되지 않을까 싶은 우려가 있었다.

 

만약 파이썬에서 제공하는 DB 관련 라이브러리를 사용하여 바로 쿼리를 실행하게 한다면

VALUES에 대한 처리를 알아서 해주었겠지만

나는 sql 파일을 쌩으로 날려도 문제가 없도록 처리를 해주어야했다.

그래서 util 개념의 to_string() 메서드를 만들어서 values를 처리해주었다.

# 매핑 데이터를 쿼리 VALUES에 넣을 string으로 반환
def to_string(item):
    if item is None:
        return "NULL"
    elif isinstance(item, str):
        return f"'{item}'"
    elif isinstance(item, datetime):
        return f"'{item.strftime('%Y-%m-%d %H:%M:%S')}'"
    else:
        return str(item)

STEP 04. SQL 파일 작성하기

처음에 가져온 데이터 row를 순회하며 데이터 매핑+INSERT 쿼리 생성을 했으니 이 SQL 파일에 그대로 작성해주면 된다.

소스 상으로 크게 어려운 건 없었다.

php 내장 함수를 활용해 SQL 파일을 열어서 Insert 쿼리를 계속 작성해주었다.

 

 첨부파일 마이그레이션

DB 데이터 뿐만 아니라, 게시글 첨부파일도 이전이 필요했다.

게시글 첨부파일의 경우 NHN Cloud OBS(Object Storage)로 업로드하고,

게시글 본문 내 사진의 경우 웹서버로 다운로드 받는 방식으로 마이그레이션을 해주기로 했다.

(현 서비스의 정책을 따른 결과!)

 

🔅  OBS 업로드

이전 업체의 경우 게시글 첨부파일은 총 4개까지 업로드가 가능했었다.

upfile1~4, ori_upfile1~4 컬럼이 게시판 테이블에 있었고,

현재 우리 서비스의 첨부파일 테이블로 가져올 INSERT 쿼리의 value 값을 완성하기 위해서는 아래 과정이 필요했다.

1. URL Request Response 값 확인
2. 200이라면? -> OBS 업로드 진행
3. OBS 업로드 성공했다면? -> INSERT 쿼리 작성

중간에 실패처리 된 케이스는 별도로 로그 파일로 정리해두었고, 성공한 첨부파일만 이전을 완료했다.

 

근데 첨부파일이 엄청 많았다.. 2000개 업로드에 1시간 정도 걸리는데 업로드 대상 파일은 약 6만개였다.

아무리 생각해도 직렬로 6만개를 처리하는 건 무리인 듯 싶어서

한 실행 파일에서 2000개를 처리할 수 있도록 30개의 파일로 분리하여 30개를 동시에 실행했다.

 

(사실 파일 30개로 분리하는 과정에서 파이썬에는 비동기 처리 함수가 없나..? 싶었는데

일단 급건이라 살짝 노가다로 먼저 처리했다..

추후 찾아보니 비동기 처리가 가능해보이는데 나중에 더 공부해봐야지

https://www.daleseo.com/python-asyncio/)

 

🔅  웹서버로 다운로드

제일 마지막에 처리한 작업이었다.

사실 게시글 첨부파일은 따로 컬럼으로 관리하고 있는 upfile만 챙기면 될 것이라고 생각하고 작업했는데,

실 데이터를 확인해보니 이전 업체에서는 게시글 본문에 통째로 html을 박아서 사용하고 있었고

그 안에 무수히 많은 img src가 있었다.

 

즉, 게시글 본문에 해당하는 contents 컬럼의 값 중에서 src에 해당하는 값들을 파싱하여 

이미지도 웹서버로 다운로드 해주고, 데이터 update도 해주어야 했다.

 

1) contents를 전체 가져온다

2) contents 내 img src 값을 파싱한다

3) URL request 결과값이 200일 경우, 경로를 웹서버로 변경해준다

4) 아래 bash 파일의 List에 값을 추가한다.

#!/bin/bash

list=(
	(다운로드 받을 경로) (다운로드 할 URL)
)

5) 웹서버 경로로 변경된 contents로 UPDATE 해주는 쿼리를 작성한다.

 

까지 완료되면 다운로드 받을 목록을 조회하여 실제로 웹서버로 이미지를 다운로드 받는다.

#!/bin/bash

# 실행 명령어: sh img_download.sh >> img_download.log

# img_download_list.sh 파일을 가져옴
source ./img_download_list.sh

# 배열 순회
log_file = "./download.log"
echo "[START]"
for pair in "${list[@]}"; do
    read -r local_path image_url <<< "$pair"
    
    # 이미지 URL 형식 검증
    if [[ $image_url != http* ]]; then
        echo "[RESULT][FAIL][INVALID_URL][$image_url]"
        continue
    fi
    
    # 이미지 파일명 추출
    image_filename=$(basename "$local_path")
    
    # 이미지 다운로드
    curl -o "$local_path" "$image_url"
    
    # 다운로드 성공 여부 확인
    if [ $? -eq 0 ]; then
        echo "[RESULT][SUCCESS][$local_path]"
    else
        echo "[RESULT][FAIL][DOWNLOAD][$image_url]"
    fi
done

 

 SQL 파일 실행

for sql_file in ./(경로)/*.sql; do
    /usr/bin/mysql --binary-mode -h(Host) -u (DB ID) -p(패스워드) -P(Port) (DB NAME) -f < "$sql_file"
done

 

 

🐶 후기

최종 데이터 이전 날, 고객사 페이지의 셧다운과 동시에 DB Dump 파일을 받아 이전을 시작했다. 20~21시부터 시작하여 기존에 계획해둔 이전 시나리오에 맞춰 작업을 진행했고, 새벽 1시에 퇴근은 했지만 집에 가서도 이슈를 대응하며 새벽 4시쯤 잠에 들었던 걸로 기억한다. 그래도 사전 작업을 꼼꼼하게 하며 여러 이슈 케이스를 검토 및 개선한 덕분에 실제로 오픈 후에는 게시판 관련 데이터 이슈가 발생하지는 않았다. (VVVIP 고객이었기에,, 나에게는 이슈가 없었다는 것만으로도 굉장히 감사한 성과였달까) 갑작스레 투입되면서 약 2-3달 간 파이썬과 함께한 새로우면서도 재밌는 경험이었다 :)

 

본 경험 덕분에 데이터 관리 및 쿼리와도 조금더 가까워질 수 있었던 것 같다!

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.