성능을 고려한 설계

성능을 고려한 설계

REST API 성능 최적화

API 성능은 사용자 경험과 직결된다. 캐싱, 비동기 처리, 부분 업데이트 등의 기법으로 응답 속도를 높이고 서버 부하를 줄일 수 있다.

┌─────────────────────────────────────────────────────────────────┐
│                    REST API 성능 최적화 전략                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌───────────────┐  ┌───────────────┐  ┌───────────────┐      │
│   │    Caching    │  │    Async      │  │   Partial     │      │
│   │    (캐싱)     │  │   (비동기)     │  │   Update      │      │
│   ├───────────────┤  ├───────────────┤  ├───────────────┤      │
│   │ • 응답 지연↓   │  │ • 긴 작업 처리 │  │ • 대역폭 절약  │      │
│   │ • 서버 부하↓   │  │ • 논블로킹    │  │ • PATCH 활용  │      │
│   │ • 대역폭 절약  │  │ • 폴링/SSE    │  │ • JSON Patch  │      │
│   └───────────────┘  └───────────────┘  └───────────────┘      │
│                                                                 │
│   ┌───────────────┐  ┌───────────────┐  ┌───────────────┐      │
│   │  Pagination   │  │  Compression  │  │ Rate Limiting │      │
│   │  (페이지네이션) │  │    (압축)     │  │  (속도 제한)   │      │
│   ├───────────────┤  ├───────────────┤  ├───────────────┤      │
│   │ • 대량 데이터  │  │ • gzip/br    │  │ • 과부하 방지  │      │
│   │ • 커서 기반   │  │ • 전송량 감소  │  │ • 공정한 사용  │      │
│   └───────────────┘  └───────────────┘  └───────────────┘      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

캐싱의 원리

캐싱은 요청에 대한 응답을 임시 저장하여 동일한 요청에 대해 저장된 응답을 반환하는 기술이다.

┌─────────────────────────────────────────────────────────────────┐
│                      캐싱 동작 원리                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   최초 요청 (Cache Miss)                                        │
│   ┌────────┐         ┌────────┐         ┌────────┐            │
│   │ Client │ ──────► │ Cache  │ ──────► │ Server │            │
│   │        │ ◄────── │ (저장)  │ ◄────── │        │            │
│   └────────┘   응답   └────────┘   응답   └────────┘            │
│                                                                 │
│   재요청 (Cache Hit)                                            │
│   ┌────────┐         ┌────────┐         ┌────────┐            │
│   │ Client │ ──────► │ Cache  │    X    │ Server │            │
│   │        │ ◄────── │ (반환)  │         │ (호출X) │            │
│   └────────┘  캐시응답 └────────┘         └────────┘            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

캐싱의 장점

장점설명
응답 지연 감소서버 왕복 없이 즉시 응답
서버 부하 경감처리해야 할 요청 수 감소
대역폭 절약네트워크 전송량 감소
확장성 향상더 많은 동시 요청 처리 가능

캐싱 대상

┌─────────────────────────────────────────────────────────────────┐
│                     캐싱 적합 리소스                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ✅ 캐싱 권장                        ❌ 캐싱 비권장              │
│  ┌─────────────────────────┐       ┌─────────────────────────┐ │
│  │ • 정적 리소스             │       │ • 실시간 데이터          │ │
│  │   (이미지, JS, CSS)      │       │ • 사용자별 개인정보       │ │
│  │ • 자주 변경되지 않는 데이터 │       │ • 트랜잭션 결과          │ │
│  │ • CPU 집약적 연산 결과    │       │ • 쿼리 파라미터 요청      │ │
│  │ • 공용 참조 데이터        │       │ • 인증이 필요한 리소스    │ │
│  └─────────────────────────┘       └─────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

캐싱 헤더

강한 캐싱 vs 약한 캐싱

