루아의 코루틴(Coroutine)은 협력적 멀티태스킹을 구현하는 강력한 기능입니다.
일반적인 함수와 달리 코루틴은 실행을 중단했다가 나중에 재개할 수 있어,
비동기 프로그래밍과 복잡한 제어 흐름을 우아하게 처리할 수 있습니다.
이번 글에서는 루아 코루틴의 심화 개념부터 실전 비동기 패턴까지 완벽하게 마스터해보겠습니다.
코루틴 기본 개념과 동작 원리
루아 코루틴은 coroutine
라이브러리를 통해 구현되며, 협력적 멀티태스킹의 핵심 메커니즘입니다.
일반 함수는 호출되면 완전히 실행되고 종료되지만, 코루틴은 중간에 실행을 양보(yield)하고 나중에 다시 재개(resume)할 수 있습니다.
-- 기본 코루틴 생성과 실행
function simpleCoroutine()
print("코루틴 시작")
coroutine.yield("첫 번째 양보")
print("첫 번째 재개 후")
coroutine.yield("두 번째 양보")
print("두 번째 재개 후")
return "코루틴 완료"
end
-- 코루틴 객체 생성
local co = coroutine.create(simpleCoroutine)
-- 단계별 실행
print("상태:", coroutine.status(co)) -- suspended
local success, value = coroutine.resume(co)
print("반환값:", value) -- 첫 번째 양보
local success, value = coroutine.resume(co)
print("반환값:", value) -- 두 번째 양보
local success, value = coroutine.resume(co)
print("최종 반환값:", value) -- 코루틴 완료
코루틴의 상태는 suspended
, running
, dead
세 가지로 구분됩니다.
coroutine.status()
함수로 현재 상태를 확인할 수 있으며, 이를 통해 코루틴의 생명주기를 정확히 관리할 수 있습니다.
코루틴 심화 활용 패턴
데이터 생성기(Generator) 패턴
코루틴을 활용한 가장 일반적인 패턴 중 하나는 데이터 생성기입니다.
이 패턴은 대용량 데이터를 메모리 효율적으로 처리하는 데 특히 유용합니다.
-- 피보나치 수열 생성기
function fibonacciGenerator()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end
-- 생성기 사용 함수
function useFibonacci(n)
local fibGen = coroutine.create(fibonacciGenerator)
local results = {}
for i = 1, n do
local success, value = coroutine.resume(fibGen)
if success then
table.insert(results, value)
else
break
end
end
return results
end
-- 실행 예시
local firstTenFib = useFibonacci(10)
for i, value in ipairs(firstTenFib) do
print(string.format("F(%d) = %d", i-1, value))
end
상태 머신(State Machine) 구현
코루틴을 이용하면 복잡한 상태 머신을 직관적으로 구현할 수 있습니다.
각 상태를 코루틴 내의 다른 지점으로 표현하여 상태 전환을 자연스럽게 처리합니다.
-- 간단한 게임 캐릭터 AI 상태 머신
function characterAI(character)
local health = character.health
local enemyDistance = character.enemyDistance
while true do
-- 순찰 상태
print(character.name .. " 순찰 중...")
coroutine.yield("patrolling")
if enemyDistance < 50 then
-- 경계 상태
print(character.name .. " 적 발견!")
coroutine.yield("alert")
if enemyDistance < 20 then
-- 공격 상태
print(character.name .. " 공격!")
coroutine.yield("attacking")
health = health - 10
end
end
if health < 30 then
-- 도망 상태
print(character.name .. " 도망!")
coroutine.yield("fleeing")
health = health + 5 -- 회복
end
-- 상태 업데이트를 위한 대기
coroutine.yield("waiting")
end
end
-- AI 시스템 실행
local character = {name = "Guard", health = 100, enemyDistance = 60}
local ai = coroutine.create(function() return characterAI(character) end)
for turn = 1, 10 do
local success, state = coroutine.resume(ai)
print("턴 " .. turn .. ": " .. state)
-- 환경 변화 시뮬레이션
character.enemyDistance = math.random(10, 70)
end
비동기 프로그래밍 패턴과 실전 구현
비동기 HTTP 요청 시뮬레이션
실제 웹 개발에서 자주 사용되는 비동기 HTTP 요청을 코루틴으로 시뮬레이션해보겠습니다.
이 패턴은 루아 웹 프레임워크 OpenResty에서 널리 사용됩니다.
-- 비동기 HTTP 요청 시뮬레이터
local AsyncHTTP = {}
AsyncHTTP.__index = AsyncHTTP
function AsyncHTTP.new()
local self = setmetatable({}, AsyncHTTP)
self.pendingRequests = {}
return self
end
function AsyncHTTP:request(url, callback)
local requestId = #self.pendingRequests + 1
-- 요청 코루틴 생성
local requestCoroutine = coroutine.create(function()
print("요청 시작: " .. url)
-- 네트워크 지연 시뮬레이션
local delay = math.random(1, 3)
for i = 1, delay do
coroutine.yield("waiting")
end
-- 응답 생성
local response = {
status = 200,
body = "응답 데이터 from " .. url,
url = url
}
return response
end)
self.pendingRequests[requestId] = {
coroutine = requestCoroutine,
callback = callback,
url = url
}
return requestId
end
function AsyncHTTP:processRequests()
local completedRequests = {}
for id, request in pairs(self.pendingRequests) do
local success, result = coroutine.resume(request.coroutine)
if success and result ~= "waiting" then
-- 요청 완료
print("요청 완료: " .. request.url)
request.callback(result)
table.insert(completedRequests, id)
end
end
-- 완료된 요청 제거
for _, id in ipairs(completedRequests) do
self.pendingRequests[id] = nil
end
return #self.pendingRequests > 0
end
-- 비동기 HTTP 사용 예시
local httpClient = AsyncHTTP.new()
-- 여러 요청 동시 실행
httpClient:request("https://api.example.com/users", function(response)
print("사용자 데이터:", response.body)
end)
httpClient:request("https://api.example.com/posts", function(response)
print("게시글 데이터:", response.body)
end)
httpClient:request("https://api.example.com/comments", function(response)
print("댓글 데이터:", response.body)
end)
-- 이벤트 루프 시뮬레이션
print("=== 비동기 요청 처리 시작 ===")
local tick = 0
while httpClient:processRequests() do
tick = tick + 1
print("Tick " .. tick .. ": 요청 처리 중...")
-- 실제 환경에서는 여기서 다른 작업 수행 가능
end
print("=== 모든 요청 완료 ===")
이벤트 루프와 태스크 스케줄링
코루틴을 활용한 이벤트 루프 구현으로 효율적인 태스크 스케줄링을 구현할 수 있습니다.
이는 Node.js의 이벤트 루프와 유사한 개념입니다.
-- 간단한 이벤트 루프 구현
local EventLoop = {}
EventLoop.__index = EventLoop
function EventLoop.new()
local self = setmetatable({}, EventLoop)
self.tasks = {}
self.timers = {}
self.running = false
return self
end
function EventLoop:addTask(taskFunction, ...)
local args = {...}
local task = coroutine.create(function()
return taskFunction(table.unpack(args))
end)
table.insert(self.tasks, task)
end
function EventLoop:setTimeout(callback, delay, ...)
local args = {...}
local timer = {
callback = callback,
executeTime = os.time() + delay,
args = args
}
table.insert(self.timers, timer)
end
function EventLoop:processTasks()
local completedTasks = {}
for i, task in ipairs(self.tasks) do
if coroutine.status(task) ~= "dead" then
local success, result = coroutine.resume(task)
if not success then
print("태스크 오류:", result)
table.insert(completedTasks, i)
elseif coroutine.status(task) == "dead" then
table.insert(completedTasks, i)
end
else
table.insert(completedTasks, i)
end
end
-- 완료된 태스크 제거 (역순으로 제거)
for i = #completedTasks, 1, -1 do
table.remove(self.tasks, completedTasks[i])
end
end
function EventLoop:processTimers()
local currentTime = os.time()
local expiredTimers = {}
for i, timer in ipairs(self.timers) do
if currentTime >= timer.executeTime then
timer.callback(table.unpack(timer.args))
table.insert(expiredTimers, i)
end
end
-- 만료된 타이머 제거 (역순으로 제거)
for i = #expiredTimers, 1, -1 do
table.remove(self.timers, expiredTimers[i])
end
end
function EventLoop:run()
self.running = true
print("이벤트 루프 시작")
while self.running and (#self.tasks > 0 or #self.timers > 0) do
self:processTasks()
self:processTimers()
-- CPU 사용률 조절
os.execute("sleep 0.1") -- 100ms 대기
end
print("이벤트 루프 종료")
end
function EventLoop:stop()
self.running = false
end
-- 이벤트 루프 사용 예시
local loop = EventLoop.new()
-- 비동기 태스크 추가
loop:addTask(function()
for i = 1, 5 do
print("태스크 1 - 단계 " .. i)
coroutine.yield()
end
print("태스크 1 완료")
end)
loop:addTask(function()
for i = 1, 3 do
print("태스크 2 - 처리 중 " .. i)
coroutine.yield()
end
print("태스크 2 완료")
end)
-- 타이머 기반 콜백 설정
loop:setTimeout(function(message)
print("3초 후 실행: " .. message)
end, 3, "타이머 콜백 메시지")
loop:setTimeout(function()
print("5초 후 이벤트 루프 종료")
loop:stop()
end, 5)
-- 이벤트 루프 실행
loop:run()
고급 코루틴 패턴과 최적화
코루틴 풀(Pool) 관리
대량의 코루틴을 효율적으로 관리하기 위한 풀 패턴을 구현해보겠습니다.
이는 메모리 사용량을 최적화하고 성능을 향상시키는 데 중요한 역할을 합니다.
-- 코루틴 풀 관리자
local CoroutinePool = {}
CoroutinePool.__index = CoroutinePool
function CoroutinePool.new(maxSize)
local self = setmetatable({}, CoroutinePool)
self.maxSize = maxSize or 100
self.available = {}
self.active = {}
self.totalCreated = 0
return self
end
function CoroutinePool:getCoroutine(taskFunction, ...)
local args = {...}
local co
if #self.available > 0 then
-- 재사용 가능한 코루틴 사용
co = table.remove(self.available)
else
-- 새 코루틴 생성
if self.totalCreated < self.maxSize then
co = coroutine.create(function() end)
self.totalCreated = self.totalCreated + 1
else
print("경고: 코루틴 풀 한계 도달")
return nil
end
end
-- 코루틴 재초기화
co = coroutine.create(function()
return taskFunction(table.unpack(args))
end)
self.active[co] = true
return co
end
function CoroutinePool:returnCoroutine(co)
if self.active[co] then
self.active[co] = nil
if coroutine.status(co) == "dead" then
-- 죽은 코루틴은 풀에 반환하지 않음
self.totalCreated = self.totalCreated - 1
else
table.insert(self.available, co)
end
end
end
function CoroutinePool:getStats()
return {
available = #self.available,
active = self:getActiveCount(),
total = self.totalCreated,
maxSize = self.maxSize
}
end
function CoroutinePool:getActiveCount()
local count = 0
for _ in pairs(self.active) do
count = count + 1
end
return count
end
-- 작업 관리자 (코루틴 풀 활용)
local TaskManager = {}
TaskManager.__index = TaskManager
function TaskManager.new(poolSize)
local self = setmetatable({}, TaskManager)
self.pool = CoroutinePool.new(poolSize)
self.tasks = {}
return self
end
function TaskManager:addTask(taskFunction, ...)
local co = self.pool:getCoroutine(taskFunction, ...)
if co then
table.insert(self.tasks, co)
return true
end
return false
end
function TaskManager:processTasks()
local completedTasks = {}
for i, task in ipairs(self.tasks) do
local success, result = coroutine.resume(task)
if not success then
print("태스크 오류:", result)
table.insert(completedTasks, i)
elseif coroutine.status(task) == "dead" then
table.insert(completedTasks, i)
end
end
-- 완료된 태스크 정리 및 풀 반환
for i = #completedTasks, 1, -1 do
local taskIndex = completedTasks[i]
local task = self.tasks[taskIndex]
self.pool:returnCoroutine(task)
table.remove(self.tasks, taskIndex)
end
return #self.tasks
end
function TaskManager:getStats()
local poolStats = self.pool:getStats()
return {
runningTasks = #self.tasks,
poolStats = poolStats
}
end
-- 코루틴 풀 사용 예시
local taskManager = TaskManager.new(10)
-- 대량 작업 생성
for i = 1, 15 do
local taskAdded = taskManager:addTask(function(taskId)
print("작업 " .. taskId .. " 시작")
for step = 1, 3 do
print("작업 " .. taskId .. " - 단계 " .. step)
coroutine.yield()
end
print("작업 " .. taskId .. " 완료")
end, i)
if not taskAdded then
print("작업 " .. i .. " 추가 실패 - 풀 한계 초과")
end
end
-- 작업 처리
print("=== 작업 처리 시작 ===")
local iteration = 0
while taskManager:processTasks() > 0 do
iteration = iteration + 1
local stats = taskManager:getStats()
print(string.format("반복 %d: 실행 중인 작업 %d개",
iteration, stats.runningTasks))
print(string.format("풀 상태 - 사용 가능: %d, 활성: %d, 전체: %d/%d",
stats.poolStats.available, stats.poolStats.active,
stats.poolStats.total, stats.poolStats.maxSize))
print("---")
end
print("=== 모든 작업 완료 ===")
에러 처리와 복구 메커니즘
실제 프로덕션 환경에서는 강건한 에러 처리가 필수적입니다.
코루틴 환경에서의 효과적인 에러 처리 패턴을 살펴보겠습니다.
-- 안전한 코루틴 래퍼
local SafeCoroutine = {}
SafeCoroutine.__index = SafeCoroutine
function SafeCoroutine.new(taskFunction, errorHandler, maxRetries)
local self = setmetatable({}, SafeCoroutine)
self.taskFunction = taskFunction
self.errorHandler = errorHandler or function(err) print("오류:", err) end
self.maxRetries = maxRetries or 3
self.retryCount = 0
self.status = "ready"
return self
end
function SafeCoroutine:execute(...)
local args = {...}
local function safeTask()
local success, result = pcall(function()
return self.taskFunction(table.unpack(args))
end)
if not success then
error(result)
end
return result
end
self.coroutine = coroutine.create(safeTask)
self.status = "running"
return self:resume()
end
function SafeCoroutine:resume()
if not self.coroutine or coroutine.status(self.coroutine) == "dead" then
self.status = "dead"
return false, "코루틴이 종료됨"
end
local success, result = coroutine.resume(self.coroutine)
if not success then
-- 에러 발생 시 재시도 로직
self.retryCount = self.retryCount + 1
self.errorHandler(result)
if self.retryCount < self.maxRetries then
print(string.format("재시도 %d/%d", self.retryCount, self.maxRetries))
-- 코루틴 재생성 및 재시도
self.coroutine = coroutine.create(function()
return self.taskFunction()
end)
return self:resume()
else
self.status = "failed"
return false, "최대 재시도 횟수 초과: " .. result
end
end
if coroutine.status(self.coroutine) == "dead" then
self.status = "completed"
else
self.status = "suspended"
end
return true, result
end
-- 복구 가능한 네트워크 요청 시뮬레이션
function unreliableNetworkRequest(url)
print("네트워크 요청 시작:", url)
-- 랜덤하게 실패 시뮬레이션
if math.random() < 0.6 then -- 60% 실패율
error("네트워크 연결 실패")
end
coroutine.yield("연결 중...")
if math.random() < 0.3 then -- 30% 타임아웃
error("요청 타임아웃")
end
return "응답 데이터: " .. url
end
-- 안전한 코루틴 사용 예시
local safeRequest = SafeCoroutine.new(
function() return unreliableNetworkRequest("https://api.example.com") end,
function(err) print("에러 로그:", err) end,
5 -- 최대 5회 재시도
)
print("=== 안전한 네트워크 요청 테스트 ===")
local success, result = safeRequest:execute()
if success then
print("최종 결과:", result)
print("상태:", safeRequest.status)
print("재시도 횟수:", safeRequest.retryCount)
else
print("최종 실패:", result)
end
실전 코루틴 활용 사례
웹 크롤러 구현
코루틴을 활용한 효율적인 웹 크롤러를 구현해보겠습니다.
이는 Scrapy와 같은 Python 웹 크롤링 프레임워크의 비동기 처리 방식과 유사합니다.
-- 웹 크롤러 시뮬레이터
local WebCrawler = {}
WebCrawler.__index = WebCrawler
function WebCrawler.new(maxConcurrent)
local self = setmetatable({}, WebCrawler)
self.maxConcurrent = maxConcurrent or 5
self.urlQueue = {}
self.visited = {}
self.results = {}
self.activeCrawlers = {}
return self
end
function WebCrawler:addUrl(url)
if not self.visited[url] then
table.insert(self.urlQueue, url)
end
end
function WebCrawler:crawlPage(url)
return coroutine.create(function()
print("크롤링 시작:", url)
self.visited[url] = true
-- 페이지 다운로드 시뮬레이션
local downloadTime = math.random(1, 3)
for i = 1, downloadTime do
coroutine.yield("downloading")
end
-- 페이지 파싱 시뮬레이션
local content = {
title = "페이지 제목 - " .. url,
links = {},
data = "페이지 내용 데이터"
}
-- 랜덤 링크 생성 (실제로는 HTML 파싱)
local linkCount = math.random(2, 5)
for i = 1, linkCount do
local link = url .. "/page" .. i
table.insert(content.links, link)
end
coroutine.yield("parsing")
print("크롤링 완료:", url)
return content
end)
end
function WebCrawler:processQueue()
-- 새로운 크롤러 시작
while #self.activeCrawlers < self.maxConcurrent and #self.urlQueue > 0 do
local url = table.remove(self.urlQueue, 1)
local crawler = self:crawlPage(url)
self.activeCrawlers[crawler] = url
end
-- 활성 크롤러 처리
local completed = {}
for crawler, url in pairs(self.activeCrawlers) do
local success, result = coroutine.resume(crawler)
if success and result ~= "downloading" and result ~= "parsing" then
-- 크롤링 완료
self.results[url] = result
-- 새로운 링크 추가
if result.links then
for _, link in ipairs(result.links) do
self:addUrl(link)
end
end
table.insert(completed, crawler)
elseif not success then
print("크롤링 오류 (" .. url .. "):", result)
table.insert(completed, crawler)
end
end
-- 완료된 크롤러 제거
for _, crawler in ipairs(completed) do
self.activeCrawlers[crawler] = nil
end
return #self.activeCrawlers > 0 or #self.urlQueue > 0
end
function WebCrawler:start(seedUrls)
for _, url in ipairs(seedUrls) do
self:addUrl(url)
end
print("=== 웹 크롤링 시작 ===")
local iteration = 0
while self:processQueue() do
iteration = iteration + 1
print(string.format("반복 %d: 활성 크롤러 %d개, 대기 URL %d개, 완료 %d개",
iteration,
self:getActiveCount(),
#self.urlQueue,
self:getCompletedCount()))
-- 과도한 크롤링 방지
if iteration > 20 then
print("최대 반복 횟수 도달, 크롤링 중단")
break
end
end
print("=== 웹 크롤링 완료 ===")
self:printResults()
end
function WebCrawler:getActiveCount()
local count = 0
for _ in pairs(self.activeCrawlers) do
count = count + 1
end
return count
end
function WebCrawler:getCompletedCount()
local count = 0
for _ in pairs(self.results) do
count = count + 1
end
return count
end
function WebCrawler:printResults()
print("\n=== 크롤링 결과 ===")
for url, result in pairs(self.results) do
print("URL:", url)
print("제목:", result.title)
print("링크 수:", #result.links)
print("---")
end
end
-- 웹 크롤러 사용 예시
local crawler = WebCrawler.new(3) -- 최대 3개 동시 크롤링
crawler:start({
"https://example.com",
"https://test.com"
})
게임 시스템에서의 코루틴 활용
게임 개발에서 코루틴은 애니메이션, AI, 이벤트 시스템 등에 광범위하게 활용됩니다.
Unity 게임 엔진의 코루틴과 유사한 패턴을 루아로 구현해보겠습니다.
-- 게임 애니메이션 시스템
local AnimationSystem = {}
AnimationSystem.__index = AnimationSystem
function AnimationSystem.new()
local self = setmetatable({}, AnimationSystem)
self.animations = {}
self.gameTime = 0
return self
end
function AnimationSystem:lerp(start, target, duration, easingFunc)
easingFunc = easingFunc or function(t) return t end -- 선형 보간
return coroutine.create(function()
local startTime = self.gameTime
local elapsed = 0
while elapsed < duration do
elapsed = self.gameTime - startTime
local progress = math.min(elapsed / duration, 1.0)
local easedProgress = easingFunc(progress)
local currentValue = start + (target - start) * easedProgress
coroutine.yield(currentValue)
end
return target -- 정확한 최종값 반환
end)
end
function AnimationSystem:easeInOut(t)
return t * t * (3.0 - 2.0 * t)
end
function AnimationSystem:bounce(t)
if t < 0.5 then
return 2 * t * t
else
return -1 + (4 - 2 * t) * t
end
end
function AnimationSystem:startAnimation(name, animCoroutine, callback)
self.animations[name] = {
coroutine = animCoroutine,
callback = callback or function() end,
startTime = self.gameTime
}
end
function AnimationSystem:update(deltaTime)
self.gameTime = self.gameTime + deltaTime
local completed = {}
for name, anim in pairs(self.animations) do
local success, value = coroutine.resume(anim.coroutine)
if success then
if coroutine.status(anim.coroutine) == "dead" then
-- 애니메이션 완료
anim.callback(value)
table.insert(completed, name)
print("애니메이션 완료:", name, "최종값:", value)
else
print("애니메이션 진행 중:", name, "현재값:", value)
end
else
print("애니메이션 오류:", name, value)
table.insert(completed, name)
end
end
-- 완료된 애니메이션 제거
for _, name in ipairs(completed) do
self.animations[name] = nil
end
end
-- 게임 오브젝트 시뮬레이션
local GameObject = {}
GameObject.__index = GameObject
function GameObject.new(name, x, y)
local self = setmetatable({}, GameObject)
self.name = name
self.x = x or 0
self.y = y or 0
self.scale = 1.0
self.rotation = 0
self.health = 100
return self
end
function GameObject:moveTo(targetX, targetY, duration, animSystem)
print(string.format("%s 이동 시작: (%.1f,%.1f) -> (%.1f,%.1f)",
self.name, self.x, self.y, targetX, targetY))
local startX, startY = self.x, self.y
local moveAnimation = coroutine.create(function()
local moveX = animSystem:lerp(startX, targetX, duration, animSystem.easeInOut)
local moveY = animSystem:lerp(startY, targetY, duration, animSystem.easeInOut)
while coroutine.status(moveX) ~= "dead" or coroutine.status(moveY) ~= "dead" do
local _, newX = coroutine.resume(moveX)
local _, newY = coroutine.resume(moveY)
self.x = newX or self.x
self.y = newY or self.y
coroutine.yield()
end
return "이동 완료"
end)
animSystem:startAnimation(self.name .. "_move", moveAnimation, function()
print(string.format("%s 이동 완료: (%.1f,%.1f)", self.name, self.x, self.y))
end)
end
function GameObject:scaleTo(targetScale, duration, animSystem)
local startScale = self.scale
local scaleAnim = animSystem:lerp(startScale, targetScale, duration, animSystem.bounce)
animSystem:startAnimation(self.name .. "_scale", coroutine.create(function()
while coroutine.status(scaleAnim) ~= "dead" do
local _, newScale = coroutine.resume(scaleAnim)
self.scale = newScale or self.scale
coroutine.yield()
end
return "크기 변경 완료"
end), function()
print(string.format("%s 크기 변경 완료: %.2f", self.name, self.scale))
end)
end
-- 복합 애니메이션 시퀀스
function GameObject:complexAnimation(animSystem)
local sequence = coroutine.create(function()
print(self.name .. " 복합 애니메이션 시작")
-- 1단계: 위로 점프
self:moveTo(self.x, self.y - 50, 1.0, animSystem)
-- 점프 애니메이션 완료까지 대기
while animSystem.animations[self.name .. "_move"] do
coroutine.yield()
end
-- 2단계: 회전하면서 크기 변경
self:scaleTo(1.5, 0.5, animSystem)
-- 크기 변경 완료까지 대기
while animSystem.animations[self.name .. "_scale"] do
coroutine.yield()
end
-- 3단계: 원래 위치로 복귀
self:moveTo(self.x, self.y + 50, 1.0, animSystem)
self:scaleTo(1.0, 1.0, animSystem)
-- 모든 애니메이션 완료까지 대기
while animSystem.animations[self.name .. "_move"] or
animSystem.animations[self.name .. "_scale"] do
coroutine.yield()
end
return "복합 애니메이션 완료"
end)
animSystem:startAnimation(self.name .. "_complex", sequence, function()
print(self.name .. " 복합 애니메이션 시퀀스 완료!")
end)
end
-- 게임 시뮬레이션 실행
print("=== 게임 애니메이션 시스템 테스트 ===")
local animSystem = AnimationSystem.new()
local player = GameObject.new("Player", 100, 100)
local enemy = GameObject.new("Enemy", 200, 150)
-- 동시 애니메이션 실행
player:moveTo(300, 200, 2.0, animSystem)
enemy:scaleTo(0.5, 1.5, animSystem)
-- 복합 애니메이션 실행 (1초 후)
animSystem:startAnimation("delayed_complex", coroutine.create(function()
-- 1초 대기
local waitTime = 1.0
local elapsed = 0
while elapsed < waitTime do
elapsed = elapsed + 0.1
coroutine.yield()
end
player:complexAnimation(animSystem)
return "지연된 복합 애니메이션 시작"
end))
-- 게임 루프 시뮬레이션
local totalTime = 0
local maxTime = 10 -- 10초간 실행
while totalTime < maxTime do
local deltaTime = 0.1 -- 100ms 프레임
animSystem:update(deltaTime)
totalTime = totalTime + deltaTime
-- 활성 애니메이션이 없으면 종료
local hasActiveAnimations = false
for _ in pairs(animSystem.animations) do
hasActiveAnimations = true
break
end
if not hasActiveAnimations then
print("모든 애니메이션 완료, 게임 루프 종료")
break
end
-- 프레임 대기 시뮬레이션
os.execute("sleep 0.05") -- 50ms 대기
end
print("게임 시뮬레이션 종료")
성능 최적화와 모범 사례
메모리 관리 최적화
코루틴을 대량으로 사용할 때는 메모리 관리가 중요합니다.
효율적인 메모리 사용을 위한 최적화 기법들을 살펴보겠습니다.
-- 메모리 효율적인 코루틴 관리자
local EfficientCoroutineManager = {}
EfficientCoroutineManager.__index = EfficientCoroutineManager
function EfficientCoroutineManager.new()
local self = setmetatable({}, EfficientCoroutineManager)
self.coroutines = {}
self.recycledCoroutines = {}
self.maxRecycled = 50 -- 재활용 코루틴 최대 보관 수
self.stats = {
created = 0,
recycled = 0,
destroyed = 0
}
return self
end
function EfficientCoroutineManager:createCoroutine(func, ...)
local args = {...}
local co
-- 재활용 코루틴 사용 시도
if #self.recycledCoroutines > 0 then
co = table.remove(self.recycledCoroutines)
self.stats.recycled = self.stats.recycled + 1
-- 코루틴 재초기화 (실제로는 새로 생성해야 함)
co = coroutine.create(function()
return func(table.unpack(args))
end)
else
co = coroutine.create(function()
return func(table.unpack(args))
end)
self.stats.created = self.stats.created + 1
end
self.coroutines[co] = {
status = "active",
createdTime = os.time()
}
return co
end
function EfficientCoroutineManager:resumeCoroutine(co)
if not self.coroutines[co] then
return false, "코루틴이 관리되지 않음"
end
local success, result = coroutine.resume(co)
if coroutine.status(co) == "dead" then
self:recycleCoroutine(co)
end
return success, result
end
function EfficientCoroutineManager:recycleCoroutine(co)
if self.coroutines[co] then
self.coroutines[co] = nil
-- 재활용 풀에 여유가 있으면 보관
if #self.recycledCoroutines < self.maxRecycled then
table.insert(self.recycledCoroutines, co)
else
self.stats.destroyed = self.stats.destroyed + 1
end
end
end
function EfficientCoroutineManager:cleanup()
-- 오래된 코루틴 정리
local currentTime = os.time()
local toRemove = {}
for co, info in pairs(self.coroutines) do
if currentTime - info.createdTime > 300 then -- 5분 이상 된 코루틴
table.insert(toRemove, co)
end
end
for _, co in ipairs(toRemove) do
self:recycleCoroutine(co)
print("오래된 코루틴 정리됨")
end
end
function EfficientCoroutineManager:getMemoryStats()
local activeCount = 0
for _ in pairs(self.coroutines) do
activeCount = activeCount + 1
end
return {
active = activeCount,
recycled = #self.recycledCoroutines,
stats = self.stats,
memoryUsage = collectgarbage("count") .. " KB"
}
end
-- 대용량 데이터 처리 예시
function processLargeDataset(manager, dataSize)
print("=== 대용량 데이터 처리 테스트 ===")
print("데이터 크기:", dataSize)
local processingCoroutines = {}
local batchSize = 100
local processedCount = 0
-- 배치 단위로 처리
for batch = 1, math.ceil(dataSize / batchSize) do
local startIdx = (batch - 1) * batchSize + 1
local endIdx = math.min(batch * batchSize, dataSize)
local co = manager:createCoroutine(function(start, finish)
print(string.format("배치 %d 처리 중: %d-%d", batch, start, finish))
-- 데이터 처리 시뮬레이션
for i = start, finish do
-- CPU 집약적 작업 시뮬레이션
local result = math.sin(i) * math.cos(i)
-- 주기적으로 양보하여 다른 코루틴에게 실행 기회 제공
if i % 10 == 0 then
coroutine.yield("진행 중")
end
end
return string.format("배치 %d 완료", batch)
end, startIdx, endIdx)
table.insert(processingCoroutines, co)
end
-- 모든 배치 처리
while #processingCoroutines > 0 do
local completed = {}
for i, co in ipairs(processingCoroutines) do
local success, result = manager:resumeCoroutine(co)
if success then
if coroutine.status(co) == "dead" then
processedCount = processedCount + 1
print("완료:", result)
table.insert(completed, i)
end
else
print("오류:", result)
table.insert(completed, i)
end
end
-- 완료된 코루틴 제거 (역순으로)
for i = #completed, 1, -1 do
table.remove(processingCoroutines, completed[i])
end
-- 메모리 상태 출력
if processedCount % 10 == 0 then
local stats = manager:getMemoryStats()
print(string.format("진행률: %d%%, 메모리: %s",
processedCount * 100 / math.ceil(dataSize / batchSize),
stats.memoryUsage))
end
-- 가비지 컬렉션 강제 실행 (메모리 최적화)
if processedCount % 20 == 0 then
collectgarbage("collect")
end
end
print("=== 대용량 데이터 처리 완료 ===")
local finalStats = manager:getMemoryStats()
print("최종 메모리 통계:")
for key, value in pairs(finalStats) do
if type(value) == "table" then
print(key .. ":")
for k, v in pairs(value) do
print(" " .. k .. ": " .. v)
end
else
print(key .. ": " .. value)
end
end
end
-- 메모리 최적화 테스트 실행
local manager = EfficientCoroutineManager.new()
processLargeDataset(manager, 1000)
-- 주기적 정리 작업
manager:cleanup()
디버깅과 모니터링
코루틴 기반 시스템의 디버깅은 일반적인 동기 코드보다 복잡할 수 있습니다.
효과적인 디버깅과 모니터링 도구를 구현해보겠습니다.
-- 코루틴 디버깅 및 모니터링 시스템
local CoroutineDebugger = {}
CoroutineDebugger.__index = CoroutineDebugger
function CoroutineDebugger.new()
local self = setmetatable({}, CoroutineDebugger)
self.trackedCoroutines = {}
self.executionHistory = {}
self.performanceStats = {}
self.logLevel = "INFO" -- DEBUG, INFO, WARN, ERROR
return self
end
function CoroutineDebugger:setLogLevel(level)
self.logLevel = level
end
function CoroutineDebugger:log(level, message, coroutineId)
local levels = {DEBUG = 1, INFO = 2, WARN = 3, ERROR = 4}
if levels[level] >= levels[self.logLevel] then
local timestamp = os.date("%H:%M:%S")
local prefix = coroutineId and ("[" .. coroutineId .. "] ") or ""
print(string.format("[%s][%s] %s%s", timestamp, level, prefix, message))
end
end
function CoroutineDebugger:trackCoroutine(co, name, metadata)
local id = tostring(co):gsub("thread: ", "co_")
self.trackedCoroutines[co] = {
id = id,
name = name or "unnamed",
metadata = metadata or {},
createdAt = os.time(),
resumeCount = 0,
yieldCount = 0,
totalExecutionTime = 0,
status = "created"
}
self:log("DEBUG", "코루틴 추적 시작: " .. (name or id), id)
return id
end
function CoroutineDebugger:resumeWithTracking(co, ...)
local trackInfo = self.trackedCoroutines[co]
if not trackInfo then
return coroutine.resume(co, ...)
end
local startTime = os.clock()
trackInfo.resumeCount = trackInfo.resumeCount + 1
trackInfo.status = "running"
self:log("DEBUG", "코루틴 재개 #" .. trackInfo.resumeCount, trackInfo.id)
local success, result = coroutine.resume(co, ...)
local executionTime = os.clock() - startTime
trackInfo.totalExecutionTime = trackInfo.totalExecutionTime + executionTime
local newStatus = coroutine.status(co)
if newStatus == "suspended" then
trackInfo.yieldCount = trackInfo.yieldCount + 1
trackInfo.status = "suspended"
self:log("DEBUG", "코루틴 양보 #" .. trackInfo.yieldCount ..
" (실행시간: " .. string.format("%.4f", executionTime) .. "ms)", trackInfo.id)
elseif newStatus == "dead" then
trackInfo.status = "completed"
self:log("INFO", "코루틴 완료 (총 실행시간: " ..
string.format("%.4f", trackInfo.totalExecutionTime) .. "ms)", trackInfo.id)
-- 성능 통계 업데이트
self:updatePerformanceStats(trackInfo)
end
-- 실행 히스토리 기록
table.insert(self.executionHistory, {
coroutineId = trackInfo.id,
timestamp = os.time(),
action = newStatus == "suspended" and "yield" or
(newStatus == "dead" and "complete" or "resume"),
executionTime = executionTime,
result = type(result) == "string" and result or tostring(result)
})
if not success then
trackInfo.status = "error"
self:log("ERROR", "코루틴 오류: " .. tostring(result), trackInfo.id)
end
return success, result
end
function CoroutineDebugger:updatePerformanceStats(trackInfo)
local stats = self.performanceStats
stats.totalCoroutines = (stats.totalCoroutines or 0) + 1
stats.totalExecutionTime = (stats.totalExecutionTime or 0) + trackInfo.totalExecutionTime
stats.totalResumes = (stats.totalResumes or 0) + trackInfo.resumeCount
stats.totalYields = (stats.totalYields or 0) + trackInfo.yieldCount
-- 평균 계산
stats.avgExecutionTime = stats.totalExecutionTime / stats.totalCoroutines
stats.avgResumesPerCoroutine = stats.totalResumes / stats.totalCoroutines
stats.avgYieldsPerCoroutine = stats.totalYields / stats.totalCoroutines
end
function CoroutineDebugger:getCoroutineInfo(co)
return self.trackedCoroutines[co]
end
function CoroutineDebugger:getAllCoroutineStats()
local stats = {}
for co, info in pairs(self.trackedCoroutines) do
stats[info.id] = {
name = info.name,
status = info.status,
resumeCount = info.resumeCount,
yieldCount = info.yieldCount,
executionTime = info.totalExecutionTime,
age = os.time() - info.createdAt
}
end
return stats
end
function CoroutineDebugger:printDetailedReport()
print("\n=== 코루틴 디버깅 리포트 ===")
-- 개별 코루틴 상태
print("\n개별 코루틴 상태:")
for co, info in pairs(self.trackedCoroutines) do
print(string.format(" %s (%s):", info.id, info.name))
print(string.format(" 상태: %s", info.status))
print(string.format(" 재개 횟수: %d", info.resumeCount))
print(string.format(" 양보 횟수: %d", info.yieldCount))
print(string.format(" 총 실행시간: %.4fms", info.totalExecutionTime))
print(string.format(" 생성 후 경과시간: %ds", os.time() - info.createdAt))
end
-- 전체 성능 통계
print("\n전체 성능 통계:")
local perfStats = self.performanceStats
if perfStats.totalCoroutines then
print(string.format(" 총 코루틴 수: %d", perfStats.totalCoroutines))
print(string.format(" 평균 실행시간: %.4fms", perfStats.avgExecutionTime or 0))
print(string.format(" 평균 재개 횟수: %.2f", perfStats.avgResumesPerCoroutine or 0))
print(string.format(" 평균 양보 횟수: %.2f", perfStats.avgYieldsPerCoroutine or 0))
end
-- 최근 실행 히스토리 (최근 10개)
print("\n최근 실행 히스토리:")
local recentHistory = {}
for i = math.max(1, #self.executionHistory - 9), #self.executionHistory do
table.insert(recentHistory, self.executionHistory[i])
end
for _, entry in ipairs(recentHistory) do
print(string.format(" [%s] %s: %s (%.4fms)",
os.date("%H:%M:%S", entry.timestamp),
entry.coroutineId,
entry.action,
entry.executionTime))
end
end
-- 디버깅 시스템 사용 예시
print("=== 코루틴 디버깅 시스템 테스트 ===")
local debugger = CoroutineDebugger.new()
debugger:setLogLevel("DEBUG")
-- 테스트 코루틴 1: 간단한 카운터
local counter = coroutine.create(function()
for i = 1, 5 do
print("카운터:", i)
coroutine.yield("counting_" .. i)
end
return "카운터 완료"
end)
debugger:trackCoroutine(counter, "SimpleCounter", {purpose = "테스트"})
-- 테스트 코루틴 2: 수학 계산
local calculator = coroutine.create(function()
local sum = 0
for i = 1, 100 do
sum = sum + i
if i % 20 == 0 then
coroutine.yield("계산 진행중: " .. sum)
end
end
return "계산 완료: " .. sum
end)
debugger:trackCoroutine(calculator, "MathCalculator", {purpose = "수학 계산"})
-- 코루틴 실행
while coroutine.status(counter) ~= "dead" or coroutine.status(calculator) ~= "dead" do
if coroutine.status(counter) ~= "dead" then
local success, result = debugger:resumeWithTracking(counter)
if not success then
print("카운터 오류:", result)
break
end
end
if coroutine.status(calculator) ~= "dead" then
local success, result = debugger:resumeWithTracking(calculator)
if not success then
print("계산기 오류:", result)
break
end
end
end
-- 디버깅 리포트 출력
debugger:printDetailedReport()
마무리와 실전 팁
루아 코루틴은 협력적 멀티태스킹을 통해 복잡한 비동기 프로그래밍을 우아하게 해결할 수 있는 강력한 도구입니다.
특히 게임 개발, 웹 서버 개발, 데이터 처리 등 다양한 영역에서 활용도가 높습니다.
코루틴 사용 시 주의사항
코루틴을 효과적으로 활용하기 위해서는 몇 가지 중요한 원칙을 지켜야 합니다.
메모리 관리: 대량의 코루틴을 생성할 때는 반드시 메모리 사용량을 모니터링하고, 사용하지 않는 코루틴은 적절히 정리해야 합니다.
에러 처리: 코루틴 내부에서 발생하는 에러는 일반적인 try-catch 구문으로 처리하기 어려우므로, pcall
을 활용한 안전한 에러 처리 패턴을 구현해야 합니다.
데드락 방지: 코루틴 간의 상호 의존성이 발생하지 않도록 주의깊게 설계해야 합니다.
실전 활용 가이드
코루틴의 진정한 가치는 복잡한 비동기 로직을 동기적인 코드처럼 직관적으로 작성할 수 있다는 점입니다.
Redis의 루아 스크립팅이나 Nginx의 OpenResty와 같은 실제 프로덕션 환경에서도 광범위하게 사용되고 있습니다.
웹 서버 개발: HTTP 요청 처리, 데이터베이스 쿼리, 외부 API 호출 등을 코루틴으로 처리하면 높은 동시성을 달성할 수 있습니다.
게임 개발: 애니메이션, AI 행동 패턴, 이벤트 시스템 등에서 코루틴을 활용하면 복잡한 로직을 간단하게 구현할 수 있습니다.
데이터 처리: 대용량 데이터를 스트리밍 방식으로 처리할 때 코루틴을 사용하면 메모리 효율성을 크게 향상시킬 수 있습니다.
코루틴 마스터를 위한 다음 단계로는 루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍에서 다룬 C 확장과 코루틴을 결합하여 더욱 강력한 시스템을 구축하는 것을 추천합니다.
추천 학습 자료
코루틴에 대한 더 깊은 이해를 위해서는 다음 자료들을 참고하시기 바랍니다:
루아 코루틴은 단순해 보이지만 매우 강력한 기능입니다.
이번 글에서 다룬 패턴들을 실제 프로젝트에 적용해보시고, 여러분만의 창의적인 활용법을 발견해보시기 바랍니다.
다음 글에서는 루아의 더욱 고급 주제들을 다룰 예정이니 많은 관심 부탁드립니다!
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #9: LÖVE 2D 게임 개발 입문 (0) | 2025.06.26 |
---|---|
루아 입문 시리즈 #8: OpenResty로 고성능 웹 서버 구축하기 - Nginx + Lua의 완벽한 조합 (0) | 2025.06.13 |
루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍 (0) | 2025.06.13 |
루아 입문 시리즈 #5: 루아 에러 처리와 디버깅 완벽 가이드 - 안정적인 Lua 애플리케이션 개발을 위한 실전 기법 (0) | 2025.06.13 |
루아 입문 시리즈 #4: 루아 모듈과 패키지 시스템 완벽 가이드 (0) | 2025.06.11 |