1. 캐시

앞서 응답 시간을 줄이고 처리량을 높이기 위해서 DB 서버를 수직/수평 확장할 수 있다는 걸 확인했다. 하지만 이는 비용이 많이 든다. 그렇다면 차선책으로는 캐시(Cache) 사용이 있다. 캐시를 사용하면 DB 서버를 확장하지 않고도 응답 시간과 처리량을 개선할 수 있다.

 

캐시는 일종의 (키, 값) 쌍을 저장하는 Map과 같은 형태의 데이터 저장소이다.

일반적으로 캐시에서 데이터를 읽는 속도가 DB보다 빠르기 대문에 자주 조회되는 데이터를 캐시에 보관하면 응답 시간을 줄일 수 있다.

캐시에는 DB 데이터, 복잡한 계산 결과, 외부 API 연동 결과도 저장할 수 있어 응답 시간을 줄이는 데 활용할 수 있다.

 

캐시의 동작 방식

캐시의 동작 방식은 다음과 같다. 캐시에서 키에 해당하는 값을 조회한다.

  • 값이 존재하면 바로 사용한다.
  • 값이 존재하지 않으면 DB 쿼리를 실행해 값을 조회한 후, 해당 값을 캐시에 저장한 후 사용한다. 이후 동일한 요청이 들어오면 캐시에 저장된 값을 바로 사용한다.

 

2. 캐시 적중률과 삭제 규칙

캐시 적중률

캐시가 얼마나 효율적으로 사용되는지는 적중률(hit rate)로 판단할 수 있고, 계산은 다음과 같다.

적중률 = 캐시에 존재한 건수 / 캐시에서 조회를 시도한 건수

적중률이 높을 수록 DB 연동 횟수는 줄어들기에 응답시간/DB부하는 감소하고 처리량은 증가하는 성능 향상이 발생할 수 있다.

 

적중률을 높이는 가장 간단한 방법은 캐시에 최대한 많은 데이터를 저장하는 것인데, 이는 캐시를 저장하는 메모리 자원의 한계 때문에 메모리에 무한으로 저장할 수는 없다. 이 때 일정한 규칙에 따라 캐시 데이터를 삭제해 캐시 메모리를 확보하는 방법이 캐시 삭제 규칙이다.

 

캐시 삭제 규칙

캐시 삭제 대상을 정하는 규칙으로는 다음과 같다.

 

  • LRU(Least Recently Used) : 가장 오래전에 사용된 데이터 제거(대부분 오래된 데이터 보다 최신 데이터를 더 자주 조회하기 때문)
  • LFU(Least Frequently Used) : 가장 적게 사용된 데이터 제거
  • FIFO(First In First Out) : 먼저 추가된 데이터를 먼저 삭제

 

3. 로컬 캐시, 리모트 캐시

서버가 사용하는 캐시는 로컬 캐시와 리모트 캐시가 있다.

로컬 캐시

로컬 캐시는 서버 프로세스와 동이란 메모리를 캐시 저장소로 사용하는 캐시이고, 메모리에 캐시 데이터를 저장해 인 메모리 캐시라고도 불린다. 로컬 캐시 구현 기술로는 Caffeine(자바), go-cache(Go), node-cache(Node.js) 

  • 장점
    • 속도, 서버 프로세스와 캐시가 동일한 메모리 공간을 사용하기에 캐시 데이터에 빠르게 접근 가능
    • 별도의 외부 연동이 필요 없어 구조를 단순하게 유지할 수 있다
  • 단점
    • 서버 프로세스 사용 메모리양의 물리적 한계로 캐시에 저장할 수 있는 데이터 크기에 제한이 있다는 것
    • 서버 프로세스 재시작하면 메모리에 존재하던 캐시 데이터가 삭제되므로 일시적으로 캐시 호율(적중률)이 감소한다

 

리모트 캐시

