안녕하세요
진행중이던 KOING 프로젝트 베타테스트를 앞두고 코드를 점검 했었습니다. 점검 중에 아주 간단하게 조회 성능을 개선하고 그 이유를 고민해보았는데요, 해당 내용을 공유해드리겠습니다.
프로젝트를 열심히 진행하여 앱스토어에 앱을 올릴 수 있었고, 기획자 분께서 베타테스트터를 모집하고 계셨습니다.
따라서 팀원들과 함께 큰 문제는 없는지, 앱이 느리지는 않은지 사용해보고 있었습니다.
저는 아무래도 가장 많이 호출이 될 투어 상품의 조회 부분을 살펴보며 개선의 여지가 있나 확인해보고 있었습니다.
그러던 중 투어들을 조회해와서 List에 담고, List에 담긴 투어들에 filter를 걸고 조건에 맞는 투어들만 가져오는 코드를 발견했습니다.
해당 조회 쿼리가 여러군데 쓰여야해서, 재사용성을 높이기 위해 조건을 많이 걸지않고 조회해온 투어에 filter를 걸었던 것입니다.
해당 코드가 보기에 깔끔하지 않았고 좀 더 세분화 된 조회 쿼리를 만들어 투어를 조회해보기로 했습니다.
학교에서 강의를 들을 때 Holub sql를 통해 sql 내부를 들여다볼 기회가 있었는데, sql 내부에서도 조회를 다 한 후 where 조건으로 조건에 맞는 것을 찾는 다고 기억하고 있었습니다.
결과를 다 가져오고 필터를 거는 측면에서 기존 코드와 수정 코드 속도 차이는 크게 발생하지는 않을 것 같았고 오히려 조회 쿼리에 조건을 더 넣으면 쿼리 속도가 느려질 것이라 생각했습니다.
수정을 하면 가독성이 더 좋기에 이를 시도해보고 기존에 filter를 이용하는 것과 속도차이가 많이 안나면 수정할 생각이었습니다.
시간 비교를 해보기 위해 조회할 데이터들이 필요했습니다.
로컬 데이터베이스에 투어 더미 데이터를 만들고 이를 조회하면서 시간 측정을 해보기로 했습니다. 데이터베이스로는 Mysql을 사용하고 있습니다.
시간 비교는 Test 클레스를 만들어서 진행했는데요, 더미 데이터 또한 Test 클레스에서 생성했습니다.
원래는 더미 데이터를 백만개 정도 만들어서 테스트를 진행해보려 했으나 투어 엔티티가 워낙 정보도 많고 연관된 엔티티도 많아 반복적으로 데이터를 만들면 컴퓨터가 버티질 못했습니다.
그래서 1153개의 투어만 생성하고 시간 비교를 진행했습니다.
그리고 투어의 카테고리는 0~6 중 랜덤하게 3개 가지고 있도록 생성했습니다.
투어에 카테고리가 "1"인 조건을 추가하여 테스트 했으니 참고로 알아두시면 좋을 것 같습니다.
기존 코드를 보시면 findTourByStatusRecruitment() 라는 함수를 통해 투어의 상태가 recruitement인 투어들을 조회해오고 있습니다.
조회된 투어들 중 filter를 통해 카테고리가 "1"인 투어만 List로 모아주고 있습니다.
그리고 조회 개수와 시작 시간과 끝 시간을 측정하고 걸린 시간을 나타냈습니다.
편의상 Sout으로 프린트 했습니다.
결과를 보면 1153개의 투어중 430개를 조회했고 363 밀리세컨드가 소요된 것을 볼 수 있습니다.
다음은 개선 코드입니다.
findTourByStatusRecruitmentAndTourCategory("1")는 QueryDsl을 이용한 기존 findTourByStatusRecruitment 쿼리에 where(카테고리.eq(카레고리)) 만 추가한 쿼리입니다.
조건의 추가로 인해 카테고리가 "1"인 투어만 조회가 될 것이고 filter를 이용하여 거를 필요가 없어졌죠.
결과를 확인해볼까요?
두둥?!
조회된 투어 개수는 430개로 동일한데 소요 시간이 상당히 줄어든 것을 확인할 수 있습니다.
결과를 보고 의아했습니다.
조회 조건을 추가해서 시간이 더 걸리거나,
아니면 기존 코드에서도 filter하는 시간이 있기에 크게 차이가 안나거나
둘중 하나의 결과가 나올 것이라고 생각했기 때문입니다.
근데 성능이 상당히 개선되었네요...!
결과를 보고 왜그럴까 고민을 해보았습니다.
filter를 사용하는 것이 너무 느린가? 등등 여러 생각을 해봤습니다.
딱히 이거다 싶은 생각이 떠오르지 않아 저녁을 먹으면서도 계속 고민해보았습니다.
기존 코드는 1153개의 투어를 다 조회해와서 filter로 거르는 것이고...
수정한 코드는 1153개의 투어에서 해당 카테고리만 조회하는 것이고...
하는 순간 번쩍하고 떠오르는 생각이 있었습니다.
바로 서버와 데이터베이스 사이의 I/O 였습니다.
로컬 서버에서 로컬 데이터베이스를 연결해 테스트를 했지만 이 역시 데이터 전송을 위한 I/O가 발생합니다.
기존 코드에서는 데이터베이스에서 모든 투어인 1153개의 투어를 서버로 전송하고, 수정 코드에서는 카테고리가 "1"인 430개의 투어만을 서버로 전송하기 때문에 여기서 가장 큰 차이가 발생할 것이라는 생각이 들었습니다.
filter 속도나, 조건 추가 등 다른 요인도 속도에 영향을 미칠 수 있지만 I/O가 속도에 상당한 영향을 미친다고 알고있기에, 충분히 가능성 있다 생각했습니다.
그래서 한가지 테스트를 통해 생각을 검증해보려 했습니다.
기존에는 filter를 통해 필요한 투어를 걸러냈고, 수정된 코드에서는 쿼리 자체에서 where 조건을 통해 투어를 걸러냈었습니다.
기존과 수정된 코드의 시간차이가 filter와 where 조건 때문에 발생하는 것이라면 데이터베이스 서버에서 I/O되는 투어 개수가 같아도 시간차이가 발생할 것이라 생각했습니다.
반대로 데이터베이스 서버에서 I/O되는 투어 개수가 같을 때 조회 시간이 거의 비슷하다면 투어 I/O 개수로 인한 차이가 가장 유의미한 차이를 만들어 내는 것이죠.
기존 코드 filter에 모든 카테고리를 넣어 조회해온 모든 투어가 다 나올 수 있도록 했습니다.
결과를 보시면 1153개의 투어를 다 조회해오는 것을 볼 수 있죠.
수정 코드 쿼리 where 조건에 모든 카테고리를 넣어서 모든 투어가 조회될 수 있도록 했습니다.
결과를 보시면 역시 1153개의 투어를 다 조회해오는 것을 확인할 수 있죠.
그리고!
두 조회의 시간이 342와 359로 비슷한 것을 확인할 수 있습니다.
이것으로 다른 영향도 많겠지만, 투어 I/O개수가 가장 큰 영향을 미치고 있다고 볼 수 있습니다.
가설 검증에 성공했습니다!
조회 코드를 모든 투어를 조회해오고 filter를 거는 코드에서, where 조건을 걸고 조건에 맞는 투어만 조회해 오는 코드로 수정하면서 조회 성능을 개선할 수 있었습니다.
그리고 이는 조회 데이터의 I/O 개수를 개선하면서 가능할 수 있었습니다.
끝으로 5번 반복 테스트한 결과를 표로 정리해 보았습니다.
전체 데이터 1153개, 조회 데이터 430개 기준
조회 성능을 약 34% 개선할 수 있었습니다.