갈루아의 반서재

하스켈 웹프레임워크 - Yesod

라우팅 구문 Routing Syntax


MVC(Model-View-Controller) 프레임워크 관점에서 Yesod를 보면, 라우팅과 핸들러는 이 중 컨트롤러에 해당한다. 다른 웹개발 환경과 비교해보면 다음과 같다.

  • PHP, ASP와 같은 파일 이름 기반 처리방식
  • Django나 Rails 처럼 정규표현식에 기반하여 파싱하는 방식

Yesod는 원리상 후자에 가깝다. 그렇다고 하지만 몇 가지 차이점이 있다. Yesod는 정규표현식을 사용하는 대신에 루트의 부분에 매칭한다.

단방향 라우트-핸들러 매핑이 아니라, Yesod는 route datatype 또는 type-safe URL 라고 불리는 데이터 타입을 사용하고 양방향 전환 기능을 만들어낸다. 


Route Syntax


Yesod는 기존의 구문에 라우팅 선언을 넣을려는 것이 아니라, 라우팅만을 위한 단순한 구문 디자인을 채용했다. 이런 방식으로 인해 작성도 쉬울 뿐더러 Yesod에 대한 경험이 없는 사람도 충분히 이해할만큼 단순하다는 장점이 있다. 기본예는 다음과 같다. 

1
2
3
4
5
6
7
/             HomeR     GET
 
/blog         BlogR     GET POST
 
/blog/#BlogId BlogPostR GET POST
 
/static       StaticR   Static getStatic
cs

이하 라우팅 선언의 자세한 내용에 대해 알아보자. 


Pieces


요청을 받으면 Yesod가 가장 먼저하는 작업은 요청받은 경로를 분해하는 것이다. 그 조각들은 "/" 를 기준으로 부분으로 나눈게 된다. 예를 들면 다음과 같다.

1
2
3
toPieces "/" = []
 
toPieces "/foo/bar/baz/" = ["foo""bar""baz"""]
cs

Yesod는 대표 URL(canonical URLs)이 존재한다고 가정한다. 만약 사용자가 / 또는 // 가 포함된 URL을 요청하면, 자동적으로 대표 URL로 리다이렉트된다고 말이다. 즉, 하나의 자원에 대해 하나의 URL이 존재하는 것이며, 이는 검색 랭킹 향상에도 도움이 된다. 그러니깐 URLs의 정확한 구조에 대해서는 신경을 쓸 필요가 없다는 이야기이다. Yesod는 자동으로 슬래시를 끼워넣거나 문제가 있는 문자를 제거한다. 좀더 튜닝을 해보고자 한다면, Yesod 타입클래스 챕터에 있는 cleanPath와 joinPath 메소드를 살펴볼 필요가 있다.


Types of Pieces


사용할 수 있는 타입에는 아래와 같이 3가지가 있다.

Static

URL과 정확히 매칭되는 순수 문자열이다. 

Dynamic single

2개의 포워드 슬래시 ("//") 사이에 존재하는 한 조각으로, 사용자 입력 값을 나타낸다. 페이지 단위 요청에 있어 사용자의 추가적인 입력을 받는 주요한 방법이기도 하다. 이러한 조각들은 # 데이터 타잎과 같은 형태로 이루어진다. 데이터 타잎은 PathPiece의 인스턴스여야 한다. 

Dynamic multi

앞선 내용과 동일하다. 다만 다수의 URL 조각을 받을 수 있다는 점에서 차이가 난다. 리소스 패턴에서 마지막 조각으로, 애스터리스트(*)와 PathMultiPiece의 인스턴스인 데이터타잎으로 구성된다. 하지만 Multi pieces 는 앞선 2개의 타잎만큼 흔하지는 않지만, 파일 구조나 임의적인 계층구조를 가진 위키 등 정적 트리 구조를 구현하는데 중요하다.

Yesod 1.4 버전 이후로, 다이나믹 멀티를 나타내기 위해 추가적으로 + 를 사용할 수 있다.