리모트 캐시는 별도 프로세스를 캐시 저장소로 사용하는 캐시이고, 구현 기술로는 Redis가 대표적이다. 

  • 장점
    • 별도의 캐시 서버를 가지고 있기에 캐시 크기를 유연하게 확장 가능(Redis는 수평 확장 기능 제공)
    • 서버 프로세스가 재시작되더라도 레디스에 저장된 캐시 데이터는 그대로 유지
  • 단점
    • 캐시 서버가 별도로 있기에 서버 프로세스와 캐시 프로세스는 네트워크 통신을 해야하기에 인 메모리보다 속도가 느리다
    • 리모트 캐시 운영을 위한 별도의 서버 장비와 프로세스가 필요하므로 시스템 구조가 복잡해진다

 

 

로컬 캐시와 리모트 캐시는 각각 장단점이 뚜렷하므로 데이터 규모, 변경 빈도, 응답 시간, 처리량 등을 판단 기준으로 삼아 상황에 맞게 선택해야 한다. 

  • 캐시에 보관할 데이터 규모가 적고 변경빈도가 매우 낮은 경우(ex. 홈 화면의 최신 공지글) -> 로컬 캐시
  • 캐시에 보관할 데이터 규모가 크거나 배포 빈도가 높은 경우 -> 리모트 캐시

 

4. 캐시 사전 적재

트래픽이 특정 이벤트 이후로 순간적으로 급증하는 패턴을 보인다면 해당 데이터를 캐시에 미리 저장하면 성능 저하를 예방할 수 있다.

 

5. 캐시 무효화

캐시를 사용할 때 반드시 신경 써야 할 점은 유효하지 않은 데이터를 적절한 시점에 캐시에서 삭제하는 것이다. 캐시에 보관된 데이터의 원본이 바뀌면, 그에 맞춰 캐시에 보관된 데이터도 함께 변경하거나 삭제해야한다. 업데이트가 되지 않은 정보를 조회하는 것은 의미가 없기 때문이다.

 

캐시에 저장된 데이터의 특성에 따라 캐시를 무효화하는 시점을 달리해야한다.

 

  • 민감 데이터
    • 예시 : 가격 정보, 게시글 내용
    • 민감 데이터는 변경되는 즉시 캐시를 무효화 필요
    • 리모트 캐시 보관(로컬 캐시를 사용하지 않는 경우는 보통 서버는 여러대가 있고 로컬 캐시 무효화는 해당 서버의 캐시만 적용하고 다른 서버의 로컬 캐시에는 영향을 줄 수 없기 때문)
  • 민감하지 않거나 크기가 작은 데이터
    • 이 경우 캐시 유효 시간을 설정해 주기적으로 갱신하는 방식으로 사용해도 됨

 

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

 

가비지 컬렉터

가비지 컬렉터를 사용하는 언어는 사용이 끝난 객체를 힙 메모리에서 바로 삭제하지 않고 정해진 규칙에 따라 사용하지 않는 메모리를 찾아서 반환하는데, 실행 규칙은 다음과 같다.

 

1. 힙 메모리 사용량 일정 비율 초과

2. 일정주기로 자동 실행됨

 

  • 장점
    • 개발자가 메모리를 직접 관리해야 하는 부담을 줄여준다
    • 보안 이슈 감소(코드로 메모리를 관리할 수 없기 때문에)
  • 단점
    • 가비지 컬렉터 실행을 위해 애플리케이션 실행이 중단되기 때문에(Stop-The-World) 응답 시간에 영향을 준다
      • 사용하는 메모리양과 객체 수가 많을수록 GC 실행 시간이 길어질 수 있는데, 이는 반대로 메모리 사용을 줄이면 GC 시간도 줄어들 가능성이 높다(검사 대상 객체 수가 그만큼 줄어들기 때문). 따라서 실제 메모리 사용 패턴에 맞게 최대 힙 크기를 조정해야 한다.

 

주의 사항

  • 실제 메모리 사용 패턴에 맞게 최대 힙 크기를 조정해야 한다.
  • 객체 조회 범위를 제한해서 대량 객체 생성에 주의하는 것이 좋다. 하나의  객체가 차지하는 용량이 크면 힙에서 메모리가 부족할 수 있기에 한 번에 조회할 수 있는 데이터 개수도 트래픽 규모, 메모리 크기에 적합하게 제한해야 한다.

 

