Cookie 


생성하기 

def login (id : String, pwd : String) = Action {
Ok("success").withCookies( Cookie("coki_name", id))
} 

Result 객체의 메소드인 withCookies 를 호출하여 내부 인자로 쿠키를 등록 할 수 있습니다.
위에 "identify" 는 name 이고 , id 는 value 입니다.


가져오기

def authnticate (request: Request[AnyContent]) : Boolean = {

val ck : Option[Cookie] = request.cookies.get("coki_name")

Request 에 있는 cookies 로 부터 특정 이름을 가진 쿠키를 get 합니다. 리턴은 Option 으로 받는 군요. 


삭제하기

def logout () = Action { request =>
Ok("logout success").discardingCookies(DiscardingCookie("coki_name"))
}

Result 객체의 메소드인 discardingCookies 를 호출하여 삭제합니다.  지우고 싶은 쿠키 name 을 넣어주네요.


Cookie 클래스 살펴보기

마지막으로 Cookie 클래스 원본입니다. name 과 value 말고도 넣을 수 있는 인자는 많죠.

/**
* An HTTP cookie.
*
* @param name the cookie name
* @param value the cookie value
* @param maxAge the cookie expiration date in seconds, `None` for a transient cookie, * or a value less than 0 to expire a cookie now
* @param path the cookie path, defaulting to the root path `/`
* @param domain the cookie domain
* @param secure whether this cookie is secured, sent only for HTTPS requests
* @param httpOnly whether this cookie is HTTP only, i.e. not accessible from * client-side JavaScipt code
*/
case class Cookie(name: String, value: String, maxAge: Option[Int] = None,                   path: String = "/", domain: Option[String] = None,                  secure: Boolean = false, httpOnly: Boolean = true)


Session

생성하기 

def login (id : String, pwd : String) = Action {
Ok("success").withSession("session key"-> "value")

} 

Result 객체의 메소드인 withSession 을 호출하여 맵 형태의 세션을 등록합니다.
인자는 session 클래스 또는 위처럼 session key 는 키이고 "value" 는 값인 tuple 이 들어갑니다.

request.session.+()
request.session.-()

+ , - 를 통해 추가 삭제 할 수 있습니다.


가져오기

def authnticate (request: Request[AnyContent]) : Boolean = {

val se : Option[String] = request.session.get("session key")

Request 에 있는 session 이 가진 특정 키에 대한 값을 리턴 받습니다. 리턴은 Option[String] 으로 받는군요. 


삭제하기

def logout () = Action { request =>
Ok("logout success").withNewSession

}

Result 객체의 메소드인 withNewSession 을 호출하여 삭제합니다. 



Actions, Controllers and Results

Action 이란 무엇인가?


스프링에서 컨트롤러 함수의 예가  다음과 같은 모습을 취하는 반면 

@RequestMapping(method = RequestMethod.GET)
public String printHello(ModelMap model) {
model.addAttribute("message", "Hello Spring MVC Framework!");
return "hello";
}

스칼라언어 기반 Play 프레임워크 웹개발에서는 (위의 자바 예와 동일한 내용의 예가 아닙니다) 

def doSomething = Action{
Ok.apply(views.html.index("Hi there"))
}

이런 모양새로 이루어지는데 여기서 Action 이 무엇일까요? 


위의 예는 다음과 같이 풀어지게 되는데요

def doSomething: Action[AnyContent] = Action.apply({
    Ok.apply(views.html.index("Hi there"))
})

위의 apply 메소드의 정의는  apply(block: => Result) 이라는 인자없고 리턴이 Result 인 익명의 함수가 들어가게 됩니다. 다시 말해서 Action.apply 함수의 apply() 는 생략가능이고 apply 함수내에는 인자가 없고 Result 를 리턴하는 익명함수가 들어가네요. 

이 함수 말이죠. 

{
Ok.apply(views.html.index("Hi there"))
}

그리고 doSomething 메소드는  Action[AnyContent]  클래스의 인스턴스를 리턴합니다.

아래는 공식 문서 내용입니다.

액션은 무엇인가?

Play2 애플리케이션이 수신한 대부분의 요청은 액션에 의해 처리됩니다.
play.api.mvc.Action은 기본적으로 요청을 처리하고 클라이언트에 보낼 결과를
생성하는 (play.api.mvc.Request => play.api.mvc.Result) 함수입니다.

val echo = Action { request =>

  Ok("Got request [" + request + "]")

}

액션은 웹 클라이언트에 보낼 HTTP 응답을 나타내는 play.api.mvc.Result 값을 반환합니다.
이 예에서 Ok는 text / plain 응답을 포함하는 200 OK 응답을 생성합니다.


액션 만들기 

play.api.mvc.Action 동반자 객체는 Action 값을 생성하는 여러 가지 도우미 메서드를 제공합니다.
첫 번째 간단한 방법은 결과를 반환하는 표현식 블록(익명함수)을 인수로 취하는 것입니다.

Action {

  Ok("Hello world")

}

이것은 액션을 만드는 가장 간단한 방법이지만 들어오는 Request 에 대한 자세한 참조를 얻지는 못합니다.
따라서 Request => Result 함수를 인수로 취하는 또 다른 Action 빌더가 있습니다.

Action { request =>

  Ok("Got request [" + request + "]")

}

요청 매개 변수를 암시적으로 표시하면 필요로 하는 다른 API에서 암시적으로 사용할 수 있습니다.

Action { implicit request =>

  Ok("Got request [" + request + "]")

}

Action 값을 만드는 마지막 방법은 추가 BodyParserargument를 지정하는 것입니다.

Action(parse.json) { implicit request =>

  Ok("Got request [" + request + "]")

}

Body 파서는 이 설명서 뒷부분에서 다룰 것입니다. =>  body parser 이해하기 
지금은
기본 Any Content body 파서를 사용한다는 것을 알면 됩니다.


컨트롤러는 액션 생성기이다.

컨트롤러는 Action 값을 생성하는 싱글톤 객체일 뿐 입니다. 액션 생성기를 정의하는 가장 간단한
사용 사례는 액션 값을 반환하는 매개 변수가 없는 메소드입니다.

package controllers

import play.api.mvc._

class Application extends Controller {

  def index = Action {

    Ok("It works!")

  }

}

물론 액션 생성기 메소드는 매개 변수를 가질 수 있으며 이러한 매개 변수는 액션 클로저에 의해 캡처 될 수 있습니다.

def hello(name: String) = Action {

  Ok("Hello " + name)

}


단순 results

지금은 단순한 결과에만 관심이 있습니다. 상태 코드, HTTP 헤더 세트 및 웹 클라이언트로 보낼 본문이 있는 HTTP 결과. 이 결과는 play.api.mvc.Result에 의해 정의됩니다

def index = Action { 

 Result(

    header = ResponseHeader(200, Map(CONTENT_TYPE -> "text/plain")),

    body = Enumerator("Hello world!".getBytes())

  )

}

물론 위의 예제에서 Ok result와 같은 일반적인 결과를 만들 수 있는 여러 도우미가 있습니다.

def index = Action {

  Ok("Hello world!")

}


val ok = Ok("Hello world!")

val notFound = NotFound

val pageNotFound = NotFound(<h1>Page not found</h1>)

val badRequest = BadRequest(views.html.form(formWithErrors))

val oops = InternalServerError("Oops")

val anyStatus = Status(488)("Strange response type")

이러한 헬퍼는 모두 play.api.mvc.Results 특성 및 동반자 개체에서 찾을 수 있습니다.


Redirects 또한 simple results 이다.

브라우저를 새로운 URL로 리디렉션하는 것은 다른 종류의 간단한 Result 타입일 뿐 입니다. 그러나
이 결과 타입은 응답 바디를 사용하지는 않습니다.

리다이렉션 결과를 만들 수있는 여러 도우미가 있습니다.

def index = Action {

  Redirect("/user/home")

}

기본값은 303 SEE_OTHER 응답 유형을 사용하는 것이지만 필요한 경우보다 구체적인 상태 

코드를 설정할 수도 있습니다.

def index = Action {

  Redirect("/user/home", MOVED_PERMANENTLY)

}



'PlayFramework2' 카테고리의 다른 글

[Play2] DI (의존성 주입)  (0) 2016.10.10
[Play2] Cookie 와 Session  (0) 2016.09.29
[Play2] Filter (번역)  (0) 2016.09.28
[Play2] Action composition (번역)  (0) 2016.09.28
Play2.4 로 웹 개발 시작하기 - (4) 로깅처리  (0) 2016.09.27

플레이는 간단한 필터 API 를 각 요청에 대해 전역적으로 대응하기 위해서 제공한다.

필터 vs  액션  컴포지션

필터 API 는 모든 요청에 대해 동일하게 작동하기 위해서 존재한다. (Cross-Cutting Concern 이라고 함) 예를들어 아래와 같은 공통 관심사 말이다. 

대조적으로  action composition 는 특별한 관심사항에 대해서만 작동하는 의도를 가지고 있다.  인증/인가 및 캐싱 등 말이다. 만약 필터가 모든 라우트에 작동하길 원하지 않는다고 하자. 그 때 사용하라 그러면  더 효과적이다. 당신 스스로의 액션 빌터를 만들수 있다는것을 잊지 말자. 행사(얼개)코드를 상당히 줄일 수 있을 것이다. 

간단한 로깅 필터 만들기

플레이 프레임워크에서 얼마나 오랫동안 리퀘스트가 실행되는지 로깅하는 예제이다. 

import play.api.Logger
import play.api.mvc._
import scala.concurrent.Future
import play.api.libs.concurrent.Execution.Implicits.defaultContext

class LoggingFilter extends Filter {

def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {

val startTime = System.currentTimeMillis

nextFilter(requestHeader).map { result =>

val endTime = System.currentTimeMillis
val requestTime = endTime - startTime

Logger.info(s"${requestHeader.method} ${requestHeader.uri} " +
s"took ${requestTime}ms and returned ${result.header.status}")

result.withHeaders("Request-Time" -> requestTime.toString)
}
}
}

여기서 일어나는 일을 이해해 보자면  첫 번째로 주목해야 할 것은 theapply 메소드의 서명입니다. 첫 번째 매개변수인 nextFilter는 요청헤더를 가져 와서 결과를 생성하는 함수이고,두 번째 매개 변수 인 requestHeader는 들어오는 요청의 실제 요청 헤더입니다.

nextFilter 매개 변수는 필터 체인의 다음 동작을 나타냅니다.어떤 이유로든 요청을 차단하려는 경우 호출하지 않을 수도 있습니다.

체인에서 다음 필터를 호출하기 전에 타임 스탬프를 저장합니다. 다음 필터를 호출하면 결국 [결과]가 반환되는 Future [결과]가 반환됩니다. 비동기 결과에 대한 자세한 내용은 비동기 결과 처리 장을 참조하십시오. 그런 다음 결과를 취하는 클로저로 map 메소드를 호출하여 Future 의 결과를 조작합니다. request.withHeaders ( "Request-Time"-> requestTime.toString)를 호출하여 요청을 처리하는 데 걸린 시간을 계산하고 응답 한 다음 응답 헤더에서 클라이언트로 다시 보냅니다.

필터 사용하기 

The simplest way to use a filter is to provide an implementation of the HttpFilters trait in the root package:

import javax.inject.Inject
import play.api.http.HttpFilters
import play.filters.gzip.GzipFilter

class Filters @Inject() (
gzip: GzipFilter,
log: LoggingFilter
) extends HttpFilters {

val filters = Seq(gzip, log)
}

다른 환경에서 다른 필터를 사용하거나 이 클래스를 루트 패키지에 포함하지 않으려는 경우 application.conf의 play.http.filters를 다음과 같이 정규 클래스 이름으로 설정하여 Play에서 클래스를 찾을 위치를 구성 할 수 있습니다. 클래스. 예 :

play.http.filters=com.example.MyFilters

필터들은 어디에 적합할까?

필터를 사용하여 라우터에 영향을 주는 경로, 메소드 또는 쿼리 매개 변수를 변환 할 수 없습니다. 그러나 필터에서 직접 해당 작업을 호출하여 다른 작업으로 요청을 보낼 수 있지만 나머지 필터 체인은 무시할 수 있습니다. 라우터를 호출하기 전에 요청을 수정해야하는 경우 논리를Global.onRouteRequest에 배치하는 것이 더 좋습니다.

필터는 라우팅이 완료된 후에 적용되기 때문에 RequestHeader의 태그 맵을 통해 요청으로부터 라우팅 정보에 액세스 할 수 있습니다. 예를 들어, 작업 메소드에 대해 시간을 기록 할 수 있습니다. 이 경우 logTime 메소드를 다음과 같이 업데이트 할 수 있습니다.

import play.api.mvc.{Result, RequestHeader, Filter}
import play.api.{Logger, Routes}
import scala.concurrent.Future
import play.api.libs.concurrent.Execution.Implicits.defaultContext

object LoggingFilter extends Filter {
  def apply(nextFilter: RequestHeader => Future[Result])
           (requestHeader: RequestHeader): Future[Result] = {

    val startTime = System.currentTimeMillis

    nextFilter(requestHeader).map { result =>

      val action = requestHeader.tags(Routes.ROUTE_CONTROLLER) +
        "." + requestHeader.tags(Routes.ROUTE_ACTION_METHOD)
      val endTime = System.currentTimeMillis
      val requestTime = endTime - startTime

      Logger.info(s"${action} took ${requestTime}ms" +
        s" and returned ${result.header.status}")

      result.withHeaders("Request-Time" -> requestTime.toString)
    }
  }
}

Routing tags are a feature of the Play router. If you use a custom router, or return a custom action in Global.onRouteRequest, these parameters may not be available.

좀 더 파워풀한 필터들

Play는 EssentialFilter라는 하위 수준의 필터 API를 제공하여 요청 본문에 대한 모든 액세스 권한을 제공합니다. 이 API를 사용하면 EssentialAction을 다른 작업으로 래핑 할 수 있습니다.

다음은 EssentialFilter로 다시 작성된 필터 예제입니다.

import play.api.Logger
import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits.defaultContext

class LoggingFilter extends EssentialFilter {
  def apply(nextFilter: EssentialAction) = new EssentialAction {
    def apply(requestHeader: RequestHeader) = {

      val startTime = System.currentTimeMillis

      nextFilter(requestHeader).map { result =>

        val endTime = System.currentTimeMillis
        val requestTime = endTime - startTime

        Logger.info(s"${requestHeader.method} ${requestHeader.uri}" +
          s" took ${requestTime}ms and returned ${result.header.status}")
        result.withHeaders("Request-Time" -> requestTime.toString)

      }
    }
  }
}


여기에서 핵심적인 차이점은 전달 된 다음 액션을 감싸는 새로운 EssentialAction을 만드는 것과 별개로, 우리가 다음에 호출 할 때 반복문을 가져 오는 것입니다. 당신이 원한다면 Enumeratee에서 이것을 변환하여 변환 할 수 있습니다. 그런 다음 iteratee의 결과를 매핑하여 처리합니다.

두 가지 필터 API가있는 것 같지만 EssentialFilter는 하나뿐입니다. 이전 예제 인 extendsEssentialFilter의 간단한 필터 API는 새로운 EssentialAction을 만들어 구현합니다. 전달 된 콜백은 body 파싱 및 나머지 액션이 비동기 적으로 실행되는 동안 theResult에 대한 Promise을 만들어 본문 파싱을 건너 뛰는 것처럼 보입니다.


액션 컴포지션 

커스텀 액션 빌더


이전에 요청 매개 변수없이 요청 매개 변수를 사용하고 본문 파서 등을 사용하여 여러 가지 방법으로 동작을 선언하는 방법을 보았습니다. 실제로는 비동기 프로그래밍에 대한 장에서 설명 할 것입니다.

이러한 액션 작성 메소드는 실제로 모두 ActionBuilder라는 특성에 의해 정의되며 우리가 액션을 선언하는 데 사용하는 Action 객체는이 특성의 인스턴스에 지나지 않습니다. 자신의 ActionBuilder를 구현함으로써 재사용 가능한 액션 스택을 선언 할 수 있으며, 액션 스택을 사용하여 액션을 빌드 할 수 있습니다.

로깅 데코레이터의 간단한 예제부터 시작하여이 액션에 대한 각 호출을 기록하려고합니다.

첫 번째 방법은 invokeBlock 메서드에서이 기능을 구현하는 것입니다.이 메서드는 ActionBuilder에서 빌드 한 모든 액션에 대해 호출됩니다.

import play.api.mvc._

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    Logger.info("Calling action")
    block(request)
  }
}

