본문 바로가기

3.구현/Lua

Lua에서 객체지향적 코딩 하기

오랜만에 lua 관련 글을 써본다. 이번 내용에서는 루아에서 객체지향적 코딩 구조를 만들려고 한다. 이런 내용은 이미 다른 곳에서 있는 내용을 단지 참고하여 정리하는 수준이지만, 조금 더 나름대로 정리할려고 한다.
객체지향이 나온지는 정말 오랜(?) 시간이 흘렀다. 그러나 루아에서는 객체지향을 지원하지 않는다. 이를 흉내내는 수준일 뿐이다.

객체지향 흉내내기

루아에서는 객체지향에서 캡슐화 정도을 흉내낼 것이다. 상속과 다형성 측면은 다루지 않을 것이다.
캡슐화라고 해서 완벽한 것은 아니고, 단지 멤버 변수와 멤버 함수를 하나의 객체로 묶어 놓았을 뿐, 외부에서 멤버 변수나 멤버 함수에 대한 접근을 제한할 수는 없다.
루아에서 캡슐화 흉내는 테이블이라는 막강한(?) 데이터 형을 기반으로 구현한다. 테이블과 메타테이블을 이용하여 흉내내는 것이다.
흉내내는 방식이 2가지가 있는데, 하나는 전체를 함수로 정의해서 사용하는 방법과 테이블을 별도로 생성하고 생성시 메타데이블을 지정하는 방식이 있다. 두가지 방식을 차례로 살펴보자.
다음에 구현할 예는 Dictionary로 키,값 쌍을 가지는 형태이다. 물론 이는 테이블을 이용해서 바로 값을 입출력이 가능하지만, 여기서는 예제로서 참고하기 바란다.

함수를 이용하여 갭슐화하기

기본 생성자를 이용하여 객체를 생성하듯이 사용하는 방법이다.

dic = Dictionary()
dic:set("val1", 100)
print(dic:get("val1"))

위의 예를 보면 단순 호출하는 방식이다. Dictionary()에 의해서 객체가 생성된다. 그리고 흥미로운 부분이 멤버함수 set과 get를 호출하는 방식이다. 콜론(":")을 사용하여 함수를 호출하고 있다. 물론 콤마(".")을 이용하여 호출 할 수도 있지만 번거로울 뿐이다.
그럼 실제 루아 코드를 보자

function Dictionary(obj)
  local dictionary = obj or {}
  dictionary.values = {}

  function dictionary:set(key, value)
    self.values[key] = value
  end

  function dictionary:get(key)
    return self.values[key]
  end
  return dictionary -- 중요한 부분, 마지막에 와야 한다.
end

재미난 코드이다. Dictionary()안에서 dictionary 테이블을 생성하고 이 테이블 안에 함수를 추가한다. 그리고 이렇게 생성된 테이블 객체를 마지막에서 반환하고 있다. 내부에서 테이블에 대한 정의를 하고 있다.
앞의 예제를 실행하면 결과는 아래와 같다.

dic = Dictionary()
dic:set("val1", 100)
print(dic:get("val1"))
---
100

재미있는 부분은 함수명을 입력할 때에 콜론(":")과 함수 내에 "self"라는 키워드이다. 테이블 내에 함수 호출할 때 사용하는 콜론은 일반 마침표(".")을 사용할 수 도 있다. 그러나 콜론(":")을 사용했을 경우 내부적으로 self가는 객체가 정의되어 있다. 그리고 self객체가 호출하는 객체가 담겨져 있다. 콤마(".")을 사용한 호출은 self가 설정되지 않는다. 즉 아래는 같은 표현이다.

function dictionary:get(key)
  return self.values[key]
end

function dictionary.get(self, key)
  return self.values[key]
end 

마침표(".")를 사용한 함수 정의에서는 self가 추가되어야 하며, 테이블의 함수 호출할 때 자동적으로 self에 테이블 객체가 넘겨진다. 콜론(":")을 이용한 함수 정의는 내부적으로 self가 사용되고 감춰진 구조이다.
함수 호출할 때도 콜론(":")을 사용했을 경우는 내부적으로 자동으로 self에 객체가 넘겨지지만, 마침표(".")을 사용해서 함수를 호출할 경우는 명시적으로 별도로 self 객체를 넘겨줘야 한다.

dic = Dictionary()
dic:get("val1") -- 자동을 dic 객체가 self로 넘겨진다.
dic.get(dic, "val1")  -- 직접 함수 호출로 dic을 self 인자로 넘겨야 한다.

즉 콜론(":")으로 사용으로 많이 간편해졌다.
함수를 이용한 캡슐화의 단점이라고 하면 매 객체 생성마다 새로운 데이블과 정의를 생성한다는 부분이다. 이는 실제 내부 루아 구현이 어떻게 되어있는지 몰라서 정확한 동작을 설명할 수는 없다. 그러나 단순하게 생각하면, 매 함수 정의시 새로운 테이블 정의가 생성된다는 점이다. 그렇기게 매번 객체 생성에 리소스 오버헤드도 있지만, 생성 시간도 오래 걸리거라고 생각이 든다.
그리고 함수 정의할 때 위치가 중요하다. 즉 마지막 리턴 이전에 위치해야 한다. 그리고 들여쓰기 맞추기가 까다롭다.
이는 일반 클래스 정의하듯이 블록에 의해 묶이고 멤버 변수 초기화가 쉽게 분리되어 있어서 직관적이다. 초기화 위치도 마지막 리턴하기 전에 해도 된다. C++의 멤버 변수 같은 구조를 보인다는게 장점이다.

