루아 Lua 프로그래밍 : 모듈과 패키지 가이드

53 sec read

지금까지 우리는 함수로 코드를 묶고, 테이블로 데이터를 구조화하는 방법을 익혔습니다. 하지만 프로젝트의 규모가 커지기 시작하면, 모든 코드를 단 하나의 파일에 담는 것은 금세 한계에 부딪힙니다. 코드는 뒤엉키고, 원하는 기능을 찾기 어려워지며, 유지보수는 악몽이 됩니다.

이 문제를 해결하기 위해 프로그래밍 세계는 모듈(Module)이라는 해법을 제시합니다. 모듈은 관련된 기능들을 별도의 파일로 분리하고, 필요할 때마다 간편하게 가져와 사용할 수 있게 해주는 코드 관리 시스템입니다.

이 글에서는 Lua의 간단하면서도 강력한 모듈 시스템을 파헤쳐 봅니다. 모듈을 직접 만들고 사용하는 방법부터, Lua가 모듈을 찾아내는 원리, 그리고 전 세계 개발자들이 만든 유용한 라이브러리(패키지)를 손쉽게 설치하고 관리하는 방법까지, 실질적인 애플리케이션 개발에 필수적인 지식을 모두 담았습니다.

모듈이란 무엇인가? 코드의 체계적인 정리함

Lua에서 모듈(Module)은 일반적으로 하나의 파일로 구현되며, 관련된 함수, 변수, 데이터의 집합을 캡슐화한 코드 단위입니다. 모듈을 사용하는 목적은 명확합니다.

  • 코드 조직화: 관련된 기능들(예: 수학 함수, 네트워크 통신 함수)을 논리적인 단위로 묶어 관리합니다.
  • 재사용성: 한번 잘 만들어 둔 모듈은 여러 다른 프로젝트나 파일에서 언제든지 불러와 재사용할 수 있습니다.
  • 이름 충돌 방지 (Namespace): 모듈 내의 함수나 변수는 기본적으로 그 모듈 안에서만 유효합니다. 외부로 공개할 것들만 명시적으로 내보내므로, 다른 모듈과 같은 이름의 함수를 사용해도 서로 충돌하지 않습니다.
  • 정보 은닉: 모듈 내부에서만 사용되는 도우미 함수나 변수는 외부에 노출시키지 않고 숨길 수 있습니다.

현대적인 Lua에서 모듈은 결국 하나의 테이블을 반환하는 .lua 파일입니다. 이 테이블이 바로 모듈이 외부에 제공하는 기능들의 목록, 즉 인터페이스가 됩니다.

나만의 모듈 만들기: 표준 패턴 익히기

직접 간단한 수학 관련 모듈을 만들어 보겠습니다. mymath.lua라는 이름으로 파일을 생성하고 다음 코드를 작성합니다.

-- mymath.lua

-- 1. 모듈이 외부에 공개할 내용을 담을 테이블을 생성합니다.
local M = {}

-- 2. 모듈 내부에서만 사용할 변수나 함수는 지역(local)으로 선언합니다.
local PI = 3.14159265

-- 3. 외부에 공개할 함수들도 일단 지역(local) 함수로 정의합니다.
local function add(a, b)
  return a + b
end

local function subtract(a, b)
  return a - b
end

local function circle_area(radius)
  return PI * radius * radius
end

-- 4. 공개할 함수들을 모듈 테이블(M)의 필드로 추가합니다.
M.add = add
M.subtract = subtract
M.circle_area = circle_area

-- 5. 마지막으로, 완성된 모듈 테이블을 반환합니다.
return M

이것이 바로 Lua 모듈을 만드는 가장 표준적이고 권장되는 패턴입니다. 모든 것을 지역(local) 변수와 함수로 정의하여 전역 공간을 깨끗하게 유지하고, 공개할 것만 골라 테이블에 담아 반환하는 방식입니다.

모듈 사용하기: require 함수

다른 스크립트에서 방금 만든 mymath 모듈을 사용하려면 내장 함수인 require를 사용합니다. require는 모듈 이름을 문자열로 받으며, 파일 확장자 .lua는 생략합니다.

mymath.lua와 같은 위치에 main.lua 파일을 만들고 다음과 같이 작성해 보세요.

-- main.lua

-- 'mymath' 모듈을 불러오고, 반환된 테이블을 'math_module' 변수에 저장합니다.
local math_module = require("mymath")

-- 모듈 테이블을 통해 공개된 함수들을 사용합니다.
local sum = math_module.add(10, 5)
print("10 + 5 =", sum) -- 출력: 10 + 5 = 15

local area = math_module.circle_area(2)
print("반지름 2인 원의 넓이:", area) -- 출력: 반지름 2인 원의 넓이: 12.5663706

-- 모듈 내부의 지역 변수(PI)에는 직접 접근할 수 없습니다.
-- print(math_module.PI) -- 출력: nil (M 테이블에 추가되지 않았기 때문)

-- require는 모듈을 딱 한 번만 로드하고 그 결과를 캐싱(기억)합니다.
local math_module_again = require("mymath")
print(math_module == math_module_again) -- 출력: true (두 변수는 완전히 같은 테이블을 가리킴)

require 함수는 지정된 모듈 파일을 딱 한 번만 실행하고 그 결과를 내부 캐시에 저장합니다. 같은 모듈을 여러 번 require 해도 실제 파일 실행은 최초 한 번만 일어나고, 이후에는 캐시된 값을 즉시 반환합니다. 이는 성능을 높이고 모듈 상태의 일관성을 보장하는 중요한 특징입니다.

Lua는 모듈을 어떻게 찾을까?: package.path

