mod_rewrite 는 규칙 기반으로 URL 을 동적으로 전환(redirecting) 및 재작성(rewriting)할 수 있는 아파치 확장 모듈이다. 알아 두면 웹 서비스 운영시 많은 도움이 되는 모듈로써 다음과 같은 경우에 유용하게 사용할 수 있다.

적절한 사용처

Clean URL

Ruby On Rails 등 웹 개발 프레임워크들이 URL 라우팅(routing) 기능을 제공한다. 이 기능은 http://www.example.com/Blog/Posts.php?Year=2006&Month=12&Day=19  과 같이 복잡한 URL 을 http://www.example.com/Blog/2006/12/19/  처럼 깔끔한 URL 로 재작성하는 기능이다.

짧은 URL 은 사용자 친화적이라 기억하기도 쉽고 RESTFul 웹서비스를 제공하기에도 용이하다.

mod_rewrite 는 아파치 웹서버에 내장되어 있으므로 개발 언어나 프레임워크에서 URL 라우팅 기능이 없어도 Clean URL 형식으로 웹서비스를 제공할 수 있다.


대규모 가상 호스트

몇 개의 가상 호스트가 있는 웹서버라면 가상 호스트 추가/변경시마다 설정 파일에 변경을 기술하면 될 것이다.

만약 호스팅 업체처럼 개별 사용자마다 username.example.com 형식의 가상 호스트를 제공해야 한다면 어떻게 해야 할까?

새로운 사용자가 추가되거나 변경될 때마다 웹서버의 설정에 <VirtualHost> 항목을 수정하는 것은 힘들고 고통스러운 작업이 될 것이다.

mod_rewrite 를 사용하면 동적으로 호스트명을 웹서버의 디렉토리 경로로 변환할 수 있으므로 대규모 가상 호스트를 손쉽게 처리할 수 있다.


웹사이트 재배치

필요에 의해 웹 사이트를 재개발했다고 가정해 보자. 기존 웹사이트는 꽤 오래 서비스했으므로 검색 엔진에 등록도 되어 있고 외부에서 링크된 페이지도 꽤 있다.

재개발시 새로운 목적에 맞게 최상위 URL 이 변경(old.example.com 에서 new.example.com)되었고 사이트의 구성도 새로 만들었지만 검색 엔진에 등록되거나 외부에서 링크된 기존의 URL 도 잃고 싶지 않을수 있다.

먼저 기존 URL 에 있는 HTML 마다 새로운 URL 에 대한 링크를 다음과 같이 추가할 수 있다.

<meta http-equiv="refresh" content="0; url=http://new.example.com/" />

이 방법은 제일 간단하지만 페이지가 많을 경우  꽤 많은 작업을 해야 하며 meta 태그를 넣은 페이지들도 유지해야 하는 부담이 있다.

mod_rewrite 를 사용하여 기존 URL 로 들어올 경우 새로운 URL 로 전환할 수 있게 규칙을 만들면 직접 소스를 수정하는 것 보다 간편하고 손쉽게 대응할 수 있다.


조건에 따른 처리

새로운 서비스는 보안을 위해 HTTPS 를 기본으로 서비스를 제공하기로 했다. 이럴 경우 사용자는 명시적으로 URL 에 HTTPS 를 지정해야 SSL 이 적용된 웹 사이트로 연결할 수 있다.

사용자가 실수로 URL 에 HTTP 를 쓰면 SSL 이 적용되지 않은 웹서비스로 연결될 수 있다.

사용자 실수를 막기 위해 HTTP 포트인 80 을 닫아야 할까? 아니면 사이트의 메인 페이지 코드에 프로토콜이 HTTP인지 HTTPS 인지 확인하여 HTTP 일 경우 HTTPS 로 전환하는 기능을 넣어야 할까? 메인에만 코드를 넣는다면 하위 페이지로 바로 들어올 경우 어떻게 해야 할까? 

mod_rewrite 를 사용하면 프로토콜을 검사하여 HTTP일 경우 https 프로토콜로 전환하는 설정을 넣어 간단하게 해결할 수 있다.


적절하지 않은 사용처

