프로덕션 서버에서 데이터 마이그레이션 하기 (with bulk insert)

크로코 (Croco)·2021년 2월 28일
6

myWallets

목록 보기
4/4
post-thumbnail

저기,, 배포가 안되는데요?

프롤로그

안녕하세요. 저는 myWallets에서 백엔드 개발을 담당하고 있는 형주입니다.
얼마전 데이터 분석을 위해 통계 기능을 추가해달라는 요청을 받고 유저 가입 정보를 새로운 테이블에 추가하는 마이그레이션 코드를 만들었습니다. (유저 외에도 패스 생성, 발급등 총 3개의 자료를 이런 방식으로 추가했습니다... )

for (const user of users) {
  await userRegistrationRepository.save({
    userId: user.id,
    registeredAt: user.createdAt,
  });
}

로컬에서는 정말 잘 작동했지만 저는 1시간 후 어떤 일이 벌어질지 몰랐습니다.


내가 알지 못했던..

마이 월렛에는 6,000명 이상의 유저와 15,000개가 넘는 멤버십 카드가 있습니다.

이런 상황에서 적어도 35,000번 이상의 Save 작업이 이루어졌을 것으로 생각할 수 있는데, save의 경우 한 번에 정말 많은 작업을 수행합니다.

데이터베이스에 엔티티가 존재하는지 여부를 확인하는 작업부터 데이터를 삽입할 때는 (매번!) 트랜잭션까지 사용합니다.

게다가 트랜잭션은 Insert 시간에 큰 영향을 준다고 합니다...

그래서 결국 35,000번의 반복문과 Save로 인해 배포되기까지 1시간 30분이 넘게 걸렸습니다.

조금 더 데이터가 많았다면 서비스가 터져버렸을지도 모릅니다.

생각만 해도 무서운 일이니 그만 알아보도록 합시다

개선해보기

구글에서 저와 같은 경험을 한 사람이 있지 않을까 싶어 찾아보았는데, 다행히 비슷한 사람이 있었습니다.

https://prosaist0131.tistory.com/entry/insert와-bulk-insert-무엇을-써야할까요

위 블로그 글을 참고해서 저도 save 대신 bulk insert를 사용하기로 했습니다.

기존에 사용되던 save와 배포 시간 지연의 주범인 반복문을 없애고, 쿼리 빌더를 사용해 벌크 인서트를 하도록 만들었습니다.

await userRegistrationRepository
      .createQueryBuilder()
      .insert()
      .into(UserRegistration)
      .values(
        users.map(user => ({
          userId: user.id,
          registeredAt: user.createdAt,
        })),
      )
      .execute();

벌크 인서트는 매번 Insert를 할때 전후로 발생되는 작업들의 시간이 줄어듦으로 더 빨리 처리된다고 합니다.

아직 저희 데이터가 그렇게 크지는 않지만 조금 더 규모가 커진다면 더욱 더 체감이 될것입니다.

결론

벌크 인서트가 뭔지 몰랐던 주니어 개발자는 새 지식을 얻었습니다.

달리는 차의 엔진을 바꾸는 데이터 마이그레이션 작업을 수행하던 1시간 30분이 걸리던 35,000개의 쿼리들은 3개로 대체했습니다.

정말 큰 일이 날 수도 있었지만 빨리 발견해 미래의 사건을 막을 수 있지 않았을까 생각해봅니다.

profile
세상을 바꾸는 사이드 프로젝트 팀, 크로코입니다.

0개의 댓글