Kong API Gateway와 루아 스크립팅을 활용한 고성능 API 게이트웨이 구축부터 Redis 연동 기반 캐시 전략과 원자적 연산 구현까지 실전 개발 가이드를 통해 마이크로서비스 아키텍처의 핵심 기술을 마스터하는 완벽한 튜토리얼입니다.
Kong API Gateway 소개와 루아의 역할
Kong은 현재 가장 인기 있는 오픈소스 API 게이트웨이 중 하나로, 마이크로서비스 아키텍처에서 핵심적인 역할을 담당합니다.
Kong의 가장 큰 특징은 루아 스크립팅을 통한 플러그인 시스템으로, 이를 통해 개발자는 API 게이트웨이의 동작을 세밀하게 제어할 수 있습니다.
Kong은 OpenResty 위에 구축되어 있으며, 이는 Nginx와 LuaJIT이 결합된 웹 플랫폼입니다.
이러한 구조 덕분에 Kong은 높은 성능을 유지하면서도 유연한 확장성을 제공합니다.
루아 스크립팅은 Kong에서 다음과 같은 역할을 수행합니다:
- 인증 및 권한 부여: JWT, OAuth, API Key 등 다양한 인증 방식 구현
- 속도 제한: 클라이언트별, API별 요청 제한 설정
- 로깅 및 모니터링: 커스텀 로그 포맷과 메트릭 수집
- 캐싱: Redis 연동을 통한 응답 캐싱 전략
- 변환: 요청/응답 데이터 변환 및 라우팅
Kong 설치 및 기본 설정
Kong을 설치하는 방법은 여러 가지가 있지만, Docker를 사용한 설치가 가장 간편합니다.
# PostgreSQL 데이터베이스 실행
docker run -d --name kong-database \
-p 5432:5432 \
-e POSTGRES_DB=kong \
-e POSTGRES_USER=kong \
-e POSTGRES_PASSWORD=kong \
postgres:13
# Kong 데이터베이스 마이그레이션
docker run --rm \
--link kong-database:kong-database \
-e KONG_DATABASE=postgres \
-e KONG_PG_HOST=kong-database \
-e KONG_PG_DATABASE=kong \
-e KONG_PG_USER=kong \
-e KONG_PG_PASSWORD=kong \
kong:latest kong migrations bootstrap
# Kong 실행
docker run -d --name kong \
--link kong-database:kong-database \
-e KONG_DATABASE=postgres \
-e KONG_PG_HOST=kong-database \
-e KONG_PG_DATABASE=kong \
-e KONG_PG_USER=kong \
-e KONG_PG_PASSWORD=kong \
-e KONG_ADMIN_ACCESS_LOG=/dev/stdout \
-e KONG_ADMIN_ERROR_LOG=/dev/stderr \
-e KONG_ADMIN_LISTEN=0.0.0.0:8001 \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
kong:latest
Kong이 정상적으로 실행되면 다음 포트로 접근할 수 있습니다:
- 8000: API 게이트웨이 포트 (HTTP)
- 8443: API 게이트웨이 포트 (HTTPS)
- 8001: Admin API 포트 (HTTP)
- 8444: Admin API 포트 (HTTPS)
Admin API를 통해 Kong의 설정을 확인해보겠습니다:
curl -i http://localhost:8001/
기본 서비스와 라우트 설정
Kong에서 API를 관리하기 위해서는 서비스(Service)와 라우트(Route)의 개념을 이해해야 합니다.
서비스는 업스트림 API의 추상화된 표현이며, 라우트는 클라이언트 요청을 특정 서비스로 라우팅하는 규칙입니다.
서비스 생성
curl -i -X POST http://localhost:8001/services/ \
--data "name=example-service" \
--data "url=http://httpbin.org"
라우트 생성
curl -i -X POST http://localhost:8001/services/example-service/routes \
--data "hosts[]=example.com" \
--data "paths[]=/api"
이제 Kong을 통해 업스트림 서비스에 요청을 보낼 수 있습니다:
curl -i -X GET http://localhost:8000/api/get \
--header "Host: example.com"
Redis 연동과 루아 스크립팅 기초
Kong에서 Redis를 활용한 캐싱 전략을 구현하기 위해서는 Redis 연동이 필요합니다.
Redis는 Kong의 데이터 저장소로 사용될 수 있으며, 특히 세션 관리와 캐싱에서 중요한 역할을 합니다.
Redis 설치 및 연결
# Redis 컨테이너 실행
docker run -d --name kong-redis \
-p 6379:6379 \
redis:latest
# Kong에서 Redis 연결 테스트
docker exec -it kong lua -e "
local redis = require 'resty.redis'
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect('host.docker.internal', 6379)
if not ok then
print('Redis 연결 실패: ', err)
else
print('Redis 연결 성공')
red:close()
end
"
루아에서 Redis 조작
Kong의 루아 환경에서 Redis를 사용하는 기본 패턴입니다:
local redis = require "resty.redis"
local function connect_redis()
local red = redis:new()
red:set_timeout(1000) -- 1초 타임아웃
local ok, err = red:connect("redis-host", 6379)
if not ok then
kong.log.err("Redis 연결 실패: ", err)
return nil, err
end
return red
end
local function get_cached_data(key)
local red, err = connect_redis()
if not red then
return nil, err
end
local result, err = red:get(key)
red:set_keepalive(10000, 100)
if not result or result == ngx.null then
return nil, "캐시 데이터 없음"
end
return result
end
이전 글 참조: 루아 입문 시리즈 #11: Redis와 루아 스크립팅에서 Redis 루아 스크립트의 기초를 다뤘습니다.
루아 입문 시리즈 #11: Redis와 루아 스크립팅
Redis Lua 스크립트를 활용한 원자적 연산과 고성능 캐시 전략으로 데이터베이스 부하를 줄이고 애플리케이션 성능을 극대화하는 실전 가이드입니다.Redis Lua 스크립팅이란?Redis는 인메모리 데이터
notavoid.tistory.com
Kong 플러그인 개발 실전
Kong의 진정한 파워는 커스텀 플러그인 개발에 있습니다.
플러그인을 통해 API 게이트웨이의 동작을 세밀하게 제어할 수 있으며, 비즈니스 로직을 구현할 수 있습니다.
플러그인 구조
Kong 플러그인은 다음과 같은 파일 구조를 가집니다:
my-plugin/
├── handler.lua
├── schema.lua
└── migrations/
└── 001_initial.lua
기본 플러그인 템플릿
-- handler.lua
local MyPlugin = {
PRIORITY = 1000, -- 플러그인 실행 순서
VERSION = "1.0.0",
}
function MyPlugin:access(conf)
-- 요청 처리 전 실행
kong.log.info("MyPlugin access phase")
-- 요청 헤더 검증
local api_key = kong.request.get_header("X-API-Key")
if not api_key then
return kong.response.exit(401, {
message = "API Key required"
})
end
-- Redis에서 API Key 검증
local red = connect_redis()
if red then
local valid, err = red:get("api_key:" .. api_key)
if not valid or valid == ngx.null then
return kong.response.exit(401, {
message = "Invalid API Key"
})
end
red:set_keepalive(10000, 100)
end
end
function MyPlugin:response(conf)
-- 응답 처리 시 실행
kong.response.set_header("X-Powered-By", "Kong-MyPlugin")
end
return MyPlugin
스키마 정의
-- schema.lua
local typedefs = require "kong.db.schema.typedefs"
local schema = {
name = "my-plugin",
fields = {{
config = {
type = "record",
fields = {
{ api_key_header = { type = "string", default = "X-API-Key" } },
{ redis_host = { type = "string", default = "localhost" } },
{ redis_port = { type = "integer", default = 6379 } },
{ cache_ttl = { type = "integer", default = 300 } },
},
},
}},
}
return schema
Redis 루아 스크립트와 원자적 연산
Redis 루아 스크립트를 활용하면 원자적 연산을 보장하면서 복잡한 로직을 구현할 수 있습니다.
Kong에서 이는 특히 속도 제한(Rate Limiting)과 캐싱 전략에서 중요합니다.
속도 제한 구현
-- 속도 제한 루아 스크립트
local rate_limit_script = [[
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])
-- 현재 윈도우 시작 시간 계산
local window_start = math.floor(current_time / window) * window
local window_key = key .. ":" .. window_start
-- 현재 요청 수 조회
local current_requests = redis.call('GET', window_key)
if not current_requests then
current_requests = 0
else
current_requests = tonumber(current_requests)
end
-- 제한 확인
if current_requests >= limit then
return {0, current_requests, window - (current_time - window_start)}
end
-- 요청 수 증가
local new_requests = redis.call('INCR', window_key)
redis.call('EXPIRE', window_key, window)
return {1, new_requests, window - (current_time - window_start)}
]]
-- Kong 플러그인에서 사용
function MyPlugin:access(conf)
local red = connect_redis()
if not red then
kong.log.err("Redis 연결 실패")
return
end
local client_id = kong.client.get_forwarded_ip()
local current_time = ngx.time()
local result, err = red:eval(rate_limit_script, 1,
"rate_limit:" .. client_id,
conf.time_window,
conf.limit,
current_time)
if not result then
kong.log.err("속도 제한 스크립트 실행 실패: ", err)
return
end
local allowed, requests, reset_time = result[1], result[2], result[3]
-- 응답 헤더 설정
kong.response.set_header("X-RateLimit-Limit", conf.limit)
kong.response.set_header("X-RateLimit-Remaining", conf.limit - requests)
kong.response.set_header("X-RateLimit-Reset", reset_time)
if allowed == 0 then
return kong.response.exit(429, {
message = "Rate limit exceeded"
})
end
red:set_keepalive(10000, 100)
end
캐시 전략 구현
-- 스마트 캐싱 루아 스크립트
local smart_cache_script = [[
local cache_key = KEYS[1]
local lock_key = KEYS[2]
local ttl = tonumber(ARGV[1])
local lock_ttl = tonumber(ARGV[2])
-- 캐시 데이터 확인
local cached_data = redis.call('GET', cache_key)
if cached_data then
-- TTL 연장 (자주 사용되는 데이터)
redis.call('EXPIRE', cache_key, ttl)
return {1, cached_data, 'hit'}
end
-- 캐시 미스 시 락 획득 시도
local lock_acquired = redis.call('SET', lock_key, '1', 'NX', 'EX', lock_ttl)
if lock_acquired then
return {0, nil, 'miss_with_lock'}
else
return {0, nil, 'miss_no_lock'}
end
]]
function get_or_set_cache(key, data_fetcher, ttl)
local red = connect_redis()
if not red then
return data_fetcher()
end
local cache_key = "cache:" .. key
local lock_key = "lock:" .. key
local result, err = red:eval(smart_cache_script, 2,
cache_key, lock_key, ttl or 300, 10)
if not result then
kong.log.err("캐시 스크립트 실행 실패: ", err)
return data_fetcher()
end
local hit, data, status = result[1], result[2], result[3]
if hit == 1 then
-- 캐시 히트
red:set_keepalive(10000, 100)
return cjson.decode(data)
elseif status == 'miss_with_lock' then
-- 락 획득 성공, 데이터 생성
local fresh_data = data_fetcher()
-- 캐시에 저장
red:setex(cache_key, ttl, cjson.encode(fresh_data))
red:del(lock_key) -- 락 해제
red:set_keepalive(10000, 100)
return fresh_data
else
-- 락 획득 실패, 잠시 대기 후 재시도 또는 직접 데이터 생성
red:set_keepalive(10000, 100)
ngx.sleep(0.01) -- 10ms 대기
-- 재귀 호출로 재시도 (최대 3회)
local retry_count = ngx.ctx.cache_retry_count or 0
if retry_count < 3 then
ngx.ctx.cache_retry_count = retry_count + 1
return get_or_set_cache(key, data_fetcher, ttl)
else
return data_fetcher()
end
end
end
성능 최적화와 모니터링
Kong API Gateway의 성능을 최적화하기 위해서는 다양한 요소를 고려해야 합니다.
특히 루아 스크립트의 효율성과 Redis 연결 관리가 중요합니다.
연결 풀 최적화
-- 효율적인 Redis 연결 관리
local redis_pool = {}
local function get_redis_connection(conf)
local pool_key = conf.redis_host .. ":" .. conf.redis_port
if redis_pool[pool_key] then
return redis_pool[pool_key]
end
local red = redis:new()
red:set_timeout(conf.timeout or 1000)
local ok, err = red:connect(conf.redis_host, conf.redis_port)
if not ok then
kong.log.err("Redis 연결 실패: ", err)
return nil, err
end
-- 연결 풀에 저장
redis_pool[pool_key] = red
return red
end
-- 연결 재사용
local function safe_redis_call(conf, callback)
local red, err = get_redis_connection(conf)
if not red then
return nil, err
end
local result, call_err = callback(red)
-- 연결 상태 확인 및 재설정
if call_err and string.find(call_err, "closed") then
redis_pool[conf.redis_host .. ":" .. conf.redis_port] = nil
red, err = get_redis_connection(conf)
if red then
result, call_err = callback(red)
end
end
if red then
red:set_keepalive(10000, 100)
end
return result, call_err
end
메트릭 수집
-- 성능 메트릭 수집
local metrics = {
cache_hits = 0,
cache_misses = 0,
redis_errors = 0,
total_requests = 0
}
local function update_metrics(metric_name, value)
metrics[metric_name] = (metrics[metric_name] or 0) + (value or 1)
-- 주기적으로 Redis에 메트릭 저장
if metrics.total_requests % 100 == 0 then
safe_redis_call(conf, function(red)
local timestamp = os.time()
local metrics_key = "metrics:" .. timestamp
for key, val in pairs(metrics) do
red:hset(metrics_key, key, val)
end
red:expire(metrics_key, 3600) -- 1시간 보관
return true
end)
end
end
-- 플러그인에서 메트릭 사용
function MyPlugin:access(conf)
update_metrics("total_requests")
-- 캐시 로직 실행
local cached_result = get_or_set_cache("user_data", function()
update_metrics("cache_misses")
return fetch_user_data()
end, 300)
if cached_result then
update_metrics("cache_hits")
end
end
실제 사용 사례와 패턴
Kong과 Redis를 활용한 실제 프로덕션 환경에서의 사용 사례를 살펴보겠습니다.
JWT 토큰 블랙리스트 관리
-- JWT 블랙리스트 플러그인
local jwt_blacklist_script = [[
local token_key = KEYS[1]
local blacklist_key = KEYS[2]
local exp_time = tonumber(ARGV[1])
local current_time = tonumber(ARGV[2])
-- 토큰이 이미 블랙리스트에 있는지 확인
local is_blacklisted = redis.call('SISMEMBER', blacklist_key, token_key)
if is_blacklisted == 1 then
return {0, 'blacklisted'}
end
-- 토큰 만료 시간 확인
if exp_time <= current_time then
return {0, 'expired'}
end
return {1, 'valid'}
]]
function MyPlugin:access(conf)
local token = kong.request.get_header("Authorization")
if not token then
return kong.response.exit(401, { message = "Token required" })
end
-- Bearer 토큰 추출
local jwt_token = string.match(token, "Bearer%s+(.+)")
if not jwt_token then
return kong.response.exit(401, { message = "Invalid token format" })
end
-- JWT 디코딩 (간단한 예시)
local jwt_parts = {}
for part in string.gmatch(jwt_token, "[^%.]+") do
table.insert(jwt_parts, part)
end
if #jwt_parts ~= 3 then
return kong.response.exit(401, { message = "Invalid JWT format" })
end
-- 페이로드 디코딩
local payload_json = ngx.decode_base64(jwt_parts[2])
local payload = cjson.decode(payload_json)
safe_redis_call(conf, function(red)
local result, err = red:eval(jwt_blacklist_script, 2,
jwt_token, "jwt_blacklist",
payload.exp, ngx.time())
if not result or result[1] == 0 then
kong.response.exit(401, { message = result[2] or "Token validation failed" })
end
return true
end)
end
-- 토큰 블랙리스트 추가 API
function add_to_blacklist(token, exp_time)
safe_redis_call(conf, function(red)
red:sadd("jwt_blacklist", token)
red:expireat("jwt_blacklist", exp_time)
return true
end)
end
API 사용량 통계 및 빌링
-- API 사용량 통계 스크립트
local usage_tracking_script = [[
local user_key = KEYS[1]
local api_key = KEYS[2]
local timestamp = tonumber(ARGV[1])
local cost = tonumber(ARGV[2])
-- 일별 통계
local daily_key = user_key .. ":daily:" .. os.date("%Y%m%d", timestamp)
local api_daily_key = api_key .. ":daily:" .. os.date("%Y%m%d", timestamp)
-- 월별 통계
local monthly_key = user_key .. ":monthly:" .. os.date("%Y%m", timestamp)
local api_monthly_key = api_key .. ":monthly:" .. os.date("%Y%m", timestamp)
-- 사용량 증가
redis.call('HINCRBY', daily_key, 'requests', 1)
redis.call('HINCRBY', daily_key, 'cost', cost)
redis.call('EXPIRE', daily_key, 86400 * 7) -- 7일 보관
redis.call('HINCRBY', api_daily_key, 'requests', 1)
redis.call('HINCRBY', api_daily_key, 'cost', cost)
redis.call('EXPIRE', api_daily_key, 86400 * 7)
redis.call('HINCRBY', monthly_key, 'requests', 1)
redis.call('HINCRBY', monthly_key, 'cost', cost)
redis.call('EXPIRE', monthly_key, 86400 * 32) -- 32일 보관
redis.call('HINCRBY', api_monthly_key, 'requests', 1)
redis.call('HINCRBY', api_monthly_key, 'cost', cost)
redis.call('EXPIRE', api_monthly_key, 86400 * 32)
-- 현재 월 사용량 반환
local current_usage = redis.call('HMGET', monthly_key, 'requests', 'cost')
return {tonumber(current_usage[1]) or 0, tonumber(current_usage[2]) or 0}
]]
function MyPlugin:response(conf)
local user_id = kong.ctx.shared.user_id
local api_name = kong.router.get_route().name
local response_size = kong.response.get_header("Content-Length") or 0
-- API 비용 계산 (예: 응답 크기 기반)
local cost = math.ceil(tonumber(response_size) / 1024) -- KB당 1포인트
safe_redis_call(conf, function(red)
local result, err = red:eval(usage_tracking_script, 2,
"user:" .. user_id, "api:" .. api_name,
ngx.time(), cost)
if result then
local monthly_requests, monthly_cost = result[1], result[2]
kong.log.info("사용량 - 요청: ", monthly_requests, ", 비용: ", monthly_cost)
end
return true
end)
end
Kong과 Redis 성능 비교
다른 솔루션들과의 성능 비교를 통해 Kong + Redis 조합의 우수성을 확인해보겠습니다.
항목 | Kong + Redis | AWS API Gateway | Zuul + Eureka | Ambassador |
---|---|---|---|---|
처리량 (RPS) | 15,000+ | 10,000+ | 8,000+ | 12,000+ |
응답 시간 (ms) | < 5 | < 10 | < 15 | < 8 |
캐싱 성능 | 매우 우수 | 제한적 | 보통 | 우수 |
확장성 | 수평 확장 | 자동 확장 | 복잡 | 쿠버네티스 기반 |
비용 효율성 | 높음 | 중간 | 높음 | 중간 |
커스터마이징 | 매우 유연 | 제한적 | 유연 | 유연 |
Kong과 Redis의 조합은 특히 다음 영역에서 뛰어난 성능을 보입니다:
캐싱 효율성: Redis의 인메모리 특성으로 캐시 히트율 95% 이상 달성 가능
원자적 연산: 루아 스크립트를 통한 복잡한 비즈니스 로직을 원자적으로 처리
실시간 처리: 마이크로초 단위의 빠른 응답 시간
트러블슈팅과 베스트 프랙티스
Kong과 Redis를 운영하면서 자주 발생하는 문제들과 해결 방법을 정리했습니다.
메모리 관리
-- 메모리 효율적인 데이터 처리
local function process_large_dataset(data_key, batch_size)
batch_size = batch_size or 1000
local cursor = 0
local results = {}
repeat
local batch_result = safe_redis_call(conf, function(red)
local scan_result = red:scan(cursor, "MATCH", data_key .. "*", "COUNT", batch_size)
cursor = scan_result[1]
return scan_result[2]
end)
if batch_result then
for _, key in ipairs(batch_result) do
-- 개별 키 처리
table.insert(results, process_single_key(key))
end
end
-- 메모리 압박 시 중간 결과 저장
if #results > 10000 then
store_intermediate_results(results)
results = {}
collectgarbage("collect") -- 가비지 컬렉션 강제 실행
end
until cursor == "0"
return results
end
에러 핸들링
-- 포괄적인 에러 처리
local function robust_redis_operation(operation_func, fallback_func)
local max_retries = 3
local retry_delay = 0.1
for attempt = 1, max_retries do
local success, result, err = pcall(operation_func)
if success and result then
return result
end
kong.log.warn("Redis 작업 실패 (시도 ", attempt, "/", max_retries, "): ", err or "알 수 없는 오류")
if attempt < max_retries then
ngx.sleep(retry_delay * attempt) -- 지수 백오프
end
end
-- 모든 재시도 실패 시 폴백 함수 실행
kong.log.err("Redis 작업 최종 실패, 폴백 모드로 전환")
return fallback_func and fallback_func() or nil
end
-- 사용 예시
local function get_user_data_with_fallback(user_id)
return robust_redis_operation(
function()
return get_or_set_cache("user:" .. user_id, function()
return fetch_user_from_database(user_id)
end, 600)
end,
function()
-- Redis 실패 시 직접 데이터베이스 조회
return fetch_user_from_database(user_id)
end
)
end
성능 모니터링
-- 실시간 성능 모니터링
local monitoring = {}
function monitoring.track_performance(operation_name, start_time)
local duration = (ngx.now() - start_time) * 1000 -- ms 단위
safe_redis_call(conf, function(red)
local stats_key = "perf_stats:" .. operation_name
local minute_key = stats_key .. ":" .. math.floor(ngx.time() / 60)
-- 분 단위 통계 수집
red:lpush(minute_key .. ":durations", duration)
red:expire(minute_key .. ":durations", 300) -- 5분 보관
-- 응답 시간 히스토그램
local bucket = math.floor(duration / 10) * 10 -- 10ms 단위 버킷
red:hincrby(minute_key .. ":histogram", bucket, 1)
red:expire(minute_key .. ":histogram", 300)
return true
end)
end
-- 플러그인에서 모니터링 사용
function MyPlugin:access(conf)
local start_time = ngx.now()
ngx.ctx.start_time = start_time
-- 비즈니스 로직 실행
local result = process_request(conf)
monitoring.track_performance("request_processing", start_time)
return result
end
고급 캐시 전략과 패턴
더 정교한 캐싱 전략을 구현하여 성능을 극대화할 수 있습니다.
계층형 캐싱
-- L1 (로컬) + L2 (Redis) 캐싱 시스템
local lru_cache = require "resty.lrucache"
local l1_cache = lru_cache.new(1000) -- 로컬 캐시
local function get_multi_level_cache(key, data_fetcher, l1_ttl, l2_ttl)
l1_ttl = l1_ttl or 60
l2_ttl = l2_ttl or 300
-- L1 캐시 확인
local l1_data = l1_cache:get(key)
if l1_data then
return l1_data, "l1_hit"
end
-- L2 캐시 확인
local l2_data = safe_redis_call(conf, function(red)
return red:get("cache:" .. key)
end)
if l2_data and l2_data ~= ngx.null then
local decoded_data = cjson.decode(l2_data)
-- L1 캐시에 저장
l1_cache:set(key, decoded_data, l1_ttl)
return decoded_data, "l2_hit"
end
-- 캐시 미스 - 원본 데이터 생성
local fresh_data = data_fetcher()
if fresh_data then
-- L2 캐시에 저장
safe_redis_call(conf, function(red)
red:setex("cache:" .. key, l2_ttl, cjson.encode(fresh_data))
return true
end)
-- L1 캐시에 저장
l1_cache:set(key, fresh_data, l1_ttl)
end
return fresh_data, "miss"
end
예측적 캐시 갱신
-- 예측적 캐시 갱신 스크립트
local predictive_cache_script = [[
local cache_key = KEYS[1]
local refresh_key = KEYS[2]
local ttl = tonumber(ARGV[1])
local refresh_threshold = tonumber(ARGV[2])
local cached_data = redis.call('GET', cache_key)
local remaining_ttl = redis.call('TTL', cache_key)
-- 캐시 데이터가 있고 만료 임박 시 백그라운드 갱신 트리거
if cached_data and remaining_ttl > 0 and remaining_ttl < refresh_threshold then
local refresh_lock = redis.call('SET', refresh_key, '1', 'NX', 'EX', 30)
if refresh_lock then
return {1, cached_data, 'refresh_needed'}
end
end
if cached_data then
return {1, cached_data, 'hit'}
else
return {0, nil, 'miss'}
end
]]
local function get_predictive_cache(key, data_fetcher, ttl, refresh_threshold)
ttl = ttl or 300
refresh_threshold = refresh_threshold or math.floor(ttl * 0.2) -- TTL의 20%
local cache_key = "cache:" .. key
local refresh_key = "refresh:" .. key
local result = safe_redis_call(conf, function(red)
return red:eval(predictive_cache_script, 2,
cache_key, refresh_key, ttl, refresh_threshold)
end)
if not result then
return data_fetcher()
end
local hit, data, status = result[1], result[2], result[3]
if hit == 1 then
local decoded_data = cjson.decode(data)
-- 백그라운드 갱신이 필요한 경우
if status == 'refresh_needed' then
-- 비동기 갱신 (실제 구현에서는 작업 큐 사용 권장)
ngx.timer.at(0, function()
local fresh_data = data_fetcher()
if fresh_data then
safe_redis_call(conf, function(red)
red:setex(cache_key, ttl, cjson.encode(fresh_data))
red:del(refresh_key)
return true
end)
end
end)
end
return decoded_data
else
-- 캐시 미스 - 동기 데이터 생성
local fresh_data = data_fetcher()
if fresh_data then
safe_redis_call(conf, function(red)
red:setex(cache_key, ttl, cjson.encode(fresh_data))
return true
end)
end
return fresh_data
end
end
보안 고려사항
Kong과 Redis 환경에서 보안을 강화하기 위한 주요 고려사항들입니다.
인증 및 암호화
-- Redis 연결 보안 강화
local function secure_redis_connect(conf)
local red = redis:new()
red:set_timeout(conf.timeout or 1000)
-- SSL/TLS 연결 (Redis 6.0+)
local connect_opts = {
ssl = conf.ssl_enabled,
ssl_verify = conf.ssl_verify,
server_name = conf.ssl_server_name
}
local ok, err = red:connect(conf.redis_host, conf.redis_port, connect_opts)
if not ok then
return nil, "Redis 연결 실패: " .. err
end
-- Redis AUTH
if conf.redis_password then
local auth_result, auth_err = red:auth(conf.redis_password)
if not auth_result then
return nil, "Redis 인증 실패: " .. auth_err
end
end
-- 데이터베이스 선택
if conf.redis_database then
local select_result, select_err = red:select(conf.redis_database)
if not select_result then
return nil, "Redis DB 선택 실패: " .. select_err
end
end
return red
end
-- 데이터 암호화/복호화
local aes = require "resty.aes"
local function encrypt_data(data, key)
local aes_256_cbc = aes:new(key, nil, aes.cipher(256, "cbc"))
return aes_256_cbc:encrypt(data)
end
local function decrypt_data(encrypted_data, key)
local aes_256_cbc = aes:new(key, nil, aes.cipher(256, "cbc"))
return aes_256_cbc:decrypt(encrypted_data)
end
-- 보안 캐시 저장
local function secure_cache_set(key, data, ttl, encryption_key)
local serialized_data = cjson.encode(data)
local encrypted_data = encrypt_data(serialized_data, encryption_key)
return safe_redis_call(conf, function(red)
return red:setex("secure_cache:" .. key, ttl, encrypted_data)
end)
end
local function secure_cache_get(key, encryption_key)
local encrypted_data = safe_redis_call(conf, function(red)
return red:get("secure_cache:" .. key)
end)
if not encrypted_data or encrypted_data == ngx.null then
return nil
end
local decrypted_data = decrypt_data(encrypted_data, encryption_key)
return cjson.decode(decrypted_data)
end
액세스 제어
-- IP 화이트리스트/블랙리스트 관리
local ip_control_script = [[
local client_ip = ARGV[1]
local whitelist_key = KEYS[1]
local blacklist_key = KEYS[2]
-- 블랙리스트 확인
local is_blacklisted = redis.call('SISMEMBER', blacklist_key, client_ip)
if is_blacklisted == 1 then
return {0, 'blacklisted'}
end
-- 화이트리스트 확인 (화이트리스트가 있는 경우)
local whitelist_exists = redis.call('EXISTS', whitelist_key)
if whitelist_exists == 1 then
local is_whitelisted = redis.call('SISMEMBER', whitelist_key, client_ip)
if is_whitelisted == 0 then
return {0, 'not_whitelisted'}
end
end
return {1, 'allowed'}
]]
function MyPlugin:access(conf)
local client_ip = kong.client.get_forwarded_ip()
local result = safe_redis_call(conf, function(red)
return red:eval(ip_control_script, 2,
"ip_whitelist", "ip_blacklist", client_ip)
end)
if result and result[1] == 0 then
kong.log.warn("IP 액세스 거부: ", client_ip, " - ", result[2])
return kong.response.exit(403, {
message = "Access denied"
})
end
end
배포 및 운영 가이드
프로덕션 환경에서 Kong과 Redis를 안정적으로 운영하기 위한 가이드입니다.
Docker Compose 구성
# docker-compose.yml
version: '3.8'
services:
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf
ports:
- "6379:6379"
environment:
- REDIS_PASSWORD=${REDIS_PASSWORD}
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
kong-database:
image: postgres:13
environment:
POSTGRES_DB: kong
POSTGRES_USER: kong
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kong"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
kong-migration:
image: kong:latest
command: kong migrations bootstrap
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-database
KONG_PG_DATABASE: kong
KONG_PG_USER: kong
KONG_PG_PASSWORD: ${POSTGRES_PASSWORD}
depends_on:
kong-database:
condition: service_healthy
restart: on-failure
kong:
image: kong:latest
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-database
KONG_PG_DATABASE: kong
KONG_PG_USER: kong
KONG_PG_PASSWORD: ${POSTGRES_PASSWORD}
KONG_ADMIN_ACCESS_LOG: /dev/stdout
KONG_ADMIN_ERROR_LOG: /dev/stderr
KONG_ADMIN_LISTEN: 0.0.0.0:8001
KONG_PROXY_ACCESS_LOG: /dev/stdout
KONG_PROXY_ERROR_LOG: /dev/stderr
# Redis 설정
KONG_REDIS_HOST: redis
KONG_REDIS_PORT: 6379
KONG_REDIS_PASSWORD: ${REDIS_PASSWORD}
ports:
- "8000:8000"
- "8443:8443"
- "8001:8001"
- "8444:8444"
volumes:
- ./plugins:/usr/local/share/lua/5.1/kong/plugins/custom
depends_on:
kong-database:
condition: service_healthy
redis:
condition: service_healthy
kong-migration:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "kong", "health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
volumes:
redis_data:
postgres_data:
모니터링 및 로깅
-- 구조화된 로깅
local function structured_log(level, event, data)
local log_entry = {
timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ"),
level = level,
event = event,
plugin = "my-plugin",
request_id = kong.ctx.shared.request_id or "unknown",
data = data or {}
}
kong.log[level](cjson.encode(log_entry))
end
-- 성능 메트릭 로깅
local function log_performance_metrics()
local metrics = {
request_time = ngx.now() - (ngx.ctx.start_time or ngx.now()),
upstream_time = ngx.var.upstream_response_time,
cache_status = ngx.ctx.cache_status or "miss",
redis_operations = ngx.ctx.redis_ops or 0
}
structured_log("info", "performance_metrics", metrics)
end
-- 에러 추적
local function log_error_with_context(err, context)
local error_data = {
error = err,
context = context,
stack_trace = debug.traceback(),
request_headers = kong.request.get_headers(),
client_ip = kong.client.get_forwarded_ip()
}
structured_log("error", "plugin_error", error_data)
end
마무리
Kong API Gateway와 Redis를 활용한 고성능 API 게이트웨이 구축은 현대적인 마이크로서비스 아키텍처의 핵심 요소입니다.
본 가이드에서 다룬 내용들을 통해 다음과 같은 이점을 얻을 수 있습니다:
성능 최적화: Redis 루아 스크립트를 통한 원자적 연산으로 높은 처리량과 낮은 지연시간 달성
확장성: 수평 확장이 가능한 아키텍처로 트래픽 증가에 유연하게 대응
보안: 다층 보안 전략으로 API 보안 강화
모니터링: 실시간 성능 모니터링과 메트릭 수집으로 운영 효율성 향상
Kong과 Redis의 조합은 단순한 API 프록시를 넘어서 비즈니스 로직을 포함한 완전한 API 관리 플랫폼을 구축할 수 있게 해줍니다.
특히 원자적 연산을 보장하는 Redis 루아 스크립트와 Kong의 플러그인 시스템이 결합될 때, 복잡한 비즈니스 요구사항도 효율적으로 처리할 수 있습니다.
다음 시리즈에서는 Kong의 고급 플러그인 개발과 대규모 분산 환경에서의 운영 전략에 대해 더 자세히 다룰 예정입니다.
관련 글 및 참고 자료:
루아 입문 시리즈 #11: Redis와 루아 스크립팅
Redis Lua 스크립트를 활용한 원자적 연산과 고성능 캐시 전략으로 데이터베이스 부하를 줄이고 애플리케이션 성능을 극대화하는 실전 가이드입니다.Redis Lua 스크립팅이란?Redis는 인메모리 데이터
notavoid.tistory.com
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #14: 루아 성능 최적화와 프로파일링 (0) | 2025.07.03 |
---|---|
루아 입문 시리즈 #13: Wireshark 루아 플러그인 개발 (0) | 2025.07.03 |
애플의 스위프트 프로그래밍 언어가 안드로이드를 지원합니다 (0) | 2025.06.29 |
루아 입문 시리즈 #11: Redis와 루아 스크립팅 (0) | 2025.06.28 |
루아 입문 시리즈 #10: NodeMCU IoT 프로젝트 (0) | 2025.06.28 |