---
title: "OpenResty가 특별한 이유는 무엇인가요? - API7.ai"
description: "OpenResty는 NGINX와 LuaJIT로 구축되어 있으며, 여기서부터 시작하여 기본 개념과 작동 방식을 배울 수 있습니다."
url: "https://api7.ai/ko/learning-center/openresty/openresty-getting-started"
---

# OpenResty가 특별한 이유는 무엇인가요?

API7.ai

October 14, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

이전 글에서 OpenResty의 두 기둥인 NGINX와 LuaJIT에 대해 배웠고, 이제 OpenResty가 제공하는 API를 배울 준비가 되셨을 거라 생각합니다.

하지만 너무 서두르지 마세요. 그 전에 OpenResty의 원리와 기본 개념을 좀 더 익히는 데 시간을 투자해야 합니다.

## 원리

![Diagram1](https://static.api7.ai/2022/10/13/634826e45b52e.png?imageMogr2/format/webp)

OpenResty의 `Master`와 `Worker` 프로세스 모두 LuaJIT VM을 포함하고 있으며, 이는 동일한 프로세스 내의 모든 코루틴에 의해 공유되며, Lua 코드가 실행되는 곳입니다.

그리고 동일한 시간대에 각 `Worker` 프로세스는 한 명의 사용자 요청만 처리할 수 있습니다. 이는 한 번에 하나의 코루틴만 실행된다는 것을 의미합니다. 여기서 의문이 들 수 있습니다: NGINX가 C10K(수만 개의 동시성)를 지원한다면, 10,000개의 요청을 동시에 처리해야 하는 것 아닌가요?

물론 아닙니다. NGINX는 `epoll`을 사용하여 이벤트를 구동하여 대기와 유휴 시간을 줄이고, 가능한 많은 CPU 자원을 사용자 요청 처리에 사용합니다. 결국 개별 요청이 충분히 빠르게 처리될 때 전체 시스템이 고성능을 달성할 수 있습니다. 만약 다중 스레드 모드를 사용하여 하나의 요청이 하나의 스레드에 대응된다면, C10K 상황에서 자원이 쉽게 고갈될 수 있습니다.

OpenResty 수준에서 Lua의 코루틴은 NGINX의 이벤트 메커니즘과 협력하여 작동합니다. Lua 코드에서 MySQL 데이터베이스 조회와 같은 I/O 작업이 발생하면, 먼저 Lua 코루틴의 `yield`를 호출하여 자신을 중단하고, NGINX에 콜백을 등록합니다. I/O 작업이 완료되면(타임아웃이나 오류일 수도 있음), NGINX 콜백 `resume`이 Lua 코루틴을 깨웁니다. 이렇게 하여 Lua 동시성과 NGINX 이벤트 드라이버 간의 협력이 완료되며, Lua 코드에서 콜백을 작성할 필요가 없습니다.

다음 다이어그램은 이 전체 과정을 설명합니다. `lua_yield`와 `lua_resume`은 모두 Lua가 제공하는 `lua_CFunction`의 일부입니다.

![Diagram2](https://static.api7.ai/2022/10/13/634828bd1e4fe.png?imageMogr2/format/webp)

반면에, Lua 코드에 I/O나 `sleep` 작업이 없다면, 예를 들어 모든 집중적인 암호화 및 복호화 작업이라면, LuaJIT VM은 Lua 코루틴에 의해 점유되어 전체 요청이 처리될 때까지 계속됩니다.

이를 더 명확히 이해하기 위해 `ngx.sleep`의 소스 코드 일부를 제공합니다. 이 코드는 `lua-nginx-module` 프로젝트의 `src` 디렉토리에 있는 `ngx_http_lua_sleep.c`에 위치합니다.

`ngx_http_lua_sleep.c`에서 `sleep` 함수의 구체적인 구현을 볼 수 있습니다. 먼저 Lua API `ngx.sleep`을 C 함수 `ngx_http_lua_ngx_sleep`에 등록해야 합니다.

```
void ngx_http_lua_inject_sleep_api(lua_State *L)
{
     lua_pushcfunction(L, ngx_http_lua_ngx_sleep);
     lua_setfield(L, -2, "sleep");
}
```

다음은 `sleep`의 주요 함수이며, 여기서는 주요 코드 몇 줄만 추출했습니다.

```
static int ngx_http_lua_ngx_sleep(lua_State *L)
{
    coctx->sleep.handler = ngx_http_lua_sleep_handler;
    ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay);
    return lua_yield(L, 0);
}
```

여기서 볼 수 있듯이:

- 먼저 콜백 함수 `ngx_http_lua_sleep_handler`를 추가합니다.
- 그런 다음 NGINX가 제공하는 인터페이스인 `ngx_add_timer`를 호출하여 NGINX의 이벤트 루프에 타이머를 추가합니다.
- 마지막으로 `lua_yield`를 사용하여 Lua 동시성을 중단하고, NGINX 이벤트 루프에 제어권을 넘깁니다.

`sleep` 작업이 완료되면 `ngx_http_lua_sleep_handler` 콜백 함수가 트리거됩니다. 이 함수는 `ngx_http_lua_sleep_resume`을 호출하고, 최종적으로 `lua_resume`을 사용하여 Lua 코루틴을 깨웁니다. 코드에서 호출의 세부 사항을 직접 확인할 수 있으므로 여기서는 자세히 설명하지 않겠습니다.

`ngx.sleep`은 가장 간단한 예제이지만, 이를 분석함으로써 `lua-nginx-module` 모듈의 기본 원리를 이해할 수 있습니다.

## 기본 개념

원리를 분석한 후, OpenResty의 두 가지 중요한 개념인 단계와 논블로킹에 대해 다시 한 번 상기해 보겠습니다.

OpenResty는 NGINX와 마찬가지로 단계 개념이 있으며, 각 단계는 고유한 역할을 합니다:

- `set_by_lua`: 변수를 설정하는 데 사용됩니다.
- `rewrite_by_lua`: 전달, 리디렉션 등에 사용됩니다.
- `access_by_lua`: 접근, 권한 등에 사용됩니다.
- `content_by_lua`: 반환 내용을 생성하는 데 사용됩니다.
- `header_filter_by_lua`: 응답 헤더 필터 처리에 사용됩니다.
- `body_filter_by_lua`: 응답 본문 필터링에 사용됩니다.
- `log_by_lua`: 로깅에 사용됩니다.

물론, 코드의 논리가 너무 복잡하지 않다면 모든 것을 `rewrite` 또는 `content` 단계에서 실행할 수도 있습니다.

하지만 OpenResty의 API는 단계 사용 제한이 있습니다. 각 API는 사용할 수 있는 단계 목록이 있으며, 범위를 벗어나 사용하면 오류가 발생합니다. 이는 다른 개발 언어와 매우 다릅니다.

예를 들어 `ngx.sleep`을 사용해 보겠습니다. 문서에서 알 수 있듯이, 이 API는 다음 컨텍스트에서만 사용할 수 있으며 `log` 단계는 포함되지 않습니다.

```
context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
```

그리고 이를 모르고 지원하지 않는 `log` 단계에서 `sleep`을 사용하면:

```
location / {
    log_by_lua_block {
        ngx.sleep(1)
     }
}
```

NGINX 오류 로그에 `error` 수준의 표시가 나타납니다.

```
[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua*
stack traceback:
    [C]: in function 'sleep'
```

따라서 API를 사용하기 전에 항상 문서를 참조하여 코드 컨텍스트에서 사용할 수 있는지 확인하세요.

단계 개념을 다시 살펴본 후, 논블로킹에 대해 다시 상기해 보겠습니다. 먼저 OpenResty가 제공하는 모든 API는 논블로킹입니다.

**1초 sleep** 요구사항을 예로 들어보겠습니다. Lua에서 이를 구현하려면 다음과 같이 해야 합니다.

```
function sleep(s)
   local ntime = os.time() + s
   repeat until os.time() > ntime
end
```

표준 Lua에는 `sleep` 함수가 없으므로 여기서는 루프를 사용하여 지정된 시간에 도달했는지 계속 확인합니다. 이 구현은 블로킹이며, `sleep`이 실행되는 동안 Lua는 아무 작업도 하지 않고, 처리해야 할 다른 요청들은 그냥 기다립니다.

하지만 `ngx.sleep(1)`로 전환하면, 위에서 분석한 소스 코드에 따르면 OpenResty는 이 1초 동안 다른 요청(예: `request B`)을 처리할 수 있습니다. 현재 요청(예: `request A`)의 컨텍스트는 저장되었다가 NGINX 이벤트 메커니즘에 의해 깨어나고, 다시 `request A`로 돌아가므로 CPU는 항상 자연스럽게 작동 상태를 유지합니다.

## 변수와 생명 주기

이 두 가지 중요한 개념 외에도, 변수의 생명 주기는 OpenResty 개발에서 쉽게 실수할 수 있는 부분입니다.

앞서 말했듯이, OpenResty에서는 모든 변수를 로컬 변수로 선언하고, `luacheck` 및 `lua-releng`과 같은 도구를 사용하여 전역 변수를 감지하는 것을 권장합니다. 이는 모듈에도 동일하게 적용됩니다. 예를 들어 다음과 같습니다.

```
local ngx_re = require "ngx.re"
```

OpenResty에서는 `init_by_lua`와 `init_worker_by_lua` 두 단계를 제외하고, 모든 단계에 대해 전역 변수의 독립적인 테이블이 설정되어 처리 중 다른 요청을 오염시키지 않습니다. 이 두 단계에서도 전역 변수를 정의하는 것을 가능한 한 피해야 합니다.

일반적으로 전역 변수로 해결하려는 문제는 모듈의 변수로 해결하는 것이 더 좋고 훨씬 명확합니다. 다음은 모듈의 변수 예제입니다.

```
local _M = {}

_M.color = {
      red = 1,
      blue = 2,
      green = 3
  }

  return _M
```

`hello.lua`라는 파일에 모듈을 정의했으며, 이 모듈에는 `color` 테이블이 포함되어 있습니다. 그리고 `nginx.conf`에 다음 구성을 추가했습니다.

```
location / {
    content_by_lua_block {
        local hello = require "hello"
        ngx.say(hello.color.green)
     }
}
```

이 구성은 `content` 단계에서 모듈을 요청하고, HTTP 응답 본문으로 `green`의 값을 출력합니다.

모듈 변수가 왜 이렇게 놀라운지 궁금할 수 있습니다.

모듈은 동일한 `Worker` 프로세스에서 한 번만 로드됩니다. 이후 해당 `Worker`가 처리하는 모든 요청은 모듈의 데이터를 공유합니다. "전역" 데이터는 모듈로 캡슐화하는 것이 적합하다고 말하는 이유는 OpenResty의 `Worker`들이 서로 완전히 격리되어 있기 때문입니다. 따라서 각 `Worker`는 모듈을 독립적으로 로드하며, 모듈의 데이터는 `Worker` 간에 공유되지 않습니다.

`Worker` 간에 공유해야 하는 데이터를 처리하는 방법은 나중 장에서 다룰 예정이므로 여기서는 깊이 파고들지 않아도 됩니다.

하지만 여기서 한 가지 실수할 수 있는 부분이 있습니다: 모듈 변수에 접근할 때는 읽기 전용으로 유지하고 수정하려고 하지 않는 것이 좋습니다. 그렇지 않으면 높은 동시성 상황에서 `race`가 발생할 수 있으며, 이는 단위 테스트로 감지할 수 없는 버그로, 가끔 온라인에서 발생하며 위치를 파악하기 어렵습니다.

예를 들어, 모듈 변수 `green`의 현재 값이 `3`이고, 코드에서 `1`을 더하는 작업을 한다면, `green`의 값은 이제 `4`일까요? 반드시 그렇지는 않습니다. `4`, `5`, 또는 `6`일 수 있습니다. 왜냐하면 OpenResty는 모듈 변수에 쓰기를 할 때 잠금을 걸지 않기 때문입니다. 따라서 여러 요청이 동시에 모듈 변수의 값을 업데이트할 수 있습니다.

전역, 로컬, 모듈 변수에 대해 말했으니, 이제 단계를 넘나드는 변수에 대해 논의해 보겠습니다.

단계를 넘나들며 읽고 쓸 수 있는 변수가 필요한 상황이 있습니다. NGINX에서 익숙한 `$host`, `$scheme` 등의 변수는 동적으로 생성할 수 없으며, 구성 파일에서 먼저 정의해야 사용할 수 있습니다. 예를 들어 다음과 같이 작성하면:

```
location /foo {
      set $my_var ; # 먼저 $my_var 변수를 생성해야 함
      content_by_lua_block {
          ngx.var.my_var = 123
      }
  }
```

OpenResty는 이러한 문제를 해결하기 위해 `ngx.ctx`를 제공합니다. 이는 현재 요청과 동일한 생명 주기를 가진 Lua 테이블로, 요청 기반의 Lua 데이터를 저장하는 데 사용할 수 있습니다. 공식 문서의 예제를 살펴보겠습니다.

```
location /test {
      rewrite_by_lua_block {
          ngx.ctx.foo = 76
      }
      access_by_lua_block {
          ngx.ctx.foo = ngx.ctx.foo + 3
      }
      content_by_lua_block {
          ngx.say(ngx.ctx.foo)
      }
  }
```

여기서 `ngx.ctx`에 저장된 변수 `foo`를 정의했습니다. 이 변수는 `rewrite`, `access`, `content` 단계를 넘나들며, 최종적으로 `content` 단계에서 값을 출력하며, 예상대로 `79`가 출력됩니다.

물론 `ngx.ctx`에도 제한이 있습니다.

예를 들어, `ngx.location.capture`로 생성된 자식 요청은 부모 요청의 `ngx.ctx`와 독립적인 별도의 `ngx.ctx` 데이터를 가집니다.

또한 `ngx.exec`로 생성된 내부 리디렉션은 원래 요청의 `ngx.ctx`를 파괴하고, 빈 `ngx.ctx`로 다시 생성합니다.

이 두 가지 제한 사항은 공식 문서에 [상세한 코드 예제](https://github.com/openresty/lua-nginx-module#ngxctx)가 있으므로 관심이 있다면 직접 확인할 수 있습니다.

## 요약

마지막으로 몇 마디 더 하겠습니다. 우리는 OpenResty의 원리와 몇 가지 중요한 개념을 배우고 있지만, 이를 외울 필요는 없습니다. 결국 이들은 실제 요구사항과 코드와 결합할 때 의미를 갖고 생생해집니다.

여러분은 어떻게 이해하셨나요? 댓글을 남겨 저와 토론해 보시고, 이 글을 동료와 친구들과 공유해 보세요. 함께 소통하며 함께 성장합시다.

Share article link

Share

Related Posts

[![Lua 규칙과 NGINX 설정 간에 충돌이 발생하면 어떻게 될까요?](https://static.api7.ai/2022/10/14/634974e117ebd.png)](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

## [Lua 규칙과 NGINX 설정 간에 충돌이 발생하면 어떻게 될까요?](https://api7.ai/learning-center/openresty/conflict-between-lua-rules-and-nginx)

[![최고의 팁: Lua에서 독창적인 개념과 함정 식별하기](https://static.api7.ai/2022/10/14/634974e2dd70a.png)](https://api7.ai/learning-center/openresty/lua-basic-concepts-and-pitfalls)

## [최고의 팁: Lua에서 독창적인 개념과 함정 식별하기](https://api7.ai/learning-center/openresty/lua-basic-concepts-and-pitfalls)

Table of Contents

- [원리](#%EC%9B%90%EB%A6%AC)
- [기본 개념](#%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90)
- [변수와 생명 주기](#%EB%B3%80%EC%88%98%EC%99%80-%EC%83%9D%EB%AA%85-%EC%A3%BC%EA%B8%B0)
- [요약](#%EC%9A%94%EC%95%BD)