mod_rewrite 는 아파치 웹서버의 기본 지시자인  Alias, AliasMatch, Redirect, RedirectMatch 그리고 Location과 LocationMatch 지시자와 기능이 중복된다.

위 지시자의 사용처마다 mod_rewrite 를 사용할수도 있지만 전용 지시자로 간단히 해결될 수 있는게 mod_rewrite 사용시 매우 복잡해 질 수 있다.

간단한 URL 에 대한 전환이 필요할 경우 굳이 복잡한 mod_rewrite 대신 Redirect 지시자를 사용하는 게 좋다. 사이트를 개편하여 메인의 html 호출을 전환해야 한다면 다음과 같이 설정할 수 있다. 

Redirect /index.html http://www.example.com/index.jsp


가상 호스트도 단순히 도메인 이름과 디렉터리를 연결해야 한다면 이 용도에 맞는 mod_vhost_alias 라는 아파치 모듈이 이미 있으므로 mod_rewrite 적용전에 기존에 별도의 모듈이나 지시자가 있는지 확인해 보고 없을 경우 mod_rewrite 사용을 고려해 보자.


설정

CentOS 에 포함된 아파치 웹서버의 경우 mod_rewrite 가 기본 탑재되어 있다. httpd.conf 에도 다음과 같이 기본적으로 모듈이 로딩되므로 별도로 추가 설치나 설정이 필요하지 않다.

LoadModule rewrite_module modules/mod_rewrite.so

환경 변수와 주요 지시자

환경 변수

mod_rewrite 에서는 아파치의 다양한 환경 변수를 사용할 수 있다.

이제 mod_rewrite 에서 제공하는 주요 지시자들에 대해서 알아 보자. 


엔진과 로깅

RewriteEngine On|Off

모듈의 사용 여부를 지정하는 RewriteEngine 이다. Off 일 경우 동작하지 않는다.


RewriteLog, RewriteLogLevel

mod_rewrite가 제대로 동작하지 않을때 디버깅하기는 매우 어려운 작업이다. RewriteLog 지시자는 로그를 남길 파일을 지정할 수 있다.

RewriteLog logs/rewrite.log
RewriteLogLevel 9

RewriteLogLevel 은 로그의 레벨을 지정하며 숫자가 클수록 로그에 남기는 내용이 많아진다. 9가 최대 값이며 0 일 경우 전혀 로그를 남기게 되지 않으므로 안정적인 운영 환경에서는 0 으로 지정하고 사용하는 게 좋다.

RewriteRule

mod_rewrite 의 핵심 지시자로 조건에 따른 rewrite 규칙을 설정하고 규칙에 따른 동작을 지정한다. RewriteRule 의 사용 범위는 httpd.conf 에 사용하면 전역적으로 반영되며 <VirtualHost>나 <Directory>, <Location> 에 사용할 경우 해당 범위에만 효과를 미친다.


문법

RewriteRule 문법은 다음과 같은 형식으로 사용한다.

RewriteRule pattern target_url [flag,flag,flag,…]


pattern 은 정규식을 사용할 수 있으며 일치시킬 패턴을 지정한다. target_url 은 전환할 URL 을 적어주며 여기에는 pattern 에서 일치한 내용을 사용할 수 있다. 맨 뒤에 flag 는 처리할 동작으로 , 를 구분자로 하여 여러 개를 지정할 수 있다.


Flag