require("mymodule")을 호출했을 때, Lua는 어떻게 mymodule.lua 파일을 찾을까요? 그 비밀은 package.path라는 변수에 있습니다.

package.path는 Lua가 모듈을 찾기 위해 검색할 경로들의 목록입니다. 이 목록은 세미콜론(;)으로 구분된 여러 경로 템플릿으로 이루어져 있으며, 각 템플릿의 물음표(?) 부분이 require에 전달된 모듈 이름으로 치환됩니다.

Lua 인터프리터에서 print(package.path)를 실행해보면 다음과 비슷한 경로들을 볼 수 있습니다.

./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;...

require("mymath")를 호출하면, Lua는 위 경로 순서대로 다음 파일들을 찾아 나섭니다.

  1. ./mymath.lua (현재 디렉터리에 mymath.lua 파일이 있는가?)
  2. ./mymath/init.lua (현재 디렉터리에 mymath 폴더가 있고, 그 안에 init.lua 파일이 있는가?)
  3. /usr/local/share/lua/5.4/mymath.lua (시스템 표준 라이브러리 경로에 있는가?)

파일을 찾는 즉시 로드를 멈추고, 모든 경로에서 찾지 못하면 오류를 발생시킵니다. 대부분의 경우, 우리가 작성한 모듈은 현재 작업 디렉터리에 있으므로 첫 번째 경로에서 바로 찾아집니다.

패키지와 LuaRocks: 더 큰 세상으로

  • 패키지(Package): 일반적으로 관련된 여러 모듈의 집합을 의미합니다. 점(.)을 사용하여 계층적으로 구성할 수 있습니다. 예를 들어, require("mylib.utils")mylib라는 디렉터리(패키지) 안에 있는 utils.lua 모듈을 찾으라는 의미입니다.
  • LuaRocks: Lua를 위한 공식 패키지 관리자입니다. Python의 pip, Node.js의 npm과 같은 역할을 합니다. LuaRocks를 사용하면 전 세계 개발자들이 만들어 공유하는 수많은 유용한 라이브러리(이를 “rock”이라 부름)를 단 몇 줄의 명령어로 쉽게 검색하고, 설치하고, 관리할 수 있습니다.
# 파일 시스템 관련 라이브러리 설치하기
luarocks install luafilesystem

# 설치된 라이브러리 목록 보기
luarocks list

# 라이브러리 검색하기
luarocks search json

LuaRocks로 패키지를 설치하면, Lua가 require를 통해 바로 찾을 수 있는 경로에 자동으로 설치되므로 매우 편리합니다.

주의사항: 오래된 방식과 디버깅 팁

  • module() 함수 (사용 금지!): 아주 오래된 Lua 코드에서는 파일 상단에 module("mymodule", package.seeall)과 같은 구문을 볼 수 있습니다. 이 방식은 전역 공간을 오염시키는 등 여러 심각한 단점이 있어 현재는 절대 사용하지 않는 것이 좋습니다. 항상 위에서 소개한 local M = {}; return M 패턴을 사용하세요.
  • 캐시 무효화: 개발 중에 모듈을 수정한 후 변경 사항을 즉시 반영하고 싶을 때가 있습니다. require는 캐싱을 하므로 다시 호출해도 소용없습니다. 이때 package.loaded["mymath"] = nil과 같이 캐시를 수동으로 제거한 후 다시 require하면 모듈을 새로 로드할 수 있습니다. 단, 이는 디버깅 용도로만 사용해야 합니다.

마무리하며

이번 글에서는 Lua 코드를 구조화하고 재사용하는 핵심 기술인 모듈과 패키지에 대해 배웠습니다.

  • 모듈은 관련된 코드를 캡슐화한 재사용 단위이며, 코드 조직화와 이름 충돌 방지에 필수적입니다.
  • local M = {}; ...; return M 패턴은 모듈을 만드는 가장 표준적이고 안전한 방법입니다.
  • require() 함수는 모듈을 단 한 번만 로드하고 그 결과를 캐싱하여 반환합니다.
  • package.pathrequire가 모듈 파일을 찾는 경로의 규칙을 정의합니다.
  • LuaRocks는 Lua의 표준 패키지 관리자로, 강력한 외부 라이브러리 생태계를 활용할 수 있게 해줍니다.

모듈 시스템은 실질적인 애플리케이션을 구축하는 데 반드시 필요한 초석입니다. 코드를 깔끔하게 분리하고 관리하는 습관을 통해, 이제 우리는 더 크고 복잡한 프로그램을 만들 준비가 되었습니다.

루아 (Lua) 프로그래밍: 테이블과 메타테이블의 모든 것

Lua 프로그래밍의 여정에서 가장 중요하고 흥미로운 지점에 도달했습니다. 바로 Lua 언어의 심장이자 가장 중심적인 기능인 테이블(Table)입니다. Lua에는 배열, 딕셔너리, 리스트, 객체 등을 위한 별도의...
eve
1 min read

루아(Lua) 프로그래밍: 제어 구조 조건과 반복

지금까지 우리는 변수에 데이터를 저장하고, 연산자로 이 데이터들을 계산하고 비교하는 방법을 배웠습니다. 하지만 프로그램이 단순히 위에서 아래로 순서대로만 실행된다면, 매우 단순한 작업밖에 할 수...
eve
1 min read

Lua 프로그래밍: 연산자와 표현식

이전 글에서 데이터에 이름을 붙여 변수에 저장하고, Lua가 데이터를 어떤 종류로 다루는지 알아보았습니다. 하지만 데이터는 그 자체로 두면 아무 일도 하지 않습니다. 이 데이터에...
eve
1 min read