루아 모듈의 이름 충돌 방지(1/n)

Twitter icon류광, 2015-12-30 22:12
이번 글에서는 순수 루아 스크립트 모듈의 다양한 형태를 나열하고, 이름 충돌 방지에 가장 좋은 형태를 제시합니다.

다음은 가장 “원시적인” 형태의 루아 모듈의 예입니다. 흔히 볼 수 있는 C 함수 라이브러리와 비슷한 '함수 모음' 형태입니다.

-- mymp3lib.lua --

function open(filename)
    -- MP3 파일 적재 --
    return handle -- 음원 자료에 대한 핸들
end

function play(handle)
    -- 해당 MP3 음원 재생 --
end

function stop(handle)
    -- 해당 MP3 음원 중지 --
end

function close(handle)
    -- 해당 MP3 음원 자료 해제 --
end

이 모듈은 이를테면 이런 식으로 사용합니다.

require"mymp3lib"

...

local music = open(filename)
play(music)

...

if some_condition(music) then
    stop(music)
end

...

close(music)

그런데 open, play, stop, close 같은 이름은 다른 라이브러리에도 많이 쓰입니다. 예를 들어 위의 mymp3lib.lua를 만든 사람과는 다른 누군가가 다음과 같은 동영상 재생 모듈을 가능성은 언제나 존재합니다.

-- movie_lib.lua --

function open(filename)
    -- 동영상 파일 적재
end

function play(handle)
    -- ...
end

function stop(handle)
    -- ...
end

function close(handle)
    -- ...
end

이 두 모듈은 함께 사용할 수 없습니다. 만일 클라이언트 스크립트에서

require"mymp3lib"
require"movie_lib"

라고 하면, 둘째 require에서 적재한 open, play 등의 함수들이 첫 require에서 적재된 해당 함수들을 덮어 쓰기 때문입니다. 아시다시피 루아에서 함수는 일급 객체입니다. 다른 말로 하면 함수는 함수 형식의 값을 가진 변수입니다. open 하나만 놓고 보면, 위의 코드는 사실상 다음과 같습니다.

-- require"mymp3lib" 에 의해
open = function(filename)
    -- MP3 파일 적재 --
    return handle -- 음원 자료에 대한 핸들
end

-- require"movie_lib" 에 의해
open = function(filename)
    -- 동영상 파일 적재
    ...
end

같은 전역 변수에 두 번 값을 배정한다는 점에서 이는 본질적으로

a = 1
a = 2

와 마찬가지입니다.

이런 문제가 발생할 확률을 줄이는 한 가지 방법은 함수 이름에 접두어를 붙이는 것입니다(이를테면 movie_lib_open 등). OpenGL이나 pthreads 같은 C 함수 라이브러리가 그런 방법을 사용합니다. 루아에서는 다음처럼 함수들을 하나의 테이블로 묶는 방법도 흔히 쓰입니다.

-- mymp3lib.lua --

mymp3 = {}
function mymp3.open(filename)
    -- MP3 파일 적재 --
    return handle -- 음원 자료에 대한 핸들
end

function mymp3.play(handle)
    -- 해당 MP3 음원 재생 --
end

function mymp3.stop(handle)
    -- 해당 MP3 음원 중지 --
end

function mymp3.close(handle)
    -- 해당 MP3 음원 자료 해제 --
end
-- movie_lib.lua --

movie_lib = {}
movie_lib.open = function(filename)
    -- 동영상 파일 적재
end

movie_lib.play = function(handle)
    -- ...
end

movie_lib.stop = function(handle)
    -- ...
end

movie_lib.close = function(handle)
    -- ...
end

이전 방식이 C 함수 라이브러리와 비슷했다면 이번 것은 C++ 이름공간(namespace)을 활용하는 함수 라이브러리와 비슷합니다. 이제 클라이언트 스크립트에서 둘을 섞어 쓸 수 있습니다.

require"mymp3lib"
require"movie_lib"

local music = mymp3lib.open(arg[1])

local movie = movie_lib.open(arg[2])

...

그런데 이름공간(테이블) 이름 자체가 충돌할 수 있습니다. 예를 들어 누군가가 "movie_manager.lua"라는 이름으로 다음과 같은 모듈을 만들 수도 있습니다. 하필이면 movie_lib이라는 테이블 이름(앞의 movie_lib.lua의 것과 같은)을 사용한다는 점에 주목하세요.

-- movie_manager.lua --
movie_lib = {}

movie_lib.register = function(title, year, director, actors)

  -- 주어진 제목, 연도, 감독, 배우들로 
  -- 영화 라이브러리에 영화 항목 추가

end

-- ... 기타 등등

그러면 다음과 같은 스크립트는 오류를 일으킵니다.

require"movie_lib"
require"movie_manager"

movie_lib.open(arg[1]) -- movie_lib에 open이라는 함수가 없음

두 번째 require 때문에 전역 테이블 movie_lib이 통채로 덮어 쓰였기(좀 더 정확하게는 전역 변수 movie_lib가 다른 테이블을 가리키기) 때문에, movie_lib에는 더 이상 open이라는 함수가 없습니다.

이전 방식이 이름 충돌의 확률이 높았다면, 이번 방식은 이름 충돌의 확률은 줄었지만 피해가 커졌습니다.