RewriteRule 문법중 마지막은 패턴이 일치할 경우 어떤 동작을 취할지 지정하는 부분이며 이를 플래그라고 부른다. 지정할 수 있는 플래그중 자주 사용되는 것들은 다음과 같은 항목이 있다. 플래그는 전체 단어를 써도 되지만 축약어로 써도 된다. 개인적으로는 축약어를 사용하면 알아 보기가 어려워서 전체 단어를 사용하는 것을 선호하는 편이다. 전체 단어를 사용할 때는 공백을 제거하고 사용해야 한다.

  • Env:E - Env 플래그는 조건이 맞을 경우 특정 환경 변수를 설정하거나 해제할 수 있다.
    다음은 클라이언트가 요청한 파일이 이미지 파일일 경우 로그를 남기지 않게 하는 설정으로 전 절의 mod_setenvif 로 사용한 예와 동일한 결과를 갖게 된다.

    RewriteEngine On
    RewriteRule \.(jpe?g|gif|png)$ - [env=dontlog:1]
    CustomLog logs/access_log combined env=!dontlog

  • Last:|L – Last 플래그는 더이상 mod_rewrite 룰 처리를 하지 않겠다는 의미이다. 프로그래밍 언어에서 break 와 비슷한 용도로 사용되며 여러 개의 RewriteRule  룰을 연결해서 사용하는 경우 더 이상의 처리를 하지 않아도 될 때 유용하게 사용할 수 있다. 
  • Nocase:NC – Nocase 는 패턴 매칭할 경우 대소문자 구분을 하지 않겠다는 의미이다. 다음과 같이 설정하면 브라우저가 /article/3 으로 요청한 것을 index.php에 articleID=$1 형식으로 전달한다. $1 은 첫 번째 일치하는 변수이므로 3 이 설정된다. 그러므로 example.com/article/3 과 example.com/index.php?articleID=3 과 동일해 진다. NC 이므로 대소문자를 구분하지 않으므로 example.com/ARticle/3 도 같은 의미가 된다.

    RewriteRule ^/article/(\d*) /index.php?articleID=$1 [NC]

  • Forbidden:F – Forbidden 플래그는 규칙에 일치할 경우 해당 리소스에 대해 HTTP 403 Forbidden 응답을 클라이언트에게 전송한다. 
    외부에서 접근하면 안 되는 리소스가 있을 경우 사용하면 유용하다. Forbidden 은 Last 의 의미를 포함하므로 더 이상의 Rewrite 규칙을 처리하지 않고 바로 리턴한다.
    다음 예제는 브라우저가 요청한 리소스의 확장자가 exe 또는 msi와 같은 실행 파일일 경우 403 응답을 전송하는 예제이다.

    RewriteEngine On
    RewriteRule \.(exe|msi) - [F] 

  • Redirect:R – 리다이렉트 플래그는 규칙에 일치할 경우 HTTP 302 Redirect 응답을 클라이언트에게 전송한다. 만약 HTTP 301 Moved Permanently 응답을 전송해야 한다면 R=301 로 적어주면 된다.
    다음은 images 경로 밑에 모든 gif 요청을 jpg URL로 전환하는 예제이다.

    RewriteRule ^/images/(.*)\.gif /jpegs/$1.jpg [L,R=301]
  • Passthru:PT – RewriteRule 의 target_url 이 http 나 https 로 시작하지 않으면 mod_rewrite 는 target_url 을 로컬 파일의 경로로 인식한다. / 로 시작할 경우 절대 경로로 / 가 아닐 경우 상대 경로로 처리하게 된다. 파일 경로로 처리할 경우 스크립트 파일의 경우 파일의 내용이 그대로 노출될 우려가 있다.

    파일 경로로 처리할 경우 아파치 설정에 따른 적절한 핸들러가 실행되지 않고 해당 파일을 읽어서 브라우저에 전송하게 된다.

    이로 인해 <Alias>, <Redirect>, <ScriptAlias> 등의 지시자로 설정한 내용이 적용되지 않거나 스크립트 타입에 따른 스크립트 핸들러가 실행되지 않게 된다.

    이로 인해 404 에러가 발생하거나 스크립트의 내용이 브라우저에 그대로 노출될 수 있다.


    예로 기존의 /index.html 을 /cgi-bin/index.cgi 로 포워딩한다고 가정해 보자.

    다음과 같이 들어오는 요청은 /index.cgi 로 전환되어야 한다.

    $ curl -L -v http://www.example.com/index.html

    RewriteRule ^/index.html /cgi-bin/index.cgi [R,L]

    이때 위와 같이 설정했을 경우 PT 플래그가 없으므로 /cgi-bin 은 ScriptAlias 지시자가 처리하지 않으므로 아파치 웹서버는 DocumentRoot/cgi-bin/index.cgi 를 찾게 되므로 404 에러가 발생한다.

    RewriteRule ^/index.html /cgi-bin/index.cgi [PT]

    PT 플래그가 있다면 아파치 내부의 핸들어에 처리를 넘기므로 ScriptAlias의 지시자가 처리되고 정상적으로 서비스가 되게 된다.

  • qsappend: QSA – mod_rewrite 는 기본적으로 클라이언트가 GET 으로 전송한 변수를 무시한다. 이 변수 값은 QUERY_STRING 이라는 변수명에 저장된다. QSA(Query String Append) 플래그를 사용하면 QUERY_STRING 을 무시하지 않고 유지시키므로 URL 전환시에도 문제가 없다.
    다음 예제는 /index.cgi 를 호출할 경우 index.php 로 전환하고 GET 으로 넘기는 파라미터도 유지하게 하는 설정이다.

    RewriteRule ^/index.cgi$ /index.php [PT,QSA]