┌─────────────────────────────────────────────────────────────────┐
│                    캐싱 헤더 분류                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   강한 캐싱 헤더 (Strong)              약한 캐싱 헤더 (Weak)       │
│  ┌────────────────────────┐         ┌────────────────────────┐ │
│  │                        │         │                        │ │
│  │  "언제까지 캐시 사용"    │         │  "변경되었는지 확인"    │ │
│  │                        │         │                        │ │
│  │  • Expires            │         │  • Last-Modified       │ │
│  │  • Cache-Control      │         │  • ETag                │ │
│  │    max-age            │         │                        │ │
│  │                        │         │                        │ │
│  │  → 서버 요청 없이      │         │  → 조건부 GET으로      │ │
│  │    캐시에서 직접 반환   │         │    변경 여부 확인       │ │
│  │                        │         │                        │ │
│  └────────────────────────┘         └────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Expires vs Cache-Control

헤더설명예시
Expires리소스 만료 일시 (절대 시간)Expires: Thu, 01 Dec 2024 16:00:00 GMT
Cache-Control max-age다운로드 후 유효 시간 (초)Cache-Control: max-age=3600

참고: 둘 다 있으면 Cache-Control이 우선한다. HTTP/1.1에서는 Cache-Control 권장

Cache-Control 지시자

┌─────────────────────────────────────────────────────────────────┐
│                  Cache-Control 지시자                            │
├──────────────┬──────────────────────────────────────────────────┤
│    지시자     │                      의미                        │
├──────────────┼──────────────────────────────────────────────────┤
│  public      │  브라우저, 프록시, CDN 모두 캐시 가능             │
│  private     │  브라우저만 캐시 가능 (프록시/CDN 불가)           │
│  no-cache    │  매번 서버에 재검증 필요 (캐시 저장은 함)          │
│  no-store    │  캐시 저장 자체를 하지 않음                       │
│  max-age=N   │  N초 동안 캐시 유효                              │
│  s-maxage=N  │  공유 캐시(프록시)에서 N초 동안 유효              │
│  must-revalidate │ 만료 후 반드시 서버 확인                     │
│  immutable   │  리소스가 절대 변경되지 않음                      │
└──────────────┴──────────────────────────────────────────────────┘

캐싱 시나리오별 설정

# 1. 정적 리소스 (이미지, JS, CSS) - 장기 캐싱
Cache-Control: public, max-age=31536000, immutable

# 2. API 응답 - 짧은 캐싱
Cache-Control: private, max-age=60

# 3. 민감한 데이터 - 캐시 금지
Cache-Control: no-store

# 4. 변경 가능한 공용 데이터 - 항상 재검증
Cache-Control: public, no-cache

# 5. CDN 활용 - 브라우저와 CDN 다른 TTL
Cache-Control: public, max-age=60, s-maxage=3600

ETag (Entity Tag)

리소스의 버전을 식별하는 고유값으로, 조건부 요청에 사용된다.

ETag 작동 원리

┌─────────────────────────────────────────────────────────────────┐
│                      ETag 동작 흐름                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Client                                         Server         │
│     │                                              │            │
│     │─────── 1. GET /api/users/123 ──────────────►│            │
│     │                                              │            │
│     │◄──────── 2. 200 OK ─────────────────────────│            │
│     │          ETag: "abc123"                      │            │
│     │          Body: {...}                         │            │
│     │                                              │            │
│     │  (캐시에 저장)                                │            │
│     │                                              │            │
│     │─────── 3. GET /api/users/123 ──────────────►│            │
│     │          If-None-Match: "abc123"             │            │
│     │                                              │            │
│     │                              리소스 변경 확인 │            │
│     │                                              │            │
│  ┌──┴──────────────────────────────────────────────┴──┐        │
│  │                                                    │        │
│  │  Case A: 변경 없음           Case B: 변경됨         │        │
│  │  ┌──────────────────┐       ┌──────────────────┐  │        │
│  │  │ 304 Not Modified │       │ 200 OK           │  │        │
│  │  │ (Body 없음)       │       │ ETag: "def456"   │  │        │
│  │  │ → 캐시 사용       │       │ Body: {...}      │  │        │
│  │  └──────────────────┘       │ → 캐시 갱신       │  │        │
│  │                             └──────────────────┘  │        │
│  └────────────────────────────────────────────────────┘        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ETag 관련 헤더

