← Kursa Dön
📄 Text · 20 min

HTTP Caching: ETag, Cache-Control

HTTP düzeyinde cache, sunucu yükünü azaltır ve istemci yanıt süresini kısaltır. Uygulama katmanı cache'den (Redis, Caffeine) farklıdır: HTTP cache istemci tarafında (browser, CDN, proxy) çalışır. Doğru HTTP cache header'ları ile sunucuya hiç istek gelmeden yanıt verilebilir — bu en hızlı cache'dir çünkü network bile kullanılmaz.

Cache Katmanları

Kullanıcı → Browser Cache → CDN/Proxy Cache → Reverse Proxy → Uygulama → App Cache → DB
                 ↑                 ↑                ↑                          ↑
            HTTP Cache         HTTP Cache       HTTP Cache              Redis/Caffeine

Her katman isteği yakalayabilir. Hedef: isteğin mümkün olduğunca sol taraftaki katmanda yanıtlanması.

Cache-Control Header — Detaylı

Cache-Control, HTTP cache davranışını kontrol eden en önemli header'dır:

@RestController
@RequestMapping("/api")
public class ProductController {

    // 1. Public cache — CDN ve browser cache'leyebilir
    @GetMapping("/products/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product product = productService.getById(id);
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(Duration.ofMinutes(30))
                .cachePublic())      // CDN cache'leyebilir
            .body(product);
    }

    // 2. Private cache — sadece browser cache'leyebilir (kullanıcıya özel veri)
    @GetMapping("/profile")
    public ResponseEntity<UserProfile> getProfile(Authentication auth) {
        UserProfile profile = userService.getProfile(auth.getName());
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
                .cachePrivate())     // CDN cache'leyemez, sadece browser
            .body(profile);
    }

    // 3. No-cache — her seferde sunucuya sor (ama cache'de tutabilir)
    @GetMapping("/cart")
    public ResponseEntity<Cart> getCart(Authentication auth) {
        Cart cart = cartService.getCart(auth.getName());
        return ResponseEntity.ok()
            .cacheControl(CacheControl.noCache())  // Revalidation zorunlu
            .body(cart);
    }

    // 4. No-store — kesinlikle cache'leme (hassas veri)
    @GetMapping("/bank-account")
    public ResponseEntity<BankAccount> getBankAccount(Authentication auth) {
        return ResponseEntity.ok()
            .cacheControl(CacheControl.noStore())  // Hiç cache yok
            .body(bankService.getAccount(auth.getName()));
    }

    // 5. Immutable — asla değişmez (versioned assets için)
    @GetMapping("/assets/{hash}/{filename}")
    public ResponseEntity<Resource> getAsset(@PathVariable String hash,
                                              @PathVariable String filename) {
        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(Duration.ofDays(365))
                .cachePublic()
                .immutable())        // 1 yıl, hiç revalidation yapma
            .body(assetService.getAsset(hash, filename));
    }
}

Cache-Control Directive'leri:

DirectiveAnlamıKullanım
max-age=NN saniye cache'te tutÇoğu kaynak
s-maxage=NShared cache (CDN) için max-ageCDN'e özel TTL
publicHerkes cache'leyebilirHerkese açık veri
privateSadece browser cache'leyebilirKullanıcıya özel veri
no-cacheCache'te tut ama her seferinde revalidate etSık güncellenen veri
no-storeKesinlikle cache'lemeHassas veri (banka, sağlık)
must-revalidateExpire olunca mutlaka sunucuya sorTutarlılık gereken veri
immutableAsla değişmez, revalidation yapmaVersioned assets
stale-while-revalidate=NExpire olmuş veriyi sun, arka planda yenileUX öncelikli

ETag (Entity Tag) — Detaylı

ETag, bir kaynağın "parmak izi"dir. İçerik değişmişse ETag da değişir. İstemci ETag'i geri gönderir, sunucu değişiklik olup olmadığını kontrol eder.

Shallow ETag — Otomatik (body hash):

// Spring'in ShallowEtagHeaderFilter'ı — body'nin MD5 hash'ini ETag olarak kullanır
@Bean
public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
    FilterRegistrationBean<ShallowEtagHeaderFilter> reg = new FilterRegistrationBean<>();
    reg.setFilter(new ShallowEtagHeaderFilter());
    reg.addUrlPatterns("/api/*");
    return reg;
}

Shallow ETag akışı:

1. İlk istek:
   GET /api/products/1
   → Sunucu: Body üretir + MD5 hash hesaplar
   → Response: 200 OK
     ETag: "0a1b2c3d4e5f"
     Body: {"id": 1, "name": "iPhone", ...}

2. Tekrar istek:
   GET /api/products/1
   If-None-Match: "0a1b2c3d4e5f"
   → Sunucu: Body üretir + MD5 hash hesaplar
   → Hash AYNI → Response: 304 Not Modified (body yok!)
   → Hash FARKLI → Response: 200 OK + yeni ETag + yeni body

⚠️ Shallow ETag'in sınırı: Sunucu body'yi her seferinde üretir (DB sorgusu çalışır), sadece network transfer'inden tasarruf sağlar. Sunucu yükünü azaltmaz.

Deep ETag — Manuel (version-based):

Deep ETag ile sunucu, body üretmeden değişikliği kontrol edebilir:

@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(
        @PathVariable Long id,
        WebRequest request) {

    // 1. Sadece version bilgisini çek (hızlı, hafif sorgu)
    String currentEtag = productService.getEtag(id);

    // 2. ETag eşleşiyorsa → 304 (body üretilmez, DB sorgusu yapılmaz!)
    if (request.checkNotModified(currentEtag)) {
        return null;  // Spring otomatik 304 döner
    }

    // 3. ETag eşleşmiyorsa → tam veriyi çek
    Product product = productService.getById(id);
    return ResponseEntity.ok()
        .eTag(currentEtag)
        .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)))
        .body(product);
}

