백엔드 개발자를 위한 엣지 컴퓨팅 패턴 Edge Computing Patterns for Backend Developers
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 |
댓글남기기