Play 2.6에서 달라진 점

플레이 프레임워크 2.6이 발표되면서 이전 버전과 달라진 부분들과 새롭게 추가된 기능들에 대해서 살펴보겠습니다.
Scala 2.12 지원
플레이 2.6은 스칼라 2.12와 2.11에 모두 대응하도록 빌드된 최초의 플레이 릴리즈입니다. 따라서 두 버전을 모두 지원할 수 있게 되었습니다.
스칼라의 버전을 선택하려면 build.sbt에 scalaVersion을 지정하면 됩니다.
스칼라 2.12
scalaVersion := "2.12.2"스칼라 2.11
scalaVersion := "2.11.11"“Global-State-Free” 애플리케이션
가장 큰 변화는 플레이가 전역 상태global state에 더 이상 의존하지 않게 되었다는 점입니다. 2.6에서도 여전히 play.api.Play.current/play.Play.application()을 통해서 전역 애플리케이션에 접근할 수는 있지만 이 기능은 사용 중지 예고deprecated되었습니다. 플레이 3.0에서는 더 이상 전역 상태가 존재하지 않게 됩니다.
다음과 같이 설정하면 전역 애플리케이션에 접근하는 것을 완전히 막을 수 있습니다.
play.allowGlobalApplication=false위와 같이 설정해 두면 Play.current를 호출할 때 예외가 발생합니다.
Akka HTTP 서버 백엔드
이제 플레이는 Akka-HTTP 서버 엔진을 기본 백엔드로 사용합니다. 플레이와 Akka-HTTP의 통합에 대한 자세한 사항은 Akka HTTP 서버 페이지를 참고하기 바랍니다.
Akka HTTP 설정에 대한 설명 페이지도 있습니다.
Netty 백엔드도 여전히 사용할 수 있으며 Netty 4.1를 사용하도록 업그레이드 되었습니다. 프로젝트에 명시적으로 Netty를 사용하도록 설정하려며 Netty 서버 페이지를 참고하기 바랍니다.
HTTP/2 지원 (experimental)
이제 플레이지는 Akka HTTP 서버를 기반으로 PlayAkkaHttp2Support모듈을 사용함으로써 HTTP/2를 지원할 수 있습니다.
lazy val root = (project in file("."))
.enablePlugins(PlayJava, PlayAkkaHttp2Support)이렇게 하면 자동적으로 HTTP/2 프로세스가 설정되며, 다만 run 명령으로 디폴트로 실행되지는 않습니다. 좀 더 자세한 사항은 Akka HTTP 서버 페이지를 참고하면 됩니다.
Request attributes
플레이 2.6의 Request에 애트리뷰트attributes가 포함되었습니다. 애트리뷰트를 사용하면 request 객체 내부에 추가적인 정보를 담을 수 있습니다. 예를 들어 request 내부에 attribute를 설정하는 필터를 작성하고 이 값들을 나중에 액션 내부에서 참조할 수 있습니다.
애트리뷰트attribute는 각 요청request에 첨부된 TypedMap으로 저장됩니다. TypedMap은 불변 맵으로 타입이 지정된 키-밸류 쌍을 저장합니다. 애트리뷰트는 키로 인텍스되고 키의 타입은 애트리뷰트의 타입을 나타냅니다.
// 사용자 객체를 저장하기위한 TypedKey를 생성합니다.
object Attrs {
val User: TypedKey[User] = TypedKey.apply[User]("user")
}
// 사용자 객체를 요청으로 부터 획득합니다.
val user: User = req.attrs(Attrs.User)
// 사용자 객체를 요청에 저장합니다.
val newReq = req.addAttr(Attrs.User, newUser)애트리뷰트가 저장되는 TypedMap에 대한 자세한 사항은 Scaladoc을 참고하면 됩니다.
요청request의 태그tag는 이제 사용 중지 권고deprecated되었으므로 대신 애트리뷰트를 사용하도록 변경해야 합니다. 태그 마이그레이션에 관한 문서를 참고하면 됩니다.
Route modifier tags
routes 파일의 새로운 문법에 따라 사용자가 지정한 동작을 하도록 변경자modifier를 추가할 수 있게 되었습니다. CSRF 필터에 구현되어 있는 변경자가 “nocsrf” 태그입니다. 아래와 같은 설정을 통해 해당 라우트에 CSRF 필터가 적용되지 않도록 합니다.
+ nocsrf # 이 라우트에는 CSRF 보안을 적용하지 않습니다.
POST /api/foo/bar ApiController.foobar사용자 정의 변경자를 만들 수 있으며 + 기호 다음에 공백 문자로 구분된 태그들을 나열해서 표시합니다.
routes 파일에 정의된 핸들러의 메타 데이터는 Request의 HandlerDef 애트리뷰트attribute를 통해서 접근할 수 있습니다.
import play.api.routing.{ HandlerDef, Router }
import play.api.mvc.RequestHeader
val handler = request.attrs(Router.Attrs.HandlerDef)
val modifiers = handler.modifiersInjectable Twirl Templates
Twirl 템플릿에서 @this 생성자 어노테이션을 사용할 수 있게 되었습니다. 생성자 어노테이션은 Twirl 템플릿이 템플릿 내에 직접 삽입되어 자신만의 의존성 관리를 할 수 있게 만들어 주기 때문에 컨트롤러로 의존성을 관리해야할 필요가 없어집니다.
Filter Enhancements
플레이에 환경설정으로 정의할 수 있는 다음과 같은 디폴트 필터들이 활성화된 채로 배포됩니다.
play.filters.csrf.CSRFFilter는 CSRF 공격을 막아줍니다. ScalaCsrf를 참조play.filters.headers.SecurityHeadersFilter는 XSS와 frame origin 공격을 막아줍니다. SecurityHeaders를 참조play.filters.hosts.AllowedHostsFilter는 DNS rebinding 공격을 막아줍니다. AllowedHostsFilter를 참조
필터는 application.conf에서 설정할 수 있습니다. 디폴트 필터 리스트를 확장하려면 다음과 같이 +=를 사용합니다.
play.filters.enabled += MyFilter테스트를 위해서 특정 필터를 사용하지 않으려면 다음과 같이 설정합니다.
play.filters.disabled += MyFilter
CSRF.formField와 같은 CSRF 폼 헬퍼(form helpers)를 사용하지 않는 이전의 프로젝트에서 플레이 2.6으로 이전한다면 PUT이나 POST 요청에 대해서 "403 Forbidden"이 발생할 수 있습니다. 이런 동작을 확인하려면logback.xml에<logger name="play.filters.csrf" value="TRACE"/>를 추가하면 됩니다.마찬가지 이유로 localhost 이외의 설정으로 플레이 애플리케이션을 운영하려면 AllowedHostsFilter를 설정해서 연결을 허용할 호스트 이름이나 IP주소를 설정해야 합니다.
gzip filter
gzip 필터를 사용하면 어떤 응답을 gzip으로 압축할지 말지를 컨텐츠 타입을 통해서 제어할 수 있습니다.
play.filters.gzip {
contentType {
# 여기에 설정된 컨텐츠 타입에 대해서만 gzip 압축을 수행합니다.
whiteList = [ "text/*", "application/javascript", "application/json" ]
# whiteList가 빈 값으로 설정한 경우에만 blackList가 유효합니다.
# blackList에 설정된 컨텐츠 타입 이외에 모든 응답을 압축하게 됩니다.
blackList = []
}
}JWT Cookies
이제부터 플레이는 세션과 쿠키에 JSON Web Token 포맷을 사용해서 표준 서명된 쿠키 데이터 포맷과 쿠키 만료(replay 공격이 어렵도록)를 허용합니다.
좀 더 자세한 사항은 여기를 참조하면 됩니다.
Logging Marker API
play.Logger와 play.api.Logger에서 SLF4J Marker를 지원합니다.
스칼라 API에서 마커는 markerContext 트레이트를 통해서 추가되어 로거 메서드에 암시적 파라미터로 추가됩니다.
import play.api._
logger.info("some info message")(MarkerContext(someMarker))이렇게 하면 암시적 마커가 복수의 로깅 문장에 걸쳐 전달됩니다. 예를 들어 Logstash Logback Encoder와 implicit conversion chain을 사용하면, 요청request 정보가 로깅 문장에 자동으로 인코딩됩니다.
trait RequestMarkerContext {
// Adding 'implicit request' enables implicit conversion chaining
// See http://docs.scala-lang.org/tutorials/FAQ/chaining-implicits
implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
import net.logstash.logback.marker.LogstashMarker
import net.logstash.logback.marker.Markers._
val requestMarkers: LogstashMarker = append("host", request.host)
.and(append("path", request.path))
MarkerContext(requestMarkers)
}
}이렇게 해서 컨트롤러 내부에서 사용할 수 있으며 Future를 통해서 전달되어 서로 다른 실행 컨텍스트에서도 사용할 수도 있습니다.
def asyncIndex = Action.async { implicit request =>
Future {
methodInOtherExecutionContext() // implicit conversion here
}(otherExecutionContext)
}
def methodInOtherExecutionContext()(implicit mc: MarkerContext): Result = {
logger.debug("index: ") // same as above
Ok("testing")
}마커 컨텍스트를 사용하면 특정 요청에 대한 로그를 확인하기 위해서 명시적으로 로그 레벨을 변경하지 않고도 가능합니다. 예를 들어 특정 조건에 해당되는 경우에만 마커를 추가할 수 있습니다.
trait TracerMarker {
import TracerMarker._
implicit def requestHeaderToMarkerContext(implicit request: RequestHeader): MarkerContext = {
val marker = org.slf4j.MarkerFactory.getDetachedMarker("dynamic") // base do-nothing marker...
if (request.getQueryString("trace").nonEmpty) {
marker.add(tracerMarker)
}
marker
}
}
object TracerMarker {
private val tracerMarker = org.slf4j.MarkerFactory.getMarker("TRACER")
}
class TracerBulletController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) with TracerMarker {
private val logger = play.api.Logger("application")
def index = Action { implicit request: Request[AnyContent] =>
logger.trace("Only logged if queryString contains trace=true")
Ok("hello world")
}
}이렇게 하고 logback.xml에서 다음과 같은 TurboFilter를 통해서 로깅을 할 수 있습니다.
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Name>TRACER_FILTER</Name>
<Marker>TRACER</Marker>
<OnMatch>ACCEPT</OnMatch>
</turboFilter>더 자세한 사항에 대해서는 ScalaLogging을 참조하기 바랍니다.
로깅 마커에 대해서는 TurboFilters와 마커 기반 트리거를 참조하면 됩니다.
환경설정 개선사항
스칼라 API에서 play.api.Configuration 클래스에 커스텀 타입을 로딩하기위한 새로운 메서드들을 도입하였습니다. 원한다면 암시적으로 ConfigLoader를 사용해서 로드할 수 있으며 Configuration#get[T]를 통해서 해당 키에 대한 값을 T 타입으로 가져올 수 있습니다.
Security Logging
플레이에 보안관련 필터들이 디폴트로 설정되는 관계로 특정 요청이 보안에 관련된 이유로 실패하는 경우에 개발자가 이유를 확인할 수 있도록 WARN 수준으로 해당 상황이 로그로 남도록 변경되었습니다.
보안 실패가 발생하면 일반 로그와 구분되도록 보안 마커가 필터하고 실행됩니다. 예를 들어 모든 SECURITY 마커를 비활상하려면 다음과 같이 logback.xml에 설정해야합니다.
<turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
<Marker>SECURITY</Marker>
<OnMatch>DENY</OnMatch>
</turboFilter>퓨처 타임아웃과 지연에 대한 지원
플레이가 비동기 퓨처를 지원하는 방식이 Futures 트레이트를 사용하도록 개선되었습니다.
play.libs.concurrent.Futures 인터페이스를 사용해서 CompletionStage를 감싸서 비블로킹 타임아웃을 적용할 수 있습니다.
class MyClass {
@Inject
public MyClass(Futures futures) {
this.futures = futures;
}
CompletionStage<Double> callWithOneSecondTimeout() {
return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
}
}또는 스칼라 API에서 play.api.libs.concurrent.Futures 트레이트를 사용합니다.
import play.api.libs.concurrent.Futures._
class MyController @Inject()(cc: ControllerComponents)(implicit futures: Futures) extends AbstractController(cc) {
def index = Action.async {
// withTimeout is an implicit type enrichment provided by importing Futures._
intensiveComputation().withTimeout(1.seconds).map { i =>
Ok("Got result: " + i)
}.recover {
case e: TimeoutException =>
InternalServerError("timeout")
}
}
}delayed 메서드가 있어서 지정된 시간이 후에 Future를 실행하도록 할 수 있습니다.
Play WSClient 개선
플레이의 WSClient에 많은 개선이 있었습니다. 이제 플레이의 WSClient는 play-ws의 랩퍼이며 play-ws는 독립적으로 플레이 외부에서도 사용할 수 있습니다. 더불어 play-ws에 포함되어 있는 라이브러리들은 감쳐져 있기 때문에 다른 버전의 Netty를 사용하는 스파크Spark와 같은 다른 라이브러리와 충돌일 발생하지 않습니다.
Play JSON 개선
튜플의 직렬화 기능
튜플을 play-json에서 직렬화되며 Reads와 Writes의 암시적 구현이 존재합니다. 튜플은 배열로 직렬화 됩니다. 따라서 ("foo", 2, "bar")는 JSON으로 ["foo", 2, "bar]로 표현됩니다.
Scala.js 지원
플레이 JSON 2.6.0에서부터 Scala.js를 지원합니다. 다음과 같이 의존성을 추가할 수 있습니다.
libraryDependencies += "com.typesafe.play" %%% "play-json" % version테스트 개선
기능 시험을 위한 play.api.test 팩키지는 의존성 삽입dependency inject를 통해서 기능 시험을 좀 더 편리하게 할 수 있습니다.
Injecting
암시적 app을 직접 삽입할 수 있습니다.
"test" in new WithApplication() {
val executionContext = app.injector.instanceOf[ExecutionContext]
...
}Injecting 트레이트로 간략한 표현도 가능합니다.
"test" in new WithApplication() with Injecting {
val executionContext = inject[ExecutionContext]
...
}StubControllerComponents
StubControllerComponentsFactory로 ControllerComponents의 스텁을 생성하여 컨트롤로의 단위 시험에 활용할 수 있습니다.
val controller = new MyController(stubControllerComponents())StubBodyParser
StubBodyParserFactory로 BodyParser 스텁을 생성하여 컨텐트에 대한 단위 시험에 활용할 수 있습니다.
val stubParser = stubBodyParser(AnyContent("hello"))파일 업로드 개선
파일 업로딩에 TemporaryFile API를 사용할 수 있으며 스칼라 API에서 ref 애트리뷰트를 통해서 접근할 수 있습니다.
파일 업로드는 근본적으로 위험한 기능입니다. 왜냐하면 무제한의 파일 업로드는 파일 시스템을 다 채워버릴 수 있기 때문입니다.
TemporaryFileReaper
play.api.libs.Files.TemporaryFileReaper를 활성화해서 Akka 스케쥴러에 기반으로 스케쥴에 따라 임시 파일을 삭제할 수 있습니다.
reaper는 디폴트로 비활성화 되어 있으며 사용하려면 다음과 같이 application.conf에 지정해야 합니다.
play.temporaryFile {
reaper {
enabled = true
initialDelay = "5 minutes"
interval = "30 seconds"
olderThan = "30 minutes"
}
}