Lua 시작하기
API7.ai
September 23, 2022
NGINX의 기본 사항에 대한 일반적인 이해를 한 후, 우리는 Lua를 더 깊이 배울 것입니다. Lua는 OpenResty에서 사용되는 프로그래밍 언어이며, 그 기본 문법을 숙지하는 것이 필요합니다.
Lua는 작고 섬세한 스크립트 언어로, 브라질의 대학 연구실에서 태어났으며, 그 이름은 포르투갈어로 "아름다운 달"을 의미합니다. NGINX는 러시아에서, Lua는 브라질에서, 그리고 OpenResty는 저자의 나라인 중국에서 태어났습니다. 흥미롭게도, 이 세 가지 똑똑한 오픈소스 기술은 유럽이나 미국이 아닌 BRICS 국가에서 나왔습니다.
Lua는 단순하고 가벼우며, 임베디드 접착 언어로서의 위치를 차지하기 위해 설계되었으며, 크고 거대한 길을 가지 않았습니다. 비록 일상 업무에서 직접 Lua 코드를 작성하지 않을 수도 있지만, Lua는 널리 사용됩니다. 많은 온라인 게임, 예를 들어 월드 오브 워크래프트는 Lua를 사용하여 플러그인을 작성합니다. 키-값 데이터베이스인 Redis는 로직을 제어하기 위해 Lua를 내장하고 있습니다.
반면에, Lua의 라이브러리는 상대적으로 단순하지만, C 프로그래밍 언어 라이브러리를 쉽게 호출할 수 있으며, 많은 성숙한 C 프로그래밍 언어 코드를 사용할 수 있습니다. 예를 들어, OpenResty에서는 NGINX와 OpenSSL의 C 프로그래밍 언어 함수를 호출해야 하는 경우가 많으며, 이는 Lua와 LuaJIT가 C 라이브러리에 쉽게 접근할 수 있는 능력 덕분입니다.
여기서는 Lua 데이터 타입과 문법을 빠르게 익히도록 하여, 나중에 OpenResty를 더 원활하게 배울 수 있도록 도와드리겠습니다.
환경과 hello world
우리는 표준 Lua 5.1 환경을 별도로 설치할 필요가 없습니다. 왜냐하면 OpenResty는 더 이상 표준 Lua를 지원하지 않고, LuaJIT만 지원하기 때문입니다. 여기서 제시하는 Lua 문법도 LuaJIT와 호환되며, 최신 Lua 5.3을 기반으로 하지 않습니다.
OpenResty 설치 디렉토리에서 LuaJIT 디렉토리와 실행 파일을 찾을 수 있습니다. 저는 Mac 환경에서 brew를 사용하여 OpenResty를 설치했기 때문에, 여러분의 로컬 경로는 아래와 다를 수 있습니다.
$ ll /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit
lrwxr-xr-x 1 ming admin 18B 4 2 14:54 /usr/local/Cellar/openresty/1.13.6.2/luajit/bin/luajit -> luajit-2.1.0-beta3
시스템의 실행 파일 디렉토리에서도 찾을 수 있습니다.
$ which luajit
/usr/local/bin/luajit
LuaJIT의 버전을 확인합니다.
$ luajit -v
LuaJIT 2.1.0-beta2 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/
이 정보를 확인한 후, 새로운 1.lua
파일을 생성하고 LuaJIT를 사용하여 hello world
코드를 실행할 수 있습니다.
$ cat 1.lua
print("hello world")
$ luajit 1.lua
hello world
물론, resty
를 사용하여 직접 실행할 수도 있으며, 이는 결국 LuaJIT로 실행된다는 것을 알고 있습니다.
$ resty -e 'print("hello world")'
hello world
이 두 가지 방법으로 hello world
를 실행할 수 있습니다. 저는 resty
방식을 선호합니다. 왜냐하면 많은 OpenResty 코드도 나중에 resty
로 실행되기 때문입니다.
데이터 타입
Lua에는 데이터 타입이 많지 않으며, type
함수를 사용하여 값의 타입을 반환할 수 있습니다. 예를 들어 다음과 같습니다.
$ resty -e 'print(type("hello world"))
print(type(print))
print(type(true))
print(type(360.0))
print(type({}))
print(type(nil))
'
다음 정보가 출력됩니다.
string
function
boolean
number
table
nil
이것들은 Lua의 기본 데이터 타입입니다. 간단히 소개하겠습니다.
문자열
Lua에서 문자열은 불변 값입니다. 문자열을 수정하려면 새로운 문자열을 생성해야 합니다. 이 접근 방식은 장단점이 있습니다: 장점은 동일한 문자열이 여러 번 나타나더라도 메모리에는 하나의 복사본만 존재한다는 것이지만, 단점도 명확합니다: 문자열을 수정하고 연결하려면 많은 불필요한 문자열을 생성하게 됩니다.
이 단점을 설명하기 위해 예를 들어보겠습니다. Lua에서는 두 개의 점 표시를 사용하여 문자열을 추가합니다. 다음 코드는 1부터 10까지의 숫자를 문자열로 연결합니다.
$ resty -e 'local s = ""
for i = 1, 10 do
s = s .. tostring(i)
end
print(s)'
여기서 우리는 열 번 반복하며, 마지막 결과만 필요합니다. 그 사이의 아홉 개의 새로운 문자열은 쓸모가 없습니다. 이들은 추가 공간을 차지할 뿐만 아니라 불필요한 CPU 연산을 소모합니다.
물론, 나중에 성능 최적화 섹션에서 이에 대한 해결책을 다룰 것입니다.
또한, Lua에서는 문자열을 표현하는 세 가지 방법이 있습니다: 작은따옴표, 큰따옴표, 그리고 긴 괄호([[]]
). 처음 두 가지는 비교적 이해하기 쉽고 일반적으로 다른 언어에서도 사용되므로, 긴 괄호는 어떤 용도로 사용될까요?
구체적인 예를 살펴보겠습니다.
$ resty -e 'print([[string has \n and \r]])'
string has \n and \r
긴 괄호 안의 문자열은 어떤 방식으로도 이스케이프되지 않습니다.
또 다른 질문을 할 수 있습니다: 위의 문자열에 긴 괄호가 포함되어 있다면 어떻게 해야 할까요? 답은 간단합니다: 긴 괄호 중간에 하나 이상의 =
기호를 추가하면 됩니다.
$ resty -e 'print([=[ string has a [[]]. ]=])'
string has a [[]].
불리언
이것은 간단합니다, true
와 false
. Lua에서는 nil
과 false
만 거짓입니다. 나머지는 모두 참입니다, 0과 빈 문자열도 포함됩니다. 다음 코드로 이를 확인할 수 있습니다.
$ resty -e 'local a = 0
if a then
print("true")
end
a = ""
if a then
print("true")
end'
이러한 판단은 많은 일반적인 개발 언어와 일치하지 않으므로, 이러한 문제에서 오류를 피하기 위해 비교 대상 객체를 명시적으로 작성할 수 있습니다. 예를 들어 다음과 같습니다.
$ resty -e 'local a = 0
if a == false then
print("true")
end
'
숫자
Lua의 숫자 타입은 배정밀도 부동 소수점 숫자로 구현됩니다. 주목할 만한 점은 LuaJIT가 dual-number
모드를 지원한다는 것입니다. 이는 LuaJIT가 정수를 정수로, 부동 소수점 숫자를 배정밀도 부동 소수점 숫자로 저장한다는 것을 의미하며, 상황에 따라 달라집니다.
또한, LuaJIT는 큰 정수에 대해 long-long integers
를 지원합니다. 예를 들어 다음과 같습니다.
$ resty -e 'print(9223372036854775807LL - 1)'
9223372036854775806LL
함수
함수는 Lua에서 일급 시민입니다. 함수를 변수에 저장하거나 다른 함수의 인자로 전달할 수 있습니다.
예를 들어, 다음 두 함수 선언은 정확히 동일합니다.
function foo()
end
그리고
foo = function ()
end
테이블
테이블은 Lua의 유일한 데이터 구조이며, 자연스럽게 매우 중요합니다. 따라서 나중에 특별한 섹션을 할애하여 다룰 것입니다. 먼저 간단한 예제 코드를 살펴보겠습니다.
$ resty -e 'local color = {first = "red"}
print(color["first"])'
red
널 값
Lua에서 널 값은 nil
입니다. 변수를 정의했지만 값을 할당하지 않으면 기본값은 nil
입니다.
$ resty -e 'local a
print(type(a))'
nil
OpenResty 시스템에 들어가면 많은 널 값을 발견할 수 있습니다. 예를 들어 ngx.null
등이 있습니다. 이에 대해서는 나중에 더 이야기하겠습니다.
Lua의 데이터 타입은 여기까지 소개하겠습니다. 먼저 기초를 다지는 것이 중요합니다. 나중에 글에서 더 깊이 배울 내용을 계속해서 학습할 것입니다. 실습과 사용을 통해 배우는 것이 항상 새로운 지식을 흡수하는 가장 편리한 방법입니다.
공통 표준 라이브러리
종종 언어를 배우는 것은 실제로 그 표준 라이브러리를 배우는 것입니다.
Lua는 상대적으로 작고 내장된 표준 라이브러리가 많지 않습니다. 또한 OpenResty 환경에서는 Lua 표준 라이브러리가 매우 낮은 우선순위를 차지합니다. 동일한 기능에 대해 OpenResty API를 먼저 사용하고, LuaJIT의 라이브러리 함수를 사용하며, 일반 Lua 함수를 사용하는 것을 권장합니다.
OpenResty의 API > LuaJIT의 라이브러리 함수 > 표준 Lua의 함수
는 사용성과 성능 측면에서 반복적으로 언급될 우선순위입니다.
그러나 이와 관련하여, 실제 프로젝트에서 일부 Lua 라이브러리를 사용할 수밖에 없습니다. 여기서는 몇 가지 더 일반적으로 사용되는 표준 라이브러리를 선택하여 소개하겠습니다. 더 많은 정보를 원한다면 공식 Lua 문서를 참조할 수 있습니다.
문자열 라이브러리
문자열 조작은 우리가 자주 사용하며, 함정이 가장 많은 부분입니다.
간단한 규칙 하나는 정규 표현식이 관련된 경우, 반드시 OpenResty에서 제공하는 ngx.re.*
를 사용하여 해결하고, Lua의 string.*
처리를 사용하지 않는 것입니다. 이는 Lua의 정규 표현식이 독특하며 PCRE 사양을 따르지 않기 때문입니다. 대부분의 엔지니어가 이를 다루기 어려울 것이라고 생각합니다.
가장 일반적으로 사용되는 문자열 라이브러리 함수 중 하나는 string.byte(s [, i [, j ]])
입니다. 이 함수는 문자 s[i], s[i + 1], s[i + 2], ------, s[j]
에 해당하는 ASCII 코드를 반환합니다. i
의 기본값은 1, 첫 번째 바이트이며, j
의 기본값은 i
입니다.
샘플 코드를 살펴보겠습니다.
$ resty -e 'print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 세 번째 매개변수가 누락되었으며, 세 번째 매개변수는 두 번째와 동일하게 기본값 3입니다.
print(string.byte("abc")) -- 두 번째와 세 번째 매개변수가 누락되었으며, 둘 다 기본값 1입니다.
'
출력 결과는 다음과 같습니다.
979899
99
97
테이블 라이브러리
OpenResty 컨텍스트에서는 Lua에 내장된 대부분의 테이블 라이브러리를 사용하지 않는 것을 권장합니다. 단, table.concat
및 table.sort
와 같은 몇 가지 함수는 예외입니다. 이에 대한 세부 사항은 LuaJIT 챕터에서 다루겠습니다.
여기서는 table.concat
을 간단히 언급하겠습니다. table.concat
은 일반적으로 문자열 연결 시나리오에서 사용됩니다. 예를 들어 아래 예제와 같습니다. 이는 많은 불필요한 문자열 생성을 피할 수 있습니다.
$ resty -e 'local a = {"A", "b", "C"}
print(table.concat(a))'
수학 라이브러리
Lua 수학 라이브러리는 표준 수학 함수 세트로 구성됩니다. 수학 라이브러리의 도입은 Lua 프로그래밍 언어를 풍부하게 할 뿐만 아니라 프로그램 작성을 더 쉽게 만듭니다.
OpenResty 프로젝트에서는 Lua로 수학 연산을 거의 하지 않습니다. 그러나 math.random()
및 math.randomseed()
와 같은 난수 관련 함수는 자주 사용됩니다. 예를 들어 다음 코드는 지정된 범위 내에서 두 개의 난수를 생성할 수 있습니다.
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
더미 변수
이 공통 표준 라이브러리를 이해한 후, 새로운 개념인 더미 변수를 배워보겠습니다.
함수가 여러 값을 반환하는 시나리오를 상상해보세요. 그 중 일부는 필요하지 않습니다. 그러면 이러한 값을 어떻게 받아들여야 할까요?
저는 적어도 이러한 사용되지 않는 변수에 의미 있는 이름을 붙이려고 하는 것이 고통스럽다고 느낍니다.
다행히 Lua는 이에 대한 완벽한 해결책을 제공하며, 더미 변수의 개념을 제공합니다. 더미 변수는 관례적으로 밑줄로 이름을 지어 필요 없는 값을 버리고 자리 표시자로 사용합니다.
표준 라이브러리 함수 string.find
를 예로 들어 더미 변수의 사용을 살펴보겠습니다. 이 일반 라이브러리 함수는 시작과 끝 인덱스를 나타내는 두 값을 반환합니다.
시작 인덱스만 얻으려면 다음과 같이 string.find
의 반환 값을 받는 변수를 선언하면 됩니다.
$ resty -e 'local start = string.find("hello", "he")
print(start)'
1
하지만 끝 인덱스만 얻으려면 더미 변수를 사용해야 합니다.
$ resty -e 'local _, end_pos = string.find("hello", "he")
print(end_pos)'
2
반환 값 외에도, 더미 변수는 루프에서 자주 사용됩니다. 예를 들어 다음과 같습니다.
$ resty -e 'for _, v in ipairs({4,5,6}) do
print(v)
end'
4
5
6
그리고 여러 반환 값을 무시해야 할 때, 동일한 더미 변수를 재사용할 수 있습니다. 여기서 예제를 제공하지 않겠습니다. 여러분이 직접 샘플 코드를 작성해보시길 바랍니다. 댓글 섹션에 코드를 게시하여 저와 공유하고 교류할 수 있습니다.
요약
오늘 우리는 표준 Lua의 데이터 구조와 문법을 빠르게 살펴보았으며, 이 단순하고 컴팩트한 언어에 대한 첫인상을 얻었을 것입니다. 다음 강의에서는 Lua와 LuaJIT의 관계를 살펴보겠습니다. LuaJIT는 OpenResty의 주요 초점이며, 깊이 파고들 가치가 있습니다.
마지막으로, 한 가지 생각할 거리를 남기고 싶습니다.
이 글에서 배운 수학 라이브러리에 대한 코드를 기억하시나요? 지정된 범위 내에서 두 개의 난수를 생성하는 코드입니다.
$ resty -e 'math.randomseed (os.time())
print(math.random())
print(math.random(100))'
그러나 이 코드는 현재 타임스탬프를 시드로 사용합니다. 이 접근 방식에 문제가 있을까요? 그리고 좋은 시드를 어떻게 생성해야 할까요? 종종 우리가 개발하는 난수는 무작위가 아니며, 보안 위험이 있습니다.
여러분의 의견을 공유해주시고, 이 글을 동료와 친구들에게 공유해주세요. 함께 소통하고 개선해 나가길 바랍니다.