특정 기능 속도 개선 경험담(MySQL)

2개월동안 진행했던 속도 개선 작업에 관하여 간단한 정리를 하려고 한다. 추후 진행할 프로젝트에서 참고될수 있도록 정리해 놓는 목적이 크다. 실질적으로 속도 개선은 소량 데이터(만건이하)에서 약 70%개선, 많은 데이터(5만건 이상)에서 10배 정도 개선이 있지만 만약 제일 처음에 개선한 buik insert 변경은 속도 개선 효과는 결과에 반영하지 않았다. 만약 반영한다면 소량 데이터에서도 5배 속도 개선이 이루어 졌다고 봐도 된다.

개선 작업을 요약하면 아래와 같다.

  • JPA save를 Buik insert로 수정
  • native query를 QueryDSL로 변경 및 projection 방식 채택
  • 속도 느린 query 속도 개선
  • 전체 Logic 수정하여 DB 접근을 최소화
  • 데이터 불일치 문제 해결
  • 재 배포 없이 API로만 결과 검증 가능하도록 검증 API 추가
  • 모든 코드를 kotlin로 재 작성

JPA save를 Buik insert로 수정

  • Buik Insert로 로직 수정. 개선 작업을 진행한 부분은 모든 JPA save를 사용하여 속도가 엄청 느렸다. 자료 조사를 통해 먼저 colloction에 후 saveAll 방식으로 수정 했지만 좀 더 낫은 방법으로 개선 하기 위하여 buik insert 방식을 채용. 기존 save 방식 대비 약 10배 성능 향상이 이루어 짐.

native query를 QueryDSL로 변경 및 projection 방식 채택

  • 기존 로직에서 데이터를 조회하는 부분이 native query로 작성되여 있고 가져온 object를 class 생성자를 만들어서 강제 형 변환을 하여 사용하였다. 이부분의 query 가 복잡하여 해석도 힘들고 유지보수도 힘들것 같아 queryDSL로 작성하였다. queryDSL로 변환하니 모든 relation data를 가져오는 것을 확인 하여 조회 속도가 느려진 것을 확인 하고 필요한 데이터를 projection하는 방식을 택하여 가져와서 속도 개선을 하였다.

속도 느린 query 속도 개선

  • 속도 느린 query 에 대하여 explain 명령어를 사용하여 왜 느린지를 분석하고 index가 없으면 전체 테이블 관계를 파악하여 index를 만들어 넣고 만약 기존에 index가 존재하면 기존 index를 타게끔 조건문과 join 관계를 수정하였다.

전체 로직을 수정하여 DB 접근을 최소화

  • 기존에는 필요시마다 DB에서 데이터를 계속 읽어와서 처리해야 함으로 빈번한 조회가 발생하여 전체 로직의 속도가 느려졌다. 처리 해야 할 데이터 양이 적으면 큰 문제가 되지 않지만 10만 건 이상 데이터를 처리함에 있어 많은 속도 저하가 발생하였다. 이런 문제를 해결하기 위해 처리에 필요한 데이터를 맨 처음에 읽어 온 후 비즈니스 로직으로 처리하도록 변경하였다. 또한 coroutine으로 작성된 로직에서 병렬로 처리함에 있어 DB 조회 속도가 현저히 떨어지는 것을 발견하고 coroutine 시작 시 약간의 delay을 DB을 접근을 동시에 하는 것을 막아 조회 속도를 일정하게 보장 시켰다. ( 추후 DB 성능을 높이니 delay를 주지 않아도 조회 속도에 문제 없는 것을 확인 하고 delay에 대한 코드는 제거 했다.)
    경험한 프로젝트에서는 3만건 정도면 약 70% 속도 차이를 보이지만 10만 데이터는 약 10배 속도 향상을 달성 하였다.

데이터 불일치 문제 해결

  • 기존 로직과 새로 작성한 로직 사이 결과가 불일치 문제 해결
    모든 작업을 끝내고 최종 테스트를 진행하였는데 소수점 자리수의 마지막 자리가 계속 상이한 점을 발견하고 어디에 문제가 있을지를 고민해 보았다. 많은 케이스에서 문제 없는데 오직 한 케이스만 문제 있었다. 자세히 들여다 보니 double를 소수점 타입을 사용한 것이 문제 였다. 지단로보트의 BigDecimal 사용법 정리를 보고 상이함이 발생하는 부분의 double만 BigDecimal로 수정하니 모든 문제가 해결 되였다. ( 기존 방법은 일정한 단위로 처리 후 DB에 저장 후 다시 필요하면 읽어오는 방식이 여서 저장하지 않는 값과 저장한 값 사이 약간의 차이가 발생 한 것으로 추정 된다.)

재배포없이 API로만 결과 검증 가능하도록 검증 API 추가

  • 기존 로직과 속도 비교를 위하여 서비스 중인 분산캐시를 사용하여 on/off 할 수 있게 하였다. 실시간으로 새로 개선한 로직이 기존 로직과 결과가 일치함과 서비스를 새로 배포하지 않고 바로바로 테스트 가능 하여 속도 개선 작업에서 많이 사용된다.

모든 로직을 kotlin으로 재 작성

  • 기존 로직은 JAVA로 되여 있었다. JAVA로 많은 중복 코드로 인해 코드의 가독성이 떨어져 있었다. 모든 코드를 이해하고 kotlin으로 작성하였다. 변환 과정에서 한번 더 느겼지만 코드가 휠씬 간결해지고 특히 코드 작성에서 null에서 체크를 모두 끝내니 NPE가 발생을 원춴적으로 차단 된것 같다는 느낌이 든다. 또한 다른 JAVA class를 사용함에 있어서도 null에 대한 체크를 엄격히 하니 코드 작성에 좀 더 집중할 수 있는 것 같다. 필자는 JAVA로 작성시 여러번 NPE를 발생 시킨 경험이 있다.