// Service'de ETag hesaplama
@Service
public class ProductService {
    public String getEtag(Long id) {
        // Sadece version kolonu çekilir (çok hızlı)
        Integer version = productRepository.findVersionById(id);
        return "\"product:" + id + ":v" + version + "\"";
    }
}

// Repository'de sadece version çekme
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Query("SELECT p.version FROM Product p WHERE p.id = :id")
    Integer findVersionById(@Param("id") Long id);
}

Shallow vs Deep ETag karşılaştırma:

ÖzellikShallow ETagDeep ETag
ImplementasyonOtomatik (Filter)Manuel
Sunucu yüküAzaltmaz (body hep üretilir)Azaltır (304'te body üretilmez)
DB sorgusuHer seferinde tam sorgu304'te hafif sorgu
Network tasarrufu✅ Evet✅ Evet
Sunucu tasarrufu❌ Hayır✅ Evet
KullanımBasit API'lerPerformans-kritik API'ler

Last-Modified Header

ETag'e alternatif, zamana dayalı validasyon:

@GetMapping("/api/products/{id}")
public ResponseEntity<Product> getProduct(
        @PathVariable Long id,
        WebRequest request) {

    Product product = productService.getById(id);
    long lastModifiedMs = product.getUpdatedAt()
        .atZone(ZoneId.systemDefault())
        .toInstant().toEpochMilli();

    // Değişmediyse 304
    if (request.checkNotModified(lastModifiedMs)) {
        return null;
    }

    return ResponseEntity.ok()
        .lastModified(lastModifiedMs)
        .cacheControl(CacheControl.maxAge(Duration.ofMinutes(10)))
        .body(product);
}

Vary Header — Cache Key'e Ek Boyut

Aynı URL, farklı koşullarda farklı yanıt döndürebilir:

@GetMapping("/api/products")
public ResponseEntity<List<Product>> getProducts(
        @RequestHeader(value = "Accept-Language", defaultValue = "tr") String lang) {

    List<Product> products = productService.getByLanguage(lang);

    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(Duration.ofMinutes(30)).cachePublic())
        .varyBy("Accept-Language", "Accept-Encoding")  // Dil ve encoding'e göre ayrı cache
        .body(products);
}

// Vary: Accept-Language ile:
// /api/products (tr) → ayrı cache entry
// /api/products (en) → ayrı cache entry

Spring Interceptor ile Global Cache Header

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Statik API yanıtları için
        WebContentInterceptor interceptor = new WebContentInterceptor();
        interceptor.addCacheMapping(
            CacheControl.maxAge(Duration.ofHours(1)).cachePublic(),
            "/api/categories/**", "/api/countries/**");
        interceptor.addCacheMapping(
            CacheControl.maxAge(Duration.ofMinutes(5)).cachePrivate(),
            "/api/users/*/profile");
        interceptor.addCacheMapping(
            CacheControl.noStore(),
            "/api/payments/**", "/api/auth/**");

        registry.addInterceptor(interceptor);
    }
}

Stale-While-Revalidate — Modern Cache Stratejisi

Cache süresi dolmuş veriyi hemen sun, arka planda yenile. Kullanıcı hiç beklemesin:

@GetMapping("/api/trending")
public ResponseEntity<List<Product>> getTrending() {
    return ResponseEntity.ok()
        .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
            .staleWhileRevalidate(Duration.ofMinutes(60))  // 60 dk stale tolere et
            .cachePublic())
        .body(productService.getTrending());
}
Zaman akışı:
0-5 dk:  Fresh → Cache'den direkt sun
5-65 dk: Stale → Cache'den hemen sun + arka planda sunucudan yenile
65+ dk:  Expired → Sunucudan yeniden çek

Senaryo Bazlı Cache Stratejileri

KaynakCache-ControlETagNeden
Statik JS/CSS (hashed)max-age=31536000, immutableGereksizHash değişir, URL değişir
Ürün listesimax-age=300, publicCDN cache, 5dk TTL
Kullanıcı profilimax-age=60, privateKişiye özel, kısa TTL
Sepetno-cacheHer zaman güncel, ama 304 mümkün
Ödeme bilgisino-storeAsla cache'lenmesin
API trendingmax-age=300, stale-while-revalidate=3600Hız öncelikli

Yaygın Hatalar

  • no-cache ile no-store'u karıştırmak: no-cache = cache'te tut ama her seferinde sor. no-store = hiç tutma. Hassas veri için no-store kullanın.

  • ETag'de weak/strong farkını bilmemek: W/"abc" (weak) = semantik eşdeğerlik. "abc" (strong) = byte-level eşdeğerlik. API'lerde genellikle weak yeterli.

  • Vary header koymamak: Dile/encoding'e göre farklı yanıt dönen API'lerde Vary olmadan CDN yanlış cache döner.

  • max-age olmadan public koymak: Cache süresi belirsiz olur, browser/CDN varsayılan heuristic kullanır — öngörülemez davranış.

  • Mutasyonda cache invalidation unutmak: POST/PUT/DELETE sonrası ilgili GET cache'i invalidate edilmeli.

💡 Özet: HTTP Cache, network'ten bile tasarruf sağlayan en hızlı cache katmanıdır. Cache-Control ile süre ve kapsam, ETag ile değişiklik kontrolü yapılır. Deep ETag sunucu yükünü de azaltır. Vary header ile dile/encoding'e göre ayrı cache tutulur. stale-while-revalidate ile UX iyileştirilir. Hassas veri için no-store kullanın.