스트림

파일 다운로드와 같은 기능을 구현할 때는 스트림을 활용하는 것이 좋다.

 

 

아래처럼 파일 데이터를 한꺼번에 메모리에 로딩한 후 응답하는 방식은 피해야 한다.

byte[] bytes = Files.readAllBytes(Path.of("path")); // 파일을 한 번에 메모리에 로딩
out.write(bytes);

-> 파일을 문자열이 아닌 byte로 처리하는 이유는 파일 형태 때문인데, 
   파일 형태로는 이미지, 음악, 바이너리 타입이 있다.

 

파일 크기와 동시 사용자 수에 따라 메모리 사용량이 급증할 수 있기 때문이다.

 

 

아래는 스트림을 활용한 파일 다운로드 코드이다. 이 코드를 사용할 경우 파일 전체를 한 번에 메모리에 로딩하는 용량이 800KB 밖에 안 된다.

InputStream is = Files.newInputStream(Path.of("path"));
byte[] buffer = new byte[8192]; // 8KB 메모리
int read;
while((read = is.read(buffer, 0, 8192)) >= 0) {
    out.write(buffer, 0, read);
}

// is.transferTo(out)와 동일 코드
  • Files.newInputStream() : 파일에서 바이트 단위로 데이터를 읽을 수 있는 InputStream 반환
  • OutputStream.wirte() : 파일 내용을 byte 단위로 읽는다
  • readAllBytes() 대신 newInputStream() 사용, 8KB 씩 buffer로 끊어서 read()

 

예시) 대량 데이터를 한 번에 메모리에 올려서 서버 중지된 사례

풀 GC를 했는데도 메모리 포화 현상이 발생했고, 이로 인해 서버 응답 지연 문제가 발생했다.

엑셀 다운로드 기능의 응답시간이 길어지자 사용자 요청 취소 및 재요청이 발생해 중복된 데이터가 계속 적재되어 Out of Memory Error가 발생한 상황이었다.

 

힙 메모리 : 임의로 생성한 객체들이 동적으로 할당되는 공간

힙 덤프(heap dump) : 운영 중인 애플리케이션의 힙 메모리 영역을 스냅샷으로 기록한 내역을 저장한 파일

 

해결방법

1. 메모리 생성하지 않고 로컬에 스트림 형태로 생성하도록 변경

2. 조회 엑셀 데이터를 스트림 형태로 받아 순차적으로 처리

 

 

7. 응답 데이터 압축

응답 시간에는 데이터 전송 시간이 포함되는데, 이는 1) 네트워크 속도, 2) 전송 데이터 크기에 영향을 받는다. 

이때 사용자의 네트워크 속도는 서버가 제어할 수 없지만 전송 데이터 크기는 '압축'을 통해 제어할 수 있다.

 

전송하는 파일의 크기가 줄어들면 전송 크기만큼 전송 시간도 빨라져 결과적으론 응답 시간이 빨라지고 비용이 줄어드는 효과를 얻을 수 있다.

 

1) Accept-Encoding 요청 헤더, Content-Encoding 응답 헤더

  • Accept-Encoding 헤더 : 서버에 처리할 수 있는 압축 알고리즘을 알린다. 요청 데이터는 이 압축 알고리즘으로 해제 가능하다는 의미
  • Content-Encoding : 요청에서 준 옵션에서 이걸 선택해서 압축을 풀었다는 의미
Accept-Encoding : gzip, deflate
-> gzip, deflate 알고리즘을 사용해서 압축을 풀 수 있다는 의미

 

 

2) 압축 시 고려할 사항

  • 모든 응답에 압축을 적용하지 말고 텍스트 형식의 데이터에 압축을 적용하는 것이 좋다.
    • html, css, js, json 같은 텍스트 형식의 응답 -> 압축률이 높아 효과적
    • jpeg 이미지, zip 파일 같은 이미 압축한 데이터 -> 다시 압축하더라도 비효과적
  • 웹 서버에 압축 설정을 적용했음에도 실제 응답 데이터가 압축되지 않았다면 방화벽 설정을 확인해야 한다.

 

