[LUA] pairs, ipairs의 차이

루아의 반복문

루아에서 테이블 속 내용을 돌아가며 빼와서 작업할 때는 아래와 같은 식으로 쓰게 됩니다.
-- 1.
for i, v in pairs(t) do
    -- Do something...
end

-- 2. 
for i, v in ipairs(t) do
    -- Do something...
end
간단한 작업을 할 때는 둘 중 어느 걸 사용하더라도 잘 작동할 것입니다. 하지만, 만약 특정 테이블에 대해 반복문을 실행할 때, 이터레이션을 ipairs로 돌게 된다면 심각한 로직 에러가 발생할 수 있습니다.
지금부터 이 글에서 그 원인을 설명하고, 어떤 때에 pairs를 사용해야 하며 어떤 때에 ipairs를 사용하면 좋은지 알려드리도록 하겠습니다.

루아의 테이블

반복문 얘기하다가 갑자기 테이블 얘기를 꺼낸 이유가 있습니다. pairs와 ipairs가 굳이 나누어져있는 이유를 알고자 한다면, 반드시 테이블에 대한 이해가 우선시되어야 하기 때문이죠.
루아의 테이블(table)이란 데이터 타입은, 타 언어의 array와 map 데이터 타입의 특징을 둘 다 가지는 특이한 녀석입니다. 즉 아래의 모든 것이 테이블이라는 하나의 형식으로 사용된다는 의미입니다.
local tblA = { 1, 2, 3 }
local tblB = { "A", "B", "C" }
local tblC = { nil, nil, nil }
local tblD = { tblA, tblB, tblC, { 1, 2, 3 } }
-- local tblA = { [1] = 1, [2] = 2, [3] = 3 }
-- local tblB = { [1] = "A", [2] = "B", [3] = "C" }
-- ...

local tblE = { HP = 100, MP = 100, ATK = 1.5 }
-- local tblE = { ["HP"] = 100, ["MP"] = 100, ["ATK"] = 1.5 }
local tblF = { 1, "B", nil, tblA, HP = 100, MP, 100, ["ATK"] = 1.5, OtherTable = tblE }
코드에서 주석은 주석 안 친 코드를 주석 친 코드로 써도 같은 동작을 한다는 의미입니다.
아무튼, 본론으로 돌아와 봅시다. 루아에서는 배열과 맵 타입을 따로 분리하지 않고 둘의 특징을 동시에 가지고 있는 테이블이라는 데이터 타입을 사용합니다. 그 말은 즉, 테이블에 대해 반복문으로 순회할 때, 타 언어의 Array처럼 순차 인덱스(1, 2, 3, …)로 순회할 수도 있고, 타 언어의 Map처럼 문자열 Key로 순회할 수도 있다는 뜻이 됩니다.
쓰다 보니 말이 어려워졌는데, 루아가 아닌 다른 언어를 사용해보신 경험이 있다면 도움이 될 겁니다. C#으로 예를 들어볼까요?
// Array
var arrInt = new[] { 1, 2, 3, 4 };

for (var i = 0; i < arrInt.Length; i++)
{
    var val = arrInt[i];

    Console.WriteLine($"{i}: {val}");
}

// Map
var mapStrInt = new Dictionary<string, int>()
{
    { "A", 1 },
    { "B", 2 },
    { "C", 3 },
    { "D", 4 },
};

// 방법 1. (이 방법을 보시면 루아에서 어떻게 내부적으로 처리될지 유추하기 쉽습니다.
for (var i = 0; i < mapStrInt.Keys.Count; i++)
{
    var key = mapStrInt.Keys.ToList()[i];

    var value = mapStrInt[key];

    Console.WriteLine($"{key}: {value}");
}

// 방법 2.
foreach (var keyValuePair in mapStrInt)
{
    Console.WriteLine($"{keyValuePair.Key}: {keyValuePair.Value}");
}
Array와 Map 타입이 철저하게 구분된 것이 보이시나요? 물론 최근 들어서는 dynamic과 같은 형식이 등장해 마치 루아의 테이블처럼 쓸 수도 있으나, 기본적으로 C#, Java, C++등과 같은 언어는 두 타입이 명확히 분리되어 있습니다. Array 형식의 인덱스는 정수형이고, Map 형식의 인덱스는 Key의 데이터 타입이죠. C#에서 Dictionary<dynamic, dynamic>과 같은 삽질을 하지 않는 이상, 루아의 table과 동일한 데이터 타입을 찾아보기 힘듭니다.

