최범균 - 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식
느려진 서비스, 어디부터 봐야 할까
처리량과 응답 시간
응답 시간
- 응답 시간은 중요하다. 응답 시간이 지연될 수록 서비스 이탈자는 급격히 증가한다.
- 외부 연동이 응답 시간 지연에 큰 비중을 차지한다. (I/O)
처리량
- TPS: 시스템이 처리할 수 있는 최대 요청 수
- 트래픽이 많은 시간대의 TPS와 응답 시간이 얼마인지 측정하고, 이 결과를 바탕으로 목표 TPS와 응바 시간을 설정하고 효과적인 성능 개선안 도출 필요
- TPS를 확인하는 가장 간단한 방법은 모니터링 시스템을 활용하는 것
응답 시간을 줄이기 위해서 할 수 있는 방법 두가지
1. 서버가 동시에 처리할 수 있는 요청 수를 늘려 대기 시간 줄이기
2. 처리 시간 자체를 줄여 대기 시간 줄이기
서버 성능 개선 기초
병목 지점
- 문제 지점을 찾는 간단한 방법은 처리 시간이 오래 걸리는 작업을 식별하는 것
- 실행 시간 측정에는 모니터링 도구가 유용하다.
- 성능 문제는 주로 DB나 외부 API를 연동하는 과정에서 발생한다.
커넥션 대기 시간
- 응답 시간이 중요한 서비스는 커넥션 대기 시간을 가능한 한 짧게 설정해야 한다. 보통의 경우라면 0.5초에서 3초 이내로 지정하자. (커넥션을 얻기 위해 대기하는 시간만큼 응답 시간도 길어지기 때문)
- 대기 시간을 짧게 설정하면 서버 부하를 일정 수준으로 유지할 수 있으며 서버를 안정적으로 운영하는데 도움이 된다.
최대 유지/유휴 시간
- 최대 유휴 시간은 사용되지 않는 커넥션을 풀에 유지할 수 있는 최대 시간을 의미한다.
- 최대 유휴 시간과 최대 유지 시간을 무한대로 설정하지 않는게 좋다. 커넥션 풀의 기본값을 확인한 뒤 이 두 설정의 기본값이 무제한으로 되어 있다면 DB 설정을 참고하여 알맞게 적절한 값으로 지정해야 한다.
- 지금까지는 DB의 wait_timeout 보다 hikari의 max_lifetime 설정 값이 더 짧아서 문제가 없었다.
- Connection은 원활한 운영을 위해 주기적으로 새로운 커넥션으로 변경이 필요하다.
로컬 캐시와 리모트 캐시
- 로컬 캐시: 인-메모리 캐시라고도 불린다. 서버 프로세스와 캐시가 동일한 메모리 공간을 사용한다.
- 리모트 캐시: 독립적인 저장 공간을 갖는다. (ex. redis)
- 캐시에 보관할 데이터 규모가 작고 변경 빈도가 매우 낮다면 로컬 캐시로 충분하다.
- 데이터 규모가 크다면 리모트 캐시를 사용해야 한다.
TIP : 트래픽이 순간적으로 급증하는 패턴을 보인다면 캐시에 데이터를 미리 적재하는 것도 고려할 필요가 있다.
가비지 컬렉터와 메모리 사용
- 사용하는 메모리양과 객체 수가 많을수록 GC 실행 시간은 길어진다.
- 한 번에 대량으로 객체를 생성하는 것도 주의해야 한다.
- 대량으로 객체가 생성되는 것을 방지하려면 조회 범위를 제한해야 한다.
- 파일 다운로드와 같은 기능을 구현할 때는 스트림을 활용한다.
대량 데이터 때문에 발생한 OOM 해결 절차
- 모니터링 시스템을 통해 각종 지표를 확인한다.
- 힙 덤프를 저장하고 분석한다.
- 힘 덤프를 보며 메모리를 차지하는 객체의 비중을 확인한다.
- 관련 기능을 개선한다.
엑셀 파일 생성 기능 구현 TIP
- 엑셀을 메모리에서 생성하지 않고 로컬 파일에 스트림 형태로 만들도록 변경한다.
- 엑셀을 생성하기 위해 필요한 데이터를 조회하는 방식을 List가 아닌 Stream 형태로 한다. (JPA/Hibernate 에서는 Stream 조회 방식을 지원한다. 내부적으로 커서를 사용해서 잘라가지고 데이터를 읽는다.)
응답 데이터 압축
- 응답 데이터 압축하면 전송량을 줄일 수도 있으며, 클라우드 환경에서는 비용도 줄일 수 있다.
Spring Boot 에서는 설정만으로 압축 기능을 제공할 수 있다.
주의 사항
- CPU 사용량이 증가 할 수 있다.
- 클라이언트가 'Accept-Encoding: gzip'으로 헤더를 요청해야 한다.
- API 별로 압축을 다르게 하고 싶다면 별도의 필터 구현이 필요하다.
정적 자원
- 이미지와 같은 파일을 업로드할 때는 용량 초과로 서비스가 불능이 되지 않도록 파일 크기를 신중하게 관리해야 한다.
- 웹 서버에 크기 제한 설정을 추가하는 것이 좋다.
대기 처리
- 짧은 시간 동안 폭증하는 트래픽은 서버를 미리 증설하는 방법으로 문제를 해결할 수 있지만, DB는 미리 증설하기 어렵고 DB도 그렇고 서버도 그렇고 비용이 크다.
- 다른 방안으로, 처리할 수 있는 시스템의 처리량을 무작정 늘리기보다는 수용할 수 있는 수준의 트래픽만 받아들이고 나머지는 대기 처리하는 것이다. (Ex. 인터파크 예약)
성능을 좌우하는 DB 설계와 쿼리
조회 트래픽을 고려한 인덱스 설계
전문 검색 인덱스
- like 를 사용하는 검색은 풀 스캔을 유발한다. 엘라스틱서치 같은 검색 엔진을 쓰면 좋지만 어렵다면 DB가 제공하는 전문 검색 기능 사용을 고려해보자. Oracle Text나 MySQL의 FULL TEXT 인덱스를 사용하면 풀 스캔 없이 문자열 검색 쿼리를 생성할 수 있다.
단일 인덱스와 복합 인덱스
- 단일 인덱스: column1만 인덱스로 사용
- 복합 인덱스: (column1, column2)를 인덱스로 사용
선택도를 고려한 인덱스 칼럼 설계
- 선택도가 높을수록 인덱스를 이용한 조회 효율이 높아진다.
- 선택도가 낮아도 인덱스 칼럼으로 적합한 상황도 있다. (status가 A,B,C 세 종류의 ENUM일 때 각 ENUM 별 데이터 비율이 A: 70%, B: 20%, C: 10%라고 했을 때 C에 대한 처리가 잦다면, C를 조건으로 하는 쿼리의 성능은 매우 빨라진다.)
커버링 인덱스 활용하기
- 커버링 인덱스: 특정 쿼리를 실해아하는 데 필요한 칼럼을 모두 포함하는 인덱스
- 커버링 인덱스를 사용하면 실제 데이터를 읽지 않기 때문에 조회 시간을 단축할 수 있다.
- (a, b)로 복합 인덱스 설정이 되어 있을 때 아래 쿼리는 실제 데이터에 접근하지 않는다. 필요한 a, b 컬럼이 모두 인덱스로 등록되어 있기 때문이다.
select a, b from table_a where a = 1 and b = '2';
몇 가지 조회 성능 개선 방법
미리 집계하기
- 게시글, 좋아요 테이블이 있을 때 게시글에 대한 좋아요 개수를 게시글 조회할 때 마다 계산하는 게 아니라 like_count 값을 게시글 테이블에 두어서 조회 성능을 개선할 수 있다. (동시성 고려가 필요하다.)
좋아요가 가격/금액 처럼 엄청나게 중요한 데이터가 아니기 때문에 비정확해도 문제는 되지 않는다. 주기적으로 보정만 해준다면 특별한 문제가 되지 않을 것
조회 범위를 시간 기준으로 제한하기
- 최신 데이터만 보여주는 것이 가능하다면, 제품 담당자와 협의해보자, 기능을 줄이고 구현을 단순화할 수 있게된다.
전체 개수 세지 않기
- 전체 개수를 세는 기능이 필요하다는 요청이 오면, 서비스 담당자에게 앞으로 성능 문제가 발생할 수 있다는 사실을 인지시키고 전체 개수를 세지 않도록 해야 한다.
오래된 데이터 삭제 및 분리 보관하기
- 데이터 개수가 늘어나면 늘어날수록 쿼리 실행 시간은 증가한다. 즉, 데이터 개수가 증가하지 않으면 실행 시간을 일정 수준으로 유지할 수 있다.
- 데이터 증가 폭을 낮추는 방법 중 하나는 과거 데이터를 삭제하는 것이다.
단편화와 최적화
- 데이터가 반복적으로 추가되고, 변경되고, 삭제되는 과정에서 데이터가 흩어져 저장되고 빈 공간이 생기는 단편화 현상이 발생할 수 있다.
- 단편화로 인한 성능저하를 해결하는 방법 중 하나는 최적화 작업이다.
알아두면 좋을 몇 가지 주의사항
쿼리 타임아웃
- 재시도가 반복되면 동시에 처리해야 하는 요청 수가 기하급수적으로 늘어나고 서버 부하는 폭증하게 된다. 좋은 해결 방안은 쿼리 실행 시간을 제한(타임아웃 설정)하는 것이다.
- 쿼리 타임아웃은 서비스와 기능의 특성에 따라 다르게 설정해야 한다. (일반 조회는 잠깐이라도 지연되면 이탈률이 많겠지만, 결제는 어느정도 지연되도 기다린다. 그리고 결제는 쿼리 타임아웃으로 중간에 취소되면 복구하기가 상당히 까다롭다.)
테이블 변경은 신중하게
- 데이터가 많은 테이블에 새로운 칼럼을 추가하거나 기존 열거 타입 컬럼을 변경할 때는 매우 주의해야 한다.
- MYSQL은 테이블을 변경할 때 새 테이블을 생성하고 원본 테이블의 데이터를 복사한 뒤, 복사가 완료되면 새 테이블로 대체한다. 복사 과정에서는 UPDATE, INSERT, DELETE 같은 DMS 작업을 허용하지 않기 때문에 복사 기간동안 서비스가 멈춘다.
- 수천만 건의 데이터가 있는 테이블에서 열거 타입 컬럼을 변경하는 과정을 서비스 운영 중에 하여 8시간 가까이 서비스가 중단된 적이 있다.
DB 최대 연결 개수
- DB에 설정된 최대 연결 개수를 확인해야 한다.
- DB의 최대 연결 개수 보다 많이 커넥션을 맺으려고 하면 연결 실패 현상이 발생하게 된다. 이 경우에는 DB의 최대 연결 개수를 늘려주는 것만으로도 문제를 해결할 수 있다.
- DB 서버의 CPU 사용률이 70% 이상으로 높다면 연결 개수를 늘리면 안된다. 이러한 경우 먼저 캐시 서버 구성이나 쿼리 튜닝 같은 조치를 통해 DB 부하를 낮추고 필요할 때 연결 개수를 늘려야 한다.