주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 ‐ 처리량과 응답 시간 - dnwls16071/Backend_Summary GitHub Wiki

📚 응답 시간(Response Time)⭐

  • 정의 : 사용자 요청을 처리하는 데 걸리는 시간
  • 응답 시간은 1초보다 짧을 때가 많다. 성능 측정을 위해 응답 시간을 잴 때는 1/1000초인 밀리초 단위를 사용한다. 밀리초를 표현할 때는 ms단위를 붙인다.

📚 처리량(Throughput)⭐

  • 정의 : 단위 시간당 시스템이 처리하는 작업량
  • TPS(Transaction Per Second) : 초당 트랜잭션 수
  • RPS(Request Per Second) : 초당 요청 수

📚 병목 지점⭐

  • 서비스 초기에는 성능 문제가 잘 발생하지 않는다. 사용자 수, 트래픽, 데이터베이스 크기 등이 모두 작기 때문이다.
  • 트래픽이 증가하면서 성능 문제가 발생하는 주된 이유는 시스템이 수용할 수 있는 최대 TPS를 초과하는 트래픽이 유입되기 때문이다.

📚 수직 확장(Scale-Up)과 수평 확장(Scale-Out)⭐

  • 수직 확장이란, CPU, 메모리, 디스크 등의 자원을 증가시키는 것을 말한다.
  • 수직 확장은 즉각적인 효과를 얻을 순 있지만 트래픽이 지속해서 증가하면 언젠가 또 다시 성능 문제가 발생한다. 그 때마다 수직 확장을 반복할 순 없는데 왜냐하면 수직 확장은 비용이 많이 들기 때문이다.
  • 트래픽이 증가하면 서버를 추가로 투입해 TPS를 높이는 방법도 고려를 해야 한다. 이렇게 서버를 늘리는 방법을 수평 확장이라 한다.
  • 수평 확장 시 로드 밸런서를 많이 사용하는데, 로드 밸런서란 사용자 트래픽을 각 서버에 골고루 분배해 한 서버에 사용자 트래픽이 몰리지 않도록 하는 역할을 하며 이를 통해 전체 서버 자원을 효율적으로 활용할 수 있다.
  • 로드 밸런서가 트래픽을 알맞게 분산시키기 위해 사용하는 방식에 정적 방식과 동적 방식이 있다.

정적 방식

  • 라운드 로빈 방식 : 클라이언트 요청을 각 서버에 순차적으로 배분하는 방식이다.
  • IP 해시 방식 : 클라이언트 IP 주소를 해시한 값을 기반으로 요청을 전달할 서버를 결정한다. IP 해시 값은 항상 동일하기 때문에 동일한 클라이언트는 항상 같은 서버로 연결됨을 보장한다.

동적 방식

  • 서버 현재 상태에 따라 트래픽을 분산하는 방식으로 트래픽이 적은 서버에 요청을 보내는 형태로 동작한다.

📚 DB 커넥션 풀(Connection Pool)⭐

  • DB를 사용하려면 다음과 같은 3단계를 거친다.
1. DB에 연결한다.
2. 쿼리를 실행한다.
3. 사용이 끝나면 연결을 종료한다.
  • 서버와 DB는 네트워크 통신을 통해 연결된다. 이 때, 네트워크 연결을 생성하고 종료하는 데 걸리는 시간은 0.5초에서 1초 이상 소요되기도 한다.
  • 예를 들어 실행 시간이 10ms인 쿼리를 실행하기 위해 연결과 종료에 50ms가 걸린다고 가정하면 실제로는 60ms가 소요되는 것이고 그 중 80% 이상이 DB 연결 및 종료에 쓰인다고 볼 수 있는 것이다.
  • 네트워크에서 DB를 연결하고 종료하는 시간은 전체 응답 시간에 영향을 준다. 응답 시간이 길어지면 전체 처리량은 떨어진다.
  • 트래픽이 증가하면 이런 현상은 더욱 두드러진다. 매 요청마다 DB를 연결하고 종료하는 과정을 거치면 트래픽이 증가할 떄 급격하게 처리량이 떨어지기도 한다.
  • 이런 문제를 피하기 위해 DB 커넥션 풀을 사용한다. DB 커넥션 풀은 DB에 연결된 커넥션을 미리 생성해서 보관한다. 애플리케이션은 DB 작업이 필요할 때 풀에서 커넥션을 가져와 사용하고 작업이 끝나면 다시 풀에 반환한다. 커넥션 풀을 사용하면 이미 연결된 커넥션을 재사용하기 때문에 응답 시간이 줄어든다는 장점이 있다.