RewriteCond

앞에서 RewriteRule 에 대한 설명을 보고 만약 조건문이 필요할 경우는 어떻게 해야 할지 의문이 생긴 독자들도 있을 것이다. RewriteCond 는 그런 독자들의 의문을 해소해 줄 지시자이다. 여러 가지 상황별로 조건을 확인하고 그 조건이 맞을 경우 처리를 하게 할 수 있다. RewriteCond는 다음과 같은 문법을 갖는다.

RewriteCond TestString Pattern [Flags]


TestString 에는 다음과 같은 값등을 사용할 수 있다.

  • 직전 RewriteRule 에서 일치한 캡처값 - RewriteCond 앞에 RewriteRule 을 사용했고 정규 표현식에서 그룹으로 지정된 값이 있을 경우 TestString 에 사용할 수 있다. 사용할 경우 $N (N 은 0 에서 9사이 값) 형식으로 사용한다.
  • 직전 RewriteCond 에서 일치한 캡처값 - RewriteCond 앞에 RewriteCond를 사용했고 정규 표현식에서 그룹으로 지정된 값이 일치했을 경우 %N(N 은 0 에서 9) 형식으로 사용할 수 있다.
  • 서버 변수 - 웹서버가 갖고 있는 변수값을 사용할 수 있다. 서버의 변수를 사용할 경우 %{변수명} 형식으로 사용한다. 제일 많이 사용하는 변수들은 %{HTTP_HOST}, %{QUERY_STRING} 등이다. 

     

Pattern 에는 정규식으로 일치시킬 문자열의 패턴을 기술하면 된다.

마지막 플래그는 다음과 같은 값을 사용할 수 있다.

  • NoCase:NC – RewriteRule 과 마찬가지로 대소 문자를 구분하지 않을 경우 사용한다.
  • ORNext:OR – OR 플래그는 여러 개의 RewriteCond 를 사용할 경우 논리적인 OR 조건이 필요할 때 사용할 수 있다.(여러 개의 RewriteCond 가 있을 때 기본적으로 AND 로 처리한다.)


mod_rewrite 적용 사례

여기까지 읽었지만 아파치 웹서버에 익숙하지 않거나 또는 정규식에 익숙하지 않은 독자들은 mod_rewrite 를 어떻게 활용할 지 난감할 수 있을 것이다.

이제 mod_rewrite 를 사용하여 어떻게 URL 을 다루는지 실제 유용한 사례를 알아 보자. RewriteEngine On 을 한 상태이어야 동작하므로 이 구문은 제외했다.


QUERY_STRING 을 Location 으로 전환

책의 정보를 보는 example.com/book.php?novel 라는 URL 이 있다. 이제 이 URL 을 example.com/book/novel 로 들어와도 정상 동작하게 설정해 보자.

RewriteRule ^/book/(.*) /book.php?$1 [L,PT]


여러 개의 QUERY_STRING 을 Location 으로 전환 

위 예제는 잘 동작하지만 다음과 같은 경우 제대로 동작하지 않는다. 

example.com/book.php?category=novel&bookid=1234

이 문제는 ? 뒤의 모든 파라미터를 하나의 파라미터로 전달하므로 발생한다. 다음과 같이 하면 URL 을 파싱해서 두 개의 파라미터로 변환해서 book.php 에 전달한다.

RewriteRule ^/book/([^/]*)/([^/]*) /book.php?category=$1&bookid=$2 [PT]

([^/]*)  정규식 패턴은 / 를 제외한 모든 문자를 의미하므로 URL 을 표현하게 된다.


