시스템 문제 발견
시스템 기능 중 DB to DB로 2000-3000건 정도의 데이터를 가져오는 기능이 있었습니다.
이 기능은 데이터 조회 후 List 데이터를 순차적으로 커밋합니다.
하지만 문제는 개발, 운영 환경에서 데이터가 추가되었을 경우 데이터가 있어야 테스트를 진행하는데 너무 느린 시간으로 인해 테스트를 하기에 어려움이 있었습니다.
(근무 관리 솔루션으로 인해 테스트 중 시간 초과로 수시로 꺼지는 문제가 있었습니다.. ㅠㅜ)
또한 실제 운영중에서 매니저들의 실수로 인해 눌렀다가 관련 데이터가 안나오는 문제가 발생하기도 했습니다.
이에 문제를 해결하고자 성능을 개선하기로 마음먹었습니다.
문제에 들어가기 앞서 성능을 테스트한 개발 서버의 스펙을 잠시 정리하겠습니다.
Window 프로젝트 서버
CPU: i5
메모리: 16GB
사용 기술
- Spring Framework
- MyBatis
DB 서버
CPU: 2Core
메모리: 4GB
기존 시스템(AS-IS)
기존 시스템은 이관할 DB에서 데이터 전부 가져와 List에 입력합니다.
그리고 새로운 List 데이터를 이관할 DB에 넣기 위해 기존의 데이터를 모두 삭제합니다.
마지막으로 입력된 데이터를 이관할 DB에 입력하게 됩니다.
코드
public 데이터 입력(){
...
// 이관할_데이터의 값을 가져옴
// 기존에 있던 모든 데이터 삭제
// 코드를 한건씩 커밋
for (Order order : orders){
// ... insert
}
}
입력할 데이터는 9번째 행에서 보시다싶이 for문을 돌려 이관 데이터를 한건씩 넣는 식이었습니다.
문제는 한건씩 한건씩 데이터를 처리하다보니 너무 오랜시간이 소요됐습니다.
소요 시간
그리고 실제로 소요시간을 확인하기 위해 Pin Point를 사용하였습니다.
현재 사용중인 Pin Point는 제가 직접 설치해 프로젝트와 연결해 사용하고 있는 것입니다.
아직 사용법을 제대로 아는 것은 아니기에 천천히 사용법을 익혀야 될 것 같습니다.
무튼 제가 말한 부분을 실제로 프로젝트의 배치 프로그램을 실행하며 확인해보았고 결론적으로는 매우 느리게 처리되고 있음을 확인할 수 있었습니다.
PinPoint를 통한 결과를 봐서도 알겠지만 진행시간이 13분정도 걸리게 됩니다. (756초 소요됨)
또한 클라이언트에서 요청한 객체가 13분 동안 매우 느리게(Very Slow) 유지되고 있는 것을 확인했습니다.
결국 이와 같은 문제를 해결하기 위해 다음과 같이 코드를 변경하게 됩니다.
변경 시스템(TO-BE)
코드
public insertData(){
...
ArrayList<Order> orders = new ArrayList<Order>();
// 이관할_데이터의 값을 가져옴
// 기존에 있던 모든 데이터 삭제
// 코드를 전체 입력
insertData(orders);
}
위의 코드 방식의 가장 큰 문제는 DB Call이 너무 빈번한다는 것이었습니다.
그래서 DB Call을 최소화하고자 DB가 처리할 수 있는 데이터 양만큼 한꺼번에 입력을 할 수 있는 Insert Bulk 방식의 구현을 선택했습니다.
먼저 이관 DB에서 받은 List 데이터를 바로 Service로 전송합니다.
private static final int BULK_COUNT = 300;
@Override
@Transactional(readOnly=false)
public void insertData(List<Order> orders) {
int startIdx = 0;
int totalCount = orders.size();
while(startIdx < totalCount){
int endIdx = startIdx + BULK_COUNT;
if(endIdx > totalCount){
endIdx = totalCount;
}
dao.insertMaterial(orders.subList(startIdx, endIdx));
startIdx += BULK_COUNT;
}
}
Service에선 Controller에서 조회한 타 DB에서 입력받은 List 데이터를 가져옵니다.
입력 받은 List 데이터를 bulk size인 300만큼 잘라서 insert 처리하고,
만약 마지막 인덱스가 전체 카운트보다 클 경우 마지막 인덱스를 전체 카운트로 해서 인덱스 처리를 합니다.
<insert id="insert객체" parameterType="객체경로">
INSERT INTO 테이블
(
...
)
<foreach collection="list" item="객체" separator="UNION ALL">
SELECT ...
FROM DUAL
</foreach>
</insert>
13번째 줄에선 리스트로 받은 300 정도 크기의 List를 for문으로 만들어 돌린 뒤 입력을 진행합니다.
소요 시간
변경 전 13분이 소요되었지만 변경 후 3초만에 처리되는 것을 확인할 수 있습니다.
전과 비교해 속도는 252배 향상된 것을 확인할 수 있습니다.
요청 시간 또한 매우 느린 상태에서 보통 상태로 올려 클라이언트의 요청을 빠르게 처리한 것을 확인했습니다.
하지만 위의 방식으로 처리하며 미흡한 부분도 존재했습니다.
첫번째는 백업 테이블이 구축되지 않았다는 것입니다.
기존 방식은 저장된 모든 데이터를 지우고 입력하는 방식이다 보니 백업에 대한 처리가 너무 미흡했습니다.
다음 기능 개선으로 백업 처리에 대한 부분을 다루고자 합니다.
두번째는 유효성 검증입니다.
입력받은 값의 중복 검사나 데이터 입력 건수 등을 검증하는 작업이 필요해보입니다.
현재 방식도 한계가 있었던 것이 300개를 초과해서 보내면 ArrayIndexOutOfBoundsException에러가 발생하게 됩니다.
이러한 Exception을 처리할 테스트 케이스와 유효성 검증을 할 필요가 있어보입니다.
세번째는 코드 구성을 조금 더 객체 지향적으로 만들면 좋겠다는 생각을 했습니다.
현재 코드는 타 DB에서 가져온 데이터를 우리 테이블로 일괄 입력하는데 그 과정에 있어 공통으로 처리되는 영역을 기능 단위로 캡슐화 시키면 더 좋지 않을까하는 아쉬움이 있었습니다.
이외에도 제가 현재 구현한 방식에 문제점은 충분히 더 있을 것이란 생각이듭니다.
그래서 문제를 해결을 위해 좀 더 나은 방식의 배치 작업을 할 수 있도록 노력해야 될 것 같습니다.
다음 이야기
다음엔 백업 테이블 구성을 진행해보고자 합니다.
다음을 기대해주세요ㅎㅎ
'Project&기능개선 > Spring&MyBatis' 카테고리의 다른 글
Bulk Insert를 통한 기능 개선 사례 #2 (0) | 2024.02.29 |
---|