Iteration의 문제

자, 그럼 문제가 생깁니다. 여러분도 아시다시피 프로그램이라는 것은 보통 사람이 하기엔 너무 오래 걸리는 작업을 순식간에 뚝딱뚝딱 해주는 녀석입니다. 때문에 프로그램은 오류 없이 정확한 결과를 빠르게 도출해야 의미가 있습니다. 하지만 루아의 테이블을 보세요. 그리고 그 테이블을 iterate한다고 생각해보세요. 어떻게 하시겠습니까?
바로 위 섹션에도 설명했듯, 테이블은 Array와 Map의 특성을 모두 가집니다. 때문에, Iterate할 때 문제가 없이 돌아가려면 당연히 Array의 특성을 포함하는 Map 형식에 대해 Iterate하는 방식을 사용해야 합니다. 그렇지 않으면 Array 형식의 테이블에서는 잘 도는 반복문이 Map형식 또는 Array와 Map의 특성 모두를 가지고 있는 형식의 테이블에서는 돌아가지 않게 될 테니까요.
그럼 이제 위 예제 코드에서 C#의 Map 형식의 반복문과 Array형식의 반복문의 Iteration 부분을 보시죠. C#을 모르시는 분이라도, 대충 보면 어느 쪽이 더 시스템 자원을 많이 필요로 할 것인지 유추하기 어렵지 않을 것입니다.

pairs, ipairs가 따로 존재하는 이유

바로 이 때문에 루아에서 pairs와 ipairs가 따로 분리돼 제공되는 것입니다.
pairs()는 테이블에 대해 iterate할 때, 모든 Key/Value에 대해 빠짐없이 iterate합니다. Key의 형식이 number이든 string이든 table이든 뭐든 상관없이 모든 key/value에 대해 iterate합니다.
ipairs()는 테이블에 대해 iterate할 때, 올바른 number 형식의 Key에 대해서 iterate하며, value가 nil이라면 중지합니다. 또한 index가 정렬됩니다. 올바른 number 형식이란, lua에서 허용하는 array의 범위(1 .. #tbl) 안에 있는 정수를 말합니다. index가 정렬된다는 말은, 말 그대로 index가 1, 2, 3, 4, … 로 순차적으로 정렬된다는 의미입니다. pairs로 나온 index가 정렬 없이 본래의 key값을 가지는 것에 대비해서요.
즉 아래와 같이 됩니다.
tblArray = { "A", "B", "C", "D", "E", "F", nil, "G" }
for i, v in ipairs(tblArray) do
    print(i, v)
end
-- 1    A
-- 2    B
-- 3    C
-- 4    D
-- 5    E
-- 6    F

tblGeneric = { "A", 2, 3, "D", ABC = "E", "F", nil, "G" }
for i, v in ipairs(tblGeneric) do 
    print(i, v)
end
-- 1    A
-- 2    2
-- 3    3
-- 4    D
-- 5    F

어느 때 어떤 iteration 방식을 사용해야 하나

여기까지 읽으셨으면 감이 오셨겠죠. 루아에서 테이블을 제공한다 해서 프로그래밍 로직이 바뀌는 것은 아닙니다. 당연히 코딩을 하다 보면 어떤 변수에는 Array형식으로 짜여들어가게 될 것이고, 어떤 변수에는 Map 형식으로 짜여들어가게 될 것이며, 드문 경우 테이블의 기능을 전부 활용하게 될 것입니다. 물론, Array나 Map 둘 중 하나의 형식으로만 사용하게 되는 경우가 압도적으로 많을 것이고요.
그 사이에서, Array 형식의 변수에 반복문을 돌려 순회해야 한다면, 그 때 ipairs를 쓰시면 됩니다. key값을 문자열로 검색하는 pairs와 달리 ipairs는 키값을 정수형으로 단순 반복하기에 해당 경우에는 무조건 pairs보다 빠를 수밖에 없습니다.
변수가 Array 형식의 테이블이 아니라면 pairs를 쓰시면 됩니다. Array 형식이 아닌 테이블에 ipairs로 순회할 경우 Key가 1 이상이며 테이블의 크기보다 작은 정수가 아닌 모든 요소가 순회 대상에서 제외되므로 절대로 ipairs를 쓰시면 안 됩니다. (물론, 편법을 사용하는 경우 등, pairs를 쓰는 경우도 있으나, 그런 수준의 루아 개발자가 이 글을 읽고 있을것 같진 않습니다)

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭

테일즈위버 OST 전곡 모음