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

루아 입문 시리즈 #2: 루아(Lua) 함수와 클로저 – 함수형 프로그래밍 맛보기

by devcomet 2025. 5. 15.
728x90
반응형

루아 입문 시리즈 #2: 루아(Lua) 함수와 클로저 – 함수형 프로그래밍 맛보기
루아 입문 시리즈 #2: 루아(Lua) 함수와 클로저 – 함수형 프로그래밍 맛보기

 

루아(Lua)는 브라질에서 개발된 경량 스크립트 언어로, 함수형 프로그래밍과 절차형 프로그래밍의 장점을 모두 제공합니다.

특히 게임 개발(World of Warcraft, Angry Birds 등)과 임베디드 시스템에서 널리 사용되며,

그 핵심에는 강력한 함수와 클로저 시스템이 있습니다.

이번 글에서는 루아의 함수와 클로저를 통해 함수형 프로그래밍의 핵심 개념을 실습 중심으로 살펴보겠습니다.

루아에서 함수는 일급 시민(first-class citizen)이며, 이를 활용한 클로저 패턴은 코드를 더욱 유연하고 표현력 있게 만들어줍니다.


루아 함수의 특별한 점

일급 시민으로서의 함수

루아에서 함수는 값(value)입니다.

이는 다른 언어와 구별되는 루아의 핵심 특징 중 하나입니다.

-- 1. 함수를 변수에 저장
local add = function(a, b)
    return a + b
end

-- 2. 함수를 테이블에 저장
local operations = {
    add = function(a, b) return a + b end,
    subtract = function(a, b) return a - b end,
    multiply = function(a, b) return a * b end
}

-- 3. 함수를 함수의 인자로 전달
local function calculate(operation, x, y)
    return operation(x, y)
end

print(calculate(add, 10, 5))                    -- 15
print(calculate(operations.multiply, 10, 5))    -- 50

다중 반환값과 가변 인자

루아 함수의 독특한 특징 중 하나는 다중 반환값을 지원한다는 점입니다.

-- 다중 반환값 함수
function divide_with_remainder(dividend, divisor)
    local quotient = math.floor(dividend / divisor)
    local remainder = dividend % divisor
    return quotient, remainder
end

local q, r = divide_with_remainder(17, 5)
print(q, r)  -- 3, 2

-- 가변 인자 함수
function sum(...)
    local args = {...}  -- 가변 인자를 테이블로 변환
    local total = 0
    for i, v in ipairs(args) do
        total = total + v
    end
    return total
end

print(sum(1, 2, 3, 4, 5))  -- 15

함수 오버로딩과 기본값 처리

루아는 함수 오버로딩을 직접 지원하지 않지만, 창의적인 방법으로 유사한 효과를 얻을 수 있습니다.

-- 타입 기반 함수 분기
function smart_print(value)
    local t = type(value)
    if t == "table" then
        -- 테이블 출력
        for k, v in pairs(value) do
            print(k .. ": " .. tostring(v))
        end
    elseif t == "function" then
        print("함수: " .. tostring(value))
    else
        print("값: " .. tostring(value))
    end
end

-- 기본값을 가진 함수
function create_user(name, age, role)
    name = name or "익명"
    age = age or 0
    role = role or "user"

    return {
        name = name,
        age = age,
        role = role
    }
end

local user1 = create_user("김철수", 25)
local user2 = create_user()  -- 모든 기본값 사용

클로저 완전 이해하기

클로저의 핵심 개념

클로저(closure)는 함수 + 그 함수가 선언된 환경의 조합입니다.

루아에서 내부 함수는 외부 함수의 지역 변수에 접근할 수 있으며, 외부 함수가 종료된 후에도 해당 변수들을 기억합니다.

function create_counter(initial_value, step)
    local count = initial_value or 0
    local increment = step or 1

    -- 클로저 함수들을 담은 테이블 반환
    return {
        next = function()
            count = count + increment
            return count
        end,

        current = function()
            return count
        end,

        reset = function()
            count = initial_value or 0
        end,

        set_step = function(new_step)
            increment = new_step
        end
    }
end