Now we can use it the same way we use Action:

def index = LoggingAction {
  Ok("Hello World")
}

Since ActionBuilder provides all the different methods of building actions, this also works with, for example, declaring a custom body parser:

ActionBuilder는 액션을 구현하는 다양한 방법을 제공하기 때문에, 예를 들어 커스텀 바디 파서를 선언하는 경우에도 사용할 수 있습니다.

def submit = LoggingAction(parse.text) { request =>
  Ok("Got a body " + request.body.length + " bytes long")
}

Composing actions

대부분의 애플리케이션에서 여러 액션 빌더를 원할 것입니다. 일부는 다른 유형의 인증을 수행하고, 일부는 다양한 유형의 일반 기능을 제공합니다.이 경우, 각 유형에 대한 로깅 조치 코드를 다시 작성하지 않을 것입니다 우리는 재사용 가능한 방법으로 그것을 정의하고 싶을 것이다.


재사용 가능한 액션 코드는 액션을 래핑하여 구현 될 수 있습니다.

import play.api.mvc._

case class Logging[A](action: Action[A]) extends Action[A] {

  def apply(request: Request[A]): Future[Result] = {
    Logger.info("Calling action")
    action(request)
  }

  lazy val parser = action.parser
}

We can also use the Action action builder to build actions without defining our own action class:

