namalzi

← 글 목록

Apache httpd 소스 파헤치기 7: 설정 파일과 디렉티브 — httpd.conf 한 줄이 C 구조체가 되기까지

Apache httpd · 게시

httpd.conf 에 적은 한 줄이 어떻게 모듈의 C 함수를 부르고 구조체에 값을 넣는지 server/config.c 를 따라갑니다. 파일을 트리로 읽는 단계와 그 트리를 걷는 단계, 이름으로 디렉티브를 찾는 해시, TAKE1·FLAG·ITERATE 같은 인자 방식, "not allowed here" 가 나오는 비트 검사와 AllowOverride, 그리고 Directory·Location·VirtualHost 섹션의 값이 언제 병합되는지를 직접 만든 디렉티브로 확인합니다.

이 글에서 다루는 것

"Apache httpd 소스 파헤치기" 연재의 일곱 번째 글입니다. 6편에서는 모듈 하나를 나타내는 module 구조체와, 모듈이 훅으로 서버의 여러 시점에 끼어드는 방법을 봤습니다. 그 구조체에는 훅을 등록하는 함수 말고도 설정에 쓰는 칸이 다섯 개 있었습니다. 설정 구조체를 만드는 함수 둘, 합치는 함수 둘, 그리고 디렉티브 표입니다(include/http_config.h 384–412행). 이번 글은 그 다섯 칸의 이야기입니다.

답하려는 질문은 세 가지입니다.

항목 내용
대상 Apache httpd
버전 2.5.1-dev (trunk)
커밋 e4af30fe6d (2026-10-24)
주로 읽는 파일 server/config.c, server/util_cfgtree.c, include/http_config.h, server/core.c, server/request.c(일부)

글에 나오는 줄 번호는 모두 이 커밋 기준입니다. 같은 커밋의 소스를 받고 예제 컨테이너를 만드는 방법은 1편에 있습니다.

server/config.c만 2,564줄이고 섹션을 처리하는 server/core.c는 6,225줄입니다. 이 글은 httpd 소스를 처음 여는 분이 따라올 수 있는 큰 줄기만 골랐습니다. 뺀 것은 글 끝의 "이 글에서 뺀 것"에 이름과 위치만 적어 두었습니다.

먼저 알아 둘 말

디렉티브(directive)는 설정 파일 한 줄의 맨 앞 낱말입니다. DocumentRoot /var/www에서 DocumentRoot가 디렉티브이고 /var/www가 그 인자(argument)입니다. httpd 코어에는 디렉티브가 거의 없고, 대부분은 모듈이 "나는 이런 이름의 디렉티브를 처리한다"고 등록해 둔 것입니다.

섹션(section, 소스에서는 container라고도 부릅니다)은 <Directory /var/www> … </Directory>처럼 꺾쇠로 열고 닫는 구역입니다. 안에 적은 디렉티브는 그 구역에만 적용됩니다.

서버 설정과 디렉터리 설정. 모듈은 자기 설정 값을 담을 구조체를 직접 정의합니다. 이 구조체에는 두 종류가 있습니다. 서버(주 서버와 <VirtualHost> 하나하나)마다 하나씩 생기는 서버 설정(per-server config), 그리고 <Directory>·<Location>·.htaccess 같은 구역마다 하나씩 생기는 디렉터리 설정(per-directory config)입니다. module 구조체의 create_server_config, create_dir_config가 그 구조체를 만드는 함수이고, merge_server_config, merge_dir_config가 둘을 합치는 함수입니다(include/http_config.h 384–408행). 넷 다 함수 포인터, 곧 "나중에 부를 함수의 주소를 담는 칸"입니다. C의 함수 포인터는 2편에서 설명했습니다.

설정 벡터(config vector)는 모듈마다 칸이 하나씩 있는 void * 배열입니다. httpd 코어는 각 모듈의 설정 구조체가 어떻게 생겼는지 모르기 때문에 "무엇이든 가리킬 수 있는 포인터"인 void *로만 들고 있고, 모듈은 자기 번호(module_index)의 칸에서 자기 구조체를 꺼냅니다. 꺼내는 매크로가 ap_get_module_config()입니다(include/http_config.h 553–554행). 이 설계는 2편에서, 이 벡터를 들고 있는 server_rec·request_rec은 3편에서 봤습니다.

병합(merge)은 위 구역의 설정과 아래 구역의 설정을 합쳐 새 설정을 만드는 일입니다. 무엇이 이기고 무엇이 합쳐지는지는 각 모듈의 병합 함수가 정합니다. 코어는 병합 함수를 부를 뿐입니다.

풀(pool)은 수명이 같은 메모리를 한데 묶어 한꺼번에 버리는 APR의 메모리 관리 방식입니다. 이 글에는 설정이 사는 pconf(설정 풀)와 요청 하나 동안만 사는 r->pool(요청 풀)이 나옵니다. 자세한 것은 4편에 있습니다.

1. 한 줄이 지나가는 길 — 읽기와 걷기

httpd.conf 한 줄은 두 번에 나눠 처리됩니다. 먼저 파일 전체를 읽어 트리(디렉티브 트리)로 만들고, 그다음 그 트리를 처음부터 걸으면서(walk) 디렉티브 하나하나를 실행합니다. 이 둘 사이에 pre_config 훅이 있습니다.

설정 한 줄이 지나가는 길. httpd.conf 텍스트를 ap_build_config가 한 줄씩 읽어 ap_directive_t 트리로 만들고, 이때 LoadModule·Include·IfModule·Define 같은 EXEC_ON_READ 디렉티브만 즉시 실행한다. pre_config 훅 뒤에 ap_process_config_tree와 ap_walk_config가 트리를 걸으며 이름으로 command_rec을 찾고 invoke_cmd가 자리 검사, 인자 나누기를 거쳐 모듈 함수를 부른다. 값은 모듈의 설정 구조체에 들어가고, ap_fixup_virtual_hosts가 가상 호스트에 주 서버 설정을 병합한다. 디렉터리 설정의 병합은 요청마다 일어난다. 파란색은 절차, 노란색은 값이 쌓이는 곳, 초록색은 요청이 올 때마다 일어나는 일입니다.

소스에서 이 순서는 main()에 그대로 보입니다. 5편에서 본 자리입니다.

  1. 읽기 — ap_read_config()(server/main.c 721행)가 ap_process_resource_config()를 거쳐 ap_build_config()를 부릅니다(server/config.c 1849–1894행, 1389–1465행). 결과는 전역 변수 ap_conftree에 걸린 트리입니다.
  2. pre_config 훅 — 742행.
  3. 걷기 — ap_process_config_tree()(748행)가 ap_walk_config()로 트리를 걷습니다(server/config.c 1961–1989행).
  4. 가상 호스트 마무리 — 걷기가 성공하면 곧바로 ap_fixup_virtual_hosts()가 불립니다(751행). 7절에서 봅니다.

왜 한 번에 실행하지 않고 두 단계로 나눴나

읽는 단계에서도 즉시 실행되는 디렉티브가 있습니다. 디렉티브 표에 EXEC_ON_READ 표시가 붙은 것들입니다. LoadModule(modules/core/mod_so.c 449행), Include, <IfModule>, <IfDefine>, Define, ServerRoot(server/core.c 4747–4781행, 4848행, 4873–4878행)가 그렇습니다. 모두 "설정 파일의 모양 자체를 바꾸는" 것입니다. 다른 파일을 끼워 넣거나, 조건에 따라 몇 줄을 빼거나, 모듈을 실어서 새 디렉티브 이름을 쓸 수 있게 만듭니다.

이것들을 읽기 단계에서 끝내 두면, 걷기를 시작하기 전에 실릴 모듈이 모두 정해집니다. config.c에는 이 사실에 기대는 코드가 있습니다. pre_config 훅 중 가장 마지막(APR_HOOK_REALLY_LAST)에 걸린 함수가 설정 벡터의 길이를 실제 모듈 수에 맞게 줄이는데, 그 주석이 "EXEC_ON_READ 디렉티브가 싣는 모듈은 이제 다 실렸다"로 시작합니다(server/config.c 2276–2299행). 벡터의 칸 수가 정해진 다음에야 그 벡터에 값을 채우는 걷기가 시작되는 순서입니다.

헷갈리기 쉬운 곳: 거짓인 <IfModule> 안의 오타는 아무도 모릅니다

<IfModule>의 조건이 거짓이면 start_cond_section()은 닫는 태그까지 줄을 읽어서 버립니다(server/core.c 2942–2981행). 버리는 함수 ap_soak_end_container()는 열고 닫는 태그의 짝만 맞춰 보고 디렉티브 이름은 보지 않습니다(server/config.c 1631–1680행). 그 줄들은 트리에 들어가지 않으니 걷기 단계의 이름 검사도 받지 않습니다. 아래 5절 예제의 마지막 항목에서 GreetMesage라는 오타를 없는 모듈의 <IfModule> 안에 넣었더니 httpd -t가 Syntax OK를 냈습니다. 모듈을 나중에 실으면 그제야 오류가 드러납니다.

2. 트리 — ap_directive_t

읽기 단계가 만드는 트리의 노드는 이렇게 생겼습니다.