헤더방향설명
ETag응답리소스의 버전 식별자
If-None-Match요청조건부 GET - 변경 시에만 응답
If-Match요청조건부 PUT/DELETE - 일치 시에만 수행

ETag 종류

종류표기설명
강한 ETag"abc123"바이트 단위 완전 일치
약한 ETagW/"abc123"의미상 동등 (Semantic equivalence)

Spring에서 ETag 구현

// 1. ShallowEtagHeaderFilter 사용 (간단한 방법)
@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
        FilterRegistrationBean<ShallowEtagHeaderFilter> registration =
            new FilterRegistrationBean<>();
        registration.setFilter(new ShallowEtagHeaderFilter());
        registration.addUrlPatterns("/api/*");
        return registration;
    }
}
// 2. 수동 ETag 처리 (세밀한 제어)
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {

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

        Product product = productService.findById(id);

        // ETag 생성 (버전 또는 해시 기반)
        String etag = "\"" + product.getVersion() + "\"";

        // 변경 여부 확인
        if (request.checkNotModified(etag)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }

        return ResponseEntity.ok()
                .eTag(etag)
                .body(product);
    }
}
// 3. Last-Modified와 함께 사용
@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(
        @PathVariable Long id,
        WebRequest request) {

    Product product = productService.findById(id);

    long lastModified = product.getUpdatedAt().toEpochMilli();

    if (request.checkNotModified(lastModified)) {
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
    }

    return ResponseEntity.ok()
            .lastModified(lastModified)
            .body(product);
}

캐싱 헤더 선택 가이드

┌─────────────────────────────────────────────────────────────────┐
│                    캐싱 헤더 조합 가이드                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  권장 조합 (중복 방지):                                          │
│                                                                 │
│   시간 기반        +        검증 기반                            │
│  ┌─────────────┐          ┌─────────────┐                      │
│  │ Cache-Control│   OR    │ ETag        │                      │
│  │ max-age      │         │     OR      │                      │
│  │     OR       │         │ Last-Modified│                      │
│  │ Expires      │         │             │                      │
│  └─────────────┘          └─────────────┘                      │
│                                                                 │
│  ✅ 좋은 예: Cache-Control: max-age=3600 + ETag: "v1"          │
│  ❌ 나쁜 예: Expires + Cache-Control (중복)                     │
│  ❌ 나쁜 예: ETag + Last-Modified (중복)                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

CDN과 다계층 캐싱

캐싱 레이어

┌─────────────────────────────────────────────────────────────────┐
│                    다계층 캐싱 아키텍처                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Client        CDN Edge      API Gateway    Server     DB     │
│  ┌──────┐     ┌──────────┐   ┌──────────┐  ┌──────┐  ┌──────┐ │
│  │      │────►│          │──►│          │─►│      │─►│      │ │
│  │Browser│    │ CloudFront│   │  Kong    │  │Spring│  │Redis │ │
│  │ Cache │    │ Cloudflare│   │          │  │ Boot │  │ MySQL│ │
│  │      │◄───│          │◄──│          │◄─│      │◄─│      │ │
│  └──────┘     └──────────┘   └──────────┘  └──────┘  └──────┘ │
│     ↑              ↑              ↑            ↑          ↑    │
│   L1 캐시       L2 캐시       L3 캐시       L4 캐시    원본 데이터│
│  (브라우저)     (엣지)       (게이트웨이)   (앱 캐시)            │
│                                                                 │
│  가장 빠름 ◄──────────────────────────────────────► 가장 느림   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

캐시 무효화 전략

전략설명사용 시점
TTL 기반시간 경과 후 자동 만료변경 빈도가 예측 가능할 때
버전 기반URL에 버전 포함정적 리소스 배포 시
Purge API수동으로 캐시 삭제긴급 업데이트 시
태그 기반관련 캐시 그룹 삭제연관 데이터 변경 시
# 버전 기반 캐시 무효화 (Cache Busting)
GET /static/app.v2.3.1.js HTTP/1.1