여담이지만, Java나 XML처럼 이름공간의 이름을 URI를 이용해서 짓는(이를테면 net.occamsrazr.movie_lib) 방식도 있습니다. 그런데 생각해 보면 이 방법은 이름 충돌을 방지하는 기법이라기보다는 이름 충돌이 발생했을 때 당사자간의 분쟁을 좀 더 확실하게 해소하기 위한 관례일 뿐입니다. 예를 들어 occamsrazr.net 도메인 소유자인 제가 아닌 누군가가 net.occamsrazr.movie_lib라는 모듈을 만들었다면 뭔가 이상한 것입니다...

한편, 루아 5.1에서는 이런 형태의 전역 테이블 방식의 모듈을 좀 더 편리하게 작성하기 위해 module()이라는 내장 함수가 도입되었는데, 이름 충돌 해소(방지가 아니라)용으로도 사용할 수 있는 기능이 있었지만 몇 가지 이유로 5.2에서 module() 자체가 폐기되었습니다. 이에 관해서는 나중에 한 번 다루기로 하고요.

결론적으로, 모듈 이름 충돌 문제에 대한 현재 최선의 해답은 다음과 같은 형태입니다.

-- movie_lib.lua --
local M = {}

M.open = function(filename)
    -- 동영상 파일 적재
end

M.play = function(handle)
    -- ...
end

M.stop = function(handle)
    -- ...
end

M.close = function(handle)
    -- ...
end

return M

한 마디로 요약하면 "함수들이 설정된 지역 테이블을 명시적으로 반환한다"가 되겠습니다. 모듈 안에서 그 어떤 전역 변수도 설정하지 않음을, 다시 말해서 전역 이름공간을 오염시키지 않음을 주목하세요.

전역 테이블을 사용하지 않으므로, 클라이언트 스크립트에서는 모듈이 돌려준 지역 테이블을 명시적으로 받아서 적절한 변수에 배정해야 합니다.

require "movie_manager" -- 전역 movie_lib을 설정
local my_movie_lib = require"movie_lib" -- 이제는 전역 movie_lib과 무관

my_movie_lib.open(arg[1])  -- OK
...

movie_manager.lua가 이전과 동일하다고 할 때, require"movie_manager"는 전역 movie_lib 테이블을 설정합니다. 이전과는 달리 require"movie_lib"는 전역 movie_lib을 설정하지 않고 그냥 지역 테이블을 돌려주므로 더 이상 충돌이 없습니다.

잠깐 부연 설명을 하자면, 개념적으로 require()는 주어진 모듈 이름에 해당하는 스크립트의 내용을 하나의 익명 함수 안에 담고 그 익명 함수를 호출합니다. 즉, local my_movie_lib = require"movie_lib" 는 개념적으로(실제로는 모듈 이름으로부터 스크립트 파일을 찾는 과정이 관여하고 호출 시 pcall을 적용하는 등등, 이보다 조금 더 복잡합니다...) 다음과 마찬가지입니다.

local my_movie_lib = (function()
    -- movie_lib.lua --
    local M = {}

    M.open = function(filename)
        -- 동영상 파일 적재
    end

    -- ... 생략 ...

    return M
end)()

GitHub 등에서 비교적 최근 만들어진 루아 모듈들을 보면 이런 ‘패턴’을 흔히 볼 수 있을 것입니다.

마지막으로, 몇 가지 변형 스타일들을 나열해 보자면, 우선 지역 변수 M을 생략하는 스타일이 있습니다.

local open = function(filename)
    -- 동영상 파일 적재
end

-- ... 생략 ...

return {
    open = open,
    -- ... 생략 ...
}

이 스타일은 다음 예처럼 ‘구현과 인터페이스의 분리’ 원칙과 잘 맞습니다.

local open_impl = function(filename)
    -- 동영상 파일 적재
end

...

return {
    open = open_impl
    -- ... 생략 ...
}

간단한 모듈은 다음처럼 return 문 하나로 끝낼 수도 있습니다.

return {
    open = function(filename)
        -- 동영상 파일 적재
    end,
    -- ... 생략 ...
}

좀 더 본격적인 예로, 다음은 앞에서 다루지 않은 ‘클래스’ 형태의 모듈에 위의 스타일을 결합한 예입니다.

-- movie_lib.lua --

local video_engine = require"my_video_engine" -- 가상의 C/C++ 확장 모듈
local meta = {}
meta.__index = meta

function meta:play()
    return video_engine.play(self.handle) 
end

-- ... stop, close, is_playing, seek 등등 ...

return {
    open = function(filename)
        local inst = {}
        inst.handle = video_engine.open(filename)
        setmetatable(inst, meta)
        return inst
    end
}

클라이언트 스크립트에서는 이런 식으로 사용합니다.

local movie_player = require"movie_lib"

local movie = movie_player.open(arg[1])

if not movie:play() then
    -- 오류 처리
end

...

마지막으로, 모듈이 반드시 테이블일 필요도 없습니다. 앞의 예에서 모듈 자체의 함수는 open 밖에 없으므로, 그냥 open 자체를 돌려주는 것도 가능합니다.

-- ... 앞 부분은 이전과 동일 ...

return function(filename)
    local inst = {}
    inst.handle = video_engine.open(filename)
    setmetatable(inst, meta)
    return inst
end

클라이언트에서는 이런 식으로 사용하고요:

loca open_movie = require"movie_lib"

local movie = open_movie(arg[1])

if not movie:play() then
    -- 오류 처리
end

...

다음 글에서는 이처럼 전역 이름공간을 오염시키지 않는 모듈 패턴을 순수 루아 스크립트가 아닌 C/C++ 확장 모듈에서 구현하는 방법을 살펴보겠습니다.

태그: 프로그래밍 Lua

comments powered by Disqus