struct ap_directive_t {
    /** The current directive */
    const char *directive;
    /** The arguments for the current directive, stored as a space
     *  separated list */
    const char *args;
    /** The next directive node in the tree */
    struct ap_directive_t *next;
    /** The first child node of this directive */
    struct ap_directive_t *first_child;
    /** The parent node of this directive */
    struct ap_directive_t *parent;

    /* ... */

    /* ### these may go away in the future, but are needed for now */
    /** The name of the file this directive was found in */
    const char *filename;
    /** The line number the directive was on */
    int line_num;

    /* ... */
};

include/util_cfgtree.h 46–74행

노드 하나가 설정 파일의 한 줄입니다. 디렉티브 이름(directive)과 아직 나누지 않은 인자 문자열(args), 그 줄이 있던 파일 이름과 줄 번호가 들어 있습니다. 나중에 오류 메시지의 Syntax error on line 4 of ...가 이 두 칸에서 나옵니다.

포인터 셋으로 트리를 이룹니다. 같은 높이의 다음 줄은 next, 섹션 안의 첫 줄은 first_child, 거꾸로 올라가는 길은 parent입니다. 이런 설정이라면

ServerName localhost
<Location /a>
    GreetMessage hello-a
    GreetWords a1 a2
</Location>
<VirtualHost *:8081>
    ServerName one.local
    <Location /b>
        GreetShout On
    </Location>
</VirtualHost>

트리는 이렇습니다.

위 설정의 디렉티브 트리. 최상위에 ServerName, <Location, <VirtualHost 세 노드가 next로 이어진다. <Location 노드의 first_child는 GreetMessage이고 그 next가 GreetWords다. <VirtualHost의 first_child는 ServerName이고 그 next가 <Location이며, 그 first_child가 GreetShout이다. 닫는 태그는 노드가 되지 않는다. 파란 노드가 최상위 줄, 회색 노드가 섹션 안의 줄입니다. 닫는 태그(</Location>)는 노드가 되지 않습니다.

한 줄을 노드로 만드는 곳은 ap_build_config_sub()입니다.

    cmd_name = ap_getword_conf(temp_pool, &args);
    /* ... */

    newdir = apr_pcalloc(p, sizeof(ap_directive_t));
    newdir->filename = parms->config_file->name;
    newdir->line_num = parms->config_file->line_number;
    newdir->args = apr_pstrdup(p, args);

    lname = apr_pstrdup(temp_pool, cmd_name);
    ap_str_tolower(lname);
    ml = apr_hash_get(ap_config_hash, lname, APR_HASH_KEY_STRING);

    if (ml && (cmd = ml->cmd) != NULL) {
        newdir->directive = cmd->name;
        if (cmd->req_override & EXEC_ON_READ) {
            /* ... */
            return retval;
        }
    }
    else {
        /* No known directive found?  Make a copy of what we have parsed. */
        newdir->directive = apr_pstrdup(p, cmd_name);
    }


    if (cmd_name[0] == '<') {
        if (cmd_name[1] != '/') {
            (*current) = ap_add_node(curr_parent, *current, newdir, 1);
        }
        /* ... */
    }
    else {
        *current = ap_add_node(curr_parent, *current, newdir, 0);
    }

server/config.c 1139–1245행

  1. 첫 낱말을 떼어 디렉티브 이름으로 삼습니다. 나머지는 통째로 args에 들어갑니다.
  2. 이름을 소문자로 바꿔 해시에서 찾아봅니다(3절). 알려진 이름이면 노드에 모듈이 등록한 철자(cmd->name)를 적고, 모르는 이름이면 적힌 그대로 적어 둡니다. 모르는 이름이어도 여기서는 오류가 나지 않습니다. 오류는 걷기 단계에서 납니다.
  3. EXEC_ON_READ 디렉티브면 그 자리에서 실행하고 돌아갑니다. 이때 그 디렉티브 자신의 노드는 트리에 붙지 않고, 실행 결과로 생긴 노드(예를 들어 Include가 읽은 파일의 내용)만 붙습니다(1168–1199행). 그래서 걷기 단계의 트리에는 LoadModule 노드가 없습니다.
  4. <로 시작하면 섹션의 시작입니다. ap_add_node()의 마지막 인자 1은 "이 노드를 부모로 삼고 그 아래로 내려가라"는 뜻입니다(server/util_cfgtree.c 20–44행). </로 시작하는 닫는 태그는 이름이 짝과 맞는지 확인하고 한 단계 위로 올라갈 뿐 노드를 만들지 않습니다(1211–1241행).

인자를 나눌 때 쓰는 ap_getword_conf()는 공백으로 낱말을 자르되, 큰따옴표나 작은따옴표로 감싼 부분은 공백이 있어도 한 낱말로 봅니다(server/util.c 835–877행). ErrorDocument 404 "Not here"가 인자 둘이 되는 이유입니다.

헷갈리기 쉬운 곳: <Location /a> 노드의 이름은 <Location입니다

트리 그림에서 섹션 노드의 이름은 <Location이고 인자는 /a>입니다. 첫 낱말을 공백까지 자르니 >가 인자 쪽에 남습니다(낱말 끝에 >가 붙은 경우만 떼어 냅니다, 1147–1155행). 그래서 server/core.c의 섹션 함수들은 모두 인자에서 마지막 >를 찾는 것으로 시작합니다(예: dirsection() 2519행). 디렉티브 표에 등록하는 이름도 "<Directory"처럼 닫는 꺾쇠가 없습니다(6절).

3. 이름으로 함수 찾기 — command_rec과 ap_config_hash

모듈이 내놓는 디렉티브 표

모듈은 자기가 처리할 디렉티브를 command_rec 배열로 내놓습니다.

typedef struct command_struct command_rec;
struct command_struct {
    /** Name of this command */
    const char *name;
    /** The function to be called when this directive is parsed */
    cmd_func func;
    /** Extra data, for functions which implement multiple commands... */
    void *cmd_data;
    /** What overrides need to be allowed to enable this command. */
    int req_override;
    /** What the command expects as arguments */
    enum cmd_how args_how;

    /** 'usage' message, in case of syntax errors */
    const char *errmsg;
};

include/http_config.h 203–218행

칸 뜻
name 디렉티브 이름
func 그 디렉티브를 만나면 부를 함수
cmd_data 함수에 덤으로 건넬 값. 함수 하나로 디렉티브 여럿을 처리할 때 구분용으로 씁니다
req_override 어디에 써도 되는지를 나타내는 비트 묶음(5절)
args_how 인자를 어떻게 나눠서 넘길지(4절)
errmsg 도움말. 인자 개수가 틀렸을 때 오류 메시지 뒤에 붙고 httpd -L에도 찍힙니다

이 구조체를 직접 채우지 않고 AP_INIT_TAKE1 같은 매크로를 씁니다.

# define AP_INIT_TAKE1(directive, func, mconfig, where, help) \
    { directive, { .take1=func }, mconfig, where, TAKE1, help }

include/http_config.h 128–129행

매크로는 컴파일하기 전에 글자를 바꿔 끼우는 C의 기능입니다. { .take1=func } 부분이 재미있습니다. func 칸의 타입 cmd_func는 공용체(union)로, 인자 방식마다 다른 모양의 함수 포인터를 한 칸에 겹쳐 둔 것입니다(82–101행). TAKE1용 함수는 (cmd_parms *, void *, const char *), FLAG용 함수는 (cmd_parms *, void *, int)를 받습니다. AP_INIT_FLAG를 쓰면 .flag=func로 채워지므로, 함수의 모양이 틀리면 컴파일러가 경고해 줍니다. 예제 컨테이너에서 TAKE1 모양의 함수를 AP_INIT_FLAG에 넣어 apxs -c로 컴파일해 보니 gcc 가 initialization ... from incompatible pointer type 경고를 냈습니다. 지정 초기화(.이름=값)를 지원하지 않는 컴파일러를 위한 대체 정의도 같은 파일에 있습니다(158–197행).

등록 — 소문자 이름을 열쇠로 하는 해시

모듈이 서버에 등록될 때(ap_add_module(), 645행) 디렉티브 표의 줄이 하나씩 해시에 들어갑니다.

    while (cmd && cmd->name) {
        mln = apr_palloc(tpool, sizeof(ap_mod_list));
        mln->cmd = cmd;
        mln->m = m;
        dir = apr_pstrdup(tpool, cmd->name);

        ap_str_tolower(dir);

        mln->next = apr_hash_get(ap_config_hash, dir, APR_HASH_KEY_STRING);
        apr_hash_set(ap_config_hash, dir, APR_HASH_KEY_STRING, mln);
        ++cmd;
    }

server/config.c 541–552행

열쇠는 소문자로 바꾼 이름, 값은 "어느 모듈의 어느 command_rec인가"를 담은 작은 목록(ap_mod_list)입니다. 같은 이름이 이미 있으면 새 것을 목록 맨 앞에 붙입니다(549–550행). 디렉티브 이름의 대소문자를 가리지 않는 이유가 이 소문자 열쇠입니다. GREETMESSAGE hi라고 적어도 찾아집니다.

걷기 — 노드마다 해시를 한 번 찾는다