import play.api.mvc._

def logging[A](action: Action[A])= Action.async(action.parser) { request =>
  Logger.info("Calling action")
  action(request)
}

Actions can be mixed in to action builders using the composeAction method:

object LoggingAction extends ActionBuilder[Request] {
  def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
    block(request)
  }
  override def composeAction[A](action: Action[A]) = new Logging(action)
}

Now the builder can be used in the same way as before:

def index = LoggingAction {
  Ok("Hello World")
}

We can also mix in wrapping actions without the action builder:

def index = Logging {
  Action {
    Ok("Hello World")
  }
}

More complicated actions

지금까지 요청에 전혀 영향을 미치지 않는 작업 만 보여주었습니다. 물론 들어오는 요청 객체를 읽고 수정할 수도 있습니다.

import play.api.mvc._

def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
  val newRequest = request.headers.get("X-Forwarded-For").map { xff =>
    new WrappedRequest[A](request) {
      override def remoteAddress = xff
    }
  } getOrElse request
  action(newRequest)
}

Note: Play already has built in support for X-Forwarded-For headers.

We could block the request:

import play.api.mvc._

def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
  request.headers.get("X-Forwarded-Proto").collect {
    case "https" => action(request)
  } getOrElse {
    Future.successful(Forbidden("Only HTTPS requests allowed"))
  }
}

