5 분 소요


What is Edge Computing?

Edge computing is a paradigm that processes data at locations close to the user.

Benefits

  • Low Latency: Minimize physical distance
  • Bandwidth Savings: Reduce origin server traffic
  • High Availability: Fault isolation through distributed processing
  • Global Scalability: Leverage worldwide PoPs
[Traditional]
User(Seoul) ─────────────────▶ Server(US) ─▶ Response
                200ms

[Edge]
User(Seoul) ───▶ Edge(Seoul) ───▶ Server(US)
                10ms       (if needed)

Basic CDN Utilization

Static Resource Caching

# Cloudflare Page Rules
- match: "*.example.com/static/*"
  cache_level: cache_everything
  edge_cache_ttl: 86400  # 24 hours

- match: "api.example.com/v1/products/*"
  cache_level: cache_everything
  edge_cache_ttl: 300    # 5 minutes
  cache_by_cookie: false

Cache-Control Configuration in Spring Boot

@Configuration
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/static/**")
            .addResourceLocations("classpath:/static/")
            .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS).cachePublic())
    }
}

@RestController
class ProductController(
    private val productService: ProductService
) {
    @GetMapping("/api/products/{id}")
    fun getProduct(@PathVariable id: String): ResponseEntity<Product> {
        val product = productService.getProduct(id)

        return ResponseEntity.ok()
            .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
            .eTag(product.version.toString())
            .body(product)
    }
}

Precise Cache Invalidation with Surrogate Keys

@RestController
class ProductController(
    private val productService: ProductService
) {
    @GetMapping("/api/categories/{categoryId}/products")
    fun getProductsByCategory(
        @PathVariable categoryId: String
    ): ResponseEntity<List<Product>> {
        val products = productService.getByCategory(categoryId)

        return ResponseEntity.ok()
            .header("Surrogate-Key", "category-$categoryId products")
            .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
            .body(products)
    }
}

// Cache invalidation
@Service
class CacheInvalidationService(
    private val webClient: WebClient
) {
    fun invalidateCategory(categoryId: String) {
        webClient.post()
            .uri("https://api.fastly.com/service/{serviceId}/purge/category-$categoryId")
            .header("Fastly-Key", fastlyApiKey)
            .retrieve()
            .toBodilessEntity()
            .block()
    }
}

Edge Functions

Cloudflare Workers

// workers/api-router.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Regional routing
    const country = request.cf?.country || 'US';
    const regionOrigin = getRegionOrigin(country);

    // A/B testing
    const variant = getVariant(request);

    // Check cache
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;
    let response = await cache.match(cacheKey);

    if (!response) {
      // Request to origin
      response = await fetch(regionOrigin + url.pathname, {
        headers: {
          ...request.headers,
          'X-Variant': variant,
          'X-Country': country
        }
      });

      // Store in cache
      if (response.ok) {
        const cached = response.clone();
        cached.headers.set('Cache-Control', 'public, max-age=60');
        await cache.put(cacheKey, cached);
      }
    }

    return response;
  }
};

function getRegionOrigin(country) {
  const regions = {
    'KR': 'https://api-ap.example.com',
    'JP': 'https://api-ap.example.com',
    'US': 'https://api-us.example.com',
    'DE': 'https://api-eu.example.com',
  };
  return regions[country] || regions['US'];
}

function getVariant(request) {
  const cookie = request.headers.get('Cookie');
  // Check existing variant or assign new one
  return 'A';  // or 'B'
}

AWS Lambda@Edge

// Lambda@Edge - Viewer Request
class ViewerRequestHandler : RequestHandler<CloudFrontEvent, CloudFrontResponse> {

    override fun handleRequest(event: CloudFrontEvent, context: Context): CloudFrontResponse {
        val request = event.records[0].cf.request

        // Validate authentication token
        val authHeader = request.headers["authorization"]?.get(0)?.value

        if (authHeader == null || !validateToken(authHeader)) {
            return CloudFrontResponse().apply {
                status = "401"
                statusDescription = "Unauthorized"
                body = "Invalid token"
            }
        }

        // Transform request
        request.headers["x-user-id"] = listOf(
            Header("x-user-id", extractUserId(authHeader))
        )

        return request
    }
}

Regional Data Routing

Spring Boot Multi-Region Configuration

@Configuration
class RegionalRoutingConfig {

    @Bean
    fun regionalDataSource(
        @Value("\${region}") region: String
    ): DataSource {
        val config = when (region) {
            "ap-northeast-2" -> DataSourceConfig(
                primary = "jdbc:postgresql://db-ap-northeast-2.example.com:5432/mydb",
                replica = "jdbc:postgresql://db-ap-replica.example.com:5432/mydb"
            )
            "us-east-1" -> DataSourceConfig(
                primary = "jdbc:postgresql://db-us-east-1.example.com:5432/mydb",
                replica = "jdbc:postgresql://db-us-replica.example.com:5432/mydb"
            )
            else -> throw IllegalArgumentException("Unknown region: $region")
        }

        return createRoutingDataSource(config)
    }

    private fun createRoutingDataSource(config: DataSourceConfig): AbstractRoutingDataSource {
        return object : AbstractRoutingDataSource() {
            override fun determineCurrentLookupKey(): Any {
                return if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                    "replica"
                } else {
                    "primary"
                }
            }
        }.apply {
            setTargetDataSources(mapOf(
                "primary" to createHikariDataSource(config.primary),
                "replica" to createHikariDataSource(config.replica)
            ))
            setDefaultTargetDataSource(createHikariDataSource(config.primary))
        }
    }
}

Region-Aware Service

@Service
class RegionalService(
    private val regionalApiClients: Map<String, WebClient>
) {
    fun getDataFromNearestRegion(userId: String, userRegion: String): Data {
        val client = regionalApiClients[userRegion]
            ?: regionalApiClients["default"]!!

        return client.get()
            .uri("/api/data/{userId}", userId)
            .retrieve()
            .bodyToMono(Data::class.java)
            .block()!!
    }
}

@Configuration
class RegionalClientConfig {

    @Bean
    fun regionalApiClients(): Map<String, WebClient> {
        return mapOf(
            "ap-northeast-2" to WebClient.create("https://api-ap.example.com"),
            "us-east-1" to WebClient.create("https://api-us.example.com"),
            "eu-west-1" to WebClient.create("https://api-eu.example.com"),
            "default" to WebClient.create("https://api-us.example.com")
        )
    }
}

Hybrid Architecture

Edge + Origin Combination

┌────────────────────────────────────────────────────┐
│                     Edge Layer                      │
│  ┌─────────────┐  ┌─────────────┐  ┌───────────┐  │
│  │ Auth Check  │  │ Rate Limit  │  │   Cache   │  │
│  │ Token Parse │  │ Bot Detect  │  │ Static    │  │
│  └─────────────┘  └─────────────┘  └───────────┘  │
└───────────────────────┬────────────────────────────┘
                        │
                        ▼
┌────────────────────────────────────────────────────┐
│                    Origin Layer                     │
│  ┌─────────────┐  ┌─────────────┐  ┌───────────┐  │
│  │ Business    │  │ Database    │  │ External  │  │
│  │ Logic       │  │ Operations  │  │ APIs      │  │
│  └─────────────┘  └─────────────┘  └───────────┘  │
└────────────────────────────────────────────────────┘

Tasks to Handle at the Edge

// Cloudflare Worker - Hybrid Processing
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // 1. Static resources - Fully handled at edge
    if (url.pathname.startsWith('/static/')) {
      return handleStaticResource(request, env);
    }

    // 2. Authentication - Validated at edge
    const authResult = await validateAuth(request, env);
    if (!authResult.valid) {
      return new Response('Unauthorized', { status: 401 });
    }

    // 3. Rate Limiting - Handled at edge
    const rateLimitResult = await checkRateLimit(request, env);
    if (rateLimitResult.exceeded) {
      return new Response('Too Many Requests', {
        status: 429,
        headers: { 'Retry-After': rateLimitResult.retryAfter }
      });
    }

    // 4. Cacheable API - Check edge cache
    if (isCacheable(request)) {
      const cached = await getCachedResponse(request, env);
      if (cached) return cached;
    }

    // 5. Forward to origin
    const response = await fetch(env.ORIGIN_URL + url.pathname, {
      method: request.method,
      headers: {
        ...request.headers,
        'X-User-Id': authResult.userId,
        'X-Request-Region': request.cf.colo
      },
      body: request.body
    });

    // 6. Cache response
    if (isCacheable(request) && response.ok) {
      await cacheResponse(request, response.clone(), env);
    }

    return response;
  }
};

Edge Databases

Cloudflare D1 (SQLite at Edge)

// D1 usage example
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname === '/api/products') {
      const { results } = await env.DB.prepare(
        'SELECT * FROM products WHERE category = ? LIMIT 10'
      )
        .bind(url.searchParams.get('category'))
        .all();

      return Response.json(results);
    }

    // Fallback to origin
    return fetch(request);
  }
};

Cloudflare KV (Key-Value at Edge)

// Session management with KV
export default {
  async fetch(request, env) {
    const sessionId = getCookie(request, 'session_id');

    if (!sessionId) {
      return new Response('No session', { status: 401 });
    }

    // Session lookup at edge (< 1ms)
    const session = await env.SESSIONS.get(sessionId, 'json');

    if (!session) {
      return new Response('Invalid session', { status: 401 });
    }

    // Add session info to headers and forward to origin
    const modifiedRequest = new Request(request);
    modifiedRequest.headers.set('X-User-Id', session.userId);
    modifiedRequest.headers.set('X-User-Role', session.role);

    return fetch(modifiedRequest);
  }
};

Monitoring and Analytics

Edge Log Collection

// Collecting edge information in Spring Boot
@RestController
class ApiController {

    @GetMapping("/api/data")
    fun getData(
        @RequestHeader("CF-Ray") cfRay: String?,
        @RequestHeader("CF-IPCountry") country: String?,
        @RequestHeader("X-Request-Region") region: String?
    ): ResponseEntity<Data> {
        // Record metrics
        Metrics.counter("api.requests",
            "country", country ?: "unknown",
            "edge_region", region ?: "unknown"
        ).increment()

        // Log
        logger.info("Request from country=$country, edge=$region, ray=$cfRay")

        return ResponseEntity.ok(data)
    }
}

Edge Metrics Dashboard

# Request distribution by region
sum by (country) (rate(api_requests_total[5m]))

# Edge cache hit rate
sum(rate(edge_cache_hits_total[5m])) /
sum(rate(edge_requests_total[5m])) * 100

# Response time by region
histogram_quantile(0.95,
  sum by (le, region) (
    rate(http_request_duration_seconds_bucket[5m])
  )
)

Summary

Edge computing utilization checklist:

Task Location Technology
Static Resources Edge CDN Caching
Auth/Authz Edge Edge Functions
Rate Limiting Edge Edge Functions + KV
Session Management Edge Edge KV
Business Logic Origin Spring Boot
Database Origin/Edge RDS/D1
API Caching Edge CDN + Surrogate Keys

댓글남기기