# 또는 쿼리 파라미터 사용
GET /static/app.js?v=2.3.1 HTTP/1.1

비동기 작업 처리

처리 시간이 긴 작업은 동기 방식으로 클라이언트를 대기시키면 안 된다.

비동기 처리 패턴

┌─────────────────────────────────────────────────────────────────┐
│                    비동기 요청/응답 패턴                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Client                   Server                  Worker       │
│     │                        │                       │          │
│     │─── 1. POST /reports ──►│                       │          │
│     │       (보고서 생성 요청) │                       │          │
│     │                        │                       │          │
│     │◄── 2. 202 Accepted ───│                       │          │
│     │    Location: /jobs/123 │                       │          │
│     │                        │                       │          │
│     │                        │── 3. 작업 큐에 추가 ──►│          │
│     │                        │                       │          │
│     │                        │           (백그라운드 처리)       │
│     │                        │                       │          │
│     │─── 4. GET /jobs/123 ──►│                       │          │
│     │       (상태 확인)       │                       │          │
│     │                        │                       │          │
│     │◄── 5. 200 OK ─────────│                       │          │
│     │    {"status":"processing"}                     │          │
│     │                        │                       │          │
│     │        ... (폴링 반복) ...                      │          │
│     │                        │                       │          │
│     │◄── 6. 200 OK ─────────│◄── 완료 ──────────────│          │
│     │    {"status":"completed",                      │          │
│     │     "result":"/reports/456"}                   │          │
│     │                        │                       │          │
│     │─── 7. GET /reports/456 ►│                       │          │
│     │                        │                       │          │
│     │◄── 8. 200 OK (결과) ──│                       │          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

202 Accepted 응답

# 요청
POST /api/v1/reports/generate HTTP/1.1
Content-Type: application/json

{
    "type": "sales",
    "period": "2024-Q1"
}

# 응답
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/abc123
Content-Type: application/json

{
    "jobId": "abc123",
    "status": "queued",
    "message": "Report generation has been queued",
    "estimatedTime": 30,
    "statusUrl": "/api/v1/jobs/abc123"
}

작업 상태 API

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {

    @GetMapping("/{jobId}")
    public ResponseEntity<JobStatus> getJobStatus(@PathVariable String jobId) {

        Job job = jobService.findById(jobId);

        JobStatus status = JobStatus.builder()
                .jobId(jobId)
                .status(job.getStatus()) // QUEUED, PROCESSING, COMPLETED, FAILED
                .progress(job.getProgress())
                .createdAt(job.getCreatedAt())
                .build();

        // 완료된 경우 결과 URL 포함
        if (job.isCompleted()) {
            status.setResultUrl("/api/v1/reports/" + job.getResultId());
        }

        return ResponseEntity.ok(status);
    }
}

Spring에서 비동기 처리

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-");
        executor.initialize();
        return executor;
    }
}
@Service
public class ReportService {

    @Async
    public CompletableFuture<Report> generateReportAsync(ReportRequest request) {

        // 시간이 오래 걸리는 작업
        Report report = generateReport(request);

        return CompletableFuture.completedFuture(report);
    }
}

폴링 vs SSE vs WebSocket

실시간 업데이트를 위한 방법 비교

