본문 바로가기
Java/Spring

Spring Boot, JPA로 만든 API 서비스의 성능 개선하기

by 크라크라 2023. 5. 8.

오늘은 서비스 성능 개선을 위한 고군분투기를 하나 작성해볼까 합니다. 

앞에서 작성했던 몇 가지 글들은 모두 이 목적을 위해서 고생하면서 알아낸 것들입니다.

 

2023.04.20 - [Java/Spring] - JPA로 mssql(sql server)의 문자열 데이터 조회할 때 인덱스 적용 안됨 이슈

 

JPA로 mssql(sql server)의 문자열 데이터 조회할 때 인덱스 적용 안됨 이슈

보통은 회사에서 JPA를 사용한다고 하시면 오라클을 많이 사용하시겠지만, 상황에 따라서는 MS-SQL (SQL Server) 도 많이 사용할 것입니다. 그런데, MS-SQL 에 JPA를 연동해서 사용하실 때 성능 이슈를 조

toycoding.tistory.com

2023.05.08 - [Java/Spring] - JPA을 이용하여 복합키 테이블에 데이터 삽입시 select insert 방지하기

 

JPA을 이용하여 복합키 테이블에 데이터 삽입시 select insert 방지하기(TODO)

최근에 특정 서비스를 새로 런칭하기 위해서 부하테스트를 진행할 일이 있었습니다. 현재 팀에서 운영하는 서비스는 대부분 하루에 1천~1만건 사이의 호출이 들어오기 때문에 부하로 인한 이슈

toycoding.tistory.com

 

 

 

 현 회사에서 제가 만드는 서비스들은 대부분 1천~1만건이 매일 발생합니다. 그런데, 이번에 런칭을 준비하는 서비스는 대부분의 호출은 1년 혹은 한 달에 하루에 몰빵되어서 수만~수십만건이 발생하고 나머지는 거의 서버가 놀게 될 서비스입니다. 그러다보니 그 호출을 수용할 수 있는 구조를 만들어야했습니다. 

 

시간당 5만건을 달성할 수 있으면 좋고, 최소한 4만건은 만족해야 서비스가 안정적으로 돌아갈 것이라 예상이 됩니다. 

그러나 그 당시에 개발기에서 직접 체크해본 결과는 시간당 2만건 내외의 처리수준이었습니다. 테스트는 JMeter를 가지고 진행했습니다. 아직 실제 운영을 한 것은 아니므로 모든 기준은 JMeter Thread 20개 기준입니다. 

 

실제 개발 환경은 다음과 같습니다. 

DB : MSSQLServer2019 ,윈도우

WAS : Spring Boot, JPA, 리눅스

 

대략적인 성능 개선사항은 다음과 같습니다. 일부 값은 따로 적어두진 않았어서 기억에 의존한 값입니다만 대략적인 경향성만 보면 좋을 것 같습니다. 

 

순서 DB 코어/RAM 개선사항 JMeter 호출횟수
1 2코어/16GB - 2만건 /시
2 4코어/16GB (1) DB 중복 히스토리제거
(2) 시퀀스 변경 (2개 ->1개)하고 로직으로 대체
(3) select 중복 호출 제거
2.8만건/시
3 4코어/16GB (1) JPA 에서 isNew = true 로 변경
(2) 테이블에 인덱스 설정
3.3만건/시
4 4코어/16GB (1) 인덱스컬럼 varchar -> nvarchar 로 변경
(2) 인덱스 컬럼 길이 축소
4.3만건/시
5 4코어/16GB (1) jpa selectby -> countby 4.5만건/시

 

 대략 성능이 125% 정도 증가시켰고, 최소한의 기준을 어느정도 만족시킨 것을 볼 수 있습니다. 


조금 더 자세히 살펴볼까요? 

JMeter를 이용한 초기 테스트 상황에서 2코어/16기가 DB서버를 사용하였습니다. WAS 서버는 성능이 안정적이므로 별도 체크하지 않았습니다. WAS 서버는 4코어 / 16기가 정도로 사용을 했었네요. 성능이 안정적이라는 얘기는 cpu가 최고 100% 사용을 찍지 않았다는 것입니다. 

