Beyond Frontend

API Output Caching vs ETag 본문

Frontend Essentials

API Output Caching vs ETag

dietgogo 2025. 12. 6. 23:13

 

[서버 최적화] Output Cache vs ETag: 내 서비스엔 뭘 써야 할까?

웹 서비스의 성능을 최적화하는 방법은 다양하지만, 가장 확실한 방법은 **"불필요한 작업을 하지 않는 것"**입니다. 오늘은 서버의 부하를 줄이고 응답 속도를 높이는 대표적인 두 가지 캐싱 전략, Output CachingETag를 비교해보고 어떤 상황에 무엇을 적용해야 할지 정리해 보겠습니다.


1. Output Caching — "서버가 결과를 통째로 저장해 재사용"

Output Caching은 서버가 가장 게으르게 행동하도록 만드는 전략입니다. 한마디로 **"계산하지 않고, 복사본을 던져주는 것"**입니다.

🛠 작동 방식

  1. 클라이언트가 요청을 보냅니다.
  2. 서버는 이 요청(URL + 파라미터)에 대한 응답이 메모리(RAM)나 Redis 같은 저장소에 있는지 확인합니다.
  3. 캐시가 있다면? DB 조회나 비즈니스 로직을 전혀 실행하지 않고, 저장된 HTML/JSON을 즉시 반환합니다.
  4. 캐시가 없다면? 로직을 수행하고 결과를 반환한 뒤, 그 결과물을 캐시에 저장합니다.

✅ 장점

  • 서버 부하의 극적인 감소: DB Connection을 맺거나 CPU 연산을 할 필요가 없어 서버 리소스를 가장 많이 아낄 수 있습니다.
  • 트래픽 방어: 이벤트 페이지나 메인 배너처럼 트래픽이 폭주하는 API에서 서버가 죽지 않도록 지탱해 줍니다.
  • 일정한 응답 속도: TTL(Time To Live) 동안은 연산 비용이 '0'에 수렴하므로 매우 빠른 속도를 보장합니다.

❌ 단점 및 주의사항

  • 데이터 정합성 문제: TTL이 60초라면, 10초 뒤에 DB 데이터가 바뀌어도 사용자는 50초 동안 과거 데이터를 보게 됩니다.
  • 메모리 비용: 캐시해야 할 페이지나 파라미터 조합이 너무 많으면(예: 검색 조건이 수천 가지) 메모리 사용량이 급증하고 효율이 떨어집니다.

2. ETag — "클라이언트 캐시 확인 후, 서버는 변경 여부만 판단"

ETag(Entity Tag)는 **"네트워크 대역폭을 아끼는 합리적인 검증 시스템"**입니다. 서버가 일을 아예 안 하는 것은 아니지만, 결과물을 전송하는 비용을 줄여줍니다.

🛠 작동 방식

  1. 서버는 응답 데이터의 내용을 기반으로 고유한 해시값(예: "ETag": "abc123")을 만들어 헤더에 담아 보냅니다.
  2. 브라우저는 이 값을 저장해 두었다가, 다음 요청 시 If-None-Match: abc123 헤더를 포함해 보냅니다.
  3. 서버는 현재 데이터의 해시값과 비교합니다.
    • 변경 없음: 304 Not Modified 상태 코드만 보냅니다. (Body 없음) -> 브라우저는 갖고 있던 캐시를 사용.
    • 변경 있음: 200 OK와 함께 새로운 데이터와 새로운 ETag를 보냅니다.

✅ 장점

  • 대역폭(Bandwidth) 절감: 응답 본문(Body)을 보내지 않으므로 네트워크 트래픽을 70~95%까지 줄일 수 있습니다.
  • 데이터 일관성: 서버가 항상 "변경된 게 있는지" 확인하고 응답하므로, 사용자는 항상 최신 상태의 데이터를 보장받습니다.
  • 모바일 친화적: 데이터 요금 절감 및 로딩 속도 개선 효과가 큽니다.

❌ 단점

  • 서버 로직은 실행됨: 변경 여부를 판단하려면 어쨌든 DB를 조회하거나 로직을 일부 수행해야 합니다. Output Cache만큼 CPU 부하를 줄이지는 못합니다.
  • 계산 비용: ETag 해시를 생성하는 과정 자체도 미세한 리소스를 소모합니다.

