-
성능 개선: 단계별 접근과 최적화 전략글또 2024. 3. 1. 22:54
0. 들어가며
안녕하세요.
이번에는 회사에서 진행했던 성능 개선 사례를 공유하고자 합니다
성능을 최적화하면서 고려했던 여러 가지 방법들과 최종적으로 선택한 방법에 대한 이야기입니다.
- 문제상황: 출고요청이란?
- 개선사항
- 성능개선
- 마무리
1. 문제상황: 출고요청이란?
출고요청은 재고 시스템에서 첫 단계이며, 주문된 상품의 재고를 차감하고 집품 가능 상태로 변경하는 프로세스입니다.
구체적인 프로세스는 다음과 같습니다.
1. 주문의 주문상품 조회
2. 주문 상품의 재고 조회
3. 재고 차감 및 주문 상품 상태 변경
4. 주문 상품 상태 변경
5. 2~4 단계를 주문 상품 수만큼 반복
6. 1~5 단계를 주문 수만큼 반복
7. 재고 없는 상품은 구매 요청성능 측정 결과
주문 단건 API 응답 속도는 70ms로 양호했지만, 처리 대상 레코드가 늘어날수록 처리 시간이 비례적으로 증가했습니다.
하루 평균 주문 수량인 1500개를 기준으로 계산하면 평균 5분이 소요되었습니다.
성능 개선의 이유
1. 새벽 배송 특성상 시스템 처리 시간 최소화가 중요했습니다.
창고에서의 지체는 후속 배송까지 영향을 미치고, 최악의 경우 고객에게 피해를 줄 수 있습니다.
따라서 저희가 컨트롤할 수 있는 영역인 창고에서의 처리 시간을 줄여 고객 만족도를 향상하고자 했습니다.
2. 개선 방향
현재 구조를 유지하면서 성능 개선을 목표로 했습니다.
개선 방향은 다음과 같습니다.
1. 쿼리 개선
쿼리 실행 속도를 먼저 파악했습니다.
하지만 확인 결과 모두 1,300us 수준의 양호한 속도를 보이고 있었습니다.
슬로우 쿼리들이 있는지 확인했지만 발견되지 않았습니다. 실행 계획도 전부 의도한 인덱스를 타고 있었습니다.
2. I/O 작업 줄이기
주문한 가게와 상품 수가 많아질수록 I/O 작업도 늘어납니다.
모든 것을 메모리에 올려 성능 개선을 하기보다는 단발성 I/O부터 줄이기로 했습니다.
3. 캐시
I/O작업을 줄이는 방법 중에는 묶어서 처리하는 것과 캐시를 이용하기로 했습니다..
다만 분산서버에서의 캐시를 고려해야 했습니다.
그래서 트랜잭션과 생명주기가 같은 캐시를 사용하기로 했습니다.
4. 병렬처리
기능 자체가 하나의 트랜잭션으로 묶여 동작해야 합니다.
주문 단위로 병렬 처리를 하게 된다면 성능 향상은 있겠지만 트랜잭션 롤백 할 수 없는 문제가 있습니다.
스레드를 새로 만드는 것이기 때문에 트랜잭션 전파가 되지 않기 때문입니다.
또 병렬 처리에서 발생하는 장애는 해결하기 어렵습니다.
3.성능 개선
3-1. I/O 작업 줄이기
주문 상품 수 증가에 따른 I/O 작업 빈도 증가는 성능 저하의 주요 원인이었습니다.
다시 말해, 단건이 빠른 속도로 처리되더라도 반복적인 I/O 작업이 발생하여 성능이 저하될 수 있습니다.
그래서 가장 먼저 쿼리를 줄이고자 했습니다.
재고 차감에서의 성능 저하
위에서 재고 차감을 간단하게 표시했지만 사실 안쪽에서 더 많은 일이 벌어지고 있습니다.
풀어서 보면 아래와 같습니다.
1. 재고 현재 위치 조회
2. 재고 이동 위치 조회
3. 재고 조회
4. 애플리케이션에서 재고 차감
5. 재고 반영
6. 주문 상품 상태 변경
7. 상품 현재 재고 조회
8. 재고 이동 내역 이벤트 발행I/O 작업 최적화하기 1
먼저 묶어서 처리할 수 있는 것을 살펴보겠습니다.
위에서 가장 쉽게 바꿀 수 있는 부분은 주문 상품의 상태 변경입니다.
주문 상품이 집품 가능한지 여부를 재고 기준으로 업데이트합니다.
해당 부분을 주문 상품 단위가 아닌 주문 단위로 처리하도록 수정했습니다.
DB 커넥션 타임아웃 방지를 위해 일정 횟수씩 나누어 처리했습니다.
I/O 작업 최적화하기 2
또 하나 줄일 수 있는 것이 재고 내역을 이벤트 발행입니다.
재고 이동이 있을 때마다 타 서비스에게 이벤트를 발행하는 것이 있었습니다.
이벤트를 매번 발행할 필요 없이, 나중에 상품별로 발행하도록 변경했습니다.
스레드 로컬 내부에 상품 ID 값을 저장했다가 트랜잭션이 끝났을 때 트리거를 발생시켜 이벤트 발행했습니다.
스레드 풀 사용 시 반납 전에는 사용했던 내용을 잘 지워주는 것을 잊지 말아야 합니다.
3-2. 캐시 사용하기
짧은 시간에는 변경되지 않는 자원을 계속 조회하는 부분이 있습니다.
바로 재고가 있는 물리적 위치(로케이션)에 대한 SQL입니다.
이런 부분은 캐시 해서 사용할 수 있습니다.
분산 서버이다 보니 글로벌 캐시를 해야 하지만 새로운 관리 포인트를 늘리는 것이 싫었습니다.
생각한 방법은 트랜잭션과 생명주기가 같은 캐시였습니다.
많은 라이브러리들이 트랜잭션과 생명주기가 같은 캐시를 제공해 줍니다.
대표적으로 JPA의 1차 캐시와 Mybatis의 로컬캐시가 있습니다.
Mybatis를 사용하고 있는 환경이지만 로컬캐시를 완벽하게 활용하지는 못했습니다.
JPA의 1차 캐시와는 다르게 Mybatis 로컬캐시는 insert, update 때마다 초기화되기 때문입니다.
그래서 트랜잭션과 생명주기가 같은 캐시로 스프링 캐시를 커스텀했습니다.
spring cache 커스텀으로 성능개선하기에서 자세한 이야기를 다루었으니 참고 바랍니다.
최종적으로 다음과 같은 차트가 완성됩니다.
성능 개선 측정
초기 데이터
성능 개선을 하기 전입니다.
평균 주문 상품 7000~8000건으로 테스트했을 때 도합 5분에 가까운 시간이 걸렸습니다.
I/O작업 줄이기 성능 측정
2번에 걸친 I/O작업 줄이기로 인해 30% 정도의 성능을 개선시킬 수 있었습니다.
캐시 성능 측정
마지막으로 변하지 않는 데이터에 대한 캐시까지 도입하니 이전보다 20% 정도 성능이 개선되었습니다.
4. 마치며
성능 문제가 되는 부분을 찾아 해결할 수 있는 여러 가지 방법을 정리했습니다.
그중 저희에게 맞는 최적화 방법을 정했고 성능최적화를 이뤄냈습니다.
그 방법은 I/O작업을 줄이는 것이었습니다.
이 간단한 작업만으로도 2배 정도의 성능 최적화를 이뤄냈다는 것을 주요하게 봐야 합니다.
병렬 처리를 처음부터 도입하여 성능을 개선하는 것보다 더 간단한 방법을 고려하는 것이 좋습니다.
병렬 처리는 성능을 개선할 수 있지만, 디버깅과 에러 파악이 어려워지는 단점도 고려해야 합니다.
또한, 트랜잭션이 서로 묶여 있는 경우 추가적인 고려가 필요합니다.
현재 팀과 프로젝트에 맞는 최적의 방법을 고려하고, 팀원들과 논의하여 결정해야 합니다.
'글또' 카테고리의 다른 글
IP White List: 보안 취약점과 강화 방안 (0) 2024.06.19 Spring Boot 3 마이그레이션 (0) 2024.04.14 유데미 개발자 영어 (0) 2024.02.03 spring cache 커스텀으로 성능개선하기 (0) 2024.01.21 테크니컬 라이팅 강의를 듣고나서 (0) 2024.01.06