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)
위의 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에 의해 정의됩니다
대조적으로 action composition 는 특별한 관심사항에 대해서만 작동하는 의도를 가지고 있다. 인증/인가 및 캐싱 등 말이다. 만약 필터가 모든 라우트에 작동하길 원하지 않는다고 하자. 그 때 사용하라 그러면 더 효과적이다. 당신 스스로의 액션 빌터를 만들수 있다는것을 잊지 말자. 행사(얼개)코드를 상당히 줄일 수 있을 것이다.
여기서 일어나는 일을 이해해 보자면 첫 번째로 주목해야 할 것은 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:
다른 환경에서 다른 필터를 사용하거나 이 클래스를 루트 패키지에 포함하지 않으려는 경우 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.Futureimport play.api.libs.concurrent.Execution.Implicits.defaultContext
objectLoggingFilterextendsFilter{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.Loggerimport play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
classLoggingFilterextendsEssentialFilter{def apply(nextFilter:EssentialAction)=newEssentialAction{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에서 빌드 한 모든 액션에 대해 호출됩니다.
대부분의 애플리케이션에서 여러 액션 빌더를 원할 것입니다. 일부는 다른 유형의 인증을 수행하고, 일부는 다양한 유형의 일반 기능을 제공합니다.이 경우, 각 유형에 대한 로깅 조치 코드를 다시 작성하지 않을 것입니다 우리는 재사용 가능한 방법으로 그것을 정의하고 싶을 것이다.
작업 구성을 사용하면 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의 입력 및 출력 유형 인스턴스를 만드는 것이 편리하지만, 이는 꼭 필요한 것은 아닙니다.
액션 함수의 가장 일반적인 사용 사례 중 하나가 인증입니다. 원래 요청에서 사용자를 결정하고이를 새로운 UserRequest에 추가하는 자체 인증 조치 변환기를 쉽게 구현할 수 있습니다. 이것은 간단한 요청을 입력으로 받기 때문에 anActionBuilder이기도합니다.
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") 를 사용한 거였다. 이제 자신만의 로거를 정의해 서 사용해보자.
specs2 는 테스트들이 specifications 로 구성되어 있는데 이것은 다양한 코드패스들을 통해 테스트기반한 시스템을 실행하는 여러가지 예제를 포함한다.
Specifications 는 Specification trait 를 상속하였으며should/informat 을 사용한다.
import org.specs2.mutable._
classHelloWorldSpecextendsSpecification{"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 .
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])classApplicationSpecextendsSpecification{...}
import org.specs2.mock._
import org.specs2.mutable._
import java.util._
classExampleMockitoSpecextendsSpecificationwithMockito{"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 =newMyService(){overridedef dataService = mockDataService
}
val actual = myService.isDailyData
actual must equalTo(true)}}}
Mocking 은 특별히 클래스의 공개 메소드를 테스팅하는데 유용하다. Mocking objects 와 private methods 도 가능하지만 좀 어렵다.
모델 유닛 테스팅
플레이는 특별한 데이타베이스 데이터 접근 레이어를 사용하기 위한 모델을 요구하진 않는데 , 만약 어플리케이션이 Anomr이나 Slick 을 사용한다면, 종종 모델은 내부적으로 데이타접근을 위한 참조를 가질 것이다.
이런 방식으로 isAdmin 메소드는 Mock(목) UserRepository 를 주입받고 테스팅 된다
objectUserServiceSpecextendsSpecificationwithMockito{"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 =newUserService(userRepository)
val actual = userService.isAdmin(User("11","Steve","user@example.org"))
actual must beTrue
}}}
컨트롤러 유닛 테스트
컨트롤러들이 정규 클래스라면 쉽게 플레이 테스트 헬퍼를 이용하여 유닛테스트를 할 수 있다. 하지만 만약 컨트롤러들이 또다른 클래들에 의존적이라면 dependency injection 를 사용할 수 있다. 이것은 그 의존성을 목(Mock) 으로 만들어 줄 것이다.
import play.api.mvc._
import play.api.test._
import scala.concurrent.FutureobjectExampleControllerSpecextendsPlaySpecificationwithResults{"Example Page#index" should {"should be valid"in{
val controller =newExampleController()
val result:Future[Result]= controller.index().apply(FakeRequest())
val bodyText:String= contentAsString(result)
bodyText must be equalTo "ok"}}}
objectExampleEssentialActionSpecextendsPlaySpecification{"An essential action" should {"can parse a JSON body"innewWithApplication(){
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"}}}
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 이었군요.)
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 이란것도 있습니다.
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 에 담겨집니다.