Spring Boot와 Kotlin으로 웹 애플리케이션 구축하기 이 튜토리얼은 Spring Boot 와 Kotlin 을 결합하여 예제 블로그 애플리케이션을 효율적으로 빌드하는 방법을 설명합니다..
만약 Kotlin을 처음 시작한다면 참고 문서 를 읽고 온라인 Kotlin Koans 자습서 를 따라하며 언어를 배울 수 있습니다.
Spring Kotlin 지원은 Spring Framework 및 Spring Boot 참고문서에 설명되어 있습니다. 도움이 필요하면 StackOverflow에서 Spring 및 kotlin 태그로 검색하거나 질문 을 할 수 있고, Kotlin Slack 의 #spring
채널에서 토론하십시오.
새 프로젝트 만들기 먼저 Spring Boot 애플리케이션을 만들어야하고, 이는 여러 가지 방법으로 수행 할 수 있습니다. 예를 들어, 우리는 Kotlin 생태계에서 가장 많이 사용하는 Gradle 빌드 시스템을 사용할 것이지만, 만약 원하는 경우 Maven도 자유롭게 사용할 수 있습니다.(예제 블로그 프로젝트의 일부로 Gradle 빌드와 동일한 Maven pom.xml을 사용할 수 있습니다).
Initializr 웹 사이트 사용 https://start.spring.io 로 이동하여 Kotlin 언어를 선택합니다. 또는 Kotlin을 미리 선택하려면 직접 https://start.spring.io/#!language=kotlin 로 방문할 수 있습니다.
그런 다음 Gradle 빌드 시스템, “blog” Artifact, “blog” Package Name(고급 설정)을 선택하고 “Web”, “Mustache”, “JPA”및 “H2” 종속성을 시작점으로 추가한 다음 “Generate Project”를 클릭합니다.
압축을 풀기전에 빈 디렉토리를 만듭니다. 그리고 .zip 파일의 루트 디렉토리에는 표준 Gradle 프로젝트가 포함되어 있습니다.
Command Line 사용 Command Line에서 Initializr HTTP API를 사용할 수 있습니다 (예 : UN*X 시스템의 curl).
1
2
$ mkdir blog && cd blog
$ curl https://start.spring.io/starter.zip -d type=gradle-project -d language=kotlin -d style=web,mustache,jpa,h2 -d packageName=blog -d name=Blog -o blog.zip
IntelliJ IDEA 사용 또한 Spring Initializr은 IntelliJ IDEA Ultimate 에디션에 통합되어 있어 IDE에서 Command Line 또는 웹 UI와 같이 새 프로젝트를 만들고 가져올 수 있습니다.
File | New | Project를 선택하고 Spring Initializr을 선택합니다.
각 단계에 따라 다음 파라미터를 사용합니다.
Package name: “blog”
Artifact: “blog”
Type: Gradle Project
Language: Kotlin
Name: “Blog”
Dependencies: “Web”, “Mustache”, JPA” and “H2”
생성된 프로젝트 이해하기 Gradle build Plugins 명백한 Kotlin Gradle plugin 외에도, 기본 설정은 스프링 Annotation으로 annotated 또는 meta-annotated가 달린 클래스와 메서드가 자동으로 열리는(Automatically open)kotlin-spring plugin 을 선언합니다 (Java와 달리 Kotlin의 기본 한정자(Qualifier)는 final
입니다). 예를 들어 CGLIB 프록시에 필요한 open
한정자를 추가하지 않고도 @Configuration
또는 @Transactional
Bean을 만들 수있는 경우에 유용합니다.
build.gradle 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
buildscript {
ext {
44kotlinVersion = '1.2.51'
44springBootVersion = '2.0.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
}
}
apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
컴파일러 옵션 (Compiler options) Kotlin의 핵심 기능 중 하나는 런타임시 유명한 NullPointerException
에 부딪히지 않고 컴파일 타임에 null
값을 깔끔하게 처리하는 null-safety 입니다. 따라서 애플리케이션이 null 허용 선언을 통해 안전하고, Optional
(“값 또는 값 없음” 의미를 표현)과 같은 래퍼의 비용을 지불하지 않아도됩니다. Kotlin은 nullable 값을 가진 함수 생성자를 사용할 수 있습니다. Kotlin null-safety에 대한 포괄적 가이드 를 확인하십시오.
Java는 type-system에서 null-safety를 허용하지 않지만 Spring Framework는 org.springframework.lang
패키지에 선언된 도구 친화적인(Tooling-friendly) Annotation을 통해 전체 Spring Framework API의 null-safety를 제공합니다. 기본적으로 Kotlin에서 사용되는 Java API의 타입은 null 체크가 완화된 플랫폼 타입 으로 인식됩니다. JSR 305 annotations + Spring Nullability Annotation에 대한 Kotlin 지원은 컴파일 타임에 null
관련 문제를 처리 할 수 있다는 이점을 가지고 Kotlin 개발자에게 전체 Spring Framework API에 대한 null-safety를 제공합니다.
이 기능은 -Xjsr305
컴파일러 플래그에 strict
옵션을 추가하여 활성화 할 수 있습니다.
아래 코드의 Kotlin 컴파일러는 Java 8 바이트 코드 (Java 6이 기본값)를 생성하도록 구성되어 있습니다.
build.gradle 1
2
3
4
5
6
7
8
9
10
11
12
13
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
종속성(Dependencies) Kotlin 특정 라이브러리 3개는 이러한 Spring Boot 웹 애플리케이션에 필요하며 기본 구성에 필요합니다.
kotlin-stdlib-jdk8
은 Kotlin 표준 라이브러리의 Java 8 변형입니다.
kotlin-reflect
은 Kotlin 리플렉션 라이브러리입니다 (Spring Framework 5에서 필수).
jackson-module-kotlin
은 Kotlin 클래스 및 데이터 클래스의 직렬화 / 비 직렬화에 대한 지원을 추가합니다 (단일 생성자 클래스는 자동으로 사용할 수 있고 보조 생성자 또는 정적 팩토리가 있는 클래스도 지원됩니다)
build.gradle 1
2
3
4
5
6
7
8
9
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-mustache')
compile('com.fasterxml.jackson.module:jackson-module-kotlin')
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
Spring Boot Gradle 플러그인은 Kotlin Gradle 플러그인에 선언된 Kotlin 버전을 자동으로 사용합니다.
Application src/main/kotlin/blog/BlogApplication.kt
1
2
3
4
5
6
7
8
9
10
11
package blog
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class BlogApplication
fun main (args: Array <String >) {
runApplication<BlogApplication>(*args)
}
Java와 비교하면, 세미콜론이 줄어들었고, 빈 클래스에 괄호가 없으며 (@Bean
Annotation을 통해 Bean을 선언해야하는 경우 추가 할 수 있습니다), 최상위 수준 함수 runApplication
을 사용할 수 있다는 것을 알수 있습니다. runApplication<BlogApplication>(*args)
은 SpringApplication.run(BlogApplication::class.java, *args)
에 대한 Kotlin의 대안적인 표현이며, 다음 구문을 사용하여 사용자 정의 애플리케이션을 사용할 수 있습니다.
src/main/kotlin/blog/BlogApplication.kt 1
2
3
4
5
fun main (args: Array <String >) {
runApplication<BlogApplication>(*args) {
setBannerMode(Banner.Mode.OFF)
}
}
첫 번째 Kotlin 컨트롤러 작성하기 간단한 웹 페이지를 표시하는 간단 컨트롤러를 만들어 보겠습니다.
src/main/kotlin/blog/HtmlController.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package blog
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.ui.set
import org.springframework.web.bind.annotation .GetMapping
@Controller
class HtmlController {
@GetMapping("/" )
fun blog (model: Model ) : String {
model["title" ] = "Blog"
return "blog"
}
}
Kotlin extension 을 사용하여 기존의 Spring 타입에 Kotlin 함수나 연산자를 추가 할 수 있습니다. 여기에서는 model.addAttribute("title", "Blog")
대신 model["title"] = "Blog"
를 사용할 수 있도록 org.springframework.ui.set
확장 함수를 가져옵니다.
그리고 관련된 Mustache 템플릿을 만들어야합니다.
src/main/resources/templates/header.mustache 1
2
3
4
5
<html >
<head >
<title > {{title}}</title >
</head >
<body >
src/main/resources/templates/footer.mustache
src/main/resources/templates/blog.mustache 1
2
3
4
5
{{> header}}
<h1 > {{title}}</h1 >
{{> footer}}
BlogApplication.kt
의 main
함수를 실행하여 응용프로그램을 시작하고, http://localhost:8080/
으로 이동하면 “Blog” 제목이 있는 단순한 웹 페이지가 나타납니다.
JUnit 5로 테스트하기 JUnit 4는 여전히 Spring Boot와 함께 제공되는 기본 테스트 프레임 워크이지만, JUnit 5는 null이 허용되지 않는 val
프로퍼티를 사용할 수 있는 생성자/메서드 파라미터의 Autowiring 을 포함하여 Kotlin에서 매우 편리한 다양한 기능을 제공합니다. 그리고 @BeforeAll
/@AfterAll
을 일반적인 비 정적 메서드에서 사용할 수 있습니다.
JUnit 4에서 JUnit 5로 전환 먼저 네이티브 JUnit 5 지원 을 활용할 수 있도록 ./gradlew -version
을 실행하여 Gradle 4.6 이상을 사용하고 있는지 확인하십시오. 이전 버전을 사용하는 경우 ./gradlew wrapper --gradle-version 4.7
을 실행하여 최신 버전의 Gradle 을 업데이트 할 수 있습니다.
build.gradle
파일에 다음 줄을 추가하여 JUnit 5 지원을 활성화합니다.
build.gradle 1
2
3
test {
useJUnitPlatform()
}
그런 다음 junit
을 spring-boot-starter-test
전이(transitive) 의존성에서 제외하고 junit-jupiter-api
및 junit-jupiter-engine
을 추가합니다.
build.gradle 1
2
3
4
5
6
7
dependencies {
testCompile('org.springframework.boot:spring-boot-starter-test') {
exclude module: 'junit'
}
testImplementation('org.junit.jupiter:junit-jupiter-api')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}
Gradle 설정을 새로 고치고 BlogApplicationTests
를 열어 @RunWith(SpringRunner::class)
를 @ExtendWith(SpringExtension::class)
로 대체합니다.
src/test/kotlin/blog/BlogApplicationTests.kt 1
2
3
4
5
6
7
8
9
@ExtendWith(SpringExtension::class)
@SpringBootTest
class BlogApplicationTests {
@Test
fun contextLoads () {
}
}
테스트는 Command Line과 IDE에서 잘 실행되어야합니다.
Kotlin에서 JUnit 5 테스트 작성하기 이 예제에서는 다양한 기능을 보여주기 위해 통합 테스트를 작성해 보겠습니다.
표현형 테스트 함수 이름을 제공하기 위해 camel-case 대신 백틱 사이에 실제 문장을 사용합니다.
JUnit 5는 생성자와 메서드 파라미터를 삽입(Inject) 할 수 있습니다.이 파라미터는 Kotlin 불변(Immutable) 및 Nullable 속성과 잘 어울립니다.
이 코드는 getForObject
및 getForEntity
Kotlin extension을 사용합니다 (import 할 필요가 있음).
src/test/kotlin/blog/IntegrationTests.kt 1
2
3
4
5
6
7
8
9
10
11
12
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests (@Autowired val restTemplate: TestRestTemplate) {
@Test
fun `Assert blog page title, content and status code`() {
val entity = restTemplate.getForEntity<String>("/" )
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>" )
}
}
테스트 인스턴스 생명주기(Lifecycle) 때로는 주어진 클래스의 모든 테스트 전후에 메서드를 실행해야 할 때가 있습니다. Junit 4와 마찬가지로 JUnit 5는 테스트 클래스가 테스트마다 한 번 인스턴스화되기 때문에 기본적으로 이 메서드가 정적 (이것은 Kotlin의 companion object 로 변환되며, 이는 매우 장황하고 직설적이지 않습니다.) 이어야합니다.
그러나 Junit 5에서는 이 기본 동작을 변경하고 클래스당 한 번 테스트 클래스를 인스턴스화 할 수 있습니다. 이 작업은 다양한 방법 으로 수행 할 수 있습니다. 여기서는 프로퍼티 파일을 사용하여 전체 프로젝트의 기본 동작을 변경합니다.
src/test/resources/junit-platform.properties
1
junit.jupiter.testinstance.lifecycle.default = per_class
이 구성(Configuration)을 사용하면 위의 IntegrationTest
의 업데이트된 버전과 같은 일반 메서드에서 @BeforeAll
및 @AfterAll
Annotation을 사용할 수 있습니다.
src/test/kotlin/blog/IntegrationTests.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests (@Autowired val restTemplate: TestRestTemplate) {
@BeforeAll
fun setup () {
println(">> Setup" )
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code" )
val entity = restTemplate.getForEntity<String>("/" )
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>" )
}
@Test
fun `Assert article page title, content and status code`() {
println(">> TODO" )
}
@AfterAll
fun teardown () {
println(">> Tear down" )
}
}
JPA를 이용한 Persistence Kotlin 불변 클래스(Immutable class)를 사용하려면 Kotlin JPA 플러그인 을 활성화해야합니다. @Entity
, @MappedSuperclass
또는 @Embeddable
로 Annotation된 모든 클래스에 대해 파라미터가 없는 생성자를 생성합니다.
build.gradle 1
2
3
4
5
6
buildscript {
dependencies {
classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
}
}
apply plugin: 'kotlin-jpa'
그런 다음 데이터를 보유하고 equals()
, hashCode()
, toString()
, componentN()
함수 및 copy()
를 자동으로 제공하도록 설계된 Kotlin 데이터 클래스(Data class) 를 사용하여 모델을 만듭니다.
src/main/kotlin/blog/Model.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity
data class Article (
val title: String,
val headline: String,
val content: String,
@ManyToOne @JoinColumn val author: User,
@Id @GeneratedValue val id: Long ? = null ,
val addedAt: LocalDateTime = LocalDateTime.now())
@Entity
data class User (
@Id val login: String,
val firstname: String,
val lastname: String,
val description: String? = null )
기본 값이 있는 선택적 파라미터는 위치가 있는 파라미터를 사용할 때 생략 할 수 있도록 마지막 위치에 정의됩니다 (Kotlin은 이름이 있는 파라미터 도 지원함). Kotlin에서 동일한 파일에서 간결한 클래스 선언을 그룹화하는 것은 이례적인 일이 아닙니다.
Spring Data JPA Repository를 다음과 같이 선언합니다.
src/main/kotlin/blog/Repositories.kt 1
2
3
4
5
interface ArticleRepository : CrudRepository <Article, Long > {
fun findAllByOrderByAddedAtDesc () : Iterable<Article>
}
interface UserRepository : CrudRepository <User, String >
그리고 JPA 테스트를 작성하여 기본 유스 케이스가 예상대로 작동하는지 확인합니다.
src/test/kotlin/blog/RepositoriesTests.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@ExtendWith(SpringExtension::class)
@DataJpaTest
class RepositoriesTests (@Autowired val entityManager: TestEntityManager,
@Autowired val userRepository: UserRepository,
@Autowired val articleRepository: ArticleRepository) {
@Test
fun `When findById then return Article`() {
val juergen = User("springjuergen" , "Juergen" , "Hoeller" )
entityManager.persist(juergen)
val article = Article("Spring Framework 5.0 goes GA" , "Dear Spring community ..." , "Lorem ipsum" , juergen)
entityManager.persist(article)
entityManager.flush()
val found = articleRepository.findById(article.id!!)
assertThat(found.get ()).isEqualTo(article)
}
@Test
fun `When findById then return User`() {
val juergen = User("springjuergen" , "Juergen" , "Hoeller" )
entityManager.persist(juergen)
entityManager.flush()
val found = userRepository.findById(juergen.login)
assertThat(found.get ()).isEqualTo(juergen)
}
}
나만의 확장 기능 만들기 Java에서 추상 메서드로 util 클래스를 사용하지만, Kotlin에서는 Kotlin 확장을 통해 제공하는 것이 일반적입니다. 여기서 영어 날짜 형식으로 텍스트를 생성하기 위해 기존 LocalDateTime
타입에 format()
함수를 추가 할 것입니다.
src/main/kotlin/blog/Extensions.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun LocalDateTime.format () = this .format(englishDateFormatter)
private val daysLookup = (1. .31 ).associate { it.toLong() to getOrdinal(it) }
private val englishDateFormatter = DateTimeFormatterBuilder()
.appendPattern("MMMM" )
.appendLiteral(" " )
.appendText(ChronoField.DAY_OF_MONTH, daysLookup)
.appendLiteral(" " )
.appendPattern("yyyy" )
.toFormatter(Locale.ENGLISH)
private fun getOrdinal (n: Int ) = when {
n in 11. .13 -> "${n} th"
n % 10 == 1 -> "${n} st"
n % 10 == 2 -> "${n} nd"
n % 10 == 3 -> "${n} rd"
else -> "${n} th"
}
다음 섹션에서 이 확장을 활용할 것입니다.
블로그 엔진 구현하기 우리가 구현중인 블로그 엔진은 Markdown을 HTML로 렌더링해야하며, 이를 위해 commonmark
라이브러리를 사용할 것입니다.
build.gradle 1
2
3
4
dependencies {
compile("com.atlassian.commonmark:commonmark:0.11.0")
compile("com.atlassian.commonmark:commonmark-ext-autolink:0.11.0")
}
Kotlin 함수 타입 을 활용하는 MarkdownConverter
Bean을 소개합니다.
src/main/kotlin/blog/MarkdownConverter.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
class MarkdownConverter : (String?) -> String {
private val parser = Parser.builder().extensions(listOf(AutolinkExtension.create())).build()
private val renderer = HtmlRenderer.builder().build()
override fun invoke (input: String ?) : String {
if (input == null || input == "" ) {
return ""
}
return renderer.render(parser.parse(input))
}
}
그리고 HTML을 렌더링 할 수 있는 커스텀 Mustache.Compiler
Bean을 제공합니다.
src/main/kotlin/blog/BlogApplication.kt 1
2
3
4
5
6
7
@SpringBootApplication
class BlogApplication {
@Bean
fun mustacheCompiler (loader: Mustache .TemplateLoader ?) =
Mustache.compiler().escapeHTML(false ).withLoader(loader)
}
Null을 허용하는 Mustache.TemplateLoader?
는 선택적인 Bean이라는 것을 의미합니다(JPA 전용 테스트를 실행할 때 실패를 피하기 위해서).
“Blog” Mustache 템플릿을 업데이트합니다.
src/main/resources/templates/blog.mustache
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{> header}}
<h1 > {{title}}</h1 >
<div class ="articles" >
{{#articles}}
<section >
<header class ="article-header" >
<h2 class ="article-title" > <a href ="/article/{{id}}" > {{title}}</a > </h2 >
<div class ="article-meta" > By <strong > {{author.firstname}}</strong > , on <strong > {{addedAt}}</strong > </div >
</header >
<div class ="article-description" >
{{headline}}
</div >
</section >
{{/articles}}
</div >
{{> footer}}
그리고 우리는 새로운 “article”를 만듭니다.
src/main/resources/templates/article.mustache 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{{> header}}
<section class ="article" >
<header class ="article-header" >
<h1 class ="article-title" > {{article.title}}</h1 >
<p class ="article-meta" > By <strong > {{article.author.firstname}}</strong > , on <strong > {{article.addedAt}}</strong > </p >
</header >
<div class ="article-description" >
{{article.headline}}
{{article.content}}
</div >
</section >
{{> footer}}
렌더링된 마크 다운 및 형식이 지정된 날짜로 블로그 및 기사 페이지를 렌더링하기 위해 HtmlController
를 업데이트합니다. ArticleRepository
및 MarkdownConverter
생성자 파라미터는 HtmlController
가 단일 생성자 (암시적 @Autowired
)를 가지고 있기 때문에 자동으로 Autowired 됩니다.
src/main/kotlin/blog/HtmlController.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Controller
class HtmlController (private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter) {
@GetMapping("/" )
fun blog (model: Model ) : String {
model["title" ] = "Blog"
model["articles" ] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
return "blog"
}
@GetMapping("/article/{id}" )
fun article (@PathVariable id: Long , model: Model ) : String {
val article = repository
.findById(id)
.orElseThrow { IllegalArgumentException("Wrong article id provided" ) }
.render()
model["title" ] = article.title
model["article" ] = article
return "article"
}
fun Article.render () = RenderedArticle(
title,
markdownConverter.invoke(headline),
markdownConverter.invoke(content),
author,
id,
addedAt.format()
)
data class RenderedArticle (
val title: String,
val headline: String,
val content: String,
val author: User,
val id: Long ?,
val addedAt: String)
}
BlogApplication
에 데이터 초기화를 추가합니다.
src/main/kotlin/blog/BlogApplication.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Bean
fun databaseInitializer (userRepository: UserRepository ,
articleRepository: ArticleRepository ) = CommandLineRunner {
val smaldini = User("smaldini" , "Stéphane" , "Maldini" )
userRepository.save(smaldini)
articleRepository.save(Article(
"Reactor Bismuth is out" ,
"Lorem ipsum" ,
"dolor **sit** amet https://projectreactor.io/" ,
smaldini,
1
))
articleRepository.save(Article(
"Reactor Aluminium has landed" ,
"Lorem ipsum" ,
"dolor **sit** amet https://projectreactor.io/" ,
smaldini,
2
))
}
그리고 통합 테스트(Integration test)도 업데이트합니다.
src/test/kotlin/blog/IntegrationTests.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@ExtendWith(SpringExtension::class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests (@Autowired val restTemplate: TestRestTemplate) {
@BeforeAll
fun setup () {
println(">> Setup" )
}
@Test
fun `Assert blog page title, content and status code`() {
println(">> Assert blog page title, content and status code" )
val entity = restTemplate.getForEntity<String>("/" )
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("<h1>Blog</h1>" , "Reactor" )
}
@Test
fun `Assert article page title, content and status code`() {
println(">> Assert article page title, content and status code" )
val entity = restTemplate.getForEntity<String>("/article/2" )
assertThat(entity.statusCode).isEqualTo(HttpStatus.OK)
assertThat(entity.body).contains("Reactor Aluminium has landed" ,,
"<a href=\"https://projectreactor.io/\">https://projectreactor.io/</a>" )
}
@AfterAll
fun teardown () {
println(">> Tear down" )
}
}
웹 애플리케이션을 시작(또는 재시작)하고 http://localhost:8080/
으로 이동하면 클릭 가능한 링크가있는 기사 목록이 나타나 특정 기사를 볼 수 있습니다.
HTTP API 노출 @RestController
Annotation이 달린 컨트롤러를 통해 HTTP API를 구현합니다.
src/main/kotlin/blog/HttpApi.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RestController
@RequestMapping("/api/article" )
class ArticleController (private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter) {
@GetMapping("/" )
fun findAll () = repository.findAllByOrderByAddedAtDesc()
@GetMapping("/{id}" )
fun findOne (@PathVariable id: Long , @RequestParam converter: String ?) = when (converter) {
"markdown" -> repository.findById(id).map { it.copy(
headline = markdownConverter.invoke(it.headline),
content = markdownConverter.invoke(it.content)) }
null -> repository.findById(id)
else -> throw IllegalArgumentException("Only markdown converter is supported" )
}
}
@RestController
@RequestMapping("/api/user" )
class UserController (private val repository: UserRepository) {
@GetMapping("/" )
fun findAll () = repository.findAll()
@GetMapping("/{login}" )
fun findOne (@PathVariable login: String ) = repository.findById(login)
}
테스트의 경우 통합 테스트 대신 @WebMvcTest
및 @MockBean
을 사용하여 웹 레이어만 테스트합니다.
src/test/kotlin/blog/HttpApiTests.kt 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@ExtendWith(SpringExtension::class)
@WebMvcTest
class HttpApiTests (@Autowired val mockMvc: MockMvc) {
@MockBean
private lateinit var userRepository: UserRepository
@MockBean
private lateinit var articleRepository: ArticleRepository
@MockBean
private lateinit var markdownConverter: MarkdownConverter
@Test
fun `List articles`() {
val juergen = User("springjuergen" , "Juergen" , "Hoeller" )
val spring5Article = Article("Spring Framework 5.0 goes GA" , "Dear Spring community ..." , "Lorem ipsum" , juergen, 1 )
val spring43Article = Article("Spring Framework 4.3 goes GA" , "Dear Spring community ..." , "Lorem ipsum" , juergen, 2 )
whenever(articleRepository.findAllByOrderByAddedAtDesc()).thenReturn(listOf(spring5Article, spring43Article))
whenever(markdownConverter.invoke(any())).thenAnswer { it.arguments[0 ] }
mockMvc.perform(get ("/api/article/" ).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].author.login" ).value(juergen.login))
.andExpect(jsonPath("\$.[0].id" ).value(spring5Article.id!!))
.andExpect(jsonPath("\$.[1].author.login" ).value(juergen.login))
.andExpect(jsonPath("\$.[1].id" ).value(spring43Article.id!!))
}
@Test
fun `List users`() {
val juergen = User("springjuergen" , "Juergen" , "Hoeller" )
val smaldini = User("smaldini" , "Stéphane" , "Maldini" )
whenever(userRepository.findAll()).thenReturn(listOf(juergen, smaldini))
mockMvc.perform(get ("/api/user/" ).accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(jsonPath("\$.[0].login" ).value(juergen.login))
.andExpect(jsonPath("\$.[1].login" ).value(smaldini.login))
}
}
when
은 예약된 Kotlin 키워드이므로, whenever
별칭을 제공하는 mockito-kotlin 라이브러리 (이스케이프된 `when`
도 사용 가능)를 사용하기로 결정한 이유입니다. 이를 사용하려면 다음 종속성을 추가하십시오.
build.gradle 1
2
3
dependencies {
testCompile("com.nhaarman:mockito-kotlin:1.5.0")
}
$
는 문자열 보간(Interpolation)에 사용되기 때문에 문자열에서 이스케이프 해야합니다. @MockBean
JUnit 5 파라미터 리졸버(Parameter resolver)가 아직 없으므로 지금은 lateinit var
를 사용해야합니다.
Configuration properties 애플리케이션 프로퍼티를 관리하는 가장 좋은 방법은 @ConfigurationProperties
를 활용하는 것입니다. 변경할 수 없는 프로퍼티는 아직 지원되지 않지만 Null이 허용되지 않는 프로퍼티를 처리해야하는 경우 lateinit var
을 사용할 수 있습니다.
src/main/kotlin/blog/BlogProperties.kt 1
2
3
4
5
6
7
8
9
10
11
12
@ConfigurationProperties("blog" )
class BlogProperties {
lateinit var title: String
val banner = Banner()
class Banner {
var title: String? = null
lateinit var content: String
}
}
그런 다음 BlogApplication
수준에서 사용하도록 설정합니다.
src/main/kotlin/blog/BlogApplication.kt 1
2
3
4
5
@SpringBootApplication
@EnableConfigurationProperties(BlogProperties::class)
class BlogApplication {
}
IDE에서 이러한 사용자 정의 프로퍼티를 인식하기 위해 자신의 메타 데이터 를 생성하려면 spring-boot-configuration-processor
종속성을 사용하여 다음과 같이 Kapt가 설정되어야 합니다.
build.gradle 1
2
3
4
apply plugin: 'kotlin-kapt'
dependencies {
kapt("org.springframework.boot:spring-boot-configuration-processor")
}
IntelliJ IDEA에서 :
메뉴 File | Settings | Plugins | Spring Boot 에서 Spring Boot plugin이 활성화되어 있는지 확인하십시오.
Annotation 처리를 위해 메뉴의 File | Settings | Build, Execution, Deployement | Compiler | Annotation Processors | Enable annotation processing 을 활성화 합니다.
Kapt가 아직 IDEA에 통합되지 않았으므로 메타 데이터를 생성하려면 ./gradlew kaptKotlin
명령을 수동으로 실행해야합니다.
application.properties
(Autocomplete, Validation 등)를 편집 할 때 사용자 정의 프로퍼티를 인식해야합니다.
src/main/resources/application.properties 1
2
3
blog.title=Blog
blog.banner.title=Warning
blog.banner.content=The blog will be down tomorrow.
그에 따라 템플릿과 컨트롤러를 수정합니다.
src/main/resources/templates/blog.mustache 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{> header}}
<div class ="articles" >
{{#banner.title}}
<section >
<header class ="banner" >
<h2 class ="banner-title" > {{banner.title}}</h2 >
</header >
<div class ="banner-content" >
{{banner.content}}
</div >
</section >
{{/banner.title}}
...
</div >
{{> footer}}
src/main/kotlin/blog/HtmlController.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
class HtmlController (private val repository: ArticleRepository,
private val markdownConverter: MarkdownConverter,
private val properties: BlogProperties) {
@GetMapping("/" )
fun blog (model: Model ) : String {
model["title" ] = properties.title
model["banner" ] = properties.banner
model["articles" ] = repository.findAllByOrderByAddedAtDesc().map { it.render() }
return "blog"
}
}
웹 애플리케이션을 다시 시작하고 http://localhost:8080/
을 새로 고치면 블로그 홈페이지에 배너가 나타납니다.
결론 이제 이 예제 Kotlin 블로그 애플리케이션을 빌드했습니다. 소스 코드는 Github에서 사용 할 수 있습니다. 특정 기능에 대한 자세한 내용은 Spring Framework 및 Spring Boot 참고 문서를 읽어 보십시오.
이 내용은 나중에 참고하기 위해 Spring Boot 가이드 문서를 공부하며 정리한 내용입니다. 의역, 오역, 직역이 있을 수 있음을 알려드립니다. This post is a translation of this original article Building web applications with Spring Boot and Kotlin
참고