Javalin : 자바와 코틀린을 위한 경량 웹 프레임워크 리뷰
- 원문제목 : Introducing Javalin: a Lightweight Web Framework for Java and Kotlin
- 원문링크 : https://www.infoq.com/news/2019/07/javalin/
독자분들의 이해를 돕기 위해 역자의 설명을 많이 추가하여 원본 글의 의도와는 다소 다를 수가 있으니 원본글도 같이 참고해주세요. 본문의 예제 코드 대부분은 Java10+ 문법을 기반으로 작성되었으나, 몇 개 예제는 Kotlin으로 작성되었습니다.
Javalin은 자바와 코틀린을 위한 경량 웹 프레임워크입니다. Javalin은 기본적으로 웹소켓, HTTP2 그리고 비동기 요청을 지원하며 구조가 심플하고 블로킹 모델로 설계되었습니다. 처음에는 SparkJava 프레임워크를 기반으로 만들어졌지만, 자바스크립트 프레임워크인 Koa.js로부터 영향을 받아 재작성되었습니다.
Javalin은 제티(Jetty)위에서 돌아가며, 제티로만 작성한 코드와 성능이 동일합니다. 개발자는 기존의 Servlet와 같은 프레임워크 상에서 정의한 클래스를 확장한다거나 이와 유사한 형태의 에노테이션을 사용할 필요가 없습니다. 또한, 자바와 코틀린 둘 중 어떤 언어를 쓰더라도 동일한 Javalin을 사용할 수 있습니다.
자바로 Javalin을 시작할때 개발자는 아래의 코드와 같이 오직 public static void main만 필요합니다.
1 2 3 4
public static void main(String[] args) { var app = Javalin.create().start(7000); app.get("/", ctx -> ctx.result("Hello World")); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
var app = Javalin.create(config -> { config.defaultContentType = "application/json"; config.autogenerateEtags = true; config.addStaticFiles("/public"); config.asyncRequestTimeout = 10_000L; config.dynamicGzip = true; config.enforceSsl = true; }).routes(() -> { path("users", () -> { get(UserController::getAll); post(UserController::create); path(":user-id"(() -> { get(UserController::getOne); patch(UserController::update); delete(UserController::delete); }); ws("events", userController::webSocketEvents); }); }).start(port);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// 검증이 없는 경우, String 또는 null을 리턴합니다. var myQpStr = ctx.queryParam("my-qp"); // Integer 또는 throws를 발생시킵니다. var myQpInt = ctx.pathParam("my-qp", Integer.class).get(); // 이 코드는 if (Integer > 4)와 동일합니다. var myQpInt = ctx.formParam("my-qp", Integer.class).check(i -> i > 4).get(); // 두개의 의존적인 파라미터의 검증 방법 var fromDate = ctx.queryParam("from", Instant.class).get(); var toDate = ctx.queryParam("to", Instant.class) .check(it -> it.isAfter(fromDate), "'to' has to be after 'from'") .get(); // JSON 바디 검증 로직 var myObject = ctx.bodyValidator(MyObject.class) .check(obj -> obj.myObjectProperty == someValue) .get();
위 코드에서 보다시피 요청으로 들어온 파라미터를 쉽게 검증하고, 원하는 타입에 맞게 바인딩 할 수 있습니다.
Java의 RequestFilter나 Golang의 Middleware와 같이 여러 프레임워크에서 지원하는 Request의 앞, 뒤단에서 공통적으로 처리해야 하는 기능은 Handler라는 이름으로 제공합니다. Javalin은 before-handlers, endpoint-handlers, after-handlers, exception-handlers, error-handlers등 총 5개의 Handler가 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
//before handlers app.before(ctx -> { // 모든 요청의 앞단에서 동작합니다. }); app.before("/path/*", ctx -> { // /path/* 로 시작하는 모든 요청의 앞단에서 동작합니다. }); //endpoint handlers app.get("/", ctx -> { // 구현 로직 ctx.json(object); }); app.get("/hello/*", ctx -> { // hello/* 의 하위 경로에 대한 모든 요청을 받습니다. }); //after handlers app.after(ctx -> { // 모든 요청의 뒷단에서 동작합니다. }); app.after("/path/*", ctx -> { // /path/* 로 시작하는 모든 요청의 뒷단에서 동작합니다. });
위 코드는 각각의 Handler들이 어떤 시점에 동작하는지 알 수 있습니다. 각각의 Handler들은 개발할 때 정말 유용하게 사용할 수 있을거라 생각합니다.
Javalin에선 함수형 인터페이스(functional interface)인 액세스 매니저(AccessManager)를 제공하여 인증/인가(authentication/authorization)를 처리 할수 있습니다. 개발자는 원한다면 자신만의 액세스 매니저를 구현할 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 액세스 매니저 설정 app.accessManager((handler, ctx, permittedRoles) -> { MyRole userRole = getUserRole(ctx); if (permittedRoles.contains(userRole)) { handler.handle(ctx); } else { ctx.status(401).result("Unauthorized"); } }); Role getUserRole(Context ctx) { // 요청을 확인하여 유저의 권한을 검증 // 일반적으로 Authorization 헤더를 검사한 뒤 수행함 } enum MyRole implements Role { ANYONE, ROLE_ONE, ROLE_TWO, ROLE_THREE; } app.routes(() -> { get("/un-secured", ctx -> ctx.result("Hello"), roles(ANYONE)); get("/secured", ctx -> ctx.result("Hello"), roles(ROLE_ONE)); });
액세스 매니저를 설정합니다. 이것은 Spring security의 UserDetailsService를 구현하는것과 비슷합니다. 권한이 없는 요청인 경우 HTTP 401 Unauthorized를 응답으로 내려줍니다.
app.routes의 get("/un-secured")와 get("/secured") 부분을 보면 roles에 따라 요청을 분기하는것을 알 수 있습니다.
Javalin 3.0 버전부터 OpenAPI(Swagger)도 플러그인으로 제공합니다. OpenAPI 3.0 스펙의 전체 구현은 DSL과 에노테이션 두 가지 방법으로 이용 가능합니다.
OpenAPI DSL
1 2 3 4 5 6 7 8 9
val addUserDocs = document() .body() .result("400") .result("204") fun addUserHandler(ctx: Context) { val user = ctx.body() UserRepository.addUser(user) ctx.status(204) }
OpenAPI 에노테이션
1 2 3 4 5 6 7 8 9 10 11 12
@OpenApi( requestBody = OpenApiRequestBody(User::class), responses = [ OpenApiResponse("400", Unit::class), OpenApiResponse("201", Unit::class) ] ) fun addUserHandler(ctx: Context) { val user = ctx.body() UserRepository.createUser(user) ctx.status(201) }
Javalin 애플리케이션을 배포하는데 개발자는 그저 의존성을 포함하는(maven-assembly-plugin 같은 플러그인을 이용하여) jar 파일을 만든 다음, java -jar filename.jar를 실행하기만 하면 됩니다. Javalin은 임베디드 제티를 포함하고 있어서 별도의 애플리케이션 서버는 필요하지 않습니다. 또한, 교육자를 위한 공식 페이지가 제공되고 있으며, 임베디드 제티가 포함되어 있기 때문에 서버 코딩에 필요한 서블릿 컨테이너(Servlet Container)/애플리케이션 서버 설정(Application Server configuration) 등과 같은 어려운 설정과 배경지식이 필요하지 않으므로 학습자가 쉽게 익힐 수 있다고 강조합니다.
공식 사이트에는 Running on GraalVM, Kotlin CRUD REST API 등과 같이 몇 개의 대표적인 예제들이 있습니다. 이 외에도 tutorials page에서 전체 목록을 확인할 수 있습니다.
Javalin에 대해 더 많은 정보를 찾고 싶다면 documentation page를 참고하세요. Javalin은 maven central에서 다운로드 가능합니다.