And finally we can also modify the returned result:

import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits._

def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
  action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}

Different request types

작업 구성을 사용하면 HTTP 요청 및 응답 수준에서 추가 처리를 수행 할 수 있지만 종종 요청 자체에 대한 컨텍스트를 추가하거나 유효성 검사를 수행하는 데이터 변환의 파이프 라인을 작성하려고합니다. ActionFunction은 요청에서 입력 요청 유형과 다음 계층으로 전달되는 출력 유형에 대해 매개 변수화 된 함수로 생각할 수 있습니다. 각 작업 함수는 인증, 개체에 대한 데이터베이스 조회, 사용 권한 확인 또는 여러 작업에서 작성하고 다시 사용하려는 기타 작업과 같은 모듈 식 처리를 나타낼 수 있습니다.


다양한 유형의 처리에 유용한 ActionFunction을 구현하는 몇 가지 사전 정의 된 특성이 있습니다.

  • ActionTransformer can change the request, for example by adding additional information.
  • ActionFilter can selectively intercept requests, for example to produce errors, without changing the request value.
  • ActionRefiner is the general case of both of the above.
  • ActionBuilder is the special case of functions that take Request as input, and thus can build actions.

You can also define your own arbitrary ActionFunction by implementing theinvokeBlock method. Often it is convenient to make the input and output types instances of Request (using WrappedRequest), but this is not strictly necessary.

또한 invokeBlock 메서드를 구현하여 임의의 ActionFunction을 정의 할 수도 있습니다. 종종 WrappedRequest를 사용하여 Request의 입력 및 출력 유형 인스턴스를 만드는 것이 편리하지만, 이는 꼭 필요한 것은 아닙니다.

Authentication

액션 함수의 가장 일반적인 사용 사례 중 하나가 인증입니다. 원래 요청에서 사용자를 결정하고이를 새로운 UserRequest에 추가하는 자체 인증 조치 변환기를 쉽게 구현할 수 있습니다. 이것은 간단한 요청을 입력으로 받기 때문에 anActionBuilder이기도합니다.

import play.api.mvc._

class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)

object UserAction extends
    ActionBuilder[UserRequest] with ActionTransformer[Request, UserRequest] {
  def transform[A](request: Request[A]) = Future.successful {
    new UserRequest(request.session.get("username"), request)
  }
}

Play also provides a built in authentication action builder. Information on this and how to use it can be found here.


참고 : 내장 된 인증 작업 빌더는 간단한 경우 인증을 구현하는 데 필요한 코드를 최소화하는 편리한 도우미 일 뿐이며 구현은 위의 예와 매우 비슷합니다.

내장 된 인증 조치로 충족 될 수있는 것보다 더 복잡한 요구 사항이있는 경우 자체 구현은 단순하지 않을뿐만 아니라 권장됩니다.

