루아 레퍼런스 매뉴얼 — 이전 버전 호환성

루아 레퍼런스 매뉴얼 — 이전 버전 호환성 (Incompatibilities)

도입

루아를 5.3에서 5.4로 올리면서, 기존에 잘 돌던 코드가 갑자기 안 돌거나 결과가 달라지는 경우가 생겨요. 이번 장은 바로 그 "어디가 달라졌는지"를 정리해 주는 곳이에요. 5.4 매뉴얼의 8장 "Incompatibilities with the Previous Version"은 5.3에서 5.4로 프로그램을 옮길 때 발견할 수 있는 비호환성(불완전 호환) 문제를 언어(Language), 라이브러리(Libraries), API 세 갈래로 나눠 설명해요.

원문은 참조 매뉴얼답게 아주 간결하게 목록을 나열하는데, 여기서는 각 항목을 5.3에서 어떻게 돌았고 5.4에서 어떻게 바뀌었는지, 그리고 실제 코드는 어떻게 고치면 되는지에 초점을 맞춰 풀어볼게요. 아래 표 하나씩 보면서 내 코드에 해당하는 게 있는지 확인해 보시면 좋겠어요.

출처: Lua 5.4 Reference Manual — 8. Incompatibilities with the Previous Version

본문

본격적으로 목록을 보기 전에, 매뉴얼이 아주 중요하게 짚는 원칙이 두 가지 있어요. 이걸 먼저 알아야 아래 목록이 더 의미가 있거든요.

첫째, 바이너리는 버전 간 호환이 되지 않아요. 루아는 C API를 상수 값이나 매크로 구현 같은 형태로 언제든 바꿀 수 있어서, 소스 코드가 바뀌지 않아도 컴파일된 결과물은 서로 맞지 않을 수 있어요. 그래서 새 버전의 루아를 쓸 때는 C API 클라이언트를 반드시 다시 컴파일해야 해요.

둘째, 프리컴파일드 청크(precompiled chunk)의 내부 표현도 버전마다 바뀔 수 있어요. luac로 미리 컴파일해 둔 파일 역시 다른 버전의 루아와는 호환되지 않는다고 보면 돼요. 또 공식 배포판의 표준 경로(path)도 버전 사이에서 바뀔 수 있다는 점도 함께 기억해 두시면 좋겠어요.

그리고 매뉴얼은 호환성 옵션에 대해 한 가지 조언을 해요. 일부 비호환성은 luaconf.h 파일의 옵션(컴파일 옵션)으로 피해 갈 수 있어요. 하지만 이런 호환성 옵션은 전부 언젠가 제거될 예정이에요. 그래서 기회가 될 때마다, 모든 호환성 옵션을 꺼 놓고 컴파일한 루아 버전으로 코드를 테스트해 보는 게 다음 버전으로의 전환을 훨씬 수월하게 만들어 줘요.

그럼 이제 각 영역별로 달라진 점을 표로 정리해 볼게요.

8.1 언어(Language)에서 달라진 점

항목 5.3에서는 5.4부터는 마이그레이션 노트
문자열→숫자 강제 변환(coercion) 산술·비트 연산에서 문자열을 숫자로 자동 변환 핵심 언어에서 제거. string 라이브러리가 문자열 메타메서드로 산술 연산만 유사하게 처리(비트 연산은 아님) 문자열 속 숫자의 암묵적 타입이 보존돼요. 즉 "1" + "2"의 결과가 이제 정수(integer)지 실수(float)가 아니에요
리터럴 정수 오버플로 자리 넘침(wrap around) 10진 정수 리터럴이 오버플로하면 실수로 읽힘 옛 행동(정수로 감기)을 원하면 해당 상수를 16진수 표기로 쓰세요
__le 흉내 __lt 메타메서드로 __le를 대신 처리 __lt__le를 흉내 내는 게 제거됨 <= 비교가 필요하면 __le를 명시적으로 정의해야 해요
정수 for 루프 정수에 대한 숫자 for 루프의 의미가 일부 상세히 변경. 특히 제어 변수가 더 이상 감기지 않아요 큰 수를 세는 루프를 작성할 때 주의
goto 라벨 바깥 블록에 같은 이름 라벨이 있어도 선언 가능 같은 이름의 라벨이 보이는 곳(바깥 블록 포함)에서는 선언 불가 라벨 이름을 겹치지 않게
finalizer의 __gc 함수가 아닌 __gc 메타메서드를 무시 값이 있으면 어떤 값이든 호출됨 호출할 수 없는 값은 다른 finalizer 오류처럼 경고(warning)를 냅니다

