웹의 캐싱 모델은 지난 20년 이상 우리에게 잘 작동해 왔다. 최근 들어, 개인정보 보호를 이유로 캐싱 방식에 근본적인 변화가 일어나면서 기존의 성능 최적화 가정들이 도전받고 있다. 이를 이중 키 캐싱(Double-keyed Caching) 또는 더 일반적으로 캐시 분할이라고 부른다. 이 변화가 무엇인지, 왜 중요한지, 그리고 어떻게 적응해야 하는지 알아보자.
기존 모델에서 브라우저는 캐시된 리소스를 위한 간단한 키-값 저장소를 유지했다:
cache = {
"https://cdn.example.com/jquery-3.6.0.min.js": resourceData,
"https://fonts.googleapis.com/css?family=Roboto": resourceData
}
이 방식은 사용자가 공용 CDN에서 jQuery를 로드하는 사이트를 방문하면, 동일한 CDN에서 호스팅된 jQuery를 사용하는 다른 사이트를 방문할 때 즉시 캐시 히트가 발생한다는 의미다. 이 모델은 2010년대 웹 개발을 지배한 “CDN 우선” 접근 방식을 뒷받침했다.
이 방식의 장점은 다음과 같다:
이 모델은 효율적이지만 정보 유출 문제가 있었다. 주요 공격 벡터는 다음과 같다:
이중 키 캐싱은 브라우저가 리소스를 저장하고 검색하는 방식을 근본적으로 바꾼다. 기존에는 리소스 URL만 캐시 키로 사용했지만, 이제는 요청을 보내는 최상위 사이트와 리소스 URL 두 가지 정보를 함께 사용한다. 예제를 통해 자세히 살펴보자.
site-a.com이 CDN에서 jQuery를 요청하면, 브라우저는 site-a.com(요청자)과 리소스 URL을 조합한 HTTP 캐시 항목을 생성한다. 이후 site-b.com이 동일한 jQuery 파일을 요청할 때, 캐시된 복사본을 재사용하는 대신 site-b.com을 키의 일부로 하는 완전히 새로운 캐시 항목을 만든다. 이는 현대적인 캐시 항목의 모습을 보여준다:
cache = {
{
topLevelSite: "site-a.com",
resource: "https://cdn.example.com/jquery-3.6.0.min.js"
}: resourceData,
{
topLevelSite: "site-b.com",
resource: "https://cdn.example.com/jquery-3.6.0.min.js"
}: resourceData
}
이 방식은 동일한 리소스라도 요청하는 사이트마다 별도로 캐시된다는 것을 의미한다. 즉, 캐시가 요청 사이트의 오리진에 따라 분할된다. 이는 사이트 간 추적과 같은 프라이버시 문제를 방지하지만, 동일한 리소스의 중복 복사본을 저장해야 한다는 단점도 있다. 이는 보안과 효율성 사이의 균형을 맞추는 트레이드오프다.
Chrome의 구현 데이터에 따르면, 이중 키 캐싱은 다음과 같은 영향을 미친다:
이 수치들은 작아 보일 수 있지만, 리소스 타입과 사용 패턴에 따라 그 영향은 크게 달라진다.
이중 키 캐싱은 성능 오버헤드를 유발하지만, 이는 보안과 개인정보 보호를 강화하기 위한 필수적인 절충안으로 받아들여진다. 이 메커니즘은 다음과 같은 다양한 보안 취약점을 방지하는 데 도움을 준다:
이러한 보안상의 이점은 성능 비용을 정당화한다. 하지만 이러한 영향을 이해하고 최적화하는 것은 여전히 중요하다.
캐시 미스율이 증가하면 추가적인 네트워크 요청이 발생한다. 일반적인 웹 리소스의 경우, 이로 인한 영향은 다음과 같다:
이 평균값은 중요한 예외 사례를 숨기고 있다. 실제 사례를 살펴보면:
// 이전: 하나의 캐시 항목
cdn.company.com/shared-lib.js → 2.5MB
// 이후: 여러 항목
{crm.company.com, cdn.company.com/shared-lib.js} → 2.5MB
{mail.company.com, cdn.company.com/shared-lib.js} → 2.5MB
{docs.company.com, cdn.company.com/shared-lib.js} → 2.5MB
// 결과: 총 7.5MB 캐시 사용량
// 이전
unpkg.com/react@18.2.0 한 번 캐시 → 118KB
// 이후
{site1.com, unpkg.com/react@18.2.0} → 118KB
{site2.com, unpkg.com/react@18.2.0} → 118KB
{site3.com, unpkg.com/react@18.2.0} → 118KB
인기 있는 React 라이브러리도 비슷한 영향을 받는다:
// Material-UI
{
app_domain: "myapp.com",
resource: "unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"
}
// React Router
{
app_domain: "myapp.com",
resource: "unpkg.com/react-router@6.14.0/umd/react-router.production.min.js"
}
// Redux
{
app_domain: "myapp.com",
resource: "unpkg.com/redux@4.2.1/dist/redux.min.js"
}
React 마이크로 프론트엔드에서는 다음과 같은 영향이 두드러진다:
// 메인 셸 애플리케이션 (shell.company.com)
{
app_domain: "shell.company.com",
resources: [
"unpkg.com/react@18.2.0/umd/react.production.min.js",
"unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
]
}
// 마이크로 프론트엔드 1 (app1.company.com)
{
app_domain: "app1.company.com",
resources: [
"unpkg.com/react@18.2.0/umd/react.production.min.js",
"unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"
]
}
// 결과: 각 마이크로 프론트엔드마다 React가 중복 다운로드됨
일반적인 React 애플리케이션 설정을 살펴보자:
// 번들 크기
const commonDependencies = {
react: "118 KB",
reactDom: "1.1 MB",
materialUI: "469 KB",
reactRouter: "27 KB",
redux: "22 KB"
};
// 도메인당 총합: ~1.7 MB
여러 하위 도메인이나 마이크로 프론트엔드를 사용할 경우, 각 도메인은 캐시에 별도의 복사본을 저장해야 한다.
공유 React 컴포넌트 라이브러리를 사용하는 조직의 경우:
// 공유 UI 라이브러리
{
size: "2.5 MB",
domains: [
"main-app.company.com",
"dashboard.company.com",
"admin.company.com"
],
totalCacheSize: "7.5 MB" // 2.5 MB × 3 domains
}
다음과 같이 사용하는 대신:
assets1.company.com/lib.js
assets2.company.com/lib.js
다음과 같이 고려해 본다:
static.company.com/app1/lib.js
static.company.com/app2/lib.js
// 다음과 같이 사용하는 대신:
const domains = [
'app1.company.com/static/js/react.js',
'app2.company.com/static/js/react.js'
];
// 다음과 같이 고려해 본다:
const consolidatedDomain = 'static.company.com/js/react.js';
모듈 연합:
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://static.company.com/app1/remoteEntry.js',
app2: 'app2@http://static.company.com/app2/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};
// 이전: 캐시에 의존
<script src="https://unpkg.com/react@18.2.0"></script>
// 이후: 핵심 리소스를 직접 호스팅
<script src="/vendor/react-18.2.0.min.js"></script>
각 브라우저는 캐시 분할을 다르게 구현한다:
이러한 차이로 인해 브라우저별 성능 영향이 달라진다. 2013년 도입된 Safari의 방식이 가장 강력한 분할을 적용한다.
웹 플랫폼은 개인정보 보호를 유지하면서도 잘 알려진 대용량 리소스에 대해 더 나은 크로스 오리진 캐싱을 가능하게 하는 새로운 솔루션을 고려할 필요가 있다. 특히 클라이언트 측 AI 모델과 같이 크기가 중간에서 매우 큰 리소스가 이에 해당한다. 클라이언트 측 머신러닝 애플리케이션에 미치는 영향은 특별히 주목할 만한 부분이다.
여기서는 과거의 솔루션 탐구(예: Web Bundles, Cache Transparency)에 대해 자세히 다루지 않지만, 앞서 언급한 패턴 외에도 이 문제에 대해 더 깊이 고민할 가치가 있다고 생각한다.
캐시 분할은 웹 개인정보 보호를 위한 필수적인 진화이지만, 실제 성능 저하를 동반한다. 이러한 변화에 저항하기보다는 아키텍처와 최적화 전략을 적절히 조정해야 한다.
웹 플랫폼은 계속 발전하고 있으며, 새로운 API와 패턴이 등장해 개인정보 보호와 성능 간의 균형을 맞추는 데 도움을 줄 것이다. 그때까지는 신중한 도메인 전략과 리소스 관리가 새로운 환경에서 성능을 최적화하는 최선의 도구로 남아 있다.
공용 CDN을 공유하던 시대는 끝나가고 있지만, 웹의 적응력과 진화 능력은 여전히 유효하다. 언제나 그렇듯이, 특정 사용 사례에 맞춰 측정하고 최적화하며 적응해야 한다.