3. 한눈에 보는 비교 (핵심 요약)

항목 Output Cache ETag
목적 서버 부하(CPU/DB) 최소화 네트워크 비용 절감 + 데이터 최신성
동작 위치 서버 내부 (Memory/Redis) 서버 ↔ 클라이언트 상호작용
서버 로직 실행 안 함 (Skip) 실행 함 (검증 로직 필요)
응답 전송 캐시된 완성본 그대로 전송 변경 없으면 헤더만 전송 (304)
데이터 신선도 TTL 동안 갱신 안 됨 (과거 데이터 가능) 항상 최신 데이터 반영
적합한 곳 랭킹, 메인화면, 공통 템플릿 상세페이지, 내 정보, 설정값

4. 결론: 어떤 상황에 무엇을 쓸까?

두 기술은 배타적인 관계가 아니라, 해결하려는 병목 구간이 다릅니다.

🚀 Output Cache를 써야 하는 경우 (속도/부하 중심)

  • "모든 사람에게 똑같은 화면을 보여준다."
  • 데이터가 실시간으로 1초마다 바뀌지 않아도 된다.
  • DB 쿼리가 무겁고, 동시 접속자가 많다.
  • 예시: 베스트셀러 목록, 카테고리 메뉴, 홈 화면 추천 상품, 공지사항 목록

🔄 ETag를 써야 하는 경우 (정합성/효율 중심)

  • "사용자마다, 상황마다 데이터가 다르다."
  • 데이터가 자주 바뀌지는 않지만, 바뀌었다면 즉시 반영되어야 한다.
  • 모바일 환경이라 데이터 전송량을 줄이고 싶다.
  • 예시: 마이페이지, 주문 상세 내역, 장바구니, 게시글 상세 내용, 이미지 리소스

💡 핵심 정리

Output Cache는 서버가 "아 몰라, 아까 그거 가져가" 하고 쉬는 것이고,

ETag는 서버가 "잠깐만, 바뀐 거 있나 확인해 볼게... 없네? 그냥 쓰던 거 써"라고 말해주는 것입니다.

 

서버의 CPU가 터질 것 같다면 Output Cache를, 네트워크 비용을 줄이면서 데이터 정확도를 지키고 싶다면 ETag를 우선 고려해 보세요.

 

 

C# 샘플 코드

1. Output Caching 구현 (가장 쉬운 방법)

ASP.NET MVC 5는 강력한 [OutputCache] 속성(Attribute)을 기본으로 제공합니다. 별도의 코딩 없이 컨트롤러 액션 위에 선언만 하면 됩니다.

시나리오: 메인 화면의 베스트셀러 목록 (데이터가 자주 바뀌지 않음, 60초 캐시)

using System.Web.Mvc;
using System.Web.UI;

public class HomeController : Controller
{
    // 1. Duration: 60초 동안 캐시 유지
    // 2. VaryByParam: "none"이면 파라미터 상관없이 하나만 캐시, 
    //    "category" 등으로 지정하면 파라미터 값마다 별도로 캐시 생성
    // 3. Location: Server, Client, Downstream 등 저장 위치 지정 가능 (기본은 Any)
    [OutputCache(Duration = 60, VaryByParam = "none", Location = OutputCacheLocation.Server)]
    public ActionResult Bestsellers()
    {
        // DB에서 데이터를 가져오는 무거운 로직 (최초 1회만 실행됨)
        var model = _bookService.GetTop100Books();
        
        // 캐시 확인용: 뷰에 현재 시간을 출력해보면 60초 동안 시간이 멈춰있는 것을 확인 가능
        ViewBag.CachedTime = DateTime.Now.ToString("HH:mm:ss");

        return View(model);
    }
}

 

  • 최초 요청 시에는 컨트롤러 내부 코드가 실행되고 뷰가 렌더링됩니다.
  • 이후 60초 동안 들어오는 모든 요청은 IIS 레벨에서 컨트롤러를 건너뛰고 메모리에 저장된 HTML을 즉시 반환합니다.

 

2. ETag 구현 (수동 제어 방식)

