OpenResty 코딩 스타일 가이드

API7.ai

December 15, 2022

OpenResty (NGINX + Lua)

많은 개발 언어들은 개발자들에게 해당 분야의 관례를 알려주고, 코드 작성 스타일을 일관되게 유지하며, 일반적인 함정을 피할 수 있도록 코딩 규격을 가지고 있습니다. Python의 PEP 8은 이에 대한 훌륭한 예시이며, 거의 모든 Python 개발자들은 Python 저자가 작성한 이 코딩 규격을 읽어봤을 것입니다.

OpenResty는 아직 자신만의 코딩 규격을 가지고 있지 않으며, 일부 개발자들은 PR을 제출한 후 코드 스타일을 변경하라는 리뷰를 반복적으로 받아 시간과 노력을 낭비하고 있습니다. 이는 피할 수 있는 일입니다.

OpenResty에는 코드 스타일을 자동으로 감지할 수 있는 두 가지 Lint 도구가 있습니다: luachecklj-releng입니다. 전자는 Lua와 OpenResty 세계에서 일반적으로 사용되는 Lint 도구이고, 후자는 OpenResty 자체에서 Perl로 작성한 Lint 도구입니다.

저는 VS Code 편집기에 luacheck 플러그인을 설치하여 코드를 작성할 때 자동 제안을 받을 수 있는 도구를 사용하고 있으며, 프로젝트의 CI에서는 두 도구를 모두 실행합니다. 예를 들어:

luacheck -q lua