커넥션 풀 크기⭐

  • 커넥션 풀 크기는 커넥션 풀에 미리 생성해둘 커넥션 개수를 지정하는 것이다.
  • 예를 들어 커넥션 풀 크기는 5고 한 요청에서 쿼리를 실행하는데 1초가 걸린다고 가정하자. 서버에 6개의 요청이 동시에 들어왔을 때 5개 요청은 풀에서 커넥션을 가져올 수 있다. 반면 나머지 1개 요청은 사용할 수 있는 커넥션이 없으므로 다른 요청이 커넥션 사용을 끝내고 풀에 반환할 때까지 기다려야 한다.
  • 일반적으로 트래픽은 증가했다가 감소하는 패턴을 보인다. 이런 특성에 맞게 커넥션 풀 크기도 조정하는 것이 필요하다.
  • 커넥션 풀 크기를 늘리면 처리량을 높일 수 있다. 그러나 은탄환은 없다는 말처럼 커넥션 풀 크기를 무턱대고 늘리면 안 된다. DB 상태를 보고 늘려야 한다.
  • DB 서버의 CPU 사용률이 80%에 육박하는 상황에서 커넥션 풀 크기를 늘리게 되면 DB에 가해지는 부하가 더 커져 쿼리 실행 시간이 급격히 증가할 수 있기 때문이다.

커넥션 대기 시간⭐

  • 대기 시간이란 풀에 사용할 수 있는 커넥션이 없을 때 커넥션을 얻기 위해 기다릴 수 있는 최대 시간을 의미한다.
  • 지정된 대기 시간 안에 커넥션을 구하지 못하면 DB 연결 실패 에러가 발생한다.
  • 대기 시간을 짧게 설정하면 커넥션 풀이 모두 사용 중일 때 빠르게 "일시적 오류"와 같은 에러 응답을 사용자에게 보여줄 수 있다. 에러를 응답하는게 부정적으로 보일 순 있으나 긴 시간 동안 무응답으로 유지되는 것보다는 빠르게 에러를 반환하는 것이 더 낫다.
  • 커넥션을 얻지 못했을 때 빠르게 에러를 응답해야 서버 부하가 증가하는 것도 방지할 수 있다.
  • 대기 시간을 짧게 설정하면 서버 부하를 일정 수준으로 유지할 수 있으며 서버를 안정적으로 운영하는데 도움이 된다.

최대 유휴 시간, 유효성 검사, 최대 유지 시간⭐

  • 최대 유휴 시간은 사용되지 않는 커넥션을 풀에 유지할 수 있는 최대 시간을 말한다.
    • 최대 유휴 시간을 30분으로 설정하면 30분 이상 사용되지 않은 커넥션은 종료되어 풀에서 제거된다.
    • 이 시간을 DB에 설정된 비활성화 유지 시간보다 짧게 설정하면 DB가 연결을 끊기 전에 풀에서 커넥션을 안전하게 제거할 수 있다.
    • 이렇게 되면 DB와의 연결이 끊긴 커넥션을 사용하지 않게 된다.
  • 유효성 검사는 커넥션이 정상적으로 사용할 수 있는 상태인지 여부를 확인하는 절차이다.
  • 최대 유지 시간은 커넥션이 생성된 시점부터 최대 N시간까지만 유지되는 것을 말한다. N시간이 지나면 커넥션이 유효하더라도 커넥션을 닫고 풀에서 제거된다.

📚 서버 캐시

  • DB 서버를 확장하지 않고도 응답 시간과 처리량을 개선하고 싶다면 캐시 사용을 고려할 수도 있다.
  • 캐시는 일종의 (키, 값) 쌍을 저장하는 Map과 같은 형태의 데이터 저장소이다.
  • 프로그램은 먼저 캐시에서 키에 해당하는 값을 조회한다. 값이 존재하면 바로 사용한다.
  • 존재하지 않으면 DB 쿼리를 실행해 값을 조회한 후 해당 값을 캐시에 저장하고 사용한다. 이후 동일한 요청이 들어오면 캐시에 저장된 값을 바로 사용한다.
  • 캐시에서 데이터를 읽어오면 DB와의 연동 시간이 줄어들어 응답 시간도 단축된다.