초기 테스트 시에는 약 시간당 2만건 정도의 호출을 소화할 수 있었고 WAS의 성능에 비해서 DB가 많이 부족해서 CPU가 계속 100%를 쳤습니다. 그러다보니 오래 테스트를 이어갈수록 성능이 점점 떨어지는 문제가 발생하였습니다. 사실, 해당 서비스의 특성상 제일 좋은 옵션은 클라우드로 이관하는 것이었습니다. 이것은 불가능했기 때문에 일단 DB 서버를 4코어로 업그레이드해서 테스트를 진행하였습니다.

 테스트한 API는 한 번의 호출을 통해서 여러 번의 DB 조회 및 저장, 타 사이트로의 API 호출이 섞여있었습니다. 일단 API 호출은 특별히 통제가 불가한 부분이었기 때문에 하드웨어를 변경 후에는 DB 호출을 줄이는 방향을 시도하였습니다. 

(1) DB에 히스토리를 저장할 때 중복된 부분을 삭제 
(2) 한 호출에서 두 개의 시퀀스를 각각 생성 및 사용하고 있는 것을 => 한 개의 시퀀스로 바꾸고 비즈니스 로직에서 시퀀스를 나누는 방식으로 변경
(3) 중복해서 select 쿼리를 호출하는 부분을 한 번만 호출 후, 변수로 받아서 사용하는 것으로 변경 

 위의 표에서 보다시피 아래의 변경사항 이후에 성능이 꽤 많이 개선되었습니다.(+40%) 하지만, 하드웨어를 높였기 때문에 얻을 수 있었던 성능 향상도 있다고 볼 수 있죠. 그러나 4코어로 업그레이드된 후에도 종종 DB 서버가 100%를 치는 상황을 발견하게 됩니다. 

 

 이후 테스트하면서 save()를 호출을 하면서 DB에 insert만 한 번 되는 것을 의도하였는데 실제로는 select 이후 insert가 호출되는 것을 발견하게 됩니다. 이 부분은 JPA의 엔티티 isNew 함수에서 true이면 insert만 하고 false이면 select+insert를 하기 때문이었습니다. 그래서 일단 성능 개선을 위해 무조건 true 로 설정해주게 됩니다. 또한 테이블에 인덱스도 같이 추가해주었습니다. 

(2023.04.20 - [Java/Spring] - JPA로 mssql(sql server)의 문자열 데이터 조회할 때 인덱스 적용 안됨 이슈)

(4) JPA 엔티티 클래스에 isNew = true 로 설정해서 강제로 insert 만 하도록 변경 
(5) 인덱스 설정

 


 이 개선으로 또 20% 정도의 성능 향상을 얻었습니다. insert 시에 중복이 없는데도 무조건 select + insert 를 하고 있는 상황이 해결되었기 때문입니다. 그런데 여전히 특정 상황에서의 select가 너무 느린 것을 발견했습니다. 실행계획을 확인해보니 인덱스가 걸려있는 테이블인데도 지속적으로 풀스캔을 하고 있었습니다. 

 변경 후 꽤 큰 비중을 차지하는 select 가 모두 제거된 것을 확인하였으나, 제대로 인덱스를 걸었는데도 불구하고 계속해서 풀스캔을 하고 있는 상황을 발견하였습니다. index 컬럼이 varchar 로 되어있을 때 JPA로 조회를 하게 되면 기본적으로 nvarchar로 인식되고 이 경우에 인덱스를 타지 않는 것이었죠. 

(2023.05.08 - [Java/Spring] - JPA을 이용하여 복합키 테이블에 데이터 삽입시 select insert 방지하기)

따라서 모든 index 컬럼을 nvarchar로 변경하였습니다. 또한, 인덱스 키의 길이가 길수록 성능이 떨어진다는 얘기가 있어서 인덱스 키 컬럼도 길이를 최대한 축소하였습니다.

(6) 테이블 varchar -> nvarchar 
(7) 인덱스 키 컬럼의 길이 축소

 이렇게 하고 나니 인덱스가 정상적으로 먹었고, 30% 정도의 성능 향상을 얻었습니다. 마지막으로는 데이터가 가장 많은 테이블에서 숫자를 세는 함수가 있는데, selectby로 가져와서 숫자를 세는 것을 확인하고 countby로 변경하였습니다. 

(8) jpa selectby -> countby

 

이렇게 하니 또 5%정도의 개선 효과를 얻었습니다. 

 

 

 

일단 이 정도로 초기단계 목표는 달성한 것으로 보이고, 이후에 문제가 생긴다고하면 추가로 redis 적용이나 다른 방식을 더 얹어봐야 할 것 같네요. 그래도 나름 성능 100% 이상 개선시켜서 뿌듯합니다. 

 

댓글