┌─────────────────────────────────────────────────────────────────┐
│                  실시간 통신 방식 비교                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Polling (폴링)                                                 │
│  ┌──────┐     ┌──────┐                                         │
│  │Client│────►│Server│  주기적으로 요청                         │
│  │      │◄────│      │  데이터 변경 없어도 요청                  │
│  └──────┘     └──────┘  → 리소스 낭비                           │
│                                                                 │
│  SSE (Server-Sent Events)                                       │
│  ┌──────┐     ┌──────┐                                         │
│  │Client│────►│Server│  한 번 연결                              │
│  │      │◄════│      │  서버가 일방향으로 푸시                   │
│  └──────┘     └──────┘  → 서버→클라이언트만 가능                 │
│                                                                 │
│  WebSocket                                                      │
│  ┌──────┐     ┌──────┐                                         │
│  │Client│◄═══►│Server│  양방향 통신                             │
│  │      │     │      │  실시간 상호작용                         │
│  └──────┘     └──────┘  → 가장 효율적, 복잡                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
방식장점단점사용 사례
Polling구현 간단, 호환성 좋음리소스 낭비, 지연작업 상태 확인
Long Polling실시간에 가까움연결 유지 필요채팅 (레거시)
SSE자동 재연결, 간단단방향만 가능알림, 피드
WebSocket양방향, 저지연구현 복잡게임, 실시간 협업

SSE 구현 예시

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {

    @GetMapping(value = "/{jobId}/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<ServerSentEvent<JobStatus>> streamJobStatus(@PathVariable String jobId) {

        return Flux.interval(Duration.ofSeconds(1))
                .map(i -> {
                    JobStatus status = jobService.getStatus(jobId);
                    return ServerSentEvent.<JobStatus>builder()
                            .id(String.valueOf(i))
                            .event("job-status")
                            .data(status)
                            .build();
                })
                .takeUntil(sse -> sse.data().isCompleted());
    }
}

HTTP PATCH와 부분 업데이트

PUT vs PATCH

┌─────────────────────────────────────────────────────────────────┐
│                     PUT vs PATCH 비교                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   PUT (전체 교체)                    PATCH (부분 수정)            │
│  ┌────────────────────────┐       ┌────────────────────────┐   │
│  │                        │       │                        │   │
│  │  모든 필드 전송 필요     │       │  변경할 필드만 전송      │   │
│  │                        │       │                        │   │
│  │  {                     │       │  {                     │   │
│  │    "name": "John",     │       │    "name": "John"      │   │
│  │    "email": "...",     │       │  }                     │   │
│  │    "age": 30,          │       │                        │   │
│  │    "address": "..."    │       │  → email, age, address │   │
│  │  }                     │       │    유지됨               │   │
│  │                        │       │                        │   │
│  │  멱등성: O             │       │  멱등성: X (일반적으로) │   │
│  │                        │       │                        │   │
│  └────────────────────────┘       └────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

JSON Merge Patch (RFC 7396)

가장 직관적인 부분 업데이트 방식

# 요청
PATCH /api/v1/users/123 HTTP/1.1
Content-Type: application/merge-patch+json

{
    "name": "John Updated",
    "email": null
}

# 결과
# - name: "John Updated" 변경
# - email: 삭제됨 (null)
# - 다른 필드: 유지
// Spring에서 JSON Merge Patch 처리
@PatchMapping(value = "/{id}", consumes = "application/merge-patch+json")
public ResponseEntity<User> patchUser(
        @PathVariable Long id,
        @RequestBody JsonMergePatch patch) throws JsonPatchException {

    User user = userService.findById(id);

    // 패치 적용
    JsonNode patched = patch.apply(objectMapper.valueToTree(user));
    User patchedUser = objectMapper.treeToValue(patched, User.class);

    return ResponseEntity.ok(userService.save(patchedUser));
}

JSON Patch (RFC 6902)

복잡한 변경을 명시적으로 표현

# 요청
PATCH /api/v1/orders/1234 HTTP/1.1
Content-Type: application/json-patch+json

[
    {"op": "replace", "path": "/status", "value": "COMPLETED"},
    {"op": "add", "path": "/notes/-", "value": "배송 완료"},
    {"op": "remove", "path": "/tempData"},
    {"op": "copy", "path": "/billingAddress", "from": "/shippingAddress"},
    {"op": "move", "path": "/archivedAt", "from": "/completedAt"},
    {"op": "test", "path": "/version", "value": 5}
]

JSON Patch 연산자