Adding information to requests

이제 Item 유형의 객체와 함께 작동하는 REST API를 살펴 보겠습니다. / item / : itemId 경로 아래에 경로가 많이있을 수 있으며 각 경로는 항목을 조회해야합니다. 이 경우,이 논리를 활동 함수에 넣는 것이 유용 할 수 있습니다.


우선, UserRequest에 Item을 추가하는 요청 객체를 생성합니다.

import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  def username = request.username
}

이제는 해당 항목을 조회하고 오류 (Left) 또는 새 ItemRequest (Right)를 반환하는 액션 구체화기를 만듭니다. 이 액션 구체화자는 아이템의 ID를 취하는 메소드 내에 정의됩니다.

def ItemAction(itemId: String) = new ActionRefiner[UserRequest, ItemRequest] {
  def refine[A](input: UserRequest[A]) = Future.successful {
    ItemDao.findById(itemId)
      .map(new ItemRequest(_, input))
      .toRight(NotFound)
  }
}

Validating requests

마지막으로 요청을 계속할지 여부를 확인하는 액션 함수가 필요할 수 있습니다. 예를 들어, UserAction의 사용자가 ItemAction의 항목에 액세스 할 수있는 권한이 있는지 여부를 확인하고 오류가 아닌 경우 오류를 반환합니다.

object PermissionCheckAction extends ActionFilter[ItemRequest] {
  def filter[A](input: ItemRequest[A]) = Future.successful {
    if (!input.item.accessibleByUser(input.username))
      Some(Forbidden)
    else
      None
  }
}

Putting it all together

이제 액션을 생성하기 위해 andThen을 사용하여 ActionBuilder로 시작하는 이들 액션 함수를 함께 연결할 수 있습니다.

def tagItem(itemId: String, tag: String) =
  (UserAction andThen ItemAction(itemId) andThen PermissionCheckAction) { request =>
    request.item.addTag(tag)
    Ok("User " + request.username + " tagged " + request.item.id)
  }

Play also provides a global filter API , which is useful for global cross cutting concerns.


 Play2.4 로 웹 개발 시작하기 

쉽고, 재밌고, 강력하고, 편리한 웹 프레임워크 Play2  (scala 언어기반) 을 배워봅시다.

1. Play2.4 웹 개발 시작하기 -  설치 및 프로젝트 만들기 

2. Play2.4 웹 개발 시작하기 -  프로젝트 살펴보기 

3. Play2.4 웹 개발 시작하기 -  나만의 프로젝트 만들기 

4. Play2.4 웹 개발 시작하기 -  로깅처리



4. 로깅처리

Play2 프레임워크는 기본적으로 logback 을 지원하므로 별다른 의존성을 추가할 필요는 없다.

먼저 application.conf 를 보면 기본적으로 아래와 같이 세팅되어 있는데 주석처리하고 


# Root logger:
logger.root=DEBUG

# Logger used by the framework:
logger.play=DEBUG

# Logger provided to your application:
logger.application=DEBUG


conf/logback.xml 파일을 추가 해서 logback 관련 내용을 채우자. 

기본적으로 채워지는 내용은 아래와 같다.

<configuration>

<conversionRule conversionWord="coloredLevel" converterClass="play.api.Logger$ColoredLevel" />

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home}/logs/application.log</file>
<encoder>
<pattern>%date - [%level] - from %logger in %thread %n%message%n%xException%n</pattern>
</encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%coloredLevel %logger{15} - %message%n%xException{5}</pattern>
</encoder>
</appender>

<logger name="play" level="INFO" />
<logger name="application" level="DEBUG" />

<root level="ERROR">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>

</configuration>

출력이 파일과 콘솔 두 방향으로 나온다.
개인적으로 application 의 레벨을 DEBUG 로 바꾸었다.


import play.api.Logger

// thing //
def thingByDate (kind: String, year: Int, month: Int) = Action {

Logger.debug("Attempting Logging.")

val res : List[ThingStat] = kind match {
case "1" => statisticsModel.analysisThingDayStatistics(year, month)
case "2" => statisticsModel.analysisThingMonthStatistics(year)
case "3" => statisticsModel.analysisThingYearStatistics()

case _ => Logger.error("Exception with abnormalArgument") ; Nil
}

Ok(Json.toJson(res))

}

play.api.Logger 를 임포트 한후에 Logger.debug("Attempting Logging.")  을 찍으면 잘 출력된다.


Logger.debug(s"thingByDate -> kind :  ${kind}")

이렇게  format 을 사용하는 대신해 간단하게  매개변수를 넣어서 출력 할 수 도 있다. 

