루아 개발 프로젝트의 품질을 보장하고 자동화된 배포 환경을 구축하기 위한 단위 테스트, 통합 테스트, CI/CD 파이프라인 구축 방법을 상세히 알아보겠습니다.
루아 테스팅 개요
루아 프로젝트의 안정성과 품질을 보장하기 위해서는 체계적인 테스팅 전략이 필수입니다.
테스팅은 단순히 버그를 찾는 것을 넘어서 코드의 신뢰성을 높이고 리팩토링 시 안전망 역할을 합니다.
루아는 동적 타입 언어이기 때문에 런타임 에러를 방지하기 위한 테스팅이 더욱 중요합니다.
현대적인 루아 개발에서는 다양한 테스팅 프레임워크와 도구들이 활용되고 있습니다.
루아 테스팅 프레임워크 소개
Busted - 강력한 BDD 스타일 테스팅 프레임워크
Busted는 루아 커뮤니티에서 가장 인기 있는 테스팅 프레임워크입니다.
BDD(Behavior-Driven Development) 스타일의 테스트 작성을 지원하며, 직관적인 문법을 제공합니다.
-- calculator.lua
local Calculator = {}
function Calculator:new()
local instance = {}
setmetatable(instance, self)
self.__index = self
return instance
end
function Calculator:add(a, b)
return a + b
end
function Calculator:divide(a, b)
if b == 0 then
error("Division by zero")
end
return a / b
end
return Calculator
-- spec/calculator_spec.lua
local Calculator = require("calculator")
describe("Calculator", function()
local calc
before_each(function()
calc = Calculator:new()
end)
describe("addition", function()
it("should add two positive numbers", function()
assert.are.equal(5, calc:add(2, 3))
end)
it("should handle negative numbers", function()
assert.are.equal(-1, calc:add(-3, 2))
end)
end)
describe("division", function()
it("should divide two numbers", function()
assert.are.equal(2, calc:divide(10, 5))
end)
it("should throw error on division by zero", function()
assert.has_error(function()
calc:divide(10, 0)
end, "Division by zero")
end)
end)
end)
LuaUnit - 경량 xUnit 스타일 프레임워크
LuaUnit은 전통적인 xUnit 스타일의 테스트 작성을 지원합니다.
단일 파일로 구성되어 있어 의존성이 적고 설치가 간단합니다.
-- test_calculator.lua
local lu = require('luaunit')
local Calculator = require('calculator')
TestCalculator = {}
function TestCalculator:setUp()
self.calc = Calculator:new()
end
function TestCalculator:testAdd()
lu.assertEquals(self.calc:add(2, 3), 5)
lu.assertEquals(self.calc:add(-1, 1), 0)
end
function TestCalculator:testDivide()
lu.assertEquals(self.calc:divide(10, 2), 5)
lu.assertError(function() self.calc:divide(10, 0) end)
end
os.exit(lu.LuaUnit.run())
단위 테스트 작성 방법
테스트 구조화와 명명 규칙
단위 테스트는 AAA 패턴(Arrange-Act-Assert)을 따라 구조화합니다.
describe("UserService", function()
describe("when creating a new user", function()
it("should generate unique user ID", function()
-- Arrange
local userService = UserService:new()
local userData = {name = "John", email = "john@example.com"}
-- Act
local user = userService:createUser(userData)
-- Assert
assert.is_not_nil(user.id)
assert.is_string(user.id)
assert.are.equal(user.name, "John")
end)
end)
end)
모킹과 스텁 활용
외부 의존성을 가진 코드를 테스트할 때는 모킹을 활용합니다.
-- 데이터베이스 모킹 예제
local mock = require("luassert.mock")
describe("UserRepository", function()
local db_mock
local userRepo
before_each(function()
db_mock = mock({
query = function() end,
execute = function() end
})
userRepo = UserRepository:new(db_mock)
end)
after_each(function()
mock.revert(db_mock)
end)
it("should save user to database", function()
local user = {name = "Alice", email = "alice@example.com"}
userRepo:save(user)
assert.spy(db_mock.execute).was_called_with(
match.is_string(),
match.is_table()
)
end)
end)
통합 테스트 구현
API 엔드포인트 테스트
웹 애플리케이션의 통합 테스트는 실제 HTTP 요청을 시뮬레이션합니다.
-- OpenResty/Kong 환경에서의 API 테스트
local http = require("resty.http")
local cjson = require("cjson")
describe("User API", function()
local httpc
local base_url = "http://localhost:8080"
before_each(function()
httpc = http.new()
end)
after_each(function()
httpc:close()
end)
describe("POST /api/users", function()
it("should create a new user", function()
local user_data = {
name = "Test User",
email = "test@example.com"
}
local res, err = httpc:request_uri(base_url .. "/api/users", {
method = "POST",
body = cjson.encode(user_data),
headers = {
["Content-Type"] = "application/json"
}
})
assert.is_nil(err)
assert.are.equal(201, res.status)
local response_data = cjson.decode(res.body)
assert.is_not_nil(response_data.id)
assert.are.equal(user_data.name, response_data.name)
end)
end)
end)
데이터베이스 통합 테스트
실제 데이터베이스와의 상호작용을 테스트합니다.
-- PostgreSQL 통합 테스트 예제
local pgmoon = require("pgmoon")
describe("Database Integration", function()
local db
before_each(function()
db = pgmoon.new({
host = "localhost",
port = "5432",
database = "test_db",
user = "test_user",
password = "test_pass"
})
assert(db:connect())
-- 테스트 데이터 초기화
db:query("TRUNCATE users CASCADE")
end)
after_each(function()
db:disconnect()
end)
it("should insert and retrieve user", function()
local insert_sql = [[
INSERT INTO users (name, email)
VALUES ('John Doe', 'john@example.com')
RETURNING id
]]
local result = db:query(insert_sql)
assert.is_not_nil(result[1].id)
local select_sql = "SELECT * FROM users WHERE id = " .. result[1].id
local user = db:query(select_sql)
assert.are.equal('John Doe', user[1].name)
assert.are.equal('john@example.com', user[1].email)
end)
end)
테스트 코드 최적화
테스트 데이터 관리
효율적인 테스트를 위해 테스트 데이터를 체계적으로 관리합니다.
-- test_data.lua
local TestData = {}
TestData.users = {
valid_user = {
name = "John Doe",
email = "john@example.com",
age = 30
},
invalid_user = {
name = "",
email = "invalid-email",
age = -5
}
}
TestData.products = {
laptop = {
name = "MacBook Pro",
price = 2000,
category = "Electronics"
},
book = {
name = "Lua Programming",
price = 50,
category = "Books"
}
}
return TestData
헬퍼 함수 활용
반복적인 테스트 코드를 줄이기 위해 헬퍼 함수를 작성합니다.
-- test_helpers.lua
local TestHelpers = {}
function TestHelpers.create_test_user(overrides)
local default_user = {
name = "Test User",
email = "test@example.com",
created_at = os.time()
}
if overrides then
for k, v in pairs(overrides) do
default_user[k] = v
end
end
return default_user
end
function TestHelpers.assert_user_fields(user)
assert.is_string(user.name)
assert.is_string(user.email)
assert.is_number(user.created_at)
assert.matches("@", user.email)
end
return TestHelpers
CI/CD 파이프라인 구축
GitHub Actions를 활용한 CI/CD
GitHub Actions를 사용하여 루아 프로젝트의 CI/CD 파이프라인을 구축합니다.
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
lua-version: [5.1, 5.2, 5.3, 5.4, luajit]
steps:
- uses: actions/checkout@v3
- name: Setup Lua
uses: leafo/gh-actions-lua@v9
with:
luaVersion: ${{ matrix.lua-version }}
- name: Setup LuaRocks
uses: leafo/gh-actions-luarocks@v4
- name: Install dependencies
run: |
luarocks install busted
luarocks install luacov
luarocks install --only-deps rockspec/*.rockspec
- name: Run tests
run: |
busted --coverage
luacov -r lcov
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./luacov.report.out
flags: unittests
name: codecov-umbrella
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: |
echo "Deploying to production..."
# 실제 배포 스크립트 실행
Docker를 활용한 컨테이너화
루아 애플리케이션을 Docker 컨테이너로 패키징합니다.
# Dockerfile
FROM openresty/openresty:alpine
# 의존성 설치
RUN apk add --no-cache git make gcc musl-dev
# LuaRocks 설치
RUN /usr/local/openresty/luajit/bin/luarocks install busted
RUN /usr/local/openresty/luajit/bin/luarocks install luacov
# 애플리케이션 코드 복사
COPY . /app
WORKDIR /app
# 의존성 설치
RUN /usr/local/openresty/luajit/bin/luarocks install --only-deps rockspec/*.rockspec
# 테스트 실행
RUN /usr/local/openresty/luajit/bin/busted
# 애플리케이션 실행
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
자동화 배포 전략
블루-그린 배포
무중단 배포를 위한 블루-그린 배포 전략을 구현합니다.
┌─────────────────────────────────────────────────────────────────┐
│ Load Balancer │
│ (nginx/HAProxy) │
└─────────────────────┬───────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ Traffic Switching │
│ (currently pointing to Blue) │
└─────────────────┬─────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Blue Environment│ │ Green Environment│
│ (Production) │ │ (Staging) │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ App v1.0 │ │ │ │ App v1.1 │ │
│ │ (Active) │ │ │ │ (Deploying) │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ │ │ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Database │ │ │ │ Database │ │
│ │ Connection │ │ │ │ Connection │ │
│ └─────────────┘ │ │ └─────────────┘ │
└─────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Health Check │ │ Health Check │
│ Status: OK │ │ Status: Testing │
└─────────────────┘ └─────────────────┘
배포 프로세스:
1. Green 환경에 새 버전 배포
2. Green 환경 헬스 체크 수행
3. 문제없으면 트래픽을 Green으로 전환
4. Blue 환경은 롤백용으로 유지
-- deployment_manager.lua
local DeploymentManager = {}
function DeploymentManager:new(config)
local instance = {
config = config,
current_env = "blue",
nginx_config_path = "/etc/nginx/conf.d/app.conf"
}
setmetatable(instance, self)
self.__index = self
return instance
end
function DeploymentManager:deploy(version)
local target_env = self.current_env == "blue" and "green" or "blue"
-- 새 버전을 타겟 환경에 배포
self:deploy_to_environment(target_env, version)
-- 헬스 체크 수행
if self:health_check(target_env) then
-- 트래픽 스위칭
self:switch_traffic(target_env)
self.current_env = target_env
return true
else
error("Health check failed for " .. target_env)
end
end
function DeploymentManager:health_check(env)
local http = require("resty.http")
local httpc = http.new()
local url = string.format("http://%s-app:8080/health", env)
local res, err = httpc:request_uri(url, {
method = "GET",
timeout = 5000
})
return res and res.status == 200
end
return DeploymentManager
롤백 메커니즘
배포 실패 시 이전 버전으로 롤백하는 메커니즘을 구현합니다.
-- rollback_manager.lua
local RollbackManager = {}
function RollbackManager:new(deployment_manager)
local instance = {
deployment_manager = deployment_manager,
version_history = {}
}
setmetatable(instance, self)
self.__index = self
return instance
end
function RollbackManager:record_deployment(version, timestamp)
table.insert(self.version_history, {
version = version,
timestamp = timestamp,
environment = self.deployment_manager.current_env
})
end
function RollbackManager:rollback_to_previous()
if #self.version_history < 2 then
error("No previous version to rollback to")
end
local previous_version = self.version_history[#self.version_history - 1]
self.deployment_manager:deploy(previous_version.version)
-- 롤백 후 현재 버전 제거
table.remove(self.version_history)
end
return RollbackManager
테스팅 도구 비교
다양한 루아 테스팅 도구들의 특징을 비교해보겠습니다.
도구 | 스타일 | 장점 | 단점 | 사용 케이스 |
---|---|---|---|---|
Busted | BDD | 풍부한 기능, 모킹 지원 | 의존성 많음 | 복잡한 프로젝트 |
LuaUnit | xUnit | 경량, 단순함 | 기능 제한적 | 소규모 프로젝트 |
Telescope | TDD | 직관적 문법 | 개발 중단 | 레거시 프로젝트 |
Shake | 커스텀 | 유연성 높음 | 학습 곡선 | 특수 요구사항 |
성능 테스트와 부하 테스트
LuaJIT 성능 테스트
루아 코드의 성능을 측정하고 최적화합니다.
-- performance_test.lua
local function benchmark(func, iterations)
local start_time = os.clock()
for i = 1, iterations do
func()
end
local end_time = os.clock()
return end_time - start_time
end
describe("Performance Tests", function()
it("should perform string concatenation efficiently", function()
local test_func = function()
local result = ""
for i = 1, 1000 do
result = result .. "test"
end
return result
end
local time_taken = benchmark(test_func, 100)
assert.is_true(time_taken < 1.0) -- 1초 이내
end)
it("should handle table operations efficiently", function()
local test_func = function()
local t = {}
for i = 1, 10000 do
t[i] = i * 2
end
return t
end
local time_taken = benchmark(test_func, 10)
assert.is_true(time_taken < 0.5) -- 0.5초 이내
end)
end)
부하 테스트 도구 활용
웹 애플리케이션의 부하 테스트를 위해 wrk나 Apache Bench를 활용합니다.
# wrk를 사용한 부하 테스트
wrk -t12 -c400 -d30s --script=load_test.lua http://localhost:8080/api/users
# Apache Bench를 사용한 부하 테스트
ab -n 10000 -c 100 http://localhost:8080/api/users
모니터링과 로깅
애플리케이션 모니터링
프로덕션 환경에서의 애플리케이션 상태를 모니터링합니다.
-- monitoring.lua
local Monitoring = {}
function Monitoring:new(config)
local instance = {
config = config,
metrics = {
request_count = 0,
error_count = 0,
response_times = {}
}
}
setmetatable(instance, self)
self.__index = self
return instance
end
function Monitoring:record_request(response_time, status_code)
self.metrics.request_count = self.metrics.request_count + 1
table.insert(self.metrics.response_times, response_time)
if status_code >= 400 then
self.metrics.error_count = self.metrics.error_count + 1
end
end
function Monitoring:get_health_status()
local avg_response_time = self:calculate_average_response_time()
local error_rate = self.metrics.error_count / self.metrics.request_count
return {
status = error_rate < 0.05 and "healthy" or "unhealthy",
request_count = self.metrics.request_count,
error_rate = error_rate,
avg_response_time = avg_response_time
}
end
return Monitoring
구조화된 로깅
효율적인 디버깅을 위해 구조화된 로깅을 구현합니다.
-- logger.lua
local cjson = require("cjson")
local Logger = {}
function Logger:new(level)
local instance = {
level = level or "INFO",
levels = {
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4
}
}
setmetatable(instance, self)
self.__index = self
return instance
end
function Logger:log(level, message, context)
if self.levels[level] >= self.levels[self.level] then
local log_entry = {
timestamp = os.date("%Y-%m-%dT%H:%M:%S"),
level = level,
message = message,
context = context or {}
}
print(cjson.encode(log_entry))
end
end
function Logger:info(message, context)
self:log("INFO", message, context)
end
function Logger:error(message, context)
self:log("ERROR", message, context)
end
return Logger
보안 테스트
입력 검증 테스트
악의적인 입력에 대한 보안 테스트를 수행합니다.
-- security_test.lua
describe("Security Tests", function()
local userService = UserService:new()
describe("Input Validation", function()
it("should prevent SQL injection", function()
local malicious_input = "'; DROP TABLE users; --"
assert.has_error(function()
userService:findUserByName(malicious_input)
end)
end)
it("should prevent XSS attacks", function()
local xss_payload = "<script>alert('XSS')</script>"
local user = userService:createUser({
name = xss_payload,
email = "test@example.com"
})
assert.is_false(string.find(user.name, "<script>"))
end)
end)
end)
테스트 커버리지 분석
코드 커버리지 측정
luacov를 사용하여 테스트 커버리지를 측정합니다.
-- .luacov 설정 파일
return {
statsfile = "luacov.stats.out",
reportfile = "luacov.report.out",
include = {
"src/",
"lib/"
},
exclude = {
"spec/",
"test/"
}
}
커버리지 리포트를 통해 테스트되지 않은 코드를 식별하고 개선합니다.
지속적인 개선
테스트 리팩토링
테스트 코드도 지속적으로 리팩토링하여 유지보수성을 높입니다.
-- 리팩토링 전
describe("User Management", function()
it("should create user with valid data", function()
local user = UserService:createUser({
name = "John",
email = "john@example.com",
age = 30
})
assert.is_not_nil(user.id)
assert.are.equal(user.name, "John")
assert.are.equal(user.email, "john@example.com")
assert.are.equal(user.age, 30)
end)
end)
-- 리팩토링 후
describe("User Management", function()
local function create_valid_user(overrides)
local default_data = {
name = "John",
email = "john@example.com",
age = 30
}
return UserService:createUser(merge_tables(default_data, overrides or {}))
end
it("should create user with valid data", function()
local user = create_valid_user()
assert_valid_user(user, {name = "John", email = "john@example.com", age = 30})
end)
end)
결론
루아 프로젝트의 테스팅과 CI/CD 구축은 고품질 소프트웨어 개발의 핵심입니다.
Busted나 LuaUnit과 같은 테스팅 프레임워크를 활용하여 단위 테스트와 통합 테스트를 체계적으로 구현할 수 있습니다.
GitHub Actions와 Docker를 활용한 CI/CD 파이프라인은 자동화된 배포와 품질 관리를 가능하게 합니다.
지속적인 테스트와 모니터링을 통해 안정적이고 신뢰할 수 있는 루아 애플리케이션을 구축할 수 있습니다.
다음 시리즈에서는 루아의 고급 메타프로그래밍 기법에 대해 알아보겠습니다.
참고 자료:
- Busted 테스팅 프레임워크 공식 문서
- LuaUnit 테스팅 도구 문서
- GitHub Actions 워크플로우 문서
- Docker 컨테이너화 가이드
- 루아 입문 시리즈 #14: 루아 성능 최적화와 프로파일링
루아 입문 시리즈 #14: 루아 성능 최적화와 프로파일링
루아 애플리케이션의 성능을 극대화하기 위한 메모리 관리, JIT 컴파일 활용, 병목 지점 분석 등 실전 최적화 기법을 단계별로 학습하고 적용해보세요.들어가며루아(Lua)는 가벼우면서도 강력한
notavoid.tistory.com
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
루아 입문 시리즈 #17: 분산 시스템에서의 루아 (0) | 2025.07.04 |
---|---|
루아 입문 시리즈 #16: 루아 메타프로그래밍 (0) | 2025.07.04 |
루아 입문 시리즈 #14: 루아 성능 최적화와 프로파일링 (0) | 2025.07.03 |
루아 입문 시리즈 #13: Wireshark 루아 플러그인 개발 (0) | 2025.07.03 |
루아 입문 시리즈 #12: Kong API Gateway 개발 (0) | 2025.07.01 |