연산자설명예시
add값 추가{"op":"add", "path":"/tags/-", "value":"new"}
remove값 삭제{"op":"remove", "path":"/temp"}
replace값 교체{"op":"replace", "path":"/name", "value":"New Name"}
move값 이동{"op":"move", "from":"/a", "path":"/b"}
copy값 복사{"op":"copy", "from":"/a", "path":"/b"}
test값 검증{"op":"test", "path":"/version", "value":5}
// Spring에서 JSON Patch 처리
@PatchMapping(value = "/{id}", consumes = "application/json-patch+json")
public ResponseEntity<Order> patchOrder(
        @PathVariable Long id,
        @RequestBody JsonPatch patch) throws JsonPatchException {

    Order order = orderService.findById(id);

    JsonNode patched = patch.apply(objectMapper.valueToTree(order));
    Order patchedOrder = objectMapper.treeToValue(patched, Order.class);

    return ResponseEntity.ok(orderService.save(patchedOrder));
}

JSON Patch 활용 분야

┌─────────────────────────────────────────────────────────────────┐
│                  JSON Patch 활용 사례                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  단일 페이지 애플리케이션 (SPA)                           │   │
│  │  → 대역폭 절약, 빠른 UI 반응                             │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  실시간 협업 시스템                                       │   │
│  │  → Google Docs 스타일 동시 편집, 충돌 해결               │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  오프라인 데이터 동기화                                   │   │
│  │  → 변경사항만 전송하여 동기화 효율화                      │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  대용량 문서 편집                                         │   │
│  │  → 전체 문서 대신 변경된 부분만 전송                      │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Rate Limiting

API 과부하를 방지하고 공정한 사용을 보장하는 기법

Rate Limiting 전략

전략설명특징
Fixed Window고정 시간 윈도우간단하지만 버스트 가능
Sliding Window이동 시간 윈도우더 정확한 제한
Token Bucket토큰 기반버스트 허용
Leaky Bucket일정 속도 처리안정적인 속도

Rate Limit 응답 헤더

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1609459200

# 또는 (표준화된 헤더)
RateLimit-Limit: 1000
RateLimit-Remaining: 999
RateLimit-Reset: 3600

429 Too Many Requests

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 60

{
    "error": "Too Many Requests",
    "message": "Rate limit exceeded. Please retry after 60 seconds.",
    "retryAfter": 60
}

캐싱 베스트 프랙티스

┌─────────────────────────────────────────────────────────────────┐
│                  캐싱 베스트 프랙티스                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ DO                                                          │
│  ───────────────────────────────────────────────────────────── │
│  • 정적 리소스에 장기 캐시 + 버전 기반 URL 사용                   │
│  • Cache-Control과 ETag 조합 사용                               │
│  • CDN 레이어 활용                                              │
│  • GET 요청에만 캐싱 적용                                        │
│  • 공용 데이터는 public, 개인 데이터는 private 설정              │
│                                                                 │
│  ❌ DON'T                                                       │
│  ───────────────────────────────────────────────────────────── │
│  • PUT/POST 요청 결과 캐싱                                       │
│  • 쿼리 파라미터가 있는 요청 캐싱 (파라미터 값 변경 시 무효)       │
│  • 인증이 필요한 개인 데이터를 public 캐시                        │
│  • Expires와 max-age 동시 사용 (중복)                            │
│  • ETag와 Last-Modified 동시 사용 (중복)                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

핵심 용어 정리

용어설명
Cache Hit캐시에서 응답을 찾음
Cache Miss캐시에 없어 원본 서버 요청
TTLTime To Live, 캐시 유효 시간
ETag리소스 버전 식별자
조건부 요청If-None-Match, If-Modified-Since 사용
304 Not Modified리소스 변경 없음, 캐시 사용
202 Accepted비동기 요청 접수됨
폴링주기적으로 상태 확인 요청
SSEServer-Sent Events, 서버→클라이언트 푸시
JSON PatchRFC 6902, 부분 업데이트 표준
JSON Merge PatchRFC 7396, 간단한 부분 업데이트
Rate LimitingAPI 요청 속도 제한