2024-08-12-TIL
2024-08-12-TIL
Today I Learned
Caching Best Practices
캐싱을 적용할 대상은 어떻게 정하는 것이 좋을까?
아래 두 블록은 이 질문을 정리하면서 모아 둔 답변 메모다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
캐시(Cache)는 자주 접근하는 데이터를 임시로 저장해두어 시스템 성능을 개선하는 데 사용됩니다. 캐시를 적용하기에 적절한 부분은 다음과 같습니다:
### 1. **데이터베이스 조회 결과**
- **빈번한 조회:** 특정 데이터베이스 쿼리가 반복적으로 실행되고, 그 결과가 자주 동일한 경우 캐시를 통해 조회 속도를 크게 향상시킬 수 있습니다.
- **비교적 변경이 적은 데이터:** 자주 변경되지 않는 정적인 데이터에 캐시를 적용하면 효율적입니다. 예를 들어, 제품 카탈로그나 사용자 프로필 정보 등이 이에 해당할 수 있습니다.
### 2. **복잡한 계산 결과**
- **고비용 연산:** 계산이 복잡하고 시간이 많이 걸리는 작업의 결과를 캐시에 저장해두면, 이후 동일한 요청에 대해 캐시된 결과를 사용하여 성능을 개선할 수 있습니다. 예를 들어, 통계 정보, 그래프 데이터, 머신 러닝 모델의 예측 결과 등이 있습니다.
### 3. **외부 API 호출**
- **빈번한 외부 서비스 요청:** 외부 API를 반복적으로 호출해야 하는 경우, 응답 데이터를 캐시해두면 네트워크 비용을 줄이고 성능을 향상시킬 수 있습니다.
- **외부 시스템의 레이트 리밋 제한 회피:** 외부 API가 호출 빈도에 제한을 두는 경우, 캐시를 통해 호출 수를 줄여 레이트 리밋에 걸리는 것을 방지할 수 있습니다.
### 4. **정적 자원**
- **이미지, CSS, JavaScript 파일:** 웹 애플리케이션에서 정적 자원(이미지, 스타일시트, 자바스크립트 파일 등)을 캐싱하면 페이지 로드 속도를 크게 개선할 수 있습니다.
### 5. **세션 데이터**
- **사용자 세션 관리:** 사용자 세션 정보를 캐시하면, 세션 데이터를 저장하고 조회하는 작업이 신속하게 처리될 수 있습니다.
### 6. **템플릿 엔진의 렌더링 결과**
- **동적 페이지의 캐싱:** 동적으로 생성되는 페이지나 템플릿의 렌더링 결과를 캐시하면, 동일한 페이지 요청에 대해 캐시된 결과를 반환하여 성능을 개선할 수 있습니다.
### 7. **DNS 조회 결과**
- **DNS 캐싱:** 도메인 이름을 IP 주소로 변환하는 DNS 조회 결과를 캐시하면, 같은 도메인에 대한 반복적인 요청 시 조회 시간을 절약할 수 있습니다.
캐시를 적용할 때는 캐시 갱신 주기, 캐시된 데이터의 유효성, 캐시 오버헤드 등도 고려해야 합니다. 잘못된 캐시 적용은 오히려 성능을 저하시킬 수 있으므로, 캐시의 장점과 단점을 모두 고려한 전략이 필요합니다.
도메인과 기능으로 따지면 어떤 부분에서 유용하고 적절한가? 그리고 적절하지 않은 사용은 어떤 경우가 있을까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
캐시의 유용성과 적절성은 도메인과 기능에 따라 다릅니다. 다음은 각 도메인과 기능별로 캐시의 유용성과 적절한 사용 사례, 그리고 적절하지 않은 사용 사례를 설명합니다.
### 1. **웹 애플리케이션**
- **유용한 부분:**
- **정적 콘텐츠 (CSS, JS, 이미지):** 웹 페이지 로딩 속도를 개선하기 위해 캐시를 사용하면 사용자가 동일한 리소스를 반복적으로 다운로드하지 않아도 됩니다.
- **페이지 레벨 캐시:** 뉴스 사이트나 블로그와 같이 자주 변경되지 않는 페이지의 HTML을 캐시하여 서버 부하를 줄이고 응답 속도를 높일 수 있습니다.
- **API 응답 캐시:** 자주 호출되는 API 엔드포인트의 응답을 캐시하면 네트워크 요청을 줄이고 성능을 높일 수 있습니다.
- **적절하지 않은 사용:**
- **사용자 맞춤형 데이터:** 개인화된 정보(예: 사용자 대시보드, 개인화된 추천)는 사용자마다 다르기 때문에 캐시 사용 시 비정상적인 데이터 노출 위험이 있습니다.
- **자주 변경되는 콘텐츠:** 실시간으로 변경되는 데이터(예: 소셜 미디어 피드, 주식 가격)는 캐시의 유효성을 유지하기 어렵기 때문에 부적절합니다.
### 2. **이커머스**
- **유용한 부분:**
- **제품 정보:** 자주 변경되지 않는 제품 상세 정보나 카테고리 데이터를 캐시하여 검색 및 조회 성능을 개선할 수 있습니다.
- **카트 및 주문 과정:** 주문 확인 페이지나 장바구니 상태를 캐시하면 성능을 개선할 수 있습니다. 그러나 이 경우 캐시 무효화 전략이 중요합니다.
- **적절하지 않은 사용:**
- **재고 상태:** 실시간으로 변동하는 재고 정보를 캐시하는 것은 사용자에게 잘못된 정보를 제공할 위험이 있습니다.
- **가격 정보:** 가격이 자주 변경되는 상품에 대한 정보를 캐시하면, 구식 데이터를 사용자에게 제공할 수 있어 문제가 발생할 수 있습니다.
### 3. **게임**
- **유용한 부분:**
- **레벨 데이터, 지도 정보:** 자주 사용되는 게임 레벨 정보나 맵 데이터를 캐시하여 로딩 속도를 빠르게 할 수 있습니다.
- **게임 상태 저장:** 게임 진행 상태를 캐시하여, 사용자가 게임을 중단하고 다시 시작할 때 빠르게 복구할 수 있습니다.
- **적절하지 않은 사용:**
- **실시간 점수 및 랭킹:** 실시간으로 변동하는 점수나 랭킹은 캐시하면 안 됩니다. 실시간 업데이트가 중요하기 때문입니다.
- **멀티플레이어 상태:** 멀티플레이어 게임에서 실시간 동기화가 필요한 정보는 캐시로 인해 지연되면 안 됩니다.
### 4. **금융 서비스**
- **유용한 부분:**
- **환율, 주가 히스토리:** 과거 데이터나 환율 히스토리 등 변동이 적은 금융 데이터를 캐시하여 조회 성능을 높일 수 있습니다.
- **사용자 프로필:** 사용자 프로필 정보는 자주 변경되지 않으므로 캐시를 사용하여 접근 속도를 개선할 수 있습니다.
- **적절하지 않은 사용:**
- **실시간 거래 데이터:** 실시간 거래 정보는 절대 캐시해서는 안 됩니다. 정확성과 신뢰성이 중요하기 때문입니다.
- **보안 관련 데이터:** 금융 거래와 관련된 보안 정보는 캐시하지 않아야 합니다. 보안 위험이 크기 때문입니다.
### 5. **검색 엔진**
- **유용한 부분:**
- **검색 쿼리 결과:** 자주 요청되는 검색 쿼리의 결과를 캐시하면 검색 속도를 크게 개선할 수 있습니다.
- **인덱스 데이터:** 검색 인덱스의 일부를 캐시하여 검색 요청에 빠르게 응답할 수 있습니다.
- **적절하지 않은 사용:**
- **실시간으로 업데이트되는 인덱스:** 실시간으로 크롤링되어 업데이트되는 인덱스 데이터를 캐시하면, 최신 정보를 제공하지 못할 수 있습니다.
### 적절하지 않은 사용 예시 요약:
- **실시간 업데이트가 중요한 데이터:** 금융 거래 정보, 주식 가격, 실시간 게임 상태 등은 캐시하지 않는 것이 좋습니다.
- **개인화된 사용자 데이터:** 사용자 맞춤형 데이터는 캐시 사용 시 데이터 누출이나 혼선의 위험이 있습니다.
- **보안이 중요한 정보:** 보안 정보를 캐시하면 민감한 정보가 노출될 수 있어 위험합니다.
캐시를 사용할 때는 캐시의 유효성 관리와 무효화 전략을 명확히 설정하는 것이 중요합니다. 잘못된 캐시 설정은 오히려 시스템의 신뢰성을 저하시킬 수 있습니다.
메모를 판단 기준으로 줄이면 세 가지 질문이 된다.
- 읽기에 비해 쓰기가 드문가: 읽기 비율이 높고 같은 결과가 반복될수록 캐시 적중률이 높다.
- 잠시 오래된 값을 보여 줘도 되는가: 허용할 수 있는 지연 시간이 곧 TTL의 상한이다. 재고, 잔액, 가격처럼 틀리면 손해가 생기는 값은 캐시하지 않거나, 캐시하더라도 최종 판단은 원본 저장소로 한다.
- 누구에게나 같은 값인가: 사용자별 데이터는 캐시 키에 사용자 식별자를 반드시 포함해야 하고, 그렇지 않으면 다른 사용자의 데이터가 노출된다.
여기에 더해 캐시를 넣으면 장애 지점과 정합성 문제가 하나 늘어난다는 점을 비용으로 함께 계산해야 한다. 캐시가 비었을 때 원본 저장소로 요청이 한꺼번에 몰리는 상황(cache stampede)과, 데이터 변경 시 어떤 키를 무효화할지도 설계 대상이다.
Reverse Proxy
리버스 프록시는 클라이언트 앞이 아니라 서버 앞에 서서, 클라이언트의 요청을 대신 받아 뒤쪽 서버로 전달하는 서버다. 클라이언트 입장에서는 리버스 프록시가 곧 서버처럼 보이고, 실제 원본 서버의 주소는 드러나지 않는다. 반대로 포워드 프록시는 클라이언트 앞에서 클라이언트를 대신해 외부로 요청을 보낸다.
Wikipedia의 Reverse proxy 항목은 리버스 프록시의 용도로 여러 서버로의 로드 밸런싱, 애플리케이션 방화벽 기능을 통한 DoS와 DDoS 방어, 정적이거나 동적인 콘텐츠의 캐싱, 압축, 원본 서버 대신 TLS 암호화를 처리하는 일을 든다. 로드 밸런서는 리버스 프록시가 하는 일 중 트래픽 분산에 특화된 것이라 볼 수 있고, Nginx처럼 하나의 소프트웨어가 두 역할을 모두 하는 경우가 많다. 앞의 캐싱 메모에 나온 정적 자원과 페이지 캐싱도 이 계층에서 처리할 수 있다.
JMeter
Apache JMeter는 Apache 재단의 오픈소스 부하 테스트 도구다. Thread Group으로 가상 사용자 수와 반복 횟수를 정하고, Sampler로 보낼 요청(HTTP, JDBC 등)을, Listener로 결과 수집 방식을 정해 테스트 계획을 .jmx 파일로 만든다. GUI는 테스트 계획을 만들고 디버깅하는 데 쓰고, 실제 부하를 줄 때는 GUI가 자원을 많이 쓰므로 CLI 모드로 실행한다.
1
jmeter -n -t plan.jmx -l result.jtl -e -o report/
-n은 non-GUI 모드, -t는 테스트 계획, -l은 결과 파일, -e -o는 HTML 리포트 생성이다. 응답 시간은 평균보다 p95, p99 같은 백분위로 보는 것이 꼬리 지연을 놓치지 않는 방법이다.
The Frugal Architect
The Frugal Architect는 Amazon CTO Werner Vogels가 비용을 고려한 아키텍처 원칙을 일곱 가지 법칙으로 정리한 것이다. 첫 번째 법칙이 Make Cost a Non-functional Requirement로, 비용을 보안, 가용성, 성능과 같은 비기능 요구 사항으로 다루라는 것이다. 설계 초기부터 비용을 측정 가능한 요구 사항으로 두지 않으면, 나중에 비용 문제가 드러났을 때 구조를 바꾸기 어렵다.
나머지 법칙은 비용을 비즈니스와 맞추기, 아키텍처는 트레이드오프의 연속이라는 점, 관측되지 않는 시스템은 모르는 비용을 만든다는 점, 비용 통제 장치 두기, 비용 최적화는 점진적이라는 점, 검증하지 않은 성공은 잘못된 가정으로 이어진다는 점이다. 아래 Channy의 글이 일곱 가지 원칙을 한국어로 정리한다.
FQCN
FQCN(Fully Qualified Class Name)은 패키지 이름까지 포함한 클래스의 전체 이름이다. String의 FQCN은 java.lang.String이다. 서로 다른 패키지에 같은 이름의 클래스가 있을 수 있으므로, 클래스를 유일하게 식별하려면 FQCN이 필요하다. 런타임에는 Class.getName()으로 얻고, 리플렉션의 Class.forName("com.example.Foo")나 설정 파일, 로깅 설정에서 클래스를 지정할 때 쓴다. 중첩 클래스는 getName()에서 Outer$Inner처럼 $로 구분되고, getCanonicalName()은 Outer.Inner 형태를 돌려준다.
Java 직렬화에도 FQCN이 들어간다는 점이 아래 두 항목과 이어진다. 직렬화된 데이터에 클래스의 전체 이름이 기록되므로, 패키지를 옮기거나 클래스 이름을 바꾸면 이전에 저장한 데이터를 역직렬화할 수 없다.
Java Serialization
Java Object Serialization Specification의 아키텍처 장은 직렬화를 객체 그래프를 바이트 스트림으로 쓰고 다시 읽는 메커니즘으로 설명한다. Serializable을 구현한 클래스는 ObjectOutputStream.writeObject로 쓰고 ObjectInputStream.readObject로 읽는다. 스트림에는 클래스 설명(클래스 이름과 serialVersionUID)과 필드 값이 함께 기록되고, 참조로 연결된 객체도 함께 직렬화된다.
transient와static필드는 직렬화되지 않는다.serialVersionUID를 선언하지 않으면 클래스 구조로 계산되므로, 필드 하나만 바뀌어도 기존 데이터를 읽지 못할 수 있다.writeObject,readObject를 직접 정의해 직렬화 형식을 제어할 수 있다.
이 형식은 Java와 특정 클래스 정의에 강하게 묶이므로 외부 저장소나 다른 언어와 데이터를 주고받는 용도에는 맞지 않는다.
Redis Serialization
“Redis 직렬화”는 두 가지 의미로 쓰인다. 하나는 Redis 클라이언트와 서버가 주고받는 통신 프로토콜인 RESP(Redis Serialization Protocol)이고, 다른 하나는 애플리케이션 객체를 Redis 값으로 저장할 때의 직렬화 방식이다.
RESP는 구현하기 쉽고, 파싱이 빠르고, 사람이 읽을 수 있도록 설계된 프로토콜이다. 첫 바이트가 데이터 타입을 나타내고(+ 단순 문자열, - 오류, : 정수, $ bulk string, * 배열), 각 부분은 CRLF로 구분된다. 클라이언트는 명령을 bulk string의 배열로 보낸다. 예를 들어 LLEN mylist는 다음과 같이 전송된다.
1
*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n
bulk string은 길이를 앞에 적기 때문에 바이너리 데이터도 안전하게 담을 수 있다. Redis 2.0부터 RESP2가 표준이 되었고, Redis 6.0에서 맵, 집합, null, boolean 같은 타입을 더한 RESP3를 HELLO 명령으로 선택할 수 있게 되었다.
애플리케이션 쪽 직렬화는 RESP가 아니라 클라이언트 라이브러리의 몫이다. Spring Data Redis의 기본 값 직렬화는 JDK 직렬화라서 앞의 FQCN 문제가 그대로 따라온다. 그래서 JSON이나 단순 문자열처럼 언어 중립적인 형식으로 저장하는 편이 안전하다.
참고한 자료외부 출처 17
외부 출처
- Reverse Proxy and Load Balancer: Understanding the Difference
- [리버스 프록시란? 프록시서버 설명] — Cloudflare
- (Infra) 리버스 프록시(reverse proxy) 서버 개념
- Reverse proxy
- 리버스 프록시(Reverse Proxy) 쉽게 이해하기: 개념부터 필요성, 오픈 소스 솔루션까지
- Apache JMeter - Download Apache JMeter
- apache/jmeter: Apache JMeter open-source load testing tool for analyzing and measuring the performance of a var
- (JMeter) Apache JMeter를 이용한 간단한 성능 테스트
- thefrugalarchitect.com/laws/make-cost-a-non-functional-requirement.html
- The Frugal Architecture알뜰한 아키텍처를 위한 7가지 원칙 :: Channy’s Blog
- Get the fully-qualified name of a class in Java
- quora.com/What-is-a-fully-qualified-class-name-in-Java
- FQCN (Full Qualified Class Name)
- Java Object Serialization Specification: 1 - System Architecture
- Introduction to Java Serialization
- Serialization and Deserialization in Java
- Redis serialization protocol specification
댓글
아직 댓글이 없습니다