본문 바로가기
이전글/2007

데이터 마이그레이션 수행속도 높이기

by 물개선생 2007. 2. 22.

현재 수업 모듈의 개발이 진행 중인데요, 정치적 상황으로 논리 모델 자체가 와장창 바뀌는 경우가 많아 데이터 마이그레이션을 자주 실행하게 되었습니다. 처음 개발한 마이그레이션 프로그램이 최적화를 많이 고민하지 않아서 17 시간이 걸렸는데요, 자주 수행하기 위해 최적화 작업을 수행했습니다. 최적화 작업 이후 100만건 수준의 데이터를 마이그레이션하는 시간이 20분으로 줄어들었습니다. 거의 1/50로 수행시간이 줄어들었네요. 최적화에 사용한 기본원리는 한계는 없다는 믿음과 굳어있는 머리를 깨는 것. ^^* 마이그레이션은 특정 시간에 작업을 멈추고 진행하기 때문에 메모리 옵션을 최대한 주고 실행한다는 점에 착안해서 캐쉬를 적극 활용하는 방법을 사용한 것입니다.

수강신청 데이터 마이그레이션을 샘플로 살펴보겠습니다. 마이그레이션 로직은 2단계로 나누어집니다. 우선 Legacy DB에서 수강신청이력 데이터를 모두 읽어온 다음, 각각의 수강신청 이력 데이터별로 변경된 스키마에 맞춰 값을 입력하고 마이그레이션이 끝난 개설강의, 학생, 코드(학기, 수강신청유형, 등등)의 정보를 찾아와 관계를 재설정하여 입력합니다. 그런 다음 수강신청이력 중 패턴을 분석해서 수강신청 데이터로 남아야 할 것을 찾아낸 다음 그것을 다시 수강신청 데이터로 변환하여 관계를 재 설정한 후 입력합니다.


사용자 삽입 이미지


수행되는 쿼리들을 분석해보면 처음에 Legacy에서 읽어올 때 select 문은 1번밖에 실행되지 않습니다. 그러나 각 데이터 별로 관계 설정을 위해 특정 조건에 맞는 개설강의, 학생, 코드를 찾아와서 연결해주는 쿼리가 반복적으로 호출되고 있음을 확인할 수 있습니다. 모두 5개의 코드 값을 참고 하기 때문에 패턴검사를 통과한 수강신청 데이터가 20만개라고 볼때 실행되는 쿼리는 select 1 + 100만 * (select 7 + insert 1) + 20만 * (select 7 + insert 1) 개가 됩니다. 즉, 840만건의 select 문과 120만건의 insert 문이 실행되는 거죠.

여기서 가장 문제가 되는 것이 840만건에 이르는 select 문입니다. Legacy에서 읽어오는 단 1개의 select 문이나, 개발DB로 실제로 데이터를 넣어주는 insert 문은 생략할 수 있는 부분이 아니기 때문에 그 select 문이 튜닝 대상이 됩니다. DB I/O 자체를 줄이는게 목적인거죠.

수술전
for (Map<String, Object> 리거시수강신청이력데이터 : 리거시수강신청이력데이터들) {
    수강신청이력 saHist = new 수강신청이력();
    saHist.set학생(학생Dao.get학생By학번(리거시수강신청이력데이터.get("학번"))
}

위에 색깔이 표시된 학생Dao 사용에 의해 for 문에 입력된 데이터의 갯수만큼 발생하는 select 문을 어떤 방법으로 없앨 수 있을까요? 아래와 같이 전체 학생 정보를 한번에 읽어와서 Map에 담아두고 이용하면 됩니다.

private void initStudentMap() {
  studentMap = new HashMap<Integer, Student>();
  List<Student> students = studentDao.getAll();
  for (Student student : students) {
   Student fakeStudent = new Student();
   fakeStudent.setStudId(student.getStudId());
   studentMap.put(student.getStudNo(), fakeStudent);
  }
 }

이제 앞의 문제코드는 for 문 밖에서 전체 학생정보를 읽어오는 1번의 쿼리만 DB에 날아가고, for 문 내부에서는 Map을 이용해서 메모리에 있는 학생 정보를 적당한 키 값으로 가져오는 방식으로 변경되었습니다.

수술후
initStudentMap();
for (Map<String, Object> 리거시수강신청이력데이터 : 리거시수강신청이력데이터들) {
    수강신청이력 saHist = new 수강신청이력();
    saHist.set학생(studentMap.get(리거시수강신청이력데이터.get("학번"))
}

같은 원리로 ehCache와 같은 라이브러리를 쓰는 방법도 있습니다. 사용방법은 초기화하는 부분을 빼면 Map과 별 차이가 없습니다.

또 하나의 팁은 하이버네이트를 이용하기 때문에 발생하는 문제를 해결하는 것인데요, DAO를 적정한 주기로 flush하고 clear 해줘야 합니다.

public static final int STEP=20;
public void migration() {
 initLegacyMap();
 int cnt = 0;
 List<Map<String, Object>> mapForStud = new ArrayList<Map<String,Object>>();
 for (Map<String, Object> map : maps) {
  cnt++;
  // do migration
  if (cnt%STEP == 0) studyApplyDao.flushAndClear();
 }
}

예전에 for 문을 잘못 사용하는 방법을 예로 들며 동일한 배정문이 for 문 안에 있을 때 그것 밖으로 빼내서 1번만 수행하도록 하는 코드 샘플을 본 기억이 나네요. 불필요한 Select 문을 Map이나 Cache를 이용해서 제거하는 원리는 그것과 다르지 않습니다.

사용자 삽입 이미지
불필요한 Select 문을 없애서 DB I/O 비용을 줄이는 것과, 적절한 주기로 하이버네이트 세션을 flush & Clear 해주는 이 단순한 조작만으로도 마이그레이션 수행 시간이 1/50로 줄어든다는 사실이 놀랍네요. 덕분에 저와 동료분들의 소중한 작업 시간이 조금 더 확보되었습니다. :)

이제 migration_all 태스크만 실행하면 저희 시스템에서 사용하는 모든 리거시 데이터가 개발자 PC의 PostgreSQL이나, 개발 서버의 Sybase로 자동으로 1시간 내에 마이그레이션 되니까요.

물론 아직도 개선할 여지가 어딘가에 남아 있을 거라는 사실은 잘 알고 있습니다. 히딩크처럼 늘 배고파 하며 살아야 하는 개발자의 숙명이기도 하죠. 하지만 이정도 속도면 처음의 불편함을 해소하는데 충분하기 때문에, 추가적인 성능 향상에 대한 필요성이 제기되면 그 때 다시 고민해보도록 하죠.