적중률과 삭제 규칙

  • 캐시가 얼마나 효율적으로 사용되는가는 적중률(hit rate)로 판단할 수 있다. 캐시 적중률은 다음과 같이 계산한다.
  • 적중률 = 캐시에 존재한 건수/캐시에서 조회를 시도한 건수
  • 캐시에 보관할 수 있는 데이터의 양에는 제한이 있으므로 캐시가 가득 찬 상태에서 새로운 데이터를 캐시에 저장하면 기존에 있던 데이터 중 하나를 제거해야 한다.
  • LRU(Least Recently Used) : 가장 오래전에 사용된 데이터를 제거한다.
  • LFU(Least Frequently Used) : 가장 적게 사용된 데이터를 제거한다.
  • FIFO(First In First Out) : 먼저 추가된 데이터를 삭제한다.
  • 캐시에는 유효 시간(만료 시간)을 설정하는 방식도 함께 사용한다. 일정 시간이 지나면 캐시에서 해당 데이터를 자동으로 제거하여 메모리를 효율적으로 관리할 수 있다.

로컬 캐시와 리모트 캐시

  • 서버가 사용하는 캐시에는 크게 두 종류가 있다.
  • 로컬 캐시는 서버 프로세스와 동일한 메모리를 캐시 저장소로 사용한다.
  • 리모트 캐시는 별도 프로세스를 캐시 저장소로 사용한다.
  • 로컬 캐시(Local Cache)의 경우 서버 프로세스와 캐시가 동일한 메모리 공간을 사용하기 때문에 캐시 데이터에 빠르게 접근할 수 있지만 저장할 수 있는 데이터 크기에 제한이 있다는 점이다. 서버 프로세스가 사용할 수 있는 메모리 양에 물리적인 한계가 있기 때문이다. 또한 서버 프로세스를 재시작하면 메모리에 존재하던 캐시 데이터가 모두 삭제되어 일시적으로 캐시 효율이 순간적으로 떨어진다는 단점도 있다.
  • 리모트 캐시(Remote Cache)의 경우 캐시 크기를 유연하게 확장할 수 있으나 서버 프로세스는 캐시 프로세스와 데이터를 주고받기 위해 네트워크 통신을 거쳐야 한다. 메모리에 저장된 데이터에 직접 접근하는 것과 비교하면 상대적으로 속도가 느리다.

캐시 사전 적재

  • 캐시 적중률이 낮아지면 전체 응답 시간이 느려질 뿐만 아니라 DB에 가해지는 부하도 급격히 증가하게 된다.
  • 이런 상황을 방지하는 방법 중 하나는 캐시에 데이터를 미리 넣어두는 것이다.

캐시 무효화

  • 캐시를 사용할 때 반드시 신경써야 되는 부분은 유효하지 않은 데이터를 적절한 시점에 캐시에서 삭제하는 것이다.
  • 캐시에 보관된 데이터 원본이 바뀌면 그에 맞춰 캐시에 보관된 데이터도 함께 변경하거나 삭제해야 한다.
  • 원본이 변경되었는데 캐시에 저장된 데이터가 갱신되지 않으면 사용자는 오래된 잘못된 정보를 확인하게 되는 문제가 발생할 수 있다.

가비지컬렉터와 메모리 사용

  • 메모리를 많이 사용하고 생성된 객체가 많을수록 사용하지 않는 객체를 찾는 데 시간이 오래 걸린다.
  • GC 알고리즘과 메모리 사용 패턴에 따라 차이가 있지만 사용하는 메모리양과 객체 수가 많을수록 GC 실행 시간은 길어진다.
  • 대량으로 객체가 생성되는 것을 방지하려면 조회 범위를 제한해야 한다.