그러면 몇 가지 기본 패턴을 살펴보자. 간단하게 보면, 어플리케이션의 루트는 / 일 것이다. 유사하게 FAQ 는 /page/faq 에 놓을 수 있다. 이제 피보나치 웹사이트를 작성한다고 가정해보자. /fib/#Int 와 같이 URL을 구성할 수 있을 것이다. 하지만 여기에는 약간의 문제점이 존재한다. 음수나 0 은 어플리케이션으로 전달하고 싶지 않다. 이 경우 타입 시스템이 도움이 된다. 

  1. newtype Natural = Natural Int
  2. instance PathPiece Natural where
  3.     toPathPiece (Natural i) = T.pack $ show i
  4.     fromPathPiece s =
  5.         case reads $ T.unpack s of
  6.             (i, ""):_
  7.                 | i < 1 -> Nothing
  8.                 | otherwise -> Just $ Natural i
  9.             [] -> Nothing

첫 번재 라인에서 유효하지 않은 입력으로부터 보호하기 위해 간단한 newtype wrapper 를 정의했다. 

PathPiece 가 2개의 메소드를 가진 타입클래스임을 위에서 알 수 있다. 하나는 toPathPiece 로 텍스트로 변환해주는 역할을 한다. 다른 하나는 fromPathPiece 로 텍스트를 데이터타입으로 변환한다(변환이 불가능할 때는 Nothing을 반환한다). 이 데이터타입을 사용함으로써 핸들러는 오직 자연수 입력만 받게된다. 

실제 어플리케이션에서는 절대 유효하지 않은 내부적으로 입력되는 것을 방지하기 위해서 smart constructors 같은 접근 방식을 사용할 수 있다. 

PathMultiPiece를 정의하는 것은 간단하다. 최소 2단계의 층위를 가진 위키를 만든다고 하면, 다음과 같이 데이터타입을 정의할 수 있다. 

  1. data Page = Page Text Text [Text] -- 2 or more
  2. instance PathMultiPiece Page where
  3.     toPathMultiPiece (Page x y z) = x : y : z
  4.     fromPathMultiPiece (x:y:z) = Just $ Page x y z
  5.     fromPathMultiPiece _ = Nothing


Overlap checking


기본적으로 Yesod 는 어떤 2개의 루트도 잠재적으로 서로 오버랩되지 않음을 보장한다. 예를 들어 다음과 같은 루트를 살펴보자. 

1
2
3
/foo/bar   Foo1R GET
 
/foo/#Text Foo2R GET
cs

이러한 선언은 오버래핑으로 거부될 것이다. /foo/bar 의 경우 2개에 모두 해당하기 때문이다. 하지만 오버래핑을 허용하는 2가지 경우도 있다. 

데이터타입의 정의상 오버랩은 절대 일어나지 않음을 알 수 있다. 예를 들어 위에서 텍스트를 정수로 바꾸면, 오버랩을 일으키는 루트가 없음을 손쉽게 이해할 수 있을 것이다. Yesod는 현재 그러한 분석까지는 하지 못한다. 

여러분은 어플리케이션이 어떻게 동작하는지에 대한 추가적인 지식을 가지고 있다. 그리고 그러한 상황은 허용되지 않을 것이라는 것도 안다. 예를 들어, Foo2R 는 절대 bar 파라메터를 받도록 허용되지 않는다. 

그리고 루트의 앞부분에 ! 를 넣음으로써 오버랩 체킹 기능을 사용하지 않을 수도 있다. 어떤 경로 조각의 앞부분에도 느낌표를 붙일 수 있으며다. 

1
2
3
4
5
/foo/bar    Foo1R GET
 
!/foo/#Int  Foo2R GET
 
!/foo/#Text Foo3R GET
cs

루트 오버래핑에서 하나의 문제점은 바로 모호성이다. 위의 예에서 /foo/bar 의 경우 Foo1R 또는 Foo3R 중 어디로 향해야 하는가? 그리고 /foo/42 는 Foo2R 와 Foo3R 중 어디로 가야하는가? 이 경우 Yesod의 룰은 간단하다. 첫번째 루트가 이기는 것이다. 


