본문 바로가기
프로그래밍 언어 실전 가이드

루아 입문 시리즈 #7: 코루틴과 비동기 프로그래밍 - 협력적 멀티태스킹의 완전 정복

by devcomet 2025. 6. 13.
728x90
반응형

루아 입문 시리즈 #7: 코루틴과 비동기 프로그래밍 - 협력적 멀티태스킹의 완전 정복
루아 입문 시리즈 #7: 코루틴과 비동기 프로그래밍 - 협력적 멀티태스킹의 완전 정복

 

루아의 코루틴(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 요청 플로우 차트
비동기 HTTP 요청 플로우 차트

-- 비동기 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 확장과 코루틴을 결합하여 더욱 강력한 시스템을 구축하는 것을 추천합니다.

추천 학습 자료

코루틴에 대한 더 깊은 이해를 위해서는 다음 자료들을 참고하시기 바랍니다:

루아 코루틴은 단순해 보이지만 매우 강력한 기능입니다.

이번 글에서 다룬 패턴들을 실제 프로젝트에 적용해보시고, 여러분만의 창의적인 활용법을 발견해보시기 바랍니다.

다음 글에서는 루아의 더욱 고급 주제들을 다룰 예정이니 많은 관심 부탁드립니다!

728x90
반응형