기존 html 을 JSP 로 전환

사이트를 개편해서 기존 html 대신 jsp 를 제공하고 있다. URL 은 같고 파일의 확장자만 다를 경우 예전 html 을 호출하는 클라이언트는 다음 규칙을 통해 jsp 로 전환 시킬 수 있다.

RewriteRule ^/?([a-z/]+).html$ $1.jsp[L,R=301]


요청 리소스가 없을 경우 404 페이지로 전환

클라이언트가 요청하는 리소스가 없을 경우 별도의 에러 페이지로 전환 시킬 수 있다. 쉘 스크립트에서 사용되는 test 구문중 파일이 존재하는지를 판단하는 -f 와 디렉터리인지 판단하는 -d 옵션을 RewriteCond 에 사용할 수 있다.

다음 예제는 클라이언트가 요청한 파일 및 디렉터리가 존재하지 않을 경우 별도의 404 페이지를 실행하고 rewrite 처리를 종료하는 예제이다.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

RewriteRule .? /404.php [L]


리소스의 외부 링크 차단

HTTP 프로토콜은 요청을 보낼 때 이 URI 를 어디에서 알았는지에 대한 정보를 전송한다. 다시 말하면 링크를 타고 왔을 때 이 요청을 링크한 페이지의 URL 에 대한 정보이다.
이 HTTP 헤더를 리퍼러(Referer)라고 하며 이 정보를 이용하면 내부 페이지에서 링크되지 않고 외부에서 링크하여 들어오는 경우를 차단할 수 있다.

이 기능은 보통 이미지 파일등의 외부 링크를 차단할 때 많이 사용된다. 다음은 리퍼러가 example.com, 또는 www.example.com 이 아닐 경우 차단하는 예제이다. 링크된 페이지가 URL 에 대해 http 를 썼을 수도 있고 안 썼을 수도 있으므로 http:// 부분은 ? 문자로 처리해야 한다.

RewriteCond %{HTTP_REFERER} !^(https?:\/\/)?(www.)?example\.com/ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteRule .(gif|jpe?g|png)$ - [F]


RewriteCond %{HTTP_REFERER} !^$ 가 있으면 리퍼가 정보가 없는 경우는 403 을 발생시키지 않는다.
이는 리퍼러 정보를 제대로 보내지 않는 브라우저나 또는 사용자가 페이지 북마크를 한 경우 처리하기 위함이나 이런 경우에도 다 차단하려면 이 항목을 주석 처리하면 된다.


ServerAlias 로 연결시 대표 이름으로 전환

가상 호스트 설정에 ServerName 은 example.com 이고 ServerAlias 는 www.example.com, web.example.com, home.example.com 이 있다고 하자. ServerAlias 로 연결할 경우 example.com 으로 전환하고 싶다.

다음 설정은 클라이언트가 Host: 헤더에 보낸 호스트 이름이 example.com 이 아닐 경우 example.com 으로 전환하는 예제이다. 클라이언트가 호스트 이름을 대문자로 쓸 수도 있으므로 NC 플래그를 붙이는게 좋다.

RewriteCond %{HTTP_HOST} !^example\.com$ [NC]
RewriteRule (.*) http://example.com$1 [R,L]


HTTP 로 연결이 들어올 경우 HTTPS 로 전환

secure_page 디렉터리 밑에는 보안이 필요한 페이지들이 있다. 이 디렉터리 아래에 있는 모든 리소스들은 사용자가 실수로 http 로 들어와도 자동으로 https 로 전환하고 싶다. 

다음 규칙으로 요구 사항을 충족할 수 있다.

RewriteCond %{REQUEST_URI} ^/secure_page/
RewriteCond %{HTTPS} !on
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R,L]


모든 페이지를 HTTPS 로 전환

아주 중요한 서비스를 개발했고 모든 페이지에 HTTPS 를 적용하겠다고 가정해 보자. 고객은 보통 https 라는걸 잘 모르므로 이런 경우에도 http 포트를 열고 모든 http 는 https 로 전환하는게 좋다.

다음 규칙을 사용하면 모든 http 요청을 https 로 전환할 수 있다.

RewriteCond %{HTTPS}  !on
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R,L]