ETag는 MVC 5에 자동화된 속성이 없으므로, ActionFilter를 만들거나 Controller 내부에서 직접 제어해야 합니다. 여기서는 이해를 돕기 위해 컨트롤러 내부에서 로직을 처리하는 정석적인 방법을 소개합니다.

시나리오: 도서 상세 페이지 (수정이 일어날 수 있으므로 최신 여부 검증 필요)

using System.Security.Cryptography;
using System.Text;
using System.Web.Mvc;

public class BookController : Controller
{
    public ActionResult Detail(int id)
    {
        // 1. 데이터 조회 (가벼운 조회, 혹은 버전만 조회)
        var book = _bookService.GetBook(id);

        if (book == null) return HttpNotFound();

        // 2. ETag 생성 로직
        // 예: 데이터의 수정일(LastModified) + ID 조합으로 해시 생성
        // 만약 수정일이 없다면 전체 콘텐츠를 문자열로 만들어 MD5 해시를 떠야 함 (비용 발생)
        string contentToHash = $"{book.Id}-{book.LastModified.Ticks}";
        string currentETag = GenerateETag(contentToHash);

        // 3. 클라이언트가 보낸 ETag 확인 (If-None-Match 헤더)
        string incomingETag = Request.Headers["If-None-Match"];

        // 4. 비교: 변경이 없다면 304 반환
        if (!string.IsNullOrEmpty(incomingETag) && incomingETag == currentETag)
        {
            return new HttpStatusCodeResult(System.Net.HttpStatusCode.NotModified);
        }

        // 5. 변경이 있다면 (혹은 첫 요청이라면)
        // 응답 헤더에 새 ETag 심어서 보내기
        Response.Cache.SetETag(currentETag);
        
        // (선택) OutputCache와 달리 브라우저 캐시는 허용하되, 재검증을 강제함
        Response.Cache.SetCacheability(System.Web.HttpCacheability.Public);

        return View(book);
    }

    // MD5 해시 생성 헬퍼 함수
    private string GenerateETag(string input)
    {
        using (var md5 = MD5.Create())
        {
            var bytes = Encoding.UTF8.GetBytes(input);
            var hash = md5.ComputeHash(bytes);
            return "\"" + BitConverter.ToString(hash).Replace("-", "") + "\""; // ETag는 따옴표로 감싸는 것이 표준
        }
    }
}

 

 

설명:

  • 서버는 DB를 조회하지만, 뷰(HTML)를 렌더링하고 네트워크로 전송하는 비용을 아낍니다.
  • 클라이언트가 If-None-Match 헤더로 자신이 가진 ETag를 보내오면, 서버의 현재 ETag와 비교합니다.
  • 같으면 return new HttpStatusCodeResult(304)를 통해 Body 없이 응답을 끝냅니다.

 

3. (심화) ETag를 ActionFilter로 깔끔하게 만들기

매번 컨트롤러에 코드를 쓰기 귀찮다면, 커스텀 Attribute를 만들어 재사용할 수 있습니다.

public class ETagAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        // 뷰가 렌더링된 후의 결과물(HTML)을 가로채서 해시를 뜨는 방식은 
        // MVC 파이프라인상 복잡하므로(Response Filter Stream 필요),
        // 여기서는 간단히 'Model이 가지고 있는 GetHashCode' 등을 활용하는 예시입니다.
        
        var result = filterContext.Result as ViewResult;
        if (result == null || result.Model == null) return;

        // 모델 객체의 HashCode 등을 ETag로 활용 (모델에 적절한 고유값이 있다고 가정)
        string currentToken = "\"" + result.Model.GetHashCode().ToString() + "\"";
        
        var request = filterContext.HttpContext.Request;
        var response = filterContext.HttpContext.Response;

        string clientToken = request.Headers["If-None-Match"];

        if (clientToken == currentToken)
        {
            filterContext.Result = new HttpStatusCodeResult(304);
        }
        else
        {
            response.Cache.SetETag(currentToken);
            response.Cache.SetCacheability(System.Web.HttpCacheability.Public);
        }
    }
}

// 사용 예시
[ETag]
public ActionResult Detail(int id) { ... }

ETag 코드는 304 Not Modified가 반환될 때 브라우저 네트워크 탭을 캡처해서 "전송 크기(Size)가 확 줄어든 모습"을 볼 수 있음