각 항목을 좀 더 자세히 들여다볼게요.

문자열→숫자 강제 변환 제거 — 가장 영향이 큰 변경이에요. 5.3까지는 "10" * 2 같은 코드에서 문자열 "10"이 숫자로 자동 변환됐어요. 5.4에서는 핵심 언어가 이 변환을 안 해줘요. string 라이브러리가 문자열 메타메서드를 이용해 산술 연산은 비슷한 일을 해 주지만, 비트 연산은 대상이 아니에요. 게다가 새 구현은 문자열 속 숫자의 타입을 그대로 살려 주는데, 그래서 "1" + "2"의 결과는 실수가 아니라 정수예요.

print("1" + "2")   -- 5.3: 3.0 (실수)   /   5.4: 3 (정수)

리터럴 정수 오버플로 — 예전에는 10진 정수 리터럴이 범위를 넘치면 정수로 감아(wrap) 돌렸는데, 이제는 그냥 실수로 읽혀요. 같은 상수를 정수로 감아서 쓰고 싶다면 16진수 표기로 바꿔야 옛 행동을 얻을 수 있어요. (매뉴얼 원문: "Literal decimal integer constants that overflow are read as floats, instead of wrapping around. You can use hexadecimal notation for such constants if you want the old behavior.")

__le 흉내 제거 — 옛날에는 __lt가 있으면 루아가 그걸로 <=(즉 __le)까지 추론해 줬어요. 5.4부터는 그런 자동 추론이 사라졌으니, <=를 쓸 일이 있으면 __le 메타메서드를 직접 만들어 줘야 해요.

정수 for 루프 — 정수에 대한 숫자 for 루프의 의미가 세부적으로 바뀌었어요. 특히 제어변수(var)가 더 이상 감기지 않아요. 한계값을 지나칠 때 예전과 다른 흐름이 생길 수 있으니, 큰 값으로 세는 루프는 결과를 한 번 확인해 보시길 권해요.

가장 흔한 변경은 첫 번째, 문자열 자동 변환 제거예요. 기존 코드에서 문자열을 숫자처럼 쓴 곳이 있다면 이 부분이 제일 먼저 걸릴 거예요.

8.2 라이브러리(Libraries)에서 달라진 점

항목 5.3에서는 5.4부터는 마이그레이션 노트
print 인자를 tostring으로 포맷 tostring호출하지 않고 포맷 기능을 내장(hardwired) 값을 원하는 모양으로 출력하려면 __tostring 메타메서드를 쓰세요
math.random 고정 기본 시드 대략 임의의 시드로 시작 + 다른 알고리즘 시드에 의존하던 난수 시퀀스는 결과가 달라져요
utf8 디코딩 서로게이트(surrogate) 수용 여부 동작 기본적으로 서로게이트를 유효한 코드포인트로 거부 함수에 추가 파라미터를 주면 좀 더 관대하게 받아들여요
collectgarbage "setpause", "setstepmul" 옵션 두 옵션 deprecated(비권장) 새 옵션 "incremental"로 설정하세요
io.lines 한 값을 반환 네 값을 반환 다른 함수의 유일한 인자로 쓰면 문제가 될 수 있어요. 괄호로 감싸 결과 수를 하나로 조정하세요

자세히 보면 이렇습니다.

print — 이제 print가 인자를 출력하기 위해 tostring을 호출하지 않아요. 이 기능이 루아 내부에 하드와이어드돼 있죠. 그래서 값이 출력되는 방식을 바꾸고 싶다면 __tostring 메타메서드를 이용해 주세요.

math.random — 의사 난수 생성기가 이제 다소 임의의(random) 시드로 시작하고, 알고리즘 자체도 바뀌었어요. 즉 프로그램을 새로 실행할 때마다 다른 난수열이 나올 가능성이 생겼고, 이전 버전과 난수 결과는 맞지 않아요.

utf8 디코딩 — 기본 설정에서는 UTF-8 디코딩 함수들이 서로게이트를 유효한 코드포인트로 받아들이지 않게 바뀌었어요. 좀 더 관대하게 처리하고 싶다면 이 함수들에 추가된 파라미터를 쓰면 돼요.

collectgarbage"setpause""setstepmul" 옵션이 비권장(deprecated)이 됐어요. 이 둘은 새 옵션 "incremental"로 대체돼요.