-- 각각 독립적인 클로저 환경을 가짐
local counter1 = create_counter(0, 1)
local counter2 = create_counter(100, -5)

print(counter1.next())    -- 1
print(counter1.next())    -- 2
print(counter2.next())    -- 95
print(counter1.current()) -- 2 (counter2의 영향을 받지 않음)

클로저를 활용한 프라이빗 변수 구현

루아는 클래스 문법이 없지만, 클로저를 통해 캡슐화프라이빗 변수를 구현할 수 있습니다.

function create_bank_account(initial_balance, account_type)
    local balance = initial_balance or 0
    local account_type = account_type or "일반"
    local transaction_history = {}
    local is_frozen = false

    -- 프라이빗 함수
    local function add_transaction(type, amount, description)
        table.insert(transaction_history, {
            type = type,
            amount = amount,
            description = description,
            timestamp = os.time()
        })
    end

    local function check_frozen()
        if is_frozen then
            error("계좌가 동결되어 있습니다.")
        end
    end

    -- 퍼블릭 메서드들만 반환
    return {
        deposit = function(amount, description)
            check_frozen()
            if amount <= 0 then
                error("입금액은 0보다 커야 합니다.")
            end
            balance = balance + amount
            add_transaction("입금", amount, description or "입금")
            return balance
        end,

        withdraw = function(amount, description)
            check_frozen()
            if amount <= 0 then
                error("출금액은 0보다 커야 합니다.")
            end
            if amount > balance then
                error("잔액이 부족합니다. 현재 잔액: " .. balance)
            end
            balance = balance - amount
            add_transaction("출금", amount, description or "출금")
            return balance
        end,

        get_balance = function()
            return balance
        end,

        get_account_type = function()
            return account_type
        end,

        freeze = function()
            is_frozen = true
        end,

        unfreeze = function()
            is_frozen = false
        end,

        get_history = function()
            -- 원본을 보호하기 위해 복사본 반환
            local copy = {}
            for i, transaction in ipairs(transaction_history) do
                copy[i] = {
                    type = transaction.type,
                    amount = transaction.amount,
                    description = transaction.description,
                    timestamp = transaction.timestamp
                }
            end
            return copy
        end
    }
end

-- 사용 예제
local my_account = create_bank_account(1000, "프리미엄")
my_account.deposit(500, "월급")
print(my_account.get_balance()) -- 1500

-- balance 변수에 직접 접근 불가 (프라이빗)
-- print(my_account.balance) -- nil

고차 함수와 함수형 패턴

Map, Filter, Reduce 트리오

함수형 프로그래밍의 핵심인 map, filter, reduce 연산을 루아로 구현해보겠습니다.

-- Map: 각 요소를 변환
function map(array, transform)
    local result = {}
    for i, value in ipairs(array) do
        result[i] = transform(value, i)  -- 인덱스도 전달
    end
    return result
end

-- Filter: 조건에 맞는 요소만 선택
function filter(array, predicate)
    local result = {}
    for i, value in ipairs(array) do
        if predicate(value, i) then
            table.insert(result, value)
        end
    end
    return result
end

-- Reduce: 하나의 값으로 축약
function reduce(array, reducer, initial)
    local accumulator = initial
    for i, value in ipairs(array) do
        accumulator = reducer(accumulator, value, i)
    end
    return accumulator
end

-- 실전 예제: 학생 성적 처리
local students = {
    {name = "김철수", math = 85, english = 92},
    {name = "이영희", math = 78, english = 88},
    {name = "박민수", math = 95, english = 82},
    {name = "최수진", math = 88, english = 95}
}

-- 평균 점수 추가
local students_with_avg = map(students, function(student)
    local avg = (student.math + student.english) / 2
    return {
        name = student.name,
        math = student.math,
        english = student.english,
        average = avg
    }
end)

-- 평균 80점 이상인 학생 필터링
local high_performers = filter(students_with_avg, function(student)
    return student.average >= 85
end)

-- 전체 평균 계산
local class_average = reduce(students_with_avg, function(sum, student)
    return sum + student.average
end, 0) / #students_with_avg