걷기 단계의 몸통은 짧습니다. ap_walk_config()는 next를 따라가며 노드마다 ap_walk_config_sub()를 부를 뿐입니다(1362–1387행). 섹션 안으로 들어가는 일은 여기서 하지 않습니다. 그것은 섹션 디렉티브의 함수가 스스로 합니다(6절).

    char *dir = apr_pstrdup(parms->temp_pool, current->directive);

    ap_str_tolower(dir);

    ml = apr_hash_get(ap_config_hash, dir, APR_HASH_KEY_STRING);

    if (ml == NULL) {
        /* ... */
            return apr_pstrcat(parms->pool, "Invalid command '",
                               current->directive,
                               "', perhaps misspelled or defined by a module "
                               "not included in the server configuration",
                               NULL);
        /* ... */
    }

    for ( ; ml != NULL; ml = ml->next) {
        void *dir_config = ap_set_config_vectors(parms->server,
                                                 section_vector,
                                                 parms->path,
                                                 ml->m,
                                                 parms->pool);
        const char *retval;
        cmd = ml->cmd;

        /* Once was enough? */
        if (cmd->req_override & EXEC_ON_READ) {
            continue;
        }

        retval = invoke_cmd(cmd, parms, dir_config, current->args, NULL);

        if (retval != NULL && strcmp(retval, DECLINE_CMD) != 0) {
            /* ... */
            return retval;
        }
    }

    return NULL;

server/config.c 1306–1359행

디렉티브 하나를 처리하는 길. 트리의 노드 이름을 소문자로 바꿔 ap_config_hash에서 찾는다. 없으면 Invalid command 오류. 있으면 모듈과 command_rec 목록을 얻고, ap_set_config_vectors가 이 구역의 모듈 설정을 준비한다. invoke_cmd가 req_override와 parms->override를 비트 AND 해서 0이면 not allowed 오류, 겹치면 args_how에 따라 인자를 나눠 모듈 함수를 부른다. 함수가 NULL을 돌려주면 성공, 문자열을 돌려주면 Syntax error가 된다. 빨간 상자 셋이 설정 오류가 나는 세 자리입니다. 노란 상자는 해시와 설정 벡터, 곧 값이 저장된 곳입니다.

for 안의 ap_set_config_vectors()가 중요합니다. 지금 걷는 구역의 설정 벡터(section_vector)에서 이 모듈의 칸을 꺼내 돌려주는데, 칸이 비어 있으면 그 자리에서 create_dir_config를 불러 채웁니다. 같은 김에 지금 서버의 서버 설정 칸도 비어 있으면 create_server_config로 채웁니다(1084–1104행). 그렇게 얻은 디렉터리 설정이 모듈 함수의 두 번째 인자 mconfig로 넘어갑니다. 모듈 함수 입장에서는 "지금 읽는 구역의 내 디렉터리 설정"을 받는 셈입니다.

헷갈리기 쉬운 곳

4. invoke_cmd() — 인자를 나눠 모듈 함수 부르기

해시에서 찾은 command_rec을 실제로 부르는 함수가 invoke_cmd()입니다(853–1053행). 앞부분은 자리 검사(5절)이고, 그 뒤는 args_how에 따라 갈리는 큰 switch입니다. 두 가지만 옮깁니다.

    case TAKE1:
        w = ap_getword_conf(parms->pool, &args);

        if (*w == '\0' || *args != 0)
            return apr_pstrcat(parms->pool, cmd->name, " takes one argument",
                               cmd->errmsg ? ", " : NULL, cmd->errmsg, NULL);

        return cmd->AP_TAKE1(parms, mconfig, w);
    /* ... */
    case FLAG:
        /*
         * This is safe to use temp_pool here, because the 'flag' itself is not
         * forwarded as-is
         */
        w = ap_getword_conf(parms->temp_pool, &args);

        if (*w == '\0' || (ap_cstr_casecmp(w, "on") && ap_cstr_casecmp(w, "off")))
            return apr_pstrcat(parms->pool, cmd->name, " must be On or Off",
                               NULL);

        return cmd->AP_FLAG(parms, mconfig, ap_cstr_casecmp(w, "off") != 0);

server/config.c 922–1046행

인자 개수 검사와 On/Off 검사를 코어가 대신 해 준다는 것이 요점입니다. 모듈 함수는 이미 나뉜 낱말을 받습니다. 줄인 자리에 있는 ITERATE는 낱말마다 같은 함수를 여러 번 부르고(998–1015행), FLAG는 On이면 1, Off면 0을 넘깁니다(대소문자는 가리지 않습니다). 방식은 모두 열세 가지입니다(include/http_config.h 49–67행).

예제 모듈의 디렉티브 세 개로 본 args_how. TAKE1 인 GreetMessage 는 낱말 하나를 꺼내고 남은 것이 있는지 본다. hello 면 set_message(cmd, mconfig, "hello") 를 부르고, hello world 면 GreetMessage takes one argument, the greeting text 오류가 된다. FLAG 인 GreetShout 는 낱말 하나를 꺼내 대소문자를 가리지 않고 On/Off 를 검사한다. On 이면 set_shout(cmd, mconfig, 1) 을 부르고, yes 면 GreetShout must be On or Off 오류가 된다. ITERATE 인 GreetWords a1 a2 는 invoke_cmd() 가 낱말마다 나눠 add_word 를 "a1" 로 한 번, "a2" 로 한 번 부른다 모듈 함수는 이미 나뉘고 검사된 인자만 받습니다. 오류 문장은 5절 예제의 httpd -t 출력 그대로입니다.

args_how 모듈 함수가 받는 것 실제 예 (이 커밋)
RAW_ARGS 나누지 않은 문자열 그대로 <Directory, AllowOverride (core.c 4729, 4801)
TAKE1 낱말 하나 DocumentRoot (core.c 4797)
TAKE2 / TAKE12 낱말 둘 / 하나나 둘 ErrorDocument (4799), LoadModule (mod_so.c 449) / Define (4778)
TAKE3, TAKE23, TAKE123, TAKE13 낱말 셋까지, 빈 자리는 NULL
ITERATE 낱말 하나씩, 낱말 수만큼 반복 호출 LogLevel (4881)
ITERATE2 첫 낱말 + 나머지 낱말 하나씩, 반복 호출 AddIcon (mod_autoindex.c 571)
FLAG On이면 1, Off면 0 KeepAlive (http_core.c 112)
NO_ARGS 아무것도
TAKE_ARGV argc, argv (최대 64개) AllowOverrideList (4804)

모듈 함수가 받는 cmd_parms

모든 디렉티브 함수의 첫 인자는 cmd_parms *입니다. "지금 어디를 읽고 있는가"를 담은 구조체입니다(include/http_config.h 288–334행). 자주 쓰는 칸만 추립니다.

칸 담는 것
server 지금 설정 중인 서버. <VirtualHost> 안이면 그 가상 호스트의 server_rec
path <Directory>·<Location> 안이면 그 경로, 바깥이면 NULL
override 지금 자리에서 허용되는 비트 묶음(5절)
directive 지금 처리 중인 트리 노드
pool, temp_pool 설정 값을 둘 풀(pconf)과 설정이 끝나면 버려지는 임시 풀
info command_rec의 cmd_data (887행에서 넣어 줌)

함수는 성공하면 NULL을, 실패하면 오류 메시지 문자열을 돌려줍니다. 예외가 없는 C에서 흔한 약속입니다. 그 문자열이 걷기를 거슬러 올라가 ap_process_config_tree()에서 Syntax error on line %d of %s: 다음 줄로 찍힙니다(1977–1986행). 줄 번호는 2절의 노드에 적어 둔 line_num입니다.

예제: 디렉티브 네 개를 가진 모듈

이 글을 위해 mod_greet이라는 작은 모듈을 만들었습니다. 디렉티브 넷은 각각 다른 방식과 다른 자리 규칙을 갖습니다.

디렉티브 방식 자리(req_override) 설정 종류 병합 규칙
GreetMessage TAKE1 OR_FILEINFO 디렉터리 아래 것이 있으면 아래 것
GreetShout FLAG ACCESS_CONF 디렉터리 아래 것이 있으면 아래 것
GreetWords ITERATE OR_FILEINFO 디렉터리 위 것 뒤에 아래 것을 이어 붙임
GreetTag TAKE1 RSRC_CONF 서버 아래 것이 있으면 아래 것