io.lines — 이 함수가 값을 하나가 아니라 네 개를 반환하게 바뀌었어요. 그래서 아래처럼 load의 유일한 인자로 넘기면, load가 선택 파라미터를 가지는 탓에 결과 수가 어긋나서 문제가 생겨요.

-- 문제: io.lines가 반환하는 값이 하나가 아니라 네 개라서 어긋나요
load(io.lines(filename, "L"))

-- 해결: 괄호로 감싸면 결과 수가 하나로 조정돼요
load((io.lines(filename, "L")))

io.lines를 다른 함수의 인자로 쓰는 코드라면 괄호 처리 여부를 꼭 확인하시는 게 좋아요.

8.3 API에서 달라진 점

항목 5.3에서는 5.4부터는 마이그레이션 노트
유저데이터의 유저 밸류 고정된 유저 밸류 임의 개수의 유저 밸류 지원 lua_newuserdata / lua_setuservalue / lua_getuservaluelua_newuserdatauv / lua_setiuservalue / lua_getiuservalue(추가 인자)로 대체. 옛 이름은 유저 밸류 1개를 가정하는 매크로로 여전히 동작
lua_resume 추가 out 파라미터 없음 추가 파라미터가 yield/return된 값을 스택 최상단에서 몇 개인지 반환 (예전엔 전체 스택)
lua_version 버전 숫자의 주소를 반환 버전 숫자를 직접 반환 코어가 자기 코어의 정적 복사본을 쓰는 라이브러리와도 잘 동작하므로 주소 공간 확인이 불필요
LUA_ERRGCMM 상수 존재 제거됨 finalizer에서의 오류는 전파되지 않고 경고로 처리돼요
lua_gc 옵션 LUA_GCSETPAUSE, LUA_GCSETSTEPMUL 두 옵션 deprecated(비권장) 새 옵션 LUA_GCINC로 설정하세요

유저데이터의 유저 밸류 다중화 — 큰 변화예요. 5.4부터 full userdata가 임의 개수의 연관된 유저 밸류(user value)를 가질 수 있게 됐어요. 그래서 관련 함수들이 인자 하나가 더 늘어난 새 이름으로 대체됐어요. C API를 직접 쓰고 있다면 함수명과 인자를 함께 바꿔야 해요.

-- 5.3 (유저 밸류 1개 고정)
lua_newuserdata(L, size);

-- 5.4 (유저 밸류 개수를 인자로 지정)
lua_newuserdatauv(L, size, nuv);

여기서 재미있는 점은, 옛 이름들이 호환을 위해 유저 밸류 1개를 가정하는 매크로로 여전히 동작한다는 거예요. 다만 유저 밸류가 0개인 userdata가 메모리 관점에서 더 효율적이기 때문에, 성능이 중요하다면 새 함수에 0을 넘기는 걸 고려해 볼 만해요.

lua_resume에 추가 out 파라미터 — 코루틴이 yield하거나 반환한 값들이 이제 스택 최상단에 놓이는데, 그 "몇 개가 스택에 남았는지"를 알려 주는 out 파라미터가 새로 생겼어요. 이전 버전에서는 그 값들이 곧 스택 전체였거든요. C에서 코루틴을 직접 다루는 코드라면 이 파라미터를 이용해 결과 개수를 받아와야 해요.

lua_version 반환값 변경 — 이제 버전 숫자의 주소 대신 버전 숫자 자체를 반환해요. 루아 코어가 자기 코어의 정적 복사본을 쓰는 라이브러리와도 올바르게 동작하도록 바뀌었기 때문에, 옛날처럼 "같은 주소 공간을 쓰는지" 확인할 필요가 없어졌어요.

LUA_ERRGCMM 제거 — 이 상수가 사라졌어요. finalizer(종료자)에서 발생하는 오류는 더 이상 C API로 전파되지 않고, 대신 경고를 생성해요.

lua_gc 옵션LUA_GCSETPAUSELUA_GCSETSTEPMUL이 비권장(deprecated)이 됐어요. 가비지 컬렉션 조정은 새 옵션 LUA_GCINC로 하시면 돼요.

정리하자면, 대부분의 비호환성은 문자열 강제 변환 제거(언어), print/io.lines 동작 변경(라이브러리), 유저데이터 유저 밸류 API 교체(API)에서 나와요. 이 세 지점만 먼저 확인하고, 그다음 deprecated된 GC 옵션들을 새 옵션으로 옮겨 주면 대부분의 5.3 코드가 5.4에서 무난하게 돌아갑니다.

더 알아보기