Resource name


각각의 리소스 패턴은 이와 연관된 이름을 가진다. 그 이름은 어플리케이션과 연관된 타입 세이프 URL 데이터타입의 생성자가 될 것이다. 따라서 이름은 대문자로 시작되어야 한다. 관례상, 리소스 이름은 모두 대문자 R로 끝난다. 물론 강제사항은 아니지만 보통 일반적으로 그렇게 한다는 얘기다. 

생성자의 정확한 정의는 리소스 패턴에 달려있다. 싱글이든 아니면 멀티이든 어떤 데이터타입이 사용되든지간에 이는 데이터타입에 대한 인수가 되고, 타입 세이프 URL과 어플리케이션의 유효한 URLs 사이에 1 대 1 대응관계를 만들어주게 된다. 

이것은 반드시 모든 값이 워킹 페이지임을 의미하지는 않는다. 단지 잠재적으로 유효한 URL이라는 의미일 뿐이다. 예를 들어, PersonR "Michael" 이라는 값은 데이터베이스에 Michael 이 없다면 유효한 페이지로 연결되지 

 PersonR 이라는 이름을 가진 /person/#Text 형태의 리소스 패턴, YearR 이라는 /year/#int 형태의 리소스 패턴, 마지막으로 /page/faq 형태의 FaqR 라는 이름의 리소스패턴이 있는 경우, 루트 데이터타입은 대략 다음과 같을 것이다. 

  1. data MyRoute = PersonR Text
  2.  
  3.              | YearR Int
  4.  
  5.              | FaqR

만약 사용자가 /year/2009 를 요청하면, Yesod 는 그것을 YearR 2009 라는 값으로 변환한다. /person/Michael 은 PersonR "Michael" 그리고 /page/faq 는 FaqR이 되는 것이다. 이에 반해 /year/two-thousand-nine, /person/michael/snoyman 그리고 /page/FAQ 은 모두 404 에러를 발생시킨다.


Handler specification


Yesod에는 3가지 핸들링 방식이 있다. 싱글 핸들러는 주어진 루트에 기반하여 모든 요청 메소드에 대해 작동한다. 분리 핸들러는 주어진 루트에 대한 각각의 요청 메소드에 대해 작동한다. 이외의 요청 메소스는  405 Method Not Allowed 에러를 발생시킨다. 그리고 서브사이트로 전달도 필요하다. 

먼저 싱글 핸들러의 경우 리소스 패턴과 리소스 이름으로 구성된다. 예를 들어, /page/faq FaqR 와 같은 식이다. 이 경우에서, 핸들러 함수는 handleFaqR라고 명명된다. 

각각의 요청 메소드에 대한 분리 핸들러 역시 동일하다. 요청 메소드는 모두 대문자여야 한다. 예를 들면 /person/#String PersonR GET POST DELETE 와 같은 식이다. 이 경우 getPersonR, postPersonR, deletePersonR 라는 3개의 핸들러 함수를 정의해야 한다. 

서브사이트는 매우 유용하지만 다소 복잡한 토픽이다. 주로 사용되는 서브사이트는 정적 파일을 서비스하는 정적 서브사이트이다. 정적 파일을 /static 에서 서비스하기 위해서는 리소스 라인을 다음과 같이 구성해야 한다. 

1
/static StaticR Static getStatic
cs

여기서 /static 이라는 표현은 정적 파일을 서비스하는 URL 구조의 위치를 알려줄 뿐, 특별한 비밀같은 게 숨어있는 것은 아니다. /static 을 /my/non-dynamic/files 와 같이 바꿔도 아무 문제가 없다.

다음으로 StaticR 은 리소스 이름이다. 다음 2단어는 서브사이트를 사용중임을 알려준다. Static 이라는 이름은 subsite foundation datatype의 이름이며, getStatic은 master foundation datatype의 값으로부터 정적 값을 가져오는 함수를 의미한다.