루아(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: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #6: 루아와 C 연동 프로그래밍 (0) | 2025.06.13 |
---|---|
루아 입문 시리즈 #5: 루아 에러 처리와 디버깅 완벽 가이드 - 안정적인 Lua 애플리케이션 개발을 위한 실전 기법 (0) | 2025.06.13 |
루아 입문 시리즈 #4: 루아 모듈과 패키지 시스템 완벽 가이드 (0) | 2025.06.11 |
루아 입문 시리즈 #3: 루아 테이블 완전 정복 – 연관 배열부터 메타테이블까지 (1) | 2025.05.16 |
루아 입문 시리즈 #1: 루아(Lua) 프로그래밍 언어 문법 기초: 초보자를 위한 완벽 가이드 (0) | 2025.05.15 |