print("우수 학생 수:", #high_performers)
print("학급 평균:", class_average)

커링과 부분 적용

커링(Currying)은 여러 인자를 받는 함수를 한 번에 하나의 인자만 받는 함수들의 체인으로 변환하는 기법입니다.

-- 커링 구현
function curry(func, arity)
    arity = arity or 2

    local function curried(...)
        local args = {...}

        if #args >= arity then
            return func(unpack(args))
        else
            return function(...)
                local new_args = {...}
                for i, v in ipairs(new_args) do
                    table.insert(args, v)
                end
                return curried(unpack(args))
            end
        end
    end

    return curried
end

-- 부분 적용 구현
function partial(func, ...)
    local partial_args = {...}

    return function(...)
        local full_args = {}

        -- 부분 적용된 인자들 추가
        for i, v in ipairs(partial_args) do
            table.insert(full_args, v)
        end

        -- 나머지 인자들 추가
        for i, v in ipairs({...}) do
            table.insert(full_args, v)
        end

        return func(unpack(full_args))
    end
end

-- 실제 사용 예제
local function format_log(level, module, message)
    return string.format("[%s] %s: %s", level, module, message)
end

-- 커링 사용
local curried_log = curry(format_log, 3)
local error_log = curried_log("ERROR")
local db_error_log = error_log("DATABASE")

print(db_error_log("연결 실패"))  -- [ERROR] DATABASE: 연결 실패

-- 부분 적용 사용
local info_logger = partial(format_log, "INFO", "SYSTEM")
print(info_logger("서버 시작됨"))  -- [INFO] SYSTEM: 서버 시작됨

실전 클로저 활용법

이벤트 시스템 구현

게임이나 웹 애플리케이션에서 자주 사용되는 이벤트 시스템을 클로저로 구현해보겠습니다.

function create_event_emitter()
    local listeners = {}
    local once_listeners = {}

    local emitter = {}

    function emitter:on(event, callback)
        if not listeners[event] then
            listeners[event] = {}
        end
        table.insert(listeners[event], callback)

        -- 리스너 제거를 위한 함수 반환
        return function()
            for i, cb in ipairs(listeners[event]) do
                if cb == callback then
                    table.remove(listeners[event], i)
                    break
                end
            end
        end
    end

    function emitter:once(event, callback)
        if not once_listeners[event] then
            once_listeners[event] = {}
        end
        table.insert(once_listeners[event], callback)
    end

    function emitter:emit(event, ...)
        -- 일반 리스너들 실행
        if listeners[event] then
            for _, callback in ipairs(listeners[event]) do
                callback(...)
            end
        end

        -- 일회성 리스너들 실행 후 제거
        if once_listeners[event] then
            for _, callback in ipairs(once_listeners[event]) do
                callback(...)
            end
            once_listeners[event] = nil
        end
    end

    function emitter:remove_all_listeners(event)
        if event then
            listeners[event] = nil
            once_listeners[event] = nil
        else
            listeners = {}
            once_listeners = {}
        end
    end

    return emitter
end

-- 게임 예제
local game_events = create_event_emitter()

-- 플레이어 레벨업 이벤트 리스너
local remove_level_listener = game_events:on("player_levelup", function(player_name, new_level)
    print(player_name .. "님이 " .. new_level .. "레벨이 되었습니다!")

    -- 특별한 레벨에서 보상 지급
    if new_level % 10 == 0 then
        print("축하합니다! 특별 보상을 받으셨습니다!")
    end
end)

-- 일회성 이벤트 (튜토리얼 완료)
game_events:once("tutorial_complete", function(player_name)
    print(player_name .. "님, 튜토리얼을 완료하셨습니다! 환영 선물을 드립니다.")
end)

-- 이벤트 발생
game_events:emit("player_levelup", "김철수", 5)
game_events:emit("tutorial_complete", "김철수")
game_events:emit("tutorial_complete", "이영희")  -- 이미 실행되어서 아무 일 없음

상태 머신 구현

복잡한 상태 관리를 클로저로 깔끔하게 구현할 수 있습니다.

function create_state_machine(initial_state, states)
    local current_state = initial_state
    local state_history = {initial_state}

    local machine = {}

    function machine:get_state()
        return current_state
    end

    function machine:can_transition(to_state)
        local current_config = states[current_state]
        if not current_config then return false end

        return current_config.transitions and 
               current_config.transitions[to_state] == true
    end

    function machine:transition(to_state, ...)
        if not self:can_transition(to_state) then
            error("Invalid transition from " .. current_state .. " to " .. to_state)
        end

        -- 현재 상태의 exit 콜백 실행
        local current_config = states[current_state]
        if current_config.on_exit then
            current_config.on_exit(...)
        end

        -- 상태 변경
        local previous_state = current_state
        current_state = to_state
        table.insert(state_history, to_state)

        -- 새 상태의 enter 콜백 실행
        local new_config = states[to_state]
        if new_config.on_enter then
            new_config.on_enter(previous_state, ...)
        end

        return current_state
    end

    function machine:get_history()
        return {unpack(state_history)}  -- 복사본 반환
    end

    return machine
end

-- 게임 캐릭터 상태 예제
local character_states = {
    idle = {
        transitions = {walking = true, running = true, attacking = true},
        on_enter = function() print("캐릭터가 대기 상태입니다.") end
    },
    walking = {
        transitions = {idle = true, running = true},
        on_enter = function() print("캐릭터가 걷기 시작했습니다.") end,
        on_exit = function() print("걷기를 멈췄습니다.") end
    },
    running = {
        transitions = {idle = true, walking = true},
        on_enter = function() print("캐릭터가 뛰기 시작했습니다.") end
    },
    attacking = {
        transitions = {idle = true},
        on_enter = function(prev_state, target)
            print("공격 시작! 대상: " .. (target or "없음"))
        end
    }
}

local character = create_state_machine("idle", character_states)
character:transition("walking")
character:transition("running")
character:transition("idle")

성능 최적화 테크닉

메모이제이션 고급 구현

메모이제이션을 더욱 효율적으로 구현하고, 캐시 크기 제한과 TTL(Time To Live) 기능을 추가해보겠습니다.

function create_advanced_memoizer(options)
    options = options or {}
    local max_cache_size = options.max_size or 100
    local ttl = options.ttl  -- seconds

    local function create_memoized_function(func)
        local cache = {}
        local access_order = {}  -- LRU 구현용
        local cache_times = {}   -- TTL 구현용

        return function(...)
            local key = table.concat({...}, ",")
            local current_time = os.time()

            -- TTL 체크
            if ttl and cache_times[key] then
                if current_time - cache_times[key] > ttl then
                    cache[key] = nil
                    cache_times[key] = nil
                    -- access_order에서도 제거
                    for i, k in ipairs(access_order) do
                        if k == key then
                            table.remove(access_order, i)
                            break
                        end
                    end
                end
            end

            -- 캐시 히트
            if cache[key] ~= nil then
                -- LRU 업데이트
                for i, k in ipairs(access_order) do
                    if k == key then
                        table.remove(access_order, i)
                        break
                    end
                end
                table.insert(access_order, key)

                return cache[key]
            end

            -- 캐시 미스 - 함수 실행
            local result = func(...)

            -- 캐시 크기 제한
            if #access_order >= max_cache_size then
                local oldest_key = table.remove(access_order, 1)
                cache[oldest_key] = nil
                cache_times[oldest_key] = nil
            end

            -- 새 결과 캐싱
            cache[key] = result
            cache_times[key] = current_time
            table.insert(access_order, key)

            return result
        end
    end

    return create_memoized_function
end

-- 피보나치 수열 - 고급 메모이제이션 적용
local memoizer = create_advanced_memoizer({max_size = 50, ttl = 60})

local fibonacci = memoizer(function(n)
    print("계산 중: fib(" .. n .. ")")  -- 실제 계산 확인용
    if n <= 1 then return n end
    return fibonacci(n-1) + fibonacci(n-2)
end)

print("fib(10) =", fibonacci(10))
print("fib(10) =", fibonacci(10))  -- 캐시에서 가져옴 (계산 중 출력 없음)

함수 합성과 파이프라인

복잡한 데이터 처리를 체이닝할 수 있는 파이프라인을 구현해보겠습니다.

-- 함수 합성 유틸리티
function compose(...)
    local functions = {...}

    return function(value)
        local result = value
        -- 오른쪽부터 왼쪽으로 실행 (수학적 합성)
        for i = #functions, 1, -1 do
            result = functions[i](result)
        end
        return result
    end
end

-- 파이프라인 (왼쪽부터 오른쪽으로 실행)
function pipe(...)
    local functions = {...}

    return function(value)
        local result = value
        for i = 1, #functions do
            result = functions[i](result)
        end
        return result
    end
end

-- 체이닝 가능한 함수들
local function add(n)
    return function(x) return x + n end
end

local function multiply(n)
    return function(x) return x * n end
end

local function to_string()
    return function(x) return tostring(x) end
end

local function add_prefix(prefix)
    return function(str) return prefix .. str end
end

-- 사용 예제
local process_number = pipe(
    add(5),
    multiply(2),
    to_string(),
    add_prefix("결과: ")
)

print(process_number(10))  -- "결과: 30"

-- 데이터 처리 파이프라인
local function parse_csv_line(line)
    local result = {}
    for field in line:gmatch("([^,]+)") do
        table.insert(result, field:match("^%s*(.-)%s*$"))  -- trim
    end
    return result
end

local function convert_to_record(fields)
    return {
        id = tonumber(fields[1]),
        name = fields[2],
        age = tonumber(fields[3]),
        score = tonumber(fields[4])
    }
end

local function validate_record(record)
    if record.age and record.age >= 18 and record.score and record.score >= 60 then
        return record
    end
    return nil
end

local process_csv_line = pipe(
    parse_csv_line,
    convert_to_record,
    validate_record
)

-- 테스트
local csv_lines = {
    "1,김철수,25,85",
    "2,이영희,17,95",  -- 나이 제한으로 필터됨
    "3,박민수,30,45"   -- 점수 제한으로 필터됨
}

for _, line in ipairs(csv_lines) do
    local record = process_csv_line(line)
    if record then
        print(record.name .. " (나이: " .. record.age .. ", 점수: " .. record.score .. ")")
    end
end

실제 프로젝트 적용 사례

웹 서버 미들웨어 시스템

Express.js와 유사한 미들웨어 시스템을 루아로 구현해보겠습니다.

function create_web_server()
    local middlewares = {}
    local routes = {}

    local server = {}

    function server:use(middleware)
        table.insert(middlewares, middleware)
    end

    function server:get(path, handler)
        routes["GET:" .. path] = handler
    end

    function server:post(path, handler)
        routes["POST:" .. path] = handler
    end

    function server:handle_request(method, path, request)
        local response = {
            status = 200,
            headers = {},
            body = ""
        }

        -- 미들웨어 체인 실행
        local function execute_middlewares(index)
            if index > #middlewares then
                -- 모든 미들웨어 실행 완료 - 라우터 실행
                local route_key = method .. ":" .. path
                local handler = routes[route_key]

                if handler then
                    handler(request, response)
                else
                    response.status = 404
                    response.body = "Not Found"
                end
                return
            end

            local middleware = middlewares[index]

            -- next 함수
            local function next()
                execute_middlewares(index + 1)
            end

            middleware(request, response, next)
        end

        execute_middlewares(1)
        return response
    end

    return server
end

-- 미들웨어 예제들
local function logger_middleware(request, response, next)
    local start_time = os.clock()
    print("[" .. os.date("%Y-%m-%d %H:%M:%S") .. "] " .. 
          request.method .. " " .. request.path)

    next()

    local elapsed = os.clock() - start_time
    print("응답 시간: " .. string.format("%.2fms", elapsed * 1000))
end

local function auth_middleware(request, response, next)
    if request.headers.authorization then
        request.user = {id = 1, name = "김철수"}  -- 토큰 검증 로직 생략
        next()
    else
        response.status = 401
        response.body = "Unauthorized"
    end
end

local function cors_middleware(request, response, next)
    response.headers["Access-Control-Allow-Origin"] = "*"
    response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
    next()
end

-- 서버 설정
local app = create_web_server()

app:use(logger_middleware)
app:use(cors_middleware)

app:get("/", function(request, response)
    response.body = "Hello, World!"
end)

app:get("/protected", function(request, response)
    -- 인증이 필요한 라우트에만 미들웨어 적용
    auth_middleware(request, response, function()
        response.body = "안녕하세요, " .. request.user.name .. "님!"
    end)
end)

-- 테스트
local test_request = {
    method = "GET",
    path = "/",
    headers = {}
}

local response = app:handle_request("GET", "/", test_request)
print("응답:", response.body)

게임 AI를 위한 행동 트리

게임 개발에서 NPC AI를 구현할 때 자주 사용되는 행동 트리(Behavior Tree)를 클로저로 구현해보겠습니다.

-- 행동 트리 노드 상태
local BT_SUCCESS = "SUCCESS"
local BT_FAILURE = "FAILURE"
local BT_RUNNING = "RUNNING"

function create_behavior_tree()
    local bt = {}

    -- 기본 액션 노드
    function bt.action(execute_func)
        return function(context)
            return execute_func(context) or BT_SUCCESS
        end
    end

    -- 조건 노드
    function bt.condition(check_func)
        return function(context)
            return check_func(context) and BT_SUCCESS or BT_FAILURE
        end
    end

    -- 시퀀스 노드 (모든 자식이 성공해야 성공)
    function bt.sequence(...)
        local children = {...}

        return function(context)
            for _, child in ipairs(children) do
                local result = child(context)
                if result ~= BT_SUCCESS then
                    return result
                end
            end
            return BT_SUCCESS
        end
    end

    -- 셀렉터 노드 (하나의 자식이 성공하면 성공)
    function bt.selector(...)
        local children = {...}

        return function(context)
            for _, child in ipairs(children) do
                local result = child(context)
                if result ~= BT_FAILURE then
                    return result
                end
            end
            return BT_FAILURE
        end
    end

    -- 반복 노드
    function bt.repeat_until_fail(child)
        return function(context)
            while true do
                local result = child(context)
                if result == BT_FAILURE then
                    return BT_SUCCESS
                end
                if result == BT_RUNNING then
                    return BT_RUNNING
                end
            end
        end
    end

    -- 인버터 노드
    function bt.invert(child)
        return function(context)
            local result = child(context)
            if result == BT_SUCCESS then
                return BT_FAILURE
            elseif result == BT_FAILURE then
                return BT_SUCCESS
            else
                return result
            end
        end
    end

    return bt
end

-- 게임 NPC AI 예제
local bt = create_behavior_tree()

-- NPC 행동 정의
local function find_enemy(context)
    context.enemy = context.nearby_enemies[1]  -- 간단한 적 탐지
    return context.enemy ~= nil
end

local function is_enemy_in_range(context)
    if not context.enemy then return false end
    local distance = math.abs(context.player.x - context.enemy.x)
    return distance <= context.attack_range
end

local function attack_enemy(context)
    if context.enemy then
        print(context.player.name .. "이(가) " .. context.enemy.name .. "을(를) 공격합니다!")
        context.enemy.health = context.enemy.health - context.player.damage
        if context.enemy.health <= 0 then
            print(context.enemy.name .. "이(가) 쓰러졌습니다!")
            context.enemy = nil
        end
        return BT_SUCCESS
    end
    return BT_FAILURE
end

local function move_to_enemy(context)
    if context.enemy then
        print(context.player.name .. "이(가) " .. context.enemy.name .. "에게 다가갑니다.")
        -- 이동 로직 (간단히 구현)
        if context.player.x < context.enemy.x then
            context.player.x = context.player.x + 1
        else
            context.player.x = context.player.x - 1
        end
        return BT_SUCCESS
    end
    return BT_FAILURE
end

local function patrol(context)
    print(context.player.name .. "이(가) 순찰 중입니다.")
    context.player.x = context.player.x + (math.random() > 0.5 and 1 or -1)
    return BT_SUCCESS
end

-- 행동 트리 구성
local npc_ai = bt.selector(
    -- 적이 있다면 공격 시퀀스 실행
    bt.sequence(
        bt.condition(find_enemy),
        bt.selector(
            -- 사정거리 내에 있으면 공격
            bt.sequence(
                bt.condition(is_enemy_in_range),
                bt.action(attack_enemy)
            ),
            -- 사정거리 밖에 있으면 이동
            bt.action(move_to_enemy)
        )
    ),
    -- 적이 없으면 순찰
    bt.action(patrol)
)

-- 게임 시뮬레이션
local game_context = {
    player = {name = "오크 전사", x = 0, damage = 20, health = 100},
    nearby_enemies = {
        {name = "인간 기사", x = 5, health = 50}
    },
    attack_range = 1
}

-- AI 실행 (게임 루프에서 매 프레임마다 호출)
for turn = 1, 10 do
    print("\n=== 턴 " .. turn .. " ===")
    local result = npc_ai(game_context)
    print("AI 결과:", result)

    if not game_context.nearby_enemies[1] or game_context.nearby_enemies[1].health <= 0 then
        game_context.nearby_enemies = {}
    end
end

비동기 프로그래밍 시뮬레이션

루아에서 콜백과 Promise 패턴을 클로저로 구현하여 비동기 프로그래밍을 시뮬레이션해보겠습니다.

-- Promise 구현
function create_promise()
    local promise = {}
    local state = "PENDING"  -- PENDING, RESOLVED, REJECTED
    local value = nil
    local error = nil
    local then_callbacks = {}
    local catch_callbacks = {}

    function promise:then(callback)
        if state == "RESOLVED" then
            callback(value)
        elseif state == "PENDING" then
            table.insert(then_callbacks, callback)
        end
        return self
    end

    function promise:catch(callback)
        if state == "REJECTED" then
            callback(error)
        elseif state == "PENDING" then
            table.insert(catch_callbacks, callback)
        end
        return self
    end

    function promise:resolve(result)
        if state == "PENDING" then
            state = "RESOLVED"
            value = result
            for _, callback in ipairs(then_callbacks) do
                callback(result)
            end
        end
    end

    function promise:reject(err)
        if state == "PENDING" then
            state = "REJECTED"
            error = err
            for _, callback in ipairs(catch_callbacks) do
                callback(err)
            end
        end
    end

    return promise
end

-- 비동기 작업 시뮬레이션
function fetch_user_data(user_id)
    local promise = create_promise()

    -- 실제로는 네트워크 요청이지만, 여기서는 타이머로 시뮬레이션
    local function simulate_async_request()
        -- 성공 시나리오 (80% 확률)
        if math.random() > 0.2 then
            local user_data = {
                id = user_id,
                name = "사용자" .. user_id,
                email = "user" .. user_id .. "@example.com",
                created_at = os.time()
            }
            promise:resolve(user_data)
        else
            promise:reject("네트워크 오류: 사용자 데이터를 가져올 수 없습니다.")
        end
    end

    -- 실제 환경에서는 타이머나 네트워크 콜백에서 호출
    simulate_async_request()

    return promise
end

function fetch_user_posts(user_id)
    local promise = create_promise()

    local function simulate_posts_request()
        if math.random() > 0.3 then
            local posts = {}
            for i = 1, math.random(1, 5) do
                table.insert(posts, {
                    id = i,
                    title = "포스트 " .. i,
                    content = "사용자 " .. user_id .. "의 " .. i .. "번째 포스트입니다.",
                    user_id = user_id
                })
            end
            promise:resolve(posts)
        else
            promise:reject("포스트를 불러올 수 없습니다.")
        end
    end

    simulate_posts_request()
    return promise
end

-- Promise 체이닝 예제
print("사용자 데이터 로딩 시작...")

fetch_user_data(123)
    :then(function(user)
        print("사용자 정보:", user.name .. " (" .. user.email .. ")")
        return fetch_user_posts(user.id)
    end)
    :then(function(posts)
        print("포스트 " .. #posts .. "개를 불러왔습니다:")
        for _, post in ipairs(posts) do
            print("  - " .. post.title)
        end
    end)
    :catch(function(error)
        print("오류 발생:", error)
    end)

함수형 반응형 프로그래밍 (FRP) 기초

옵서버 패턴과 함수형 프로그래밍을 결합한 반응형 프로그래밍을 루아로 구현해보겠습니다.

-- Observable 구현
function create_observable(initial_value)
    local value = initial_value
    local observers = {}

    local observable = {}

    function observable:get()
        return value
    end

    function observable:set(new_value)
        local old_value = value
        value = new_value

        -- 모든 옵서버에게 변경 알림
        for _, observer in ipairs(observers) do
            observer(new_value, old_value)
        end
    end

    function observable:subscribe(observer)
        table.insert(observers, observer)

        -- 구독 해제 함수 반환
        return function()
            for i, obs in ipairs(observers) do
                if obs == observer then
                    table.remove(observers, i)
                    break
                end
            end
        end
    end

    -- 함수형 변환 메서드들
    function observable:map(transform)
        local mapped = create_observable(transform(value))

        self:subscribe(function(new_val)
            mapped:set(transform(new_val))
        end)

        return mapped
    end

    function observable:filter(predicate)
        local filtered = create_observable(predicate(value) and value or nil)

        self:subscribe(function(new_val)
            if predicate(new_val) then
                filtered:set(new_val)
            end
        end)

        return filtered
    end

    function observable:combine(other, combiner)
        local combined = create_observable(combiner(value, other:get()))

        self:subscribe(function(new_val)
            combined:set(combiner(new_val, other:get()))
        end)

        other:subscribe(function(new_val)
            combined:set(combiner(value, new_val))
        end)

        return combined
    end

    return observable
end

-- 게임 상태 관리 예제
local player_health = create_observable(100)
local player_mana = create_observable(50)

-- 파생된 상태들
local health_percentage = player_health:map(function(health)
    return health / 100 * 100  -- 백분율
end)

local is_critically_injured = player_health:filter(function(health)
    return health <= 20
end)

local combat_power = player_health:combine(player_mana, function(health, mana)
    return (health + mana) / 2
end)

-- 상태 변화 구독
player_health:subscribe(function(new_health, old_health)
    print("체력 변화: " .. old_health .. " → " .. new_health)
end)

health_percentage:subscribe(function(percentage)
    print("체력 백분율: " .. percentage .. "%")
end)

is_critically_injured:subscribe(function(health)
    if health then
        print("⚠️ 위험! 체력이 매우 낮습니다: " .. health)
    end
end)

combat_power:subscribe(function(power)
    print("전투력: " .. string.format("%.1f", power))
end)

-- 상태 변화 시뮬레이션
print("=== 게임 시작 ===")
player_health:set(80)  -- 데미지를 받음
player_mana:set(30)    -- 마나 소모
player_health:set(15)  -- 크리티컬 상태
player_health:set(100) -- 회복

마무리하며

루아의 함수와 클로저는 단순한 문법 기능을 넘어서 강력한 추상화 도구입니다.

이번 글에서 살펴본 다양한 패턴들은 실제 프로젝트에서 코드의 재사용성, 유지보수성, 확장성을 크게 향상시킬 수 있습니다.

특히 게임 개발 분야에서 루아가 널리 사용되는 이유 중 하나가 바로 이러한 함수형 프로그래밍 지원 때문입니다.

이벤트 시스템, 상태 관리, AI 로직 등을 클로저와 고차 함수로 구현하면 더욱 직관적이고 버그가 적은 코드를 작성할 수 있습니다.

함수형 프로그래밍의 핵심은 부작용을 최소화하고 함수의 조합을 통해 복잡한 로직을 구성하는 것입니다.

루아의 클로저를 활용하면 이러한 함수형 패러다임을 자연스럽게 적용할 수 있으며, 동시에 성능과 메모리 효율성도 확보할 수 있습니다.

다음 시리즈에서는 루아의 또 다른 핵심 기능인 테이블(Table)에 대해 자세히 알아보겠습니다.

루아 테이블의 고급 활용법부터 메타테이블을 이용한 객체지향 프로그래밍까지, 더욱 깊이 있는 내용으로 찾아뵙겠습니다.


시리즈 네비게이션
← 이전글: 루아 입문 시리즈 #1: 루아(Lua) 프로그래밍 언어 문법 기초: 초보자를 위한 완벽 가이드
→ 다음글: 루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지

728x90
반응형