case t: Throwable => {
// Log error with message and Throwable.
Logger.error("Exception with somthing ", t)

 예외를 출력해주고 싶을땐 위처럼 사용하면 된다.


 위의 로그출력은 디폴트 로거 ("application") 를 사용한 거였다.
 이제 자신만의 로거를 정의해 서 사용해보자.

val accessLogger: Logger = Logger("access")

 이렇게 자신만의 로거를 만들 수 있으며 보통은 클래스 당 하나씩  맞춰서 사용한다.

val logger = Logger(this.getClass())



나머지 logback 그 자체에 대한 내용은 전용 문서를 찾아보자.

specs2 로 어플리케이션 테스트하기

어플리케이션 위한  테스트를 작성하는것은 개발 프로세스에 포함 될 수 있다. 플레이는 테스트 작성을 가능한 쉽게 할 수 있도록  기본 테스트 프레임워크를   제공한다. 

살펴보기

테스트를 위한 파일의 위치는 "test" 폴더이다. 거기엔 2개의 샘플 테스트파일이 이미 존재하는데 당신 자신의 테스트를 작성하기 위한 템플릿으로 사용될 수 있을 것 이다. 

플레이 콘솔에서 테스트를 실행 할 수 있다. (IntelliJ 같은 툴에서 실행도 가능) 

  • * 모든 테스트를 실행하기 위해 test 을 run 하라.
  • * 하나의 테스트 클래스를 실행하기 위해서는 run test-only  를 하고 이어서 클래스이름을 써라.
      다음과 같이  test-only my.namespace.MySpec.
  • * 오직 실패한 테스트를 돌리기 위해서 test-quick.
  • * 테스트를 이어서 계속하기 위해  ~test-quick.
  • *FakeRequest  같은 테스트 헬퍼에 접근하기위해 run test:console.

SBT 기반의 플레이를 테스트 하는것과 관련된 전체 문서는 testing SBT 를 참고 하시라

specs2 사용하기

플레이 specs2 를 사용하기 위해 의존성을 다음과 같이 추가하라.

libraryDependencies += specs2 % Test

specs2 는 테스트들이 specifications 로 구성되어 있는데 이것은 다양한 코드패스들을 통해 테스트기반한 시스템을 실행하는 여러가지 예제를 포함한다.

Specifications 는 Specification trait 를 상속하였으며 should/in format 을 사용한다. 

import org.specs2.mutable._

class HelloWorldSpec extends Specification {

  "The 'Hello world' string" should {
    "contain 11 characters" in {
      "Hello world" must have size(11)
    }
    "start with 'Hello'" in {
      "Hello world" must startWith("Hello")
    }
    "end with 'world'" in {
      "Hello world" must endWith("world")
    }
  }
}

Specifications 는 IntelliJ IDEA (using the Scala plugin) 또는 Eclipse (using the Scala IDE) 에서도사용할 수 있다. 자세한건 다음링크를 참조하라.IDE page .

Note:  presentation compiler 버그때문에 이클립스에서테스트들은 반드시 specific format 을 정의해야한다.

  • 패키지는 반드시 디렉토리 패스와 동일해야한다. 
  • specification 반드시 @RunWith(classOf[JUnitRunner]) 주석과 같이 사용되야한다.

여기 이클립스를 위한 올바른 specification 을 보자:

package models // this file must be in a directory called "models"

import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._

@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
  ...
}

Matchers 

 반드시 example 결과를 리턴해야한다. 보통  must을 포함한 문장을 보게 될것이다.

"Hello world" must endWith("world")

must 키워드를 가진 평가식은 matche 로 알려져있다. Matchers 는 example result 를 리턴하며, 성공 혹은 실패이다. 만약 result 를 리턴하지 않으면 컴파일하지 않을것이다.

가장 유용한 매쳐는 match results이다.  동등성 체크하는데 사용하며  Option 과 Either 그리고 예외가 발생했는지를 체크하기위해 result 를 확인한다. 

optional matchers 도 있으며 XML 와 JSON matching 도 수행한다.

Mockito

Mocks 은 유닛테스트를 외부 디펜던시와 분리시키기 위해 사용된다. 예를들어 클래스가 외부 데이터 클래스에 의존적이라고 할때  완전한 데이타서비스 객체를 만드는것을 대신해서 적절한 가짜 데이터를 사용 할 수 있게 한다.

Mockito 는 specs2 에 디폴트 mocking library로 추가되어있다.

Mockito를 사용하기위해 다음 import 를 추가하자.

import org.specs2.mock._

다음과 같이 목을 만들 수 있다.

trait DataService {
  def findData: Data
}

case class Data(retrievalDate: java.util.Date)
import org.specs2.mock._
import org.specs2.mutable._

import java.util._

class ExampleMockitoSpec extends Specification with Mockito {

  "MyService#isDailyData" should {
    "return true if the data is from today" in {
      val mockDataService = mock[DataService]
      mockDataService.findData returns Data(retrievalDate = new java.util.Date())

      val myService = new MyService() {
        override def dataService = mockDataService
      }

      val actual = myService.isDailyData
      actual must equalTo(true)
    }
  }
  
}

Mocking 은 특별히 클래스의 공개 메소드를 테스팅하는데 유용하다. Mocking objects 와  private methods 도 가능하지만 좀 어렵다.

모델 유닛 테스팅  

플레이는 특별한 데이타베이스 데이터 접근 레이어를 사용하기 위한 모델을 요구하진 않는데 , 만약 어플리케이션이 Anomr이나 Slick 을 사용한다면, 종종 모델은 내부적으로 데이타접근을 위한 참조를 가질 것이다.

import anorm._
import anorm.SqlParser._

case class User(id: String, name: String, email: String) {
   def roles = DB.withConnection { implicit connection =>
      ...
    }
}

유닛테스팅을 위해 이 경우는  roles 메소드를 목으로 만들 수 있다.

일반적인 접근법은 데이타베이스로부터 모델들을 고립시켜서  로직을 테스트하는것이다.

case class Role(name:String)

case class User(id: String, name: String, email:String)
trait UserRepository {
  def roles(user:User) : Set[Role]
}
class AnormUserRepository extends UserRepository {
  import anorm._
  import anorm.SqlParser._

  def roles(user:User) : Set[Role] = {
    ...
  }
}

그리고 서비스를 통해 그들 (목) 에 접근한다.

class UserService(userRepository : UserRepository) {

  def isAdmin(user:User) : Boolean = {
    userRepository.roles(user).contains(Role("ADMIN"))
  }
}