SetHandler greet이 걸린 주소로 요청하면 핸들러가 지금 적용된 값과, 어떤 구역들이 병합됐는지(trail)를 찍어 줍니다. 또 생성·병합 함수가 불릴 때마다 /tmp/greet.trace에 한 줄씩 남기는데, 그때가 기동 중인지 요청 처리 중인지를 ap_state_query()로 구분해 적습니다(boot#1은 첫 번째 설정 읽기, boot#2는 두 번째 읽기, request는 요청 처리 중. 설정을 두 번 읽는 이유는 5편에 있습니다).

/* 이 글을 위해 쓴 예제입니다 (httpd 의 소스가 아닙니다) */
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_protocol.h"
#include "apr_strings.h"
#include <stdio.h>
#include <unistd.h>

module AP_MODULE_DECLARE_DATA greet_module;

/* 디렉터리 설정: <Directory>/<Location>/.htaccess 마다 하나씩 생긴다 */
typedef struct {
    const char *label;          /* 어느 구역에서 만들었나 (create_dir_config 의 dir) */
    const char *message;        /* GreetMessage — NULL 이면 "설정 안 함" */
    int shout;                  /* GreetShout — -1 이면 "설정 안 함" */
    apr_array_header_t *words;  /* GreetWords — 병합할 때 이어 붙인다 */
    const char *trail;          /* 어떤 구역들이 병합됐는지 기록 */
} greet_dir_conf;

/* 서버 설정: 주 서버와 <VirtualHost> 마다 하나씩 */
typedef struct {
    const char *tag;            /* GreetTag */
} greet_srv_conf;

/* ... trace(): /tmp/greet.trace 에 한 줄 남기는 함수 ... */

static void *create_dir(apr_pool_t *p, char *dir)
{
    greet_dir_conf *c = apr_pcalloc(p, sizeof(*c));
    c->label = dir ? apr_pstrdup(p, dir) : "(server)";
    c->shout = -1;
    c->words = apr_array_make(p, 2, sizeof(const char *));
    c->trail = c->label;
    trace("create_dir_config  dir=%s%s", c->label, "");
    return c;
}

static void *merge_dir(apr_pool_t *p, void *basev, void *addv)
{
    greet_dir_conf *base = basev, *add = addv;
    greet_dir_conf *c = apr_pcalloc(p, sizeof(*c));
    c->label = add->label;
    /* 덮어쓰기: 아래 구역(add)에 값이 있으면 그것, 없으면 위(base)의 것 */
    c->message = add->message ? add->message : base->message;
    c->shout = add->shout != -1 ? add->shout : base->shout;
    /* 이어 붙이기: 위의 것 뒤에 아래 것을 붙인다 */
    c->words = apr_array_append(p, base->words, add->words);
    c->trail = apr_pstrcat(p, base->trail, " + ", add->trail, NULL);
    trace("merge_dir_config   base=[%s] add=%s", base->trail, add->label);
    return c;
}

/* ... create_srv(), merge_srv(): 서버 설정용. tag 하나를 같은 방식으로 다룬다 ... */

/* TAKE1: 인자 하나 */
static const char *set_message(cmd_parms *cmd, void *mconfig, const char *arg)
{
    greet_dir_conf *c = mconfig;     /* 지금 읽는 구역의 디렉터리 설정 */
    c->message = arg;
    return NULL;                     /* NULL = 성공, 문자열 = 오류 메시지 */
}

/* FLAG: On 이면 1, Off 면 0 */
static const char *set_shout(cmd_parms *cmd, void *mconfig, int on)
{
    ((greet_dir_conf *)mconfig)->shout = on;
    return NULL;
}

/* ITERATE: 인자 수만큼 이 함수가 여러 번 불린다 */
static const char *add_word(cmd_parms *cmd, void *mconfig, const char *w)
{
    greet_dir_conf *c = mconfig;
    *(const char **)apr_array_push(c->words) = w;
    return NULL;
}

/* 서버 설정은 mconfig 가 아니라 cmd->server 에서 꺼낸다 */
static const char *set_tag(cmd_parms *cmd, void *mconfig, const char *arg)
{
    greet_srv_conf *c = ap_get_module_config(cmd->server->module_config,
                                             &greet_module);
    c->tag = arg;
    return NULL;
}

static const command_rec greet_cmds[] = {
    AP_INIT_TAKE1("GreetMessage", set_message, NULL, OR_FILEINFO,
                  "the greeting text"),
    AP_INIT_FLAG("GreetShout", set_shout, NULL, ACCESS_CONF,
                 "On to print the greeting in upper case"),
    AP_INIT_ITERATE("GreetWords", add_word, NULL, OR_FILEINFO,
                    "one or more extra words"),
    AP_INIT_TAKE1("GreetTag", set_tag, NULL, RSRC_CONF,
                  "a tag for this server"),
    { NULL }
};

static int greet_handler(request_rec *r)
{
    /* ... */
    d = ap_get_module_config(r->per_dir_config, &greet_module);
    s = ap_get_module_config(r->server->module_config, &greet_module);
    /* ... d 와 s 의 값을 text/plain 으로 찍는다 (shout 가 1 이면 message 를 대문자로) ... */
}

/* ... register_hooks(): greet_handler 를 handler 훅에 건다 ... */

AP_DECLARE_MODULE(greet) = {
    STANDARD20_MODULE_STUFF,
    create_dir,      /* create_dir_config */
    merge_dir,       /* merge_dir_config */
    create_srv,      /* create_server_config */
    merge_srv,       /* merge_server_config */
    greet_cmds,      /* 디렉티브 표 */
    register_hooks
};

D:\openSource\httpd-examples\ex\u07-directives\mod_greet.c (/* ... */로 줄인 곳은 기록용 함수, 서버 설정용 생성·병합 함수, 출력 코드입니다)

눈여겨볼 곳은 두 군데입니다. 디렉터리 설정용 함수(set_message 등)는 받은 mconfig에 값을 쓰지만, 서버 설정용 set_tag는 mconfig를 쓰지 않고 cmd->server에서 서버 설정을 직접 꺼냅니다. 코어가 함수에 넘기는 것은 언제나 디렉터리 설정이기 때문입니다(3절의 dir_config). 그리고 "설정하지 않음"을 NULL과 -1로 표시해 두었습니다. 병합 함수가 "아래 구역이 이 값을 정했는가"를 알아야 하기 때문입니다.

이 예제는 기준 커밋(e4af30fe6d)을 그대로 빌드한 httpd 2.5.1-dev 로 도커 컨테이너(Ubuntu 24.04)에서 실행했습니다. run.sh가 apxs -c로 컴파일하고, 아래 설정으로 서버를 띄운 뒤 curl로 요청합니다(/tmp/head.conf에는 LoadModule 줄들과 ServerName localhost 같은 공통 줄이 들어 있습니다).

Include /tmp/head.conf
Listen 8080
Listen 8081
DocumentRoot /tmp/www

GreetMessage hello-from-server
GreetWords s1
GreetTag main-tag

<Location />
    SetHandler greet
</Location>
<Location /a>
    GreetMessage hello-a
    GreetWords a1 a2
</Location>
<Location /a/b>
    GreetMessage hello-b
    GreetShout On
</Location>

<Location /x/y>
    GreetMessage from-x-y
</Location>
<Location /x>
    GreetMessage from-x
</Location>

<Directory /tmp/www/d>
    GreetMessage from-directory
</Directory>
<Directory /tmp/www/ht>
    AllowOverride FileInfo
</Directory>
<Directory /tmp/www/ind>
    AllowOverride Indexes
</Directory>

<VirtualHost *:8081>
    ServerName one.local
    GreetTag one-tag
</VirtualHost>
<VirtualHost *:8081>
    ServerName two.local
</VirtualHost>

먼저 httpd -L입니다. 실린 모듈의 디렉티브 표를 그대로 읽어 찍어 주는 옵션이라(server/config.c 2413–2504행), req_override 비트가 사람 말로 어떻게 읽히는지 볼 수 있습니다.

### httpd -L (Greet only)
GreetMessage (mod_greet.c)
	the greeting text
	Allowed in *.conf anywhere and in .htaccess
	when AllowOverride includes FileInfo
GreetShout (mod_greet.c)
	On to print the greeting in upper case
	Allowed in *.conf only inside <Directory>, <Files>, <Location>, or <If>
GreetWords (mod_greet.c)
	one or more extra words
	Allowed in *.conf anywhere and in .htaccess
	when AllowOverride includes FileInfo
GreetTag (mod_greet.c)
	a tag for this server
	Allowed in *.conf only outside <Directory>, <Files>, <Location>, or <If>

나머지 실행 결과는 5절(오류)과 7절(병합)에서 나눠 봅니다.

5. 어디에 써도 되나 — req_override와 parms->override

비트 두 묶음의 AND

command_rec의 req_override에 들어가는 값은 이런 비트들입니다.

#define OR_NONE 0             /**< *.conf is not available anywhere in this override */
#define OR_LIMIT 1           /**< *.conf inside &lt;Directory&gt; or &lt;Location&gt;
                                and .htaccess when AllowOverride Limit */
#define OR_OPTIONS 2         /**< *.conf anywhere
                                and .htaccess when AllowOverride Options */
#define OR_FILEINFO 4        /**< *.conf anywhere
                                and .htaccess when AllowOverride FileInfo */
#define OR_AUTHCFG 8         /**< *.conf inside &lt;Directory&gt; or &lt;Location&gt;
                                and .htaccess when AllowOverride AuthConfig */
#define OR_INDEXES 16        /**< *.conf anywhere
                                and .htaccess when AllowOverride Indexes */
#define OR_UNSET 32          /**< bit to indicate that AllowOverride has not been set */
#define ACCESS_CONF 64       /**< *.conf inside &lt;Directory&gt; or &lt;Location&gt; */
#define RSRC_CONF 128        /**< *.conf outside &lt;Directory&gt; or &lt;Location&gt; */
#define EXEC_ON_READ 256     /**< force directive to execute a command
                which would modify the configuration (like including another
                file, or IFModule */
/* ... */

/** this directive can be placed anywhere */
#define OR_ALL (OR_LIMIT|OR_OPTIONS|OR_FILEINFO|OR_AUTHCFG|OR_INDEXES)

include/http_config.h 228–255행

(&lt;는 문서 생성 도구용으로 꺾쇠를 적은 것이라 소스에 그대로 있습니다.) 이름의 OR_는 override, 곧 AllowOverride의 낱말들(Limit, Options, FileInfo, AuthConfig, Indexes)과 하나씩 짝입니다.

한편 걷는 쪽은 cmd_parms의 override에 "지금 이 자리에서 허용하는 비트"를 들고 다닙니다. invoke_cmd()가 맨 먼저 하는 일이 이 둘의 비교입니다.

    if ((parms->override & cmd->req_override) == 0 && !override_list_ok) {
        if (parms->override & NONFATAL_OVERRIDE) {
            /* ... */
            return NULL;
        }
        else if (parms->directive && parms->directive->parent) {
            return apr_pstrcat(parms->pool, cmd->name, " not allowed in ",
                               parms->directive->parent->directive, ">",
                               " context", NULL);
        }
        else {
            return apr_pstrcat(parms->pool, cmd->name,
                               " not allowed here", NULL);
        }
    }

server/config.c 868–885행

비트 AND(&)가 0이면, 곧 겹치는 비트가 하나도 없으면 오류입니다. 하나라도 겹치면 통과입니다. 노드에 부모가 있으면(섹션 안이면) not allowed in <섹션> context, 없으면 not allowed here가 됩니다. 원본에서 /* ... */로 줄인 자리는 경고 로그를 찍는 호출입니다. <Directory>의 AllowOverride에 Nonfatal=Override를 더해 두었을 때, .htaccess의 이 오류를 경고 한 줄로 낮추고 넘어가는 길입니다.

그러면 자리마다 parms->override는 무엇일까요.

자리 parms->override 근거
설정 파일 최상위 (RSRC_CONF | OR_ALL) & ~(OR_AUTHCFG | OR_LIMIT) = RSRC_CONF, OR_OPTIONS, OR_FILEINFO, OR_INDEXES config.c 1973행
<VirtualHost> 안 최상위와 같음 (바뀌는 것은 cmd->server뿐) core.c 3028–3079행
<Directory>·<Location> 안 OR_ALL | ACCESS_CONF (RSRC_CONF가 빠짐) core.c 2543행, 2659행
.htaccess 그 디렉터리에 적용된 AllowOverride의 비트 config.c 2022행

이 표로 mod_greet의 디렉티브를 하나씩 대 보면 httpd -L이 찍은 문장이 그대로 나옵니다. GreetTag(RSRC_CONF)는 섹션 안에서 막히고, GreetShout(ACCESS_CONF)는 섹션 밖에서 막히고, GreetMessage(OR_FILEINFO)는 어디서나 통과합니다. 실제 오류를 봅니다. 예제의 run.sh가 문제 있는 설정 파일을 하나씩 만들어 httpd -t로 검사한 결과입니다(왼쪽 숫자는 설정 파일의 줄 번호입니다).

자리마다 parms->override 와 디렉티브의 req_override 를 AND 한 결과. 최상위와 VirtualHost 안에서는 parms->override 가 RSRC_CONF, OR_OPTIONS, OR_FILEINFO, OR_INDEXES 다. GreetTag(RSRC_CONF)와 GreetMessage(OR_FILEINFO)는 통과하고, GreetShout(ACCESS_CONF)는 겹치는 비트가 없어 not allowed here(VirtualHost 안이면 not allowed in VirtualHost context), Require(OR_AUTHCFG)도 not allowed here 가 된다. Location 과 Directory 안에서는 parms->override 가 OR_ALL 과 ACCESS_CONF 다. GreetTag(RSRC_CONF)는 겹치는 비트가 없어 not allowed in Location context 가 되고, GreetMessage, GreetShout, Require 는 통과한다 위 표의 자리별 비트와 디렉티브 표의 비트를 맞대 본 것입니다. 오류 문장은 아래 httpd -t 출력에서 가져왔고, Location 안의 Require 는 비트로 따진 결과입니다(OR_ALL 에 OR_AUTHCFG 가 들어 있음).

### Require at the top level (mod_authz_core, OR_AUTHCFG)
     3	Require all granted
AH00526: Syntax error on line 3 of /tmp/bad.conf:
Require not allowed here

### GreetShout at the top level
     3	GreetShout On
AH00526: Syntax error on line 3 of /tmp/bad.conf:
GreetShout not allowed here

### GreetShout inside VirtualHost
     3	<VirtualHost *:8090>
     4	    GreetShout On
     5	</VirtualHost>
AH00526: Syntax error on line 4 of /tmp/bad.conf:
GreetShout not allowed in <VirtualHost> context

### GreetTag inside Location
     3	<Location /a>
     4	    GreetTag t
     5	</Location>
AH00526: Syntax error on line 4 of /tmp/bad.conf:
GreetTag not allowed in <Location> context

### GreetShout yes
     3	<Location /a>
     4	    GreetShout yes
     5	</Location>
AH00526: Syntax error on line 4 of /tmp/bad.conf:
GreetShout must be On or Off

### GreetMessage with two arguments
     3	GreetMessage hello world
AH00526: Syntax error on line 3 of /tmp/bad.conf:
GreetMessage takes one argument, the greeting text

### a typo in the name
     3	GreetMesage hello
AH00526: Syntax error on line 3 of /tmp/bad.conf:
Invalid command 'GreetMesage', perhaps misspelled or defined by a module not included in the server configuration

### a typo inside a false IfModule
     3	<IfModule nosuch_module>
     4	    GreetMesage hello
     5	</IfModule>
Syntax OK

첫 항목은 httpd.conf를 고쳐 본 사람이면 한 번쯤 만나는 오류입니다. Require는 OR_AUTHCFG로 등록되어 있고(modules/aaa/mod_authz_core.c 688행) 최상위의 parms->override에서는 바로 그 비트가 빠져 있습니다. GreetMessage hello world의 메시지 끝에 붙은 the greeting text는 디렉티브 표의 도움말(errmsg)입니다.

두 번째 검사: ap_check_cmd_context()

비트 검사만으로는 "<VirtualHost> 안에서는 안 된다" 같은 규칙을 나타낼 수 없습니다. <VirtualHost> 안의 parms->override가 최상위와 같기 때문입니다. 그런 규칙은 디렉티브 함수가 직접 ap_check_cmd_context()를 불러 확인합니다. NOT_IN_VIRTUALHOST, NOT_IN_DIRECTORY, NOT_IN_LOCATION, NOT_IN_HTACCESS 같은 표시를 넘기면 cmd->server->is_virtual이나 트리의 부모 노드를 보고 판정합니다(server/core.c 1324–1382행, 표시 정의는 include/http_config.h 969–981행). <VirtualHost>는 이 함수에 GLOBAL_ONLY를 넘겨 다른 섹션 안에 오지 못하게 막습니다(3036행).

.htaccess 안인지는 재미있는 방법으로 알아냅니다. cmd->pool == cmd->temp_pool인지를 봅니다(server/core.c 1343행). 아래에서 볼 .htaccess 읽기만 두 풀 자리에 똑같이 요청 풀을 넣기 때문입니다.

AllowOverride는 .htaccess의 parms->override를 정한다

AllowOverride는 그 자체가 ACCESS_CONF 디렉티브라서 섹션 안에만 쓸 수 있습니다(server/core.c 4801행). 그중에서도 의미가 있는 곳은 <Directory>뿐이라, <Location> 안에 쓰면 오류는 아니지만 "Useless use of AllowOverride" 경고를 남깁니다(1884–1887행). 이 디렉티브의 함수 set_override()가 하는 일은 낱말을 비트로 바꿔 코어의 디렉터리 설정에 적는 것뿐입니다. FileInfo면 OR_FILEINFO, All이면 OR_ALL, None이면 OR_NONE(0)입니다(1876–1951행).

요청이 오면 디렉터리를 따라 내려가는 ap_directory_walk()가 그 비트를 들고 ap_parse_htaccess()를 부르고(server/request.c 1096–1105행), ap_parse_htaccess()는 그 비트를 그대로 parms.override에 넣은 뒤 설정 파일과 똑같은 두 단계, ap_build_config()와 ap_walk_config()로 .htaccess를 처리합니다.

            parms.config_file = f;
            errmsg = ap_build_config(&parms, r->pool, r->pool, &temptree);
            if (errmsg == NULL)
                errmsg = ap_walk_config(temptree, &parms, dc);

            ap_cfg_closefile(f);

            if (errmsg) {
                ap_log_rerror(APLOG_MARK, APLOG_ALERT, 0, r,
                              "%s: %s", filename, errmsg);
                return HTTP_INTERNAL_SERVER_ERROR;
            }

server/config.c 2044–2055행

.htaccess에서 생기는 오류는 서버를 멈추지 않고 그 요청만 500으로 끝냅니다. 예제에서 세 디렉터리에 똑같이 GreetMessage from-htaccess가 든 .htaccess를 두었습니다.

### curl http://127.0.0.1:8080/ht/
uri=/ht/
message=from-htaccess
words=s1,h1
tag=main-tag
trail=(server) + /tmp/www/ht/

### curl -i http://127.0.0.1:8080/ind/
HTTP/1.1 500 Internal Server Error

### curl -i http://127.0.0.1:8080/none/
HTTP/1.1 500 Internal Server Error
### error_log
[Sat Oct 24 12:17:08.494971 2026] [core:alert] [pid 162:tid 167] [client 127.0.0.1:55322] /tmp/www/ind/.htaccess: GreetMessage not allowed here
[Sat Oct 24 12:17:08.503381 2026] [core:alert] [pid 163:tid 219] [client 127.0.0.1:55336] /tmp/www/none/.htaccess: GreetMessage not allowed here

(run.sh 출력에서 이 세 요청 부분만 모았습니다. 시간, pid, tid, 클라이언트 포트는 실행마다 다릅니다.) /ht/는 AllowOverride FileInfo라서 OR_FILEINFO인 GreetMessage가 통과했습니다(/ht/의 .htaccess에는 GreetWords h1도 한 줄 있습니다). /ind/는 AllowOverride Indexes라서 겹치는 비트가 없습니다. .htaccess의 디렉티브는 부모 노드가 없으므로 메시지는 not allowed here입니다.

헷갈리기 쉬운 곳: AllowOverride를 아예 안 쓰면 None과 같지 않습니다

/none/은 AllowOverride를 어디에도 적지 않은 디렉터리입니다. 그런데도 .htaccess를 읽었고, 그 안의 디렉티브가 전부 거부되어 500이 났습니다. 이유는 코어의 디렉터리 설정 초깃값에 있습니다. create_core_dir_config()는 override를 OR_UNSET|OR_NONE, 곧 32로 시작합니다(server/core.c 170행). 디렉터리를 걷는 쪽은 이 값이 0일 때만 .htaccess를 건너뛰므로(server/request.c 1096–1098행) 32는 "읽기는 하되 아무 디렉티브도 허용하지 않음"이 됩니다. AllowOverride None을 적어야 비로소 0이 되어 파일을 열지도 않습니다. 배포되는 기본 httpd.conf가 <Directory />에 AllowOverride none을 적어 두는 것도 그래서 의미가 있습니다(docs/conf/httpd.conf.in 115–118행). 2.4.x 브랜치의 core.c와 request.c도 같은 자리의 코드가 같습니다(2.4.x 로는 돌려 보지 않았습니다).

6. 섹션 — <Directory>, <Location>, <VirtualHost>는 코어의 디렉티브입니다

섹션도 특별한 문법이 아니라 코어 모듈의 디렉티브 표에 있는 RAW_ARGS 디렉티브입니다.

AP_INIT_RAW_ARGS("<Directory", dirsection, NULL, RSRC_CONF,
  "Container for directives affecting resources located in the specified "
  "directories"),
AP_INIT_RAW_ARGS("<Location", urlsection, NULL, RSRC_CONF,
  "Container for directives affecting resources accessed through the "
  "specified URL paths"),
AP_INIT_RAW_ARGS("<VirtualHost", virtualhost_section, NULL, RSRC_CONF,
  "Container to map directives to a particular virtual host, takes one or "
  "more host addresses"),

server/core.c 4729–4737행

세 섹션 모두 RSRC_CONF입니다. 그래서 <Directory> 안에 <Directory>를 넣으면 5절의 비트 검사에서 걸립니다(안에서는 RSRC_CONF가 빠지므로). 섹션 함수가 하는 일은 같은 틀입니다. cmd_parms의 몇 칸을 바꾸고, 자기 노드의 first_child부터 ap_walk_config()를 다시 부르고, 칸을 되돌립니다. 걷기가 재귀로 섹션 안에 들어가는 길이 이것입니다.

static const char *dirsection(cmd_parms *cmd, void *mconfig, const char *arg)
{
    const char *errmsg;
    const char *endp = ap_strrchr_c(arg, '>');
    int old_overrides = cmd->override;
    char *old_path = cmd->path;
    ap_regex_t *old_regex = cmd->regex;
    core_dir_config *conf;
    ap_conf_vector_t *new_dir_conf = ap_create_per_dir_config(cmd->pool);
    const command_rec *thiscmd = cmd->cmd;

    /* ... */

    cmd->path = ap_getword_conf(cmd->pool, &arg);
    cmd->override = OR_ALL|ACCESS_CONF;

    /* ... */

    /* initialize our config and fetch it */
    conf = ap_set_config_vectors(cmd->server, new_dir_conf, cmd->path,
                                 &core_module, cmd->pool);

    errmsg = ap_walk_config(cmd->directive->first_child, cmd, new_dir_conf);
    if (errmsg != NULL)
        return errmsg;

    /* ... */

    ap_add_per_dir_conf(cmd->server, new_dir_conf);

    /* ... */

    cmd->path = old_path;
    cmd->override = old_overrides;
    cmd->regex = old_regex;

    return NULL;
}

server/core.c 2516–2631행

  1. 빈 설정 벡터를 하나 만듭니다(ap_create_per_dir_config(), 모든 칸이 NULL인 배열, server/config.c 366–369행).
  2. cmd->path에 경로를, cmd->override에 섹션 안의 비트를 넣습니다. 안쪽 디렉티브의 create_dir_config(p, dir)에 넘어가는 dir이 이 path입니다. 예제 기록의 create_dir_config dir=/tmp/www/d/가 그것이고, 경로 끝의 /는 이 함수가 붙인 것입니다(2587–2588행).
  3. 섹션 안의 노드들을 이 벡터를 대상으로 걷습니다. 안쪽 디렉티브는 모두 이 벡터의 칸에 값을 씁니다.
  4. 다 채운 벡터를 서버의 코어 설정에 있는 배열 sec_dir에 넣어 둡니다(<Location>이면 sec_url, 639–653행). 요청이 오면 이 배열을 훑어 맞는 구역을 고릅니다.
  5. 바꿨던 칸을 되돌립니다. 섹션을 빠져나오면 다시 바깥의 규칙이 적용됩니다.

섹션 함수가 걷기를 재귀로 이어 가는 순서. 바깥 걷기 ap_walk_config() 가 노드 Directory /tmp/www/d 를 만나면 dirsection() 이 바꿀 칸(old_path, old_overrides, old_regex)을 저장한다. ① ap_create_per_dir_config() 로 모든 칸이 NULL 인 빈 설정 벡터 new_dir_conf 를 만든다. ② cmd->path 를 /tmp/www/d/ 로, cmd->override 를 OR_ALL | ACCESS_CONF 로 바꾼다. ③ ap_walk_config(first_child, cmd, new_dir_conf) 로 안쪽 디렉티브가 이 벡터의 칸에 쓰게 한다. ④ ap_add_per_dir_conf() 로 벡터를 코어 설정의 sec_dir 에 넣는다(Location 이면 sec_url). ⑤ 저장한 칸을 되돌리고 바깥 걷기를 계속한다. VirtualHost 는 바꾸는 칸이 달라서, 새 server_rec s 를 목록에 끼우고 cmd->server 를 s 로 바꾼 뒤 안쪽을 s->lookup_defaults 에 걷고 cmd->server 를 되돌린다 위 다섯 단계를 dirsection() 의 실제 순서대로 이었습니다. 경로는 예제 기록의 create_dir_config dir=/tmp/www/d/ 에서 가져왔습니다.

urlsection()(<Location>)도 거의 같습니다(2633–2709행). 경로를 파일 시스템 경로로 정규화하지 않는다는 점이 다릅니다.

<VirtualHost>는 바꾸는 칸이 다릅니다. ap_init_virtual_host()로 새 server_rec을 만들어 서버 목록에 끼우고, cmd->server를 그것으로 바꾼 뒤 안쪽을 그 서버의 lookup_defaults를 대상으로 걷습니다.

    errmsg = ap_init_virtual_host(p, arg, main_server, &s);
    if (errmsg) {
        return errmsg;
    }

    s->next = main_server->next;
    main_server->next = s;

    s->defn_name = cmd->directive->filename;
    s->defn_line_number = cmd->directive->line_num;

    cmd->server = s;

    errmsg = ap_walk_config(cmd->directive->first_child, cmd,
                            s->lookup_defaults);

    cmd->server = main_server;

server/core.c 3060–3076행

lookup_defaults는 server_rec이 가진 "이 서버의 기본 디렉터리 설정" 벡터입니다(include/httpd.h 1424–1426행). 최상위에서 걷기를 시작할 때 넘기는 벡터도 주 서버의 lookup_defaults입니다(1977행). 그래서 섹션 밖에 쓴 GreetMessage hello-from-server는 주 서버의 lookup_defaults에 들어갑니다. 새 가상 호스트의 module_config와 lookup_defaults는 모든 칸이 빈 벡터로 시작합니다(server/config.c 2118–2119행). 주 서버의 값을 언제 물려받는지는 다음 절입니다.

헷갈리기 쉬운 곳: <Location>은 길이가 아니라 적은 순서대로 적용됩니다

예제의 설정에서 <Location /x/y>를 <Location /x>보다 먼저 적었습니다. /x/y/z로 요청하면 둘 다 맞습니다.

### curl http://127.0.0.1:8080/x/y/z
uri=/x/y/z
message=from-x
words=s1
tag=main-tag
trail=(server) + /x/y + /x

더 구체적인 /x/y가 이길 것 같지만 결과는 from-x입니다. trail이 보여 주듯 /x/y를 먼저 병합하고 그 위에 /x를 병합했기 때문입니다. <Directory>는 기동할 때 정렬합니다. 정규식이 아닌 것을 앞에, 그 안에서는 경로의 단계 수(/의 개수)가 적은 것을 앞에 둡니다(ap_core_reorder_directories()와 비교 함수 reorder_sorter(), server/core.c 709–784행). 그런데 이 함수가 정렬하는 것은 sec_dir뿐이고 sec_url은 손대지 않습니다. 요청이 올 때 ap_location_walk()는 배열을 앞에서부터 훑으며 맞는 것을 차례로 병합합니다(server/request.c 1561–1649행). 넓은 <Location>을 위에, 좁은 것을 아래에 적는 습관이 필요한 이유입니다.

7. 병합 — 언제, 누가 하나

만들기: 기동할 때 구역마다

예제 서버를 띄운 직후의 기록입니다.

### 2) startup trace (/tmp/greet.trace)
[boot#1  pid=158] create_server_config  main
[boot#1  pid=158] create_dir_config  dir=(server)
[boot#1  pid=158] create_dir_config  dir=/a
[boot#1  pid=158] create_dir_config  dir=/a/b
[boot#1  pid=158] create_dir_config  dir=/x/y
[boot#1  pid=158] create_dir_config  dir=/x
[boot#1  pid=158] create_dir_config  dir=/tmp/www/d/
[boot#1  pid=158] create_dir_config  dir=(server)
[boot#1  pid=158] create_server_config  vhost
[boot#1  pid=158] merge_server_config  base=main-tag add=one-tag
[boot#1  pid=158] merge_dir_config   base=[(server)] add=(server)
[boot#2  pid=158] create_server_config  main
[boot#2  pid=158] create_dir_config  dir=(server)
[boot#2  pid=159] create_dir_config  dir=/a
[boot#2  pid=159] create_dir_config  dir=/a/b
[boot#2  pid=159] create_dir_config  dir=/x/y
[boot#2  pid=159] create_dir_config  dir=/x
[boot#2  pid=159] create_dir_config  dir=/tmp/www/d/
[boot#2  pid=159] create_dir_config  dir=(server)
[boot#2  pid=159] create_server_config  vhost
[boot#2  pid=159] merge_server_config  base=main-tag add=one-tag
[boot#2  pid=159] merge_dir_config   base=[(server)] add=(server)

(pid 는 실행마다 다릅니다.) 같은 묶음이 boot#1, boot#2로 두 번 나옵니다. 설정을 두 번 읽기 때문입니다(5편). 한 묶음 안을 위에서부터 읽으면 이렇습니다.

  1. 주 서버의 서버 설정과 기본 디렉터리 설정은 파일을 읽기도 전에 만들어집니다. init_server_config()가 실린 모든 모듈의 create_server_config와 create_dir_config(p, NULL)을 부릅니다(server/config.c 2262–2263행, 270–283행, 311–322행). dir이 NULL이라 예제는 (server)로 적었습니다.
  2. 섹션의 디렉터리 설정은 걷는 도중, 그 섹션 안에서 이 모듈의 디렉티브를 처음 만났을 때 만들어집니다(3절의 ap_set_config_vectors()). <Location />은 기록에 없습니다. 그 안에는 SetHandler(코어의 디렉티브)뿐이라 greet의 칸은 끝까지 NULL로 남습니다.
  3. 가상 호스트 one.local 안의 GreetTag를 만나면, 그 서버의 lookup_defaults에 디렉터리 설정(dir=(server))이, module_config에 서버 설정(vhost)이 생깁니다. two.local에는 greet의 디렉티브가 없으니 아무것도 생기지 않습니다.
  4. 마지막 두 줄이 기동 때의 병합입니다.

boot#2의 중간에서 pid 가 158에서 159로 바뀝니다. 주 서버의 설정 두 개는 파일을 읽기 전에, 섹션의 설정은 걷는 도중에 만들어지는데, 그 사이의 pre_config 훅에서 event MPM 이 두 번째 기동 때 백그라운드 프로세스로 떨어져 나가기 때문입니다(apr_proc_detach(), server/mpm/event/event.c 4024–4040행). 이 순간은 5편 4절에서 자세히 다뤘습니다. 이 글에서는 "섹션의 설정은 pre_config 뒤, 걷기에서 생긴다"는 증거로만 봐 두면 됩니다.

서버 설정의 병합: 기동할 때 한 번

ap_fixup_virtual_hosts()는 가상 호스트마다 주 서버의 설정을 물려줍니다(server/config.c 2170–2211행). 서버 설정은 merge_server_configs()로, 기본 디렉터리 설정(lookup_defaults)은 ap_merge_per_dir_configs()로 합칩니다(2178–2182행). 서버 설정 쪽 코드입니다.

static void merge_server_configs(apr_pool_t *p, ap_conf_vector_t *base,
                                 server_rec *virt)
{
    /* Can reuse the 'virt' vector for the spine of it, since we don't
     * have to deal with the moral equivalent of .htaccess files here...
     */

    void **base_vector = (void **)base;
    void **virt_vector = (void **)virt->module_config;
    module *modp;

    for (modp = ap_top_module; modp; modp = modp->next) {
        merger_func df = modp->merge_server_config;
        int i = modp->module_index;

        if (!virt_vector[i]) {
            if (df && modp->create_server_config
                   && (ap_get_module_flags(modp) &
                       AP_MODULE_FLAG_ALWAYS_MERGE)) {
                virt_vector[i] = (*modp->create_server_config)(p, virt);
            }
            else {
                virt_vector[i] = base_vector[i];
                df = NULL;
            }
        }
        if (df) {
            virt_vector[i] = (*df)(p, base_vector[i], virt_vector[i]);
        }
    }
}

server/config.c 324–354행

가상 호스트의 칸이 비어 있으면 병합 함수를 부르지 않고 주 서버의 포인터를 그대로 복사합니다. 모듈이 AP_MODULE_FLAG_ALWAYS_MERGE를 켜 둔 경우만 예외로, 빈 설정을 만들어 병합합니다. 칸이 차 있으면 병합 함수를 부릅니다.

가상 호스트의 서버 설정 병합. 주 서버의 greet 서버 설정은 tag=main-tag다. one.local은 GreetTag one-tag를 써서 걷는 도중 서버 설정이 생겼으므로 merge_server_config가 불려 tag=one-tag가 된다. two.local은 greet 디렉티브를 쓰지 않아 칸이 NULL이므로 주 서버의 포인터를 그대로 복사하고 병합 함수는 불리지 않아 tag=main-tag가 된다. 노란 상자가 실제로 만들어진 설정 구조체, 회색 상자가 빈 칸입니다.

예제의 두 가상 호스트에 같은 주소로 요청한 결과입니다.

### curl -H 'Host: one.local' http://127.0.0.1:8081/a/page
uri=/a/page
message=hello-a
words=s1,a1,a2
tag=one-tag
trail=(server) + (server) + /a

### curl -H 'Host: two.local' http://127.0.0.1:8081/a/page
uri=/a/page
message=hello-a
words=s1,a1,a2
tag=main-tag
trail=(server) + /a

기동 기록에서 merge_server_config가 한 번(one.local)만 불렸던 것과 맞습니다. one.local의 trail에 (server)가 두 번 있는 것은 그 가상 호스트의 lookup_defaults가 주 서버의 것과 병합된 흔적입니다(기동 기록의 마지막 줄). two.local은 주 서버의 lookup_defaults 칸을 포인터째 물려받았습니다. 두 호스트 모두 <Location /a>의 값을 받은 것은, 코어 모듈의 서버 설정 병합 함수가 가상 호스트의 <Directory>·<Location> 목록을 주 서버의 목록 뒤에 이어 붙이기 때문입니다(merge_core_server_configs(), server/core.c 552–553행). 코어도 이 글에서 본 "모듈의 병합 함수" 규칙을 똑같이 따르는 모듈 하나입니다.

디렉터리 설정의 병합: 요청마다

디렉터리 설정을 합치는 코어 쪽 함수는 이것 하나입니다.

AP_CORE_DECLARE(ap_conf_vector_t *) ap_merge_per_dir_configs(apr_pool_t *p,
                                           ap_conf_vector_t *base,
                                           ap_conf_vector_t *new_conf)
{
    void **conf_vector = apr_palloc(p, sizeof(void *) * conf_vector_length);
    void **base_vector = (void **)base;
    void **new_vector = (void **)new_conf;
    int i;

    for (i = 0; i < total_modules; i++) {
        if (!new_vector[i]) {
            conf_vector[i] = base_vector[i];
        }
        else {
            const merger_func df = merger_func_cache[i];
            if (df && base_vector[i]) {
                conf_vector[i] = (*df)(p, base_vector[i], new_vector[i]);
            }
            else
                conf_vector[i] = new_vector[i];
        }
    }

    return (ap_conf_vector_t *)conf_vector;
}

server/config.c 285–309행

모듈마다 세 갈래입니다. 아래 구역(new)의 칸이 비었으면 위의 것을 그대로 쓰고, 둘 다 있으면 모듈의 병합 함수를 부르고, 위가 비었으면 아래 것을 그대로 씁니다. 결과는 새로 할당한 벡터이고 원래의 두 벡터는 건드리지 않습니다. p에는 요청 풀이 넘어오므로 병합 결과는 요청이 끝나면 함께 사라집니다.

이 함수를 부르는 곳은 server/request.c의 walk 함수들입니다. 요청 처리 중에 r->per_dir_config는 서버의 lookup_defaults에서 출발하고(290행), 코어의 map_to_storage 훅이 ap_directory_walk()와 ap_file_walk()로 <Directory>·.htaccess·<Files>를 병합하고(server/core.c 5100–5104행), 그다음 <Location>을 병합합니다(server/request.c 292–301행). <Location>이 <Directory>보다 나중에 병합되므로, 예제처럼 "아래 것이 있으면 아래 것" 규칙을 쓰는 디렉티브라면 같은 값을 두 곳에서 정했을 때 <Location>이 이깁니다. 이 단계들이 요청 처리의 어디에 있는지는 10편에서 자세히 봅니다.

<Location> 병합은 두 번에 나눠 일어납니다. 맞는 구역끼리 먼저 합치고(now_merged), 그 결과를 지금까지의 r->per_dir_config 위에 합칩니다.

            if (now_merged) {
                now_merged = ap_merge_per_dir_configs(r->pool,
                                                      now_merged,
                                                      sec_ent[sec_idx]);
            }
            else {
                now_merged = sec_ent[sec_idx];
            }
    /* ... */
    if (now_merged) {
        r->per_dir_config = ap_merge_per_dir_configs(r->pool,
                                                     r->per_dir_config,
                                                     now_merged);
    }

server/request.c 1637–1684행

/a/b/c 요청에서 디렉터리 설정이 병합되는 순서. 요청이 시작되면 r->per_dir_config가 lookup_defaults, 곧 hello-from-server와 words=s1에서 출발한다. directory walk는 /a/b/c에 맞는 Directory가 없어 그냥 지나간다. location walk가 설정 파일 순서대로 /a와 /a/b를 골라 먼저 둘끼리 병합해 message=hello-b, shout=On, words=a1,a2를 만들고, 그 결과를 (server) 위에 병합해 words=s1,a1,a2가 된다. 핸들러는 r->per_dir_config에서 이것을 읽는다. 노란 상자는 기동 때 만든 설정, 파란 상자는 요청마다 일어나는 병합입니다.

예제의 요청 결과와 그때의 기록입니다.

### curl http://127.0.0.1:8080/hello
uri=/hello
message=hello-from-server
words=s1
tag=main-tag
trail=(server)

### curl http://127.0.0.1:8080/a/page
uri=/a/page
message=hello-a
words=s1,a1,a2
tag=main-tag
trail=(server) + /a

### curl http://127.0.0.1:8080/a/b/c
uri=/a/b/c
message=HELLO-B
words=s1,a1,a2
tag=main-tag
trail=(server) + /a + /a/b

### curl http://127.0.0.1:8080/d/
uri=/d/
message=from-directory
words=s1
tag=main-tag
trail=(server) + /tmp/www/d/

### 3) trace while serving requests
[request pid=163] merge_dir_config   base=[(server)] add=/a
[request pid=163] merge_dir_config   base=[/a] add=/a/b
[request pid=163] merge_dir_config   base=[(server)] add=/a/b
[request pid=163] merge_dir_config   base=[/x/y] add=/x
[request pid=163] merge_dir_config   base=[(server)] add=/x
[request pid=163] merge_dir_config   base=[(server)] add=/tmp/www/d/
[request pid=163] create_dir_config  dir=/tmp/www/ht/
[request pid=163] merge_dir_config   base=[(server)] add=/tmp/www/ht/

### the same two URLs once more (/a/b/c, /ht/)
[request pid=163] merge_dir_config   base=[/a] add=/a/b
[request pid=163] merge_dir_config   base=[(server)] add=/a/b
[request pid=163] create_dir_config  dir=/tmp/www/ht/
[request pid=163] merge_dir_config   base=[(server)] add=/tmp/www/ht/

(run.sh 출력에서 필요한 부분만 모았습니다. pid 는 실행마다 다르고, 다시 돌렸을 때는 두 번째 /a/b/c 요청이 다른 자식 프로세스로 가기도 했습니다. 어느 쪽이든 병합이 다시 일어나는 것은 같습니다. /x/y/z와 /ht/의 응답은 6절과 5절에 있습니다. "3)"의 기록은 /hello부터 /ht/까지 여섯 요청의 것입니다. 병합된 설정의 label은 아래 구역의 것을 물려받으므로 add=/a/b처럼 보입니다.)

헷갈리기 쉬운 곳

이 글에서 뺀 것

큰 줄기를 위해 다음은 이름과 위치만 적어 둡니다. <If>·<ElseIf>·<Else>와 그 병합(ifsection(), ap_if_walk() — server/request.c 2027행부터). <Files>와 ap_file_walk()(server/core.c 2711행, server/request.c 1690행). <Limit>·<LimitExcept>(ap_limit_section()). <Proxy> 안에서만 쓰는 PROXY_CONF 비트(include/http_config.h 252행). 디렉티브 이름 단위로 허용하는 AllowOverrideList와 invoke_cmd()의 override_list 검사(server/core.c 2001행, server/config.c 862–866행). .htaccess의 오류를 경고로 낮추는 AllowOverride Nonfatal=…(NONFATAL_*). Options와 override_opts. 설정 값 안의 ${변수}를 바꿔 끼우는 ap_resolve_env()(1132행). 구조체의 칸 위치(APR_OFFSETOF)만 넘겨 함수를 따로 쓰지 않게 해 주는 ap_set_string_slot()·ap_set_flag_slot() 같은 범용 함수(server/config.c 1471–1564행). 명령줄의 -C·-c로 설정 앞뒤에 줄을 끼우는 process_command_config()(1762행).

2.4.x 브랜치와 비교하면, 이 글에서 본 함수들(ap_walk_config_sub(), ap_merge_per_dir_configs(), merge_server_configs(), ap_fixup_virtual_hosts(), 자리별 override 값)은 같은 코드입니다. trunk 에서는 invoke_cmd()에 parent 인자가, cmd_parms에 parent 칸과 regex 칸이 더해졌고(ap_walk_config_sub()가 invoke_cmd()에 NULL을 하나 더 넘기는 것, 6절 dirsection()의 old_regex 두 줄이 그 흔적입니다), ap_parse_htaccess()가 요청에 ".htaccess의 영향을 받았다"는 표시(r->taint)를 남기는 두 줄이 더해졌습니다.

정리

개념 한 줄 요약
두 단계 먼저 파일을 ap_directive_t 트리로 읽고(ap_build_config), pre_config 뒤에 트리를 걸으며 실행한다(ap_walk_config)
EXEC_ON_READ LoadModule·Include·<IfModule>처럼 파일의 모양을 바꾸는 디렉티브는 읽는 순간 실행된다
command_rec 모듈이 내놓는 디렉티브 표의 한 줄. 이름, 함수, 자리 비트, 인자 방식, 도움말
ap_config_hash 소문자 이름 → (모듈, command_rec) 목록. 찾기는 해시 한 번
args_how TAKE1·FLAG·ITERATE·RAW_ARGS 등 13가지. 낱말 나누기와 개수 검사는 코어가 한다
cmd_parms 디렉티브 함수가 받는 "지금 어디를 읽는가" — 서버, 경로, 허용 비트, 노드
자리 검사 cmd->req_override & parms->override가 0이면 not allowed here / in <X> context
AllowOverride .htaccess를 걸을 때의 parms->override를 정한다. 아예 안 쓰면 None과 다르다
섹션 코어의 RAW_ARGS 디렉티브. cmd_parms를 바꾸고 first_child부터 다시 걷는다
서버 설정 병합 기동 때 ap_fixup_virtual_hosts()에서. 가상 호스트의 칸이 비면 포인터만 복사한다
디렉터리 설정 병합 요청마다 request.c의 walk 에서. <Directory>·.htaccess 다음에 <Location>. <Location>끼리는 설정 파일에 적은 순서대로
병합 규칙 코어는 칸이 둘 다 있을 때 모듈의 병합 함수를 부를 뿐, 무엇이 이기는지는 모듈이 정한다

다음에 볼 것

설정 파일의 한 줄이 모듈의 구조체에 들어가고, 구역마다 생긴 구조체가 요청 때 하나로 합쳐지는 과정을 봤습니다. 이제 서버를 실제로 돌리는 쪽으로 갑니다. StartServers, MaxRequestWorkers, ThreadsPerChild 같은 디렉티브도 이 글에서 본 방식, 곧 MPM 모듈의 디렉티브 표에 있는 AP_INIT_TAKE1 줄을 거쳐 MPM 의 변수에 들어갑니다(server/mpm/event/event.c 4511–4524행). 그 값을 가지고 프로세스와 스레드를 몇 개 띄우고, 들어온 연결을 누구에게 맡기는지가 8편: MPM — prefork, worker, event는 연결을 어떻게 나눠 맡나에서 이어집니다. 이 글에서 "요청마다 병합한다"고만 하고 넘어간 walk 함수들의 자리는 10편에서 봅니다.

Spring과 비교하면 닮은 곳이 두 군데 있습니다. Spring도 설정을 곧바로 객체로 만들지 않고 먼저 설계도(BeanDefinition)로 읽어 등록한 뒤 나중에 객체를 만들며, 부모 설계도와 자식 설계도를 합친 병합본을 따로 만들어 씁니다(Spring 3편). httpd의 디렉티브 트리와 병합된 설정 벡터가 그 두 단계에 해당합니다. 다른 점은 값의 우선순위를 정하는 방식입니다. Spring의 Environment는 출처(PropertySource)의 목록 순서로 이기는 값을 정하고 첫 번째로 찾은 값을 쓰지만(Spring 12편), httpd는 구역이 겹칠 때마다 모듈이 쓴 병합 함수를 불러 모듈마다 다른 규칙(덮어쓰기, 이어 붙이기)을 허용합니다.

다른 글