Kotlin으로 Spring Boot Web 만들기

개요

Kotlin 언어를 이용하여 간단한 Spring Boot Web를 만들어 보겠다.

프로젝트 생성

아래와 같이 curl 명령어을 사용하여 Spring Boot 초기 프로젝트를 생성한다.

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.4.11 \
-d dependencies=web \
-d baseDir=kotlin-spring-hello-world \
-d groupId=com.devkuma \
-d artifactId=kotlin-spring-hello-world \
-d packageName=com.devkuma.hello \
-d applicationName=HelloApplication \
-d packaging=jar \
-d language=kotlin \
-d jvmVersion=11 \
-d type=gradle-project | tar -xzvf -

위 명령어를 실행하게 되면 Java 11, Spring Boot 버전은 2.4.11로 web 프로젝트가 생성된다.

생성된 프로젝트의 파일 구조는 아래와 같이 구성된다.

.
├── HELP.md
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── kotlin
    │   │   └── com
    │   │       └── devkuma
    │   │           └── hello
    │   │               └── HelloApplication.kt
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test
        └── kotlin
            └── com
                └── devkuma
                    └── hello
                        └── HelloApplicationTests.kt

생성된 프로젝트 확인

build.gradle.kts

생성된 파일에 빌드 파일의 확장자가 .kts으로 코틀린 스크립트 파일로 된것을 볼 수 있다.
(참고로, Java에서는 빌드 파일의 확장자는 .gradle이고, Go언어로 되어 있다.)

/build.gradle.kts

// ... 생략 ...

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

// ... 생략 ...

파일 내용 중에 의존성 라이브러리를 확인해 보면 Spring 프레임워크(org.springframework.boot), 코틀린용 Jackson(com.fasterxml.jackson.module), 코틀린 라이브러리(org.jetbrains.kotlin)가 추가되어 있는 것을 볼수 있다.

Application Context

/src/main/kotlin/com/devkuma/hello/controller/HelloController.kt

package com.devkuma.hello

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class HelloApplication

fun main(args: Array<String>) {
	runApplication<HelloApplication>(*args)
}

Spring Boot를 실행하기 위한 @SpringBootApplication 어노테이션과 main 함수가 보이는 것을 확인 할 수 있다.
(전반적으로 자바 코드와 비슷해 보인다.)

Controller 생성

Controller 파일을 생성하여 아래와 같이 작성한다.

/src/main/kotlin/com/devkuma/hello/controller/HelloController.kt

package com.devkuma.hello.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController {

    @GetMapping("/hello")
    fun hello(): String {
        return "hello world"
    }
}

테스트 코드를 생성하고, 실행해 본다.

/src/test/kotlin/com/devkuma/hello/controller/HelloControllerTests.kt

package com.devkuma.hello.controller

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status


@WebMvcTest(HelloController::class)
class HelloControllerTests {

    @Autowired
    lateinit var mockMvc: MockMvc

    @Test
    @DisplayName("Hello")
    fun hello() {
        // when
        val resultActions: ResultActions = mockMvc.perform(get("/hello"))
            .andDo(print())

        // then
        resultActions
            .andExpect(status().is2xxSuccessful)
            .andExpect(content().string("hello world"))
            .andDo(print())
    }
}

실행 결과에 에러가 없는지 확인하다.

실제 동작 여부도 확인을 위해 프로젝트를 실행 후에 아래 curl 명령어로 작동 여부를 확인한다.

% curl localhost:8080/hello
hello world%

Service 생성

Service 파일을 생성하고 아래와 같이 작성한다.

/src/main/kotlin/com/devkuma/hello/service/HelloService.kt

package com.devkuma.hello.service

import org.springframework.stereotype.Service

@Service
class HelloService {

    fun getHello(): String {
        return "hello service"
    }
}

Controller 파일에서 아래와 같이 ‘helloService()‘를 추가한다.

/src/main/kotlin/com/devkuma/hello/controller/HelloController.kt

package com.devkuma.hello.controller

import com.devkuma.hello.service.HelloService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController(val helloService: HelloService) {

    // ...중간 생략...

    @GetMapping("/hello-service")
    fun helloService(): String {
        return helloService.getHello()
    }
}

아래와 같이 테스트 코드 helloService()를 추가하고, 실행해 본다.

/src/test/kotlin/com/devkuma/hello/controller/HelloControllerTests.kt

package com.devkuma.hello.controller

import com.devkuma.hello.service.HelloService
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status


@WebMvcTest(HelloController::class)
class HelloControllerTests {

    @Autowired
    lateinit var mockMvc: MockMvc
    @MockBean
    lateinit var helloService: HelloService

    // ...중간 생략...

    @Test
    @DisplayName("Hello Service")
    fun helloService() {
        // given
        given(helloService.getHello()).willReturn("hello service");

        // when
        val resultActions: ResultActions = mockMvc.perform(get("/hello-service"))
            .andDo(print())

        // then
        resultActions
            .andExpect(status().is2xxSuccessful)
            .andExpect(content().string("hello service"))
            .andDo(print())
    }
}

실제 동작 여부도 확인을 위해 프로젝트를 실행 후에 아래 curl 명령어로 작동 여부를 확인한다.

% curl localhost:8080/hello-service
hello service%

DTO 생성

DTO 파일을 생성하고 아래와 같이 작성한다.

/src/main/kotlin/com/devkuma/hello/dto/HelloDto.kt

package com.devkuma.hello.dto

class HelloDto(val greeting: String)

Controller 파일에서 아래와 같이 ‘helloDto()‘를 추가한다.

/src/main/kotlin/com/devkuma/hello/controller/HelloController.kt

@RestController
class HelloController(val helloService: HelloService) {

    // ...중간 생략...

    @GetMapping("/hello-dto")
    fun helloDto(): HelloDto {
        return HelloDto("hello dto")
    }
}

아래와 같이 테스트 코드 helloDto()를 추가하고, 실행해 본다.

/src/test/kotlin/com/devkuma/hello/controller/HelloControllerTests.kt

package com.devkuma.hello.controller

import com.devkuma.hello.service.HelloService
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.ResultActions
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*


@WebMvcTest(HelloController::class)
class HelloControllerTests {

    // ...중간 생략...

    @Test
    @DisplayName("Hello DTO")
    fun helloDto() {

        // when
        val resultActions: ResultActions = mockMvc.perform(get("/hello-dto"))
            .andDo(print())

        // then
        resultActions
            .andExpect(status().is2xxSuccessful)
            .andExpect(jsonPath("greeting").value("hello dto"))
            .andDo(print())
    }
}

실제 동작 여부도 확인을 위해 프로젝트를 실행 후에 아래 curl 명령어로 작동 여부를 확인한다.

% curl localhost:8080/hello-dto
{"greeting":"hello dto"}%

그밖에