이런 방식으로 isAdmin 메소드는 Mock(목) UserRepository  를 주입받고 테스팅 된다

object UserServiceSpec extends Specification with Mockito {

  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock[UserRepository]
      userRepository.roles(any[User]) returns Set(Role("ADMIN"))

      val userService = new UserService(userRepository)
      val actual = userService.isAdmin(User("11", "Steve", "user@example.org"))
      actual must beTrue
    }
  }
}

컨트롤러 유닛 테스트 

컨트롤러들이 정규 클래스라면 쉽게 플레이 테스트 헬퍼를 이용하여 유닛테스트를 할 수 있다. 하지만 만약 컨트롤러들이 또다른 클래들에 의존적이라면 dependency injection 를 사용할 수 있다. 이것은 그 의존성을 목(Mock) 으로 만들어 줄 것이다. 

class ExampleController extends Controller {
  def index() = Action {
    Ok("ok")
  }
}

요렇게 테스트 할 수 있다

import play.api.mvc._
import play.api.test._
import scala.concurrent.Future

object ExampleControllerSpec extends PlaySpecification with Results {

  "Example Page#index" should {
    "should be valid" in {
      val controller = new ExampleController()
      val result: Future[Result] = controller.index().apply(FakeRequest())
      val bodyText: String = contentAsString(result)
      bodyText must be equalTo "ok"
    }
  }
}

EssentialAction 유닛테스트 

 Action 과  Filter 를 테스팅 하기 위하여 EssentialAction 이 활용 된다.

Helpers.call 다음과 같이 사용된다.

object ExampleEssentialActionSpec extends PlaySpecification {

  "An essential action" should {
    "can parse a JSON body" in new WithApplication() {
      val action: EssentialAction = Action { request =>
        val value = (request.body.asJson.get \ "field").as[String]
        Ok(value)
      }

      val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))

      val result = call(action, request)

      status(result) mustEqual OK
      contentAsString(result) mustEqual "value"
    }
  }
}

https://www.playframework.com/documentation/2.5.x/ScalaTestingWithSpecs2 번역


 Play2.4 로 웹 개발 시작하기 
쉽고, 재밌고, 강력하고, 편리한 최강의 웹 프레임워크 Play2  (scala 언어기반) 을 배워봅시다.

환경

- Windows 10 

- JDK 1.8

- IntelliJ 2016.2.2 

- Scala 2.11

- Play 2.48 


연재 순서 

1. Play2 웹 개발 시작하기 -  설치 및 프로젝트 만들기 

2. Play2 웹 개발 시작하기 -  프로젝트 살펴보기 

3. Play2 웹 개발 시작하기 -  나만의 프로젝트 만들기 

*  2.5 에 대한 한글 자료가 부족한듯 싶습니다. 경험 풍부한 분들의 적극적인 정보 공유가 필요합니다.


3. 나만의 프로젝트 만들기

* 디폴트 프로젝트를 그대로 이용하겠습니다.


2-.1 컨트롤러 작성하기

 기존의 Application 소스를  이렇게 바꿉니다.

package controllers
import play.api.mvc._

object Application extends Controller {

def index = Action {
Ok("Hello world")
}

def hello(name: String) = Action {
Ok(views.html.main(name))
} }

hello 라는 메소드를 추가했습니다. 클라이언트가 http://localhost/hello? n = "john"  이라고 보내면 
인자로 이름을 받아서 main view 로 보내줍니다.


2-2 routes 추가하기 

GET     /hello   controllers.Application.hello(n: String)

 http://localhost/hello? n = "john"  를 hello 라는 메소드와 매핑하기 위한 라우트를 설정합니다.
 굉장히 직관적이며 간단합니다.  n 은 GET 메소드의 인자와 이름이 같아야합니다. 
 (컨트롤러의 인자이름은 달라도 됩니다. 2-1 에서 name 이었군요.) 

2-3. html 변경하기 

 * 기존 index.html 삭제하고 main.html 내용을 아래와 같이 바꿉니다.

@(name: String)
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1>Hello <em>@name</em></h1>
</body>
</html>

젤 첫라인에 name 인자를 받습니다. 앞에 골뱅이제외하고는 평범한 Scala방식입니다.
HTML 코드 내부에 @name 으로 사용합니다.


2-4. 실행하면 

def index = Action {Ok("Hello world")} 이 호출되어서 위의  화면이 나오고 


GET /hello controllers.Application.hello(n: String) 라우트에 의해

def hello(name: String) = Action {
Ok(views.html.main(name)) }

이게 호출되어 저 화면이 나옵니다.


3. 나만의 프로젝트에 DB 데이터 가져오기 추가

  * 원하는 db 를 이용해서 테이블을 하나 만들어 놓으세요. 컬럼은 int 형 id , string name 이면 됩니다. 
    저는 postgreSQL 를 이용하였습니다.


  3-1. DB 에 접근하기 위한 디펜던시를 설정해 줍니다. build.sbt 파일을 열어서 아래처럼 넣어줍니다.

     *  anorm 라이브러리에 관해서는 다음을 참고 합니다. Anorm 이란 

libraryDependencies ++= Seq(
jdbc ,
"com.typesafe.play" %% "anorm" % "2.5.0",
cache ,
ws ,
specs2 % Test,

"org.postgresql" % "postgresql" % "9.3-1102-jdbc41")

  3-2. app 에 오른쪽 클릭해서 models 패키지를 만들고 , MyModel 을 scala 클래스로 만들어 줍니다.


    3-2.  MyModel.scala 에 코딩합니다.

