일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- onclick
- Java
- 유니온 파인드
- 동적계획법
- 코딩테스트
- union-find
- 우선순위큐
- 최단경로
- 플로이드 와샬
- bottom-up
- clean code
- scikit-learn
- compiler
- Controller
- dto
- 기술면접
- 거쳐가는 정점
- kmeans
- 음수가 포함된 최단경로
- 직무면접
- spring boot
- disjoint set
- Django
- BufferedReader
- 엔테크서비스
- Android Studio
- 벨만 포드 알고리즘
- 다익스트라
- top-down
- Python
- Today
- Total
춤추는 개발자
[Spring] API 속도 개선을 위한 여정 본문
[ 📢 세줄 요약 ]
특정 테이블의 데이터를 대량으로 삭제할 수 있는 API 개발
API 속도 개선을 위해 비즈니스 로직을 단계적으로 접근
각 단계별로 속도 개선을 위한 코드 리팩토링
✅ 배경과 상황
사업팀으로부터 하나의 요청사항을 받았다.
관리자 포탈에서 발신번호를 대량으로 삭제할 수 있는 기능을 만들어주세요.
기존에는 발신번호의 상세 조회 페이지에서 한 건씩 삭제할 수 있는 기능이 있었기에, 대량으로 삭제하는 건 크게 어렵지 않을 거라고 생각했다. 단순히 DB에서 삭제하는게 아니라 타 시스템의 발신번호 삭제 API를 호출하는 방식이였고, 삭제 대상들을 엑셀 파일로 업로드받아서 삭제 프로세스를 진행했을 때, 약 20초안에 200건까지 삭제가 가능했다. 관리자 포탈에서 WAS의 응답값을 기다리는 시간이 최대 30초이기 때문에 이정도 성능이면 충분하다고 생각했다.
하지만, 사업팀의 요청은 예상과 달랐다.
한번에 2만건까지 삭제하고 싶어요.
내 불찰이었다.
처음에 요청사항을 디테일하게 정립하고 나서 개발했어야 했는데, 이미 API 개발을 끝내고 타 시스템의 지원을 받으며 테스트를 진행하고 있었기 때문이다.
결국, 개발 내역을 뒤집고 다시 사업팀과 요청사항을 정리했고, 내용은 다음과 같다.
1. 실시간으로 삭제할 필요 없이, 사용자가 요청한 삭제 대상들을 저장
2. 백그라운드에서 삭제 프로세스 진행
3. 삭제 요청한 상태를 실시간으로 확인할 수 있는 페이지 제공
✅ 개발 과정
[ 신규 테이블과 화면 설계 ]
사용자가 삭제 요청한 발신번호 대상들을 보관하고, 요청 상태를 확인할 수 있도록 신규 테이블과 화면을 설계했다. 자세한 내용들을 언급할 수 없으나 요청 상태와 결과를 나타내는 컬럼으로 구성했다.
[ Shedlock 기반의 스케줄러 ]
이제 백그라운드에서 삭제 프로세스를 수행할 수 있도록 Shedlock 기반의 스케줄러를 개발했다. 삭제 대상들을 먼저 조회하고, 타 시스템으로 삭제 API 호출 및 응답값을 받아 DB에서 요청 상태/결과를 업데이트하는 순서로 진행했다.
운영에서 스케줄러를 담당하는 POD의 Replica set은 2개로 설정되어 있다. 스케줄러 중복 실행을 방지하기 위해 LOCK 시간을 설정했다.
[ 삭제 요청 API 성능 개선 ]
삭제 요청 API로 삭제 대상들을 요청하면 단일 Insert 쿼리문으로 한 번에 약 500건의 대상들만 저장할 수 있었다. 우리의 요청사항은 최대 2만건이기 때문에 성능 개선이 필요했다.
API 수행 시간이 가장 오래 걸리는 구간은 역시 단일 Insert문으로 구성된 DB 작업이다. 비동기 처리도 생각했지만, 서버 로그에도 쿼리 수행문 및 결과 로그가 남고, DB의 부하도, 수행 시간 등을 개선키기에는 의미가 없었다.
한 번에 대량의 데이터를 인입하기 위해 PreparedStatement를 활용했다. Statement와 달리, 동일한 쿼리문을 캐시에 저장하여 재사용하고, placeholder로 파라미터를 바인딩하여 사용하기 때문에 성능이 우수했다. (자세한 내용은 다른 포스팅에서 다룰 예정.)
쿼리를 한 번에 수행할 사이즈를 결정하는 Batch size를 조정하며 테스트한 결과, PreparedStatement 기반으로 한번에 2만건의 데이터를 저장할 수 있었다.
[ 마무리 및 리팩토링 ]
사업팀의 요청사항을 반영하여 개발을 완료했고, 로직 중에서 시간을 제일 많이 소요하는 `타 시스템의 삭제 API 호출 단계`를 테스트했을 때 200건당 30초의 시간이 소요됐다. 약 2만건의 데이터를 삭제 요청하고, DB에서 요청 상태/결과를 업데이트하면 최대 2시간을 예상했다.
실제로 약 2만건의 데이터를 삭제 요청 및 스케줄러 기반으로 삭제 처리할 수 있었다. 하지만, 삭제 프로세스가 단순 동기 처리로 되있다는 점이 아쉬웠다. 속도 개선을 위해 `타 시스템의 삭제 API 호출/응답 단계`를 단축시킬 수 있는 방법을 고민하던 중 `멀티 스레드` 방식이 떠올랐다.
병렬 작업이 가능하고, `분할 정복 알고리즘`과 유사한 방식인 forkJoinPool을 적용했다. API 호출에 대한 응답값이 필요하기 때문에 리스트를 인자로 추가하여 RecursiveAction으로 처리했고, 타 시스템의 상용 서버 환경을 고려해 스레드는 최대 3개로 제한했다. (forkJoinPool에 대한 자세한 내용은 다른 포스팅에서 다룰 예정)
✅ 마무리
운영 서버에서 테스트할 수 있는 방법이 아직 없어서 개발 환경에만 적용한 상태로 속도 자체는 2배이상 단축시킬 수 있었다. 무엇보다 소스 코드레벨에서 성능을 개선할 수 있는 방법들을 고려하고 적용함으로써 유의미한 결과를 만들어냈다는 점이 개발자로써 성장한 느낌이 들었다.
개발 역량도 중요하지만, 개발 전에 요청사항을 정확하게 정리하는 과정의 중요성도 깨달았다. 개발의 범위와 모호한 요구들을 정립하고 설계에 착수하며, 개발하는 와중에도 이해관계자와 소통하며 변경/문의 사항은 없는지 탄력적으로 개발하는 습관을 가져야 겠다.
✅ Reference
https://bigdown.tistory.com/721?category=1054942
'Developer's_til > 스프링 프레임워크' 카테고리의 다른 글
[Spring] Server to Server 연동을 위한 여정 (3) | 2024.12.15 |
---|---|
[Spring] Static 변수와 스프링 빈 (1) | 2023.12.20 |
[Spring] Mybatis의 동적쿼리와 변수 (1) | 2023.12.08 |
[Spring] XSS필터와 Surrogate pair (0) | 2023.11.10 |
[Spring] 완전한 AOP 솔루션을 제공하는 AspectJ (0) | 2023.10.25 |