./utils/lj-releng lua/*.lua lua/apisix/*.lua

결국, 테스트를 위한 도구가 하나 더 있는 것은 나쁜 일이 아닙니다.

그러나 이 두 도구는 전역 변수, 줄당 길이 및 기타 가장 기본적인 코드 스타일을 감지하는 데 더 초점이 맞춰져 있으며, Python PEP 8의 세부 수준과는 거리가 멀고 참조할 문서도 없습니다.

그래서 오늘, OpenResty 관련 오픈소스 프로젝트에서의 경험을 바탕으로 OpenResty 코딩 스타일 문서를 정리했습니다. 이 규격은 APISIX 및 Kong과 같은 일부 API 게이트웨이의 코드 스타일과도 일치합니다.

들여쓰기

OpenResty에서는 4개의 공백을 들여쓰기 표시로 사용합니다. Lua는 이러한 구문을 요구하지 않지만, 다음은 잘못된 코드와 올바른 코드의 두 가지 예시입니다.

--아니오
if a then
ngx.say("hello")
end
--예
if a then
    ngx.say("hello")
end

편의를 위해 사용 중인 편집기에서 탭을 4개의 공백으로 변경하여 작업을 단순화할 수 있습니다.

공백

연산자 양쪽에는 공백이 필요하여 구분해야 합니다. 다음은 잘못된 코드와 올바른 코드의 두 가지 예시입니다.

--아니오
local i=1
local s    =    "apisix"
--예
local i = 1
local s = "apisix"

빈 줄

많은 개발자들이 다른 언어에서의 개발 관습을 OpenResty로 가져와 줄 끝에 세미콜론을 추가합니다:

--아니오
if a then
    ngx.say("hello");
end;

하지만 실제로 세미콜론을 추가하면 Lua 코드가 매우 보기 흉해지며, 이는 불필요합니다. 또한, 여러 줄의 코드를 한 줄로 만들어 줄을 절약하려고 해서는 안 됩니다. 이렇게 하면 오류를 찾을 때 어떤 코드 섹션이 문제인지 알 수 없게 됩니다.

--아니오
if a then ngx.say("hello") end
--예
if a then
    ngx.say("hello")
end

또한, 함수 사이에는 두 개의 빈 줄로 구분해야 합니다.

--아니오
local function foo()
end
local function bar()
end
--예
local function foo()
end


local function bar()
end

여러 if elseif 분기가 있는 경우에도 빈 줄로 구분해야 합니다.

--아니오
if a == 1 then
    foo()
elseif a== 2 then
    bar()
elseif a == 3 then
    run()
else
    error()
end
--예
if a == 1 then
    foo()

elseif a== 2 then
    bar()

elseif a == 3 then
    run()

else
    error()
end

줄당 최대 길이

각 줄은 80자를 초과해서는 안 되며, 이를 초과할 경우 줄을 나누고 정렬해야 합니다. 줄을 나눌 때는 상하 줄 간의 대응 관계를 반영해야 합니다. 아래 예시에서 두 번째 줄의 함수 인수는 첫 번째 줄의 왼쪽 괄호 오른쪽에 위치해야 합니다.

--아니오
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst, conf.default_conn_delay)
--예
return limit_conn_new("plugin-limit-conn", conf.conn, conf.burst,
                    conf.default_conn_delay)

문자열을 연결할 때 정렬하는 경우, ..를 다음 줄에 위치시켜야 합니다.

--아니오
return limit_conn_new("plugin-limit-conn" ..  "plugin-limit-conn" ..
                    "plugin-limit-conn")
--예
return limit_conn_new("plugin-limit-conn" .. "plugin-limit-conn"
                    .. "plugin-limit-conn")

변수

이전 글에서도 여러 번 강조한 바와 같이, 항상 전역 변수보다는 지역 변수를 사용해야 합니다.

--아니오
i = 1
s = "apisix"
--예
local i = 1
local s = "apisix"

변수 이름은 snake_case 스타일을 사용해야 합니다.

--아니오
local IndexArr = 1
local str_Name = "apisix"
--예
local index_arr = 1
local str_name = "apisix"

반면, 상수는 all-caps 스타일을 사용해야 합니다.

--아니오
local max_int = 65535
local server_name = "apisix"

--예
local MAX_INT = 65535
local SERVER_NAME = "apisix"

테이블

OpenResty에서는 table.new를 사용하여 테이블을 미리 할당합니다.

--아니오
local t = {}
for i = 1, 100 do
   t[i] = i
 end
--예
local new_tab = require "table.new"
 local t = new_tab(100, 0)
 for i = 1, 100 do
   t[i] = i
 end

또한, 배열에서 nil을 사용해서는 안 되며, 반드시 null을 사용해야 하는 경우 ngx.null을 사용해야 합니다.

--아니오
local t = {1, 2, nil, 3}

--예
local t = {1, 2, ngx.null, 3}

문자열

핫 코드 경로에서 문자열을 연결해서는 안 됩니다.

--아니오
local s = ""
for i = 1, 100000 do
    s = s .. "a"
end
--예
local t = {}
for i = 1, 100000 do
    t[i] = "a"
end
local s =  table.concat(t, "")

함수

함수 이름도 snake_case를 따라야 합니다.

--아니오
local function testNginx()
end
--예
local function test_nginx()
end

또한, 함수는 가능한 한 빨리 반환해야 합니다.

--아니오
local function check(age, name)
    local ret = true
    if age < 20 then
        ret = false
    end

    if name == "a" then
        ret = false
    end
    -- 다른 작업 수행
    return ret
--예
local function check(age, name)
    if age < 20 then
        return false
    end

    if name == "a" then
        return false
    end
    -- 다른 작업 수행
    return true

모듈

모든 require된 라이브러리는 local화해야 합니다:

--아니오
local function foo()
    local ok, err = ngx.timer.at(delay, handler)
end
--예
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end

스타일의 일관성을 위해 requirengxlocal화해야 합니다:

--아니오
local core = require("apisix.core")
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end
--예
local ngx = ngx
local require = require
local core = require("apisix.core")
local timer_at = ngx.timer.at

local function foo()
    local ok, err = timer_at(delay, handler)
end

오류 처리

오류 정보를 반환하는 함수의 경우, 오류 정보를 판단하고 처리해야 합니다:

--아니오
local sock = ngx.socket.tcp()
local ok = sock:connect("www.google.com", 80)
ngx.say("successfully connected to google!")
--예
local sock = ngx.socket.tcp()
local ok, err = sock:connect("www.google.com", 80)
if not ok then
    ngx.say("failed to connect to google: ", err)
    return
end
ngx.say("successfully connected to google!")

또한, 직접 작성한 함수의 경우 오류 정보를 문자열 형식의 두 번째 매개변수로 반환해야 합니다:

--아니오
local function foo()
    local ok, err = func()
    if not ok then
        return false
    end
    return true
end
--아니오
local function foo()
    local ok, err = func()
    if not ok then
        return false, {msg = err}
    end
    return true
end
--예
local function foo()
    local ok, err = func()
    if not ok then
        return false, "failed to call func(): " .. err
    end
    return true
end

요약

이것은 코딩 스타일 가이드의 초기 버전이며, 지속적인 업데이트와 유지 관리를 위해 GitHub에서 제공할 예정입니다. 이 규격을 공유하여 더 많은 OpenResty 사용자들이 참여할 수 있도록 해주세요.

Share article link