응답 데이터 압축

  • 사용자의 네트워크 속도가 느리면 응답 시간이 길어진다.
  • 서버는 사용자의 네트워크 속도를 제어할 수 없지만 전송하는 데이터 크기는 제어할 수 있다. 이 때, 사용하는 방법이 바로 응답 데이터를 압축해서 전송하는 것이다.
  • 웹 서버가 전송하는 응답 데이터 중에서 HTML, CSS, JS, JSON과 같이 텍스트로 구성된 응답은 압축하면 데이터 전송량을 크게 줄일 수 있다.
  • 웹 브라우저나 HTTP 클라이언트는 Accept-Encoding 헤더를 통해 서버에 처리할 수 있는 압축 알고리즘을 알린다. 예를 들어, gzip, deflate 알고리즘을 사용해서 압축을 풀 수 있다면 다음과 같이 Accept-Encoding 헤더를 전송한다.
  • 웹 서버는 Accept-Encoding 헤더에 명시된 알고리즘 중에서 자신이 지원하는 방식이 있을 경우, 해당 압축 알고리즘으로 응답 데이터를 압축해서 전송한다. 이 때, 사용된 압축 알고리즘은 Content-Encoding 응답 헤더를 클라이언트에 전달한다.
  • HTLM, CSS, JS, JSON과 같은 텍스트 형식의 응답은 압축률이 높아 효과적이다. 반면 이미지나 이미 압축한 데이터는 다시 압축해도 효과가 없다. 따라서 모든 응답에 압축을 적용하지 말고 텍스트 형식의 데이터에 압축을 적용하는 것이 좋다.

정적 자원과 브라우저 캐시

  • 서버가 전송하는 트래픽을 줄이면서 브라우저가 더 빠르게 화면을 표시할 수 있는 방법이 있는데 바로 클라이언트 캐시를 활용하는 것이다.
  • HTTP 프로토콜에는 데이터를 응답할 때 Cache-Control이나 Expires 헤더를 이용해 클라이언트가 응답 데이터를 일정 시간 동안 저장해둘 수 있도록 설정할 수 있다.
Cache-Control : max-age=60
  • 해당 헤더의 의미는 최대 60초 동안 브라우저 캐시에 보관한다는 의미가 된다.
  • 같은 주소를 60초 이내 다시 요청하면 서버에 요청을 보내지 않고 로컬에 보관한 데이터를 사용해서 표시한다. 로컬에서 불러오기 때문에 처리 속도가 빨라진다.
  • 브라우저 캐시를 활용하면 서버 입장에서도 전송해야 할 트래픽이 줄어들어 그만큼 네트워크 전송 비용을 아낄 수 있다.

정적 자원과 CDN

  • 브라우저 캐시를 활용하면 네트워크 트래픽을 줄일 수 있지만 여전히 문제가 발생할 수 있다.
  • 브라우저 캐시는 브라우저 단위로 동작하기 때문에 동시에 많은 사용자가 접속하면 순간적으로 많은 양의 이미지, JS, CSS를 전송하게 된다.
  • 이로 인해 빠르게 네트워크가 포화되어 응답 시간이 급격히 느려진다.
  • CDN은 컨텐츠를 제공하기 위한 별도의 네트워크를 의미한다.
  • 사용자는 CDN이 제공하는 URL을 통해 컨텐츠에 접근한다. CDN 서버에 요청하는 컨텐츠가 없다면 오리진 서버에서 읽어와 제공한다.
  • 이미지, JS 같은 정적 자원을 CDN으로 제공하면 오리진 서버가 처리해야 할 트래픽을 상당히 줄일 수 있다.
  • CDN은 여러 지역에 서버를 둔다. 사용자는 가까운 곳에 위치한 서버에 연결해서 컨텐츠를 다운로드한다. 이로 인해 컨텐츠를 더 빠르게 받을 수 있고 이미지나 JS와 같은 파일이 빠르게 로드되는 만큼 브라우저도 더 빠르게 화면을 표시할 수 있다. 또한 CDN을 활용하면 오리진 서버에서 직접 컨텐츠를 제공하는 것보다 트래픽 비용도 적게 든다.

대기 처리

  • 사용자가 순간적으로 폭증할 때가 있는데 이 때, 처리할 수 있는 시스템의 처리량을 무작정 늘리기보다는 수용할 수 있는 수준의 트래픽만 받아들이고 나머지는 대기 처리하는 것이다.