생계유지형 개발자/Spring Framework

[Spring5] WebFlux + Thymeleaf + Kotlin

이 가을 2021. 2. 2. 19:09

스프링 부트 초창기에 Spring MVC 에서 Thymeleaf를 사용해보았는데, WebFlux 에서 사용하기는 처음이었다.

더군다나 코틀린 언어도 처음 접해보는 거라서 많이 낯설었다.

기존에 알던데로 application.yml에 설정을 작성하고, viewResolver를 추가하고, addResourcesHandler를 추가하고.... 

Spring MVC에서 하던대로 했는데 아무리 해도 templates/index.html을 뷰리졸버가 찾지 못했다.

구글 검색에서 나오는 많은 예시들을 따라해봤는데 여전히 안 되더라..

폭풍 검색 끝에 실행되는 소스코드를 찾았다.

(나중에 webflux에 익숙해지면 이 포스트를 보며 한심하게 느껴지려나....)

 

프로젝트 명: 두끼 (Dookki) * 그 떡볶이 맞음

build.gradle.kt

dependencies {
    // webflux
	implementation("org.springframework.boot:spring-boot-starter-webflux")
    
    // thymeleaf
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    
    // kotlin
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    
    // others
	annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")    
}

DookkiApplication

/**
 * DookkiApplication
 */
 
@ComponentScan("com.domain.dookki")
@SpringBootApplication
class DookkiApplication : SpringBootServletInitializer() {
    override fun configure(application: SpringApplicationBuilder): SpringApplicationBuilder {
        return application.sources(DookkiApplication::class.java)
    }
}


fun main(args: Array<String>) {
    val application = SpringApplication(DookkiApplication::class.java)
    application.webApplicationType = WebApplicationType.REACTIVE
    application.run(*args);

//    runApplication<DookkiApplication>(*args)
}

맨 밑에 runApplication<Application>(*args) 이 한줄만으로도 어플리케이션 실행이 가능한데, 간결한 라인을 놔두고 굳이 세 줄이나 입력하여 run 한 이유는, 소스코드에서 WebConfig를 재정의할 수가 없더라.

자동으로 @EnableWebflux를 하고 모든 것을 기본 설정으로 서버를 실행하는 것 같았다.

아마도 application.yml에 정의한 대로 적용이 되지 않을까 싶다.

그러나 여기에서는 WebConfig에 뷰 리졸버를 재정의하였기 때문에 WebFluxConfigurer를 상속한 클래스를 작성할 필요가 있다.

WebConfig

/**
 * WebConfig
 */

@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer, ApplicationContextAware {

    private var context: ApplicationContext? = null

    override fun setApplicationContext(applicationContext: ApplicationContext) {
        this.context = applicationContext
    }

    /***********************************
     * Setting thymeleaf view resolvers
     ***********************************/
     
    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.viewResolver(thymeleafReactiveViewResolver())
    }

    @Bean
    fun thymeleafReactiveViewResolver(): ThymeleafReactiveViewResolver {
        val viewResolver = ThymeleafReactiveViewResolver()
        viewResolver.templateEngine = thymeleafTemplateEngine()
        return viewResolver
    }

    @Bean
    fun thymeleafTemplateEngine(): ISpringWebFluxTemplateEngine {
        val templateEngine = SpringWebFluxTemplateEngine()
        templateEngine.setTemplateResolver(thymeleafTemplateResolver())
        return templateEngine
    }

    @Bean
    fun thymeleafTemplateResolver(): ITemplateResolver {
        val resolver = SpringResourceTemplateResolver()
        resolver.setApplicationContext(context!!)
        resolver.prefix = "classpath:templates/"
        resolver.suffix = ".html"
        resolver.templateMode = TemplateMode.HTML
        resolver.isCacheable = false
        resolver.checkExistence = false
        resolver.characterEncoding = "UTF-8"
        resolver.order = 1
        return resolver
    }

    @Bean
    fun staticResourceRouter(): RouterFunction<ServerResponse?>? {
        return RouterFunctions.resources("/**", ClassPathResource("static/"))
    }
    
    // End of setting thymeleaf view resolvers 

}

정적 리소스 경로 지정에 대해서...

처음에는 SpringMVC에서 하는것처럼, WebFluxConfigurer 클래스에 정의된 addResourcesHandler 함수를 override 했는데 아무리 해도 css, js 파일을 못찾더라.

그런데 위에 있는 소스코드처럼 RouterFunction<ServerResponse>를 반환하는 빈을 정의하니까 드디어 되더라.

뭐지? 일단 되서 좋은데 이유는 모르겠다.

templates/index.html

<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <link rel="stylesheet" type="text/css" th:href="@{/css/index.css}">
  <script th:src="@{/js/index.js}"></script>
  <title>Dookki</title>
</head>
<body>
  <div id="container">

  </div>
</body>

<script th:inline="javascript">
	console.log("")
</script>

</html>

WebRouter

/**
 * WebRouter
 */
 
@Controller
class WebRouter {

    @GetMapping("/", "/index")
    fun index (): String {
        return "index"
    }
}

위의 방식은 MVC 에서 사용하는 패턴이고 WebFlux에서는 RouterFunction 클래스를 사용하던데 내가 미숙해서 우선은 Controller를 사용하였다.

Controller 클래스의 위치는 단연 DookkiApplication 클래스 내에 @ComponentScan 에 정의한 패키지 하위에 있어야 한다.

resources 하위 디렉토리 구조

Thymeleaf 설정 위한 src/main/resources 하위 디렉토리 구조

 

참조

https://stackoverflow.com/questions/43622053/how-to-serve-static-content-using-webflux

 

How to serve static content using Webflux?

I am learning webflux and I would like to know how to serve static content on a MicroService using webflux but I didn´t find information to do it.

stackoverflow.com

https://myhappyman.tistory.com/109

 

Spring WebFlux - WebFlux 알아보기... index.html연동

최근 Spring에서 반응형 코딩이 가능하다는 이야기를 들었고 팀원끼리 학습을 해보기로 하여 rx.js를 통해 컨셉을 이해해보고 비동기, 동기~ 블로킹이니 단어나 개념들부터 차근차근 잡고 학습을

myhappyman.tistory.com