본문 바로가기

카테고리 없음

Spring + Kotlin API Server 만들기 (5) : REST API, Controller, Swagger

REST란?

Representational State Transfer 줄임말로, 서버에 존재하는 데이터들을 이름으로 구분하고, 데이터를 주고 받는 행위들을 의미한다.

REST API를 구성하는건 다음과 같다.

  • url
    • url만 봐도 어떤 자원에 접근하는지 명시된다.
      • ex) /devices : 서버에 존재하는 devices들에 접근한다.
      • ex) /devices/1 : 서버에 존재하는 devices들중 id 1번에 해당하는 것에 접근한다.
      • ex) /devices/1/value : 1번 id의 device의 value에 접근한다.
  • method
    • GET : 데이터를 가져온다.
    • POST : 데이터를 생성한다.
    • PATCH : 데이터를 수정한다.
    • PUT : 데이터가 있으면 수정, 없으면 생성
    • DELETE: 데이터 삭제
    • ...(더 있지만 생략)

Controller 설계

현재 프로젝트에선 모든 method에 대해서 API가 필요 없어서 다음과 같이 코드를 작성한다.

package com.studuy.study.device

import org.springframework.http.MediaType
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class DeviceContorller(
    private val deviceService: DeviceService
) {
    @GetMapping("/v1/devices")
    fun getDevices(): Iterable<Device> {
        return deviceService.getAllDevice()
    }

    @PostMapping("/v1/devices")
    fun createDevice(): Device {
        return deviceService.createDevice()
    }

    @GetMapping("/v1/devices/{deviceId}")
    fun getDevice(@PathVariable("deviceId") deviceId: Long): Device {
        return deviceService.getDevice(deviceId)
    }

    @PatchMapping("/v1/devices/{deviceId}/value")
    fun updateDeviceValue(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated value: String
    ): Device {
        return deviceService.updateDeviceValue(deviceId, value)
    }

    @PatchMapping("/v1/devices/{deviceId}/command")
    fun updateDeviceCommand(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated command: String
    ): Device {
        return deviceService.updateDeviceCommand(deviceId, command)
    }
}
  • 설명
    • @RestController
      • 해당 class가 Rest API를 controll하는 역할이라는 걸 명시. @Controller는 Viewer와 매핑되는 다른 어노테이션이니 주의하자
    • @RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
      • 해당 클래스로 들어오고 나가는 데이터들이 json형식을 따르게 해준다.
      • 요청 헤더에 “Contents-type: application/json”이 없으면 에러가 난다.
    • @GetMapping
      • 해당 url에 맞는 요청이 들어왔을 때 해당 함수를 실행한다.
    • @PathVariable
      • url에 {}로 표현되는 url경로상의 변수를 받아와 준다.
    • @RequestBody
      • 요청에 body를 가져와 변수에 대입해준다.
    • @Validated
      • 변수에 들어간 데이터가 유요한 형식인지 확인해 준다.

이제 controller까지 다 만들었으니 API는 모두 완성 되었다. 한번 서버를 돌려보자.

itellij에선 바로 실행할 수 있지만... 그래도 terminal 유저를 위해...

cd ~/{your workspace}
./gradlew build && java -jar build/libs/study-0.0.1-SNAPSHOT.jar

API가 잘 만들어 졌는지 확인하기 위해 curl로 요청을 보내면 잘 작동하는걸 확인 할 수 있다.

curl -X POST <http://localhost:8080/v1/devices>

정상적인 응답

curl -X PATCH <http://localhost:8080/v1/devices/1/command> -d "test command" -H "Content-Type: application/json"

정상적인 응답

이를 문서화 하기 위해 swagger라는게 존재하는데 이를 사용하기 위해 다음과 같이 build.gradle.kts를 수정한다.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
	val kotlinVersion = "1.6.10"
	id("org.springframework.boot") version "2.6.3"
	id("io.spring.dependency-management") version "1.0.11.RELEASE"
	id("org.asciidoctor.convert") version "1.5.8"
	kotlin("jvm") version kotlinVersion
	kotlin("plugin.spring") version kotlinVersion
	kotlin("plugin.jpa") version kotlinVersion
}

group = "com.studuy"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

configurations {
	compileOnly {
		extendsFrom(configurations.annotationProcessor.get())
	}
}

repositories {
	mavenCentral()
}

val snippetsDir by extra { file("build/generated-snippets") }

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-data-jpa")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("org.springframework.boot:spring-boot-starter-mustache")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	//추가
	implementation("org.springdoc:springdoc-openapi-ui:1.6.4")
	runtimeOnly("com.h2database:h2")
	runtimeOnly("org.springframework.boot:spring-boot-devtools")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}
allOpen {
	annotation("javax.persistence.Entity")
	annotation("javax.persistence.MappedSuperclass")
	annotation("javax.persistence.Embeddable")
}
tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs = listOf("-Xjsr305=strict")
		jvmTarget = "11"
	}
}

tasks.withType<Test> {
	useJUnitPlatform()
}

tasks.test {
	outputs.dir(snippetsDir)
}

tasks.asciidoctor {
	inputs.dir(snippetsDir)
	dependsOn(tasks.test)
}

그리고 main/resources에 application.yaml을 만들고 다음과 같이 작성한다.

springdoc:
  swagger-ui:
    path: /api
  • 설명
    • swagger-ui 의 base url을 /api로 설정하였다.

마지막으로 Controller를 다음과 같이 수정한다.

package com.studuy.study.device

import io.swagger.v3.oas.annotations.Operation
import org.springframework.http.MediaType
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class DeviceContorller(
    private val deviceService: DeviceService
) {
    @Operation(summary = "Device list를 주는 API")
    @GetMapping("/v1/devices")
    fun getDevices(): Iterable<Device> {
        return deviceService.getAllDevice()
    }

    @Operation(summary = "Device를 생성하는 API")
    @PostMapping("/v1/devices")
    fun createDevice(): Device {
        return deviceService.createDevice()
    }

    @Operation(summary = "Device data를 주는 API")
    @GetMapping("/v1/devices/{deviceId}")
    fun getDevice(@PathVariable("deviceId") deviceId: Long): Device {
        return deviceService.getDevice(deviceId)
    }

    @Operation(summary = "Device의 value를 수정하는 API")
    @PatchMapping("/v1/devices/{deviceId}/value")
    fun updateDeviceValue(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated value: String
    ): Device {
        return deviceService.updateDeviceValue(deviceId, value)
    }

    @Operation(summary = "Device의 command를 수정하는 API")
    @PatchMapping("/v1/devices/{deviceId}/command")
    fun updateDeviceCommand(
        @PathVariable("deviceId") deviceId: Long,
        @RequestBody @Validated command: String
    ): Device {
        return deviceService.updateDeviceCommand(deviceId, command)
    }
}
  • 설명
    • @Operation
      • 각 함수가 API operation이라고 명시, swagger에 등록된다.

다 수정한뒤 서버를 다시 빌드하고 실행시킨뒤 브라우저에서 localhost:8080/api로 들어가면 다음과 같은 화면이 보인다.

정상적으로 doc이 만들어졌다.

다음 포스팅에선 aws로 배포하는 법에 대해 포스팅할 예정이다.