case class Thing (
thing_id : Int,
name : String
)

object Thing {
implicit def jsonWrites = Json . writes [ Thing ]
implicit def jsonReads = Json . reads [ Thing ]
}

object MyModel {

def findByID(id : Int): List[Thing] = DB.withConnection {

implicit connection =>
val sql = SQL("select * from tbl_thing_info where thing_id = {id}").on("id" -> id)

sql().map ( row =>

Thing(row[Int]("thing_id"), row[String]("name"))

).toList
}

}

모델클래스 Thing를 만듭니다. VO 라고 하죠?  굉장히 간단합니다.  GET/SET 가 없습니다. 
Thing 모델클래스의 컴패니언 객체를 Json 으로 내보내는데 사용하기 위해서 추가합니다.
MyModel의 findByID 를 통해서 DB 접속을 해서 데이터를 가져옵니다.Anorm 라이브러리를 이용하였으며 타입에 엄격한 모습을 보여줍니다. 그러면서도 매우 단순하죠. (스프링을 생각해보세요 ;;) 참고로 Anorm 은 MyBatis 와 비슷하다면 하이버네이트와 비슷한 Slick 이란것도 있습니다.


 3-3.  Application.conf 에 DB 접근 설정을 해줍니다.

      db.default.url="jdbc:postgresql://localhost/YOURDB"
      db.default.username=postgres
      db.default.password=1234

      db.default.driver=org.postgresql.Driver

PostgreSQL 에 접근하기위한 설정.


4. 나만의 프로젝트에 Json 객체로 내보내기 추가 

def FindThingByID (id  : Int) = Action {
val res3 : List[models.Thing] = MyModel.findByID(id)
Ok(Json.toJson(res3)) }

Json.toJson(res3)  이 한방으로 Json 객체를 클라이언트쪽으로 보내줍니다.



ps. FrontEnd 쪽에서 Ajax 를 통해서 호출해주고 Json 객체를 받는것은 이 포스트에서 제외했습니다.

  Play2.4 로 웹 개발 시작하기 
쉽고, 재밌고, 강력하고, 편리한 웹 프레임워크 Play2  (scala 언어기반) 을 배워봅시다.

환경

- Windows 10 

- JDK 1.8

- IntelliJ 2016.2.2 

- Scala 2.11

- Play 2.48 


연재 순서 

1. Play2 웹 개발 시작하기 -  설치 및 프로젝트 만들기 

2. Play2 웹 개발 시작하기 -  프로젝트 살펴보기 

3. Play2 웹 개발 시작하기 -  나만의 프로젝트 만들기 

*  2.5 에 대한 한글 자료가 부족한듯 싶습니다. 경험 풍부한 분들의 적극적인 정보 공유가 필요합니다.


2. 기본 프로젝트 살펴보기 

  설치 포스트를 통해 프로젝트를 만들면 아래와 같이 자동으로 프로젝트가 구성 됩니다.

    

  주요 구성 요소로는 
   가. 컨트롤러 (controllers)   :  클라이언트가 보낸  호출을 컨트롤 합니다. (현재 모델은 없습니다)
   나. 뷰 (views) :  사용자에게 보여질 HTML 페이지 입니다.
   다. 설정 (conf)  : application.conf 엔 로그,DB 접속등에 관한 설정을 적어줍니다.
                         routes 는 클라이언트 보낸 HTTP 를 컨트롤러에 매핑해줍니다.
   라. public 폴더 :   여기엔 js,css,img 등 각종 리소스를 넣습니다. 
   마. build.sbt  :  필요한 디펜던시를 설정하여 자동 다운로드 받게 합니다. 

  

구체적으로 살펴보시죠.

routes 파일입니다.

빨강색 박스안에 규칙을 설정해 줍니다.
/   경로로 보내면  controllers.Application.index 컨트롤러가 반응한다는 말입니다.
GET   /search  controllers.Application.search  이렇게 추가 할 수 있습니다.
굉장히 간단하면서 직관적입니다. 


Controllers 패키지내의 Application 입니다.
Controllers 를 상속받은 object 입니다.

routes 파일의 GET   /  controllers.Application.index   에 따라서 
매핑된 index 컨트롤러 함수가 정의되 있네요.
뷰의 index html 파일로 문자열을 보내는 기능만 있습니다. 
추후에 모델클래스 만들어서 DB에서 받아온 데이터를 넘겨주면 될거 같습니다.

views 패키지내의 index.scala.html 파일 입니다.

컨트롤러의 index  함수에서  Ok(views.html.index("Your new application is ready."))
이렇게 호출했었죠?  내부의 문자열 인자는  
index.scala.html 파일의 젤 위에 
@(message: String) 에 보내집니다. 
Scala 언어의 인자받는 구문과  똑같습니다. 

@main("Welcome to Play") 는 main.scala.html 를 호출하며 인자로 Welcome to Play를 보냅니다. 내부의 @play20.welcome(message) 는 main.scala.html 파일의 @content 에서 사용될 것입니다.


views 패키지내의 main.scala.html 파일 입니다.

index.scala.html 파일에서 보낸 "Welcome to Play" 는 (title: String) 에 담겨지며 
@play20.welcome(message) 는  @content 에 담겨집니다.

리소스는  @routes.Assets.at 이런식 접근합니다. 
<script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")"

@routes.Assets.at 는 routes 파일에 아래와 같이 정의되 있구요.
GET      /assets/*file      controllers.Assets.at(path="/public", file)


디폴트 웹 어플리케이션 모습 입니다.


내용 추가 예정 .. 


+ Recent posts