테이블을 이용하여 캡슐화하기

테이블을 이용한 방법이다. 전체적인 구조는 앞의 함수를 이용하는 방법과 동일하지만, 생성되는 방법과 구현이 다르다. 특히 아래와 같은 다양한 객체 생성이 가능한 구조이다.

dic1 = Dictionary()
dic2 = Dictionary:new{"v1"=200}
dic1:set("val1", 100)
print(dic1:get("val1"))
print(dic2:get("v1"))

그럼 루아에서 어떻게 구현하는지 살펴 보자.

Dictionary = {}
Dictionary.mt = { __index = Dictionary } -- 차후 생성된 객체의 메타테이블이 되어 함수 호출에 다리 역활을 함.

function Dictionary:new(o) -- 객체 생성하는 함수
    local obj = {}
    obj.dic = o or {} -- 멤버변수를 초기화
    setmetatable(obj, Dictionary.mt)
    return obj
end

Dictionary.call = { __call = Dictionary.new } -- 별도 call위한 메타 테이블이 필요하다
setmetatable(Dictionary, Dictionary.call) -- Dictionary에 call 메타테이블을 설정

function Dictionary:set(key, value)
    self.dic[key] = value
end

function Dictionary:get(key)
    return self.dic[key]
end 

먼저 Dictionary라는 테이블 객체를 생성한다. 그리고 테이블 아이템으로 mt라는 새로운 테이블을 생성한다. 이 테이블에서 멤버함수를 호출하는 역활을 담당한다. 차후에 객체 생성시 생성된 객체의 메타테이블 역활을 한다. c++로 말하자면 함수 테이블 정도라고 말할 수 있다. 이 함수 테이블을 별도로 유지하고 싶요시 가져다가 사용한다.
그리고 Dictionary:new()라는 Dictionary에 new()라는 함수를 정의한다. 재미있는 것은 바로 다음에 Dictionary 테이블에 call이라는 call용 메타테이블을 만들었다. call은 단순 테이블이지만 아래 setmetatable()에 의해 Dictionary의 메타 테이블이 된다. 이렇게 하면 Dictionary() 처럼 호출이 가능하다.
앞에서 실행 예를 수행할 수 있는 구조를 가진다.

dic1 = Dictionary()
dic2 = Dictionary:new{v1=200}
dic1:set("val1", 100)
print(dic1:get("val1"))
print(dic2:get("v1"))
--
100
200

dic1은 새로 "val1" 키를 설정하고 값을 획득한다. dic2는 이미 기본값인 "v1"가지고 생성된다. 각 객체 내에 다른 값을 가지고 있다.
이 방식에서 객체 생성은 new() 혹은 직접호출 Dictionary()에 의해서 다양하게 지원된다. 그리고 함수 정의도 별도로 되어 있어서 확장 관리가 용이하다. 즉, 다른 파일로 분리했다가 포함시킬 수도 있다는 말이다.
단, 멤버 함수를 new()에서 수행해줘야 한다. 그렇지 않으면 다른 함수에서 참조시 오류가 발생할 가능성이 크다.
약간 복잡하지만, 말들어진 메타테이블을 그대로 사용하고 있어서 확장성과 유용성 및 성능에서도 더 많은 장점을 가지고 있다.

결론

여기까지 루아에서 객체지향의 캡슐화 흉내내기를 해보았다. 이를 사용하여 좀더 모듈화되고 유지보수 편한 코드를 생성할 수 있으리라 기대가 된다. 물론 이를 더 확장시켜 제한된 상속도 가능하지만, 일단은 여기까지만 하기로 하자.
마지막으로 흥미로운 사실은 Adobe의 lightbox가 거의 40% 정도가 루아에 의해서 만들어졌다고 한다. 루아를 사용한 플러그인 개발도 가능하다.
모드 즐프~~ ospace.

참조

[1] lua manual 5.2, lua.org, http://www.lua.org/manual/5.2/

[2] Arithmetic Metamethods, lua.org, http://www.lua.org/pil/13.1.html

[3] lightbox, adobe, http://www.adobe.com/devnet/photoshoplightroom.html

[4] Lua Classes With Metatable, http://lua-users.org/wiki/LuaClassesWithMetatable

[5] Object Oriented Programming, http://lua-users.org/wiki/ObjectOrientedProgramming

반응형

'3.구현 > Lua' 카테고리의 다른 글

Lua extention (루아 확장 모듈) 만들기  (0) 2015.02.24
lua 동향(2011/10)  (0) 2011.10.26
6. Lua의 C 바인딩  (0) 2009.01.28
5. Lua 메타테이블(metatable)  (0) 2008.02.28
Lua 5.x와 Lua 4.0 호환성  (0) 2008.01.21