8. 로컬 캐시

서버가 응답하는 데이터의 종류는 2가지가 있는데, 하나는 동적 자원이고 다른 하나는 정적 자원이다.

  • 동적 자원: 브라우저가 요청할 때마다 결과가 바뀌는 데이터, ex) 제품 목록 html, 제품 상세 JSON 응답
  • 정적 자원 : 같은 URL에 대해 응답하는 같은 데이터, ex) 이미지, JS, CSS

이때 정적 자원은 전체 트래픽에서 상당한 비중을 차지하는데, 같은 페이지에 대해 정적 자원은 항상 동일한데 매번 큰 데이터를 받아와야하니 트래픽에도 비용에도 부담이 될 수 있다. 이 문제를 클라이언트 캐시 활용을 통해 해결할 수 있다.

 

클라이언트 캐시를 통해 1) 서버가 전송하는 트래픽을 줄이면서 2) 브라우저가 더 빠르게 화면을 표시할 수 있다. 

HTTP 프로토콜에서는 데이터를 응답 할 때, Cache-Control이나 Expires 헤더를 사용해 클라이언트가 응답 데이터를 일정 시간 동안 저장해둘 수 있도록 설정할 수 있다. 

// 같은 주소를 60초 이내에 다시 요청할 경우
// 서버로 요청을 보내지 않고 로컬에 보관한 데이터를 사용해 표시한다.
Cache-Control: max-age=60

 

브라우저 캐시를 활용하면 서버 입장에서도 전송해야 할 트래픽이 줄어들어 그만큼 네트워크 전송 비용을 아낄 수 있다.

 

정적 자원 관리 시 주의할 점으로는 업로드 파일의 크기에 제한을 두거나 파일의 크기를 신중하게 관리해야 할 필요가 있다. 그렇지 않으면 트래픽 비용이 증가하는 문제가 발생할 수 있다.

 

9. CDN(Content Delivery Network)

CDN은 콘텐츠를 제공하기 위한 별도의 네트워크를 의미하고, 이를 통해 클라이언트 콘텐츠를 더 빠르고 효율적으로 전달할 수 있다.

대표적인 CDN 서비스로는 Amazon CloudFront, Akamai, Cloudflare 등이 있다.

 

1) CDN 동작 방식

  • 사용자는 CDN이 제공하는 URL을 통해 콘텐츠에 접근한다.
  • 근교 CDN 서버에 요청한 콘텐츠가 없으면 오리진 서버에서 읽어와 콘텐츠를 제공한다.
  • 오리진 서버에서 읽어온 콘텐츠는 캐시에 보관한다.
  • 이후 동일한 콘텐츠에 대한 요청이 들어오면 오리진 서버가 아닌 캐시에 보관한 데이터를 응답한다.

-> 정적 자원(이미지, JS) 같은 자원을 CDN으로 제공하면 오리진 서버가 처리해야 할 트래픽을 상당히 줄일 수 있다.

 

2) CDN 사용의 장점

  • 콘텐츠의 빠른 다운로드
  • 트래픽 비용 절감
  • 콘텐츠의 빠른 제공

 

10. 대기 처리

콘서트 예매와 같이 짧은 시간 동안만 트래픽이 폭증하는 경우에 2가지 대응 방법이 있다.

 

1) 인프라 증설

서버 증설 : 클라우드 사용시 쉽게 증설하고 축소할 수 있음

DB 증설 : 쉽게 증설하기 어렵고(미리 증설 필요, 증설 후 쉽게 축소 불가능), 사용 비용이 높다

 

2) 대기 처리(제어)

수용할 수 있는 수준의 트래픽만 받아들이고, 나머지는 대기 처리하는 방법

 

이점으로는

  • 서버 증설 없이 안정적인 서비스 제공이 가능하고
  • 사용자가 새로고침 시에는 순번이 뒤로 밀려 트래픽 증가를 방지할 수 있다
  • 여러 솔루션/레퍼런스가 존재해 빠르게 도입 가능하다

 

 

+ Recent posts