코틀린 + Spring을 사용하여 첫 프로젝트의 국룰인 RESTful한 TODO 리스트를 만들어보자.
task
엔드포인트에서 구현해야할 HTTP method는 다음과 같다
GET
: 모든 TODO 리스트를 가져옴POST
: 새로운 TODO 리스트를 추가함DELETE
: 해당 TODO 리스트를 제거함우선 코틀린의 main() 메소드로 엔트리 포인트를 만들어준다.
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class TodoApplication
fun main(args: Array<String>) {
runApplication<TodoApplication>(*args)
}
위에서 @SpringBootApplication
어노테이션은 스프링 부트의 가장 기본적인 설정을 선언해주는 역할을 한다.
프로젝트 빌드가 제대로 되어있는지를 확인해보기 위해 서버를 돌려보자. Maven, Gradle의 사용 여부에 따라서 커맨드는 달라진다
./mvnw clean install
java -jar target/todo-0.0.1-SNAPSHOT.jar
./gradlew build
java -jar build/libs/todo-0.0.1-SNAPSHOT.jar
구현하고자 하는 task
엔드포인트의 첫번째 함수 GET
의 초안을 작성해보자. 이 함수는 간단하게 호출 시 "to be implemented"
라는 문자열 응답을 준다.
package com.agiledeveloper.todo
import org.springframework.web.bind.annotation.*
import org.springframework.http.ResponseEntity
@RestController
@RequestMapping("/task")
class TaskController {
@GetMapping
fun tasks() = "to be implemented"
}
각각의 어노테이션에 대한 설명이다
@RestController
은 요청에 대한 응답을 JSON reponse로 처리해준다.@RequestMapping
은 엔드포인트의 URI를 정의해준다.@GetMapping
어노테이션은 해당 엔드포인트의 URI를 지정해주는 역할을 한다.curl을 사용해 request를 날려보자
curl -w "\n" http://localhost:8080/task
TODO 리스트의 데이터를 책임질 엔티티를 생성해주자.
package com.agiledeveloper.todo
import javax.persistence.*
@Entity
data class Task(@Id @GeneratedValue val id: Long, val description: String)
첫번째로 해당 엔티티의 pk인 id
프로퍼티를 정의해준다. @Id
와 @GeneratedValue
어노테이션은 해당 프로퍼티를 pk로 선정하고 unique value를 DB에서 자동 생성되게 해준다.
두번째 프로퍼티인 description
의 타입은 String
이다.
해당 엔티티를 애초에 데이터 클래스로 구현했으므로 equals()
, hashCode()
, toString()
과 같은 함수들은 자동적으로 구현이 되어 있다.
코틀린 스프링에서 Repository란 여러 data source들을 관리하는데 생성하는 클래스이다.
CrudRepository
는 엔티티 클래스에 대한 CRUD 기능을 제공해준다. 이걸 사용해보자.
package com.agiledeveloper.todo
import org.springframework.data.repository.CrudRepository
interface TaskRepository : CrudRepository<Task, Long>
CrudRepository<Task, Long>
을 상속받은 TaskRepository
인터페이스의 첫번째 parametric type인 Task
는 엔티티 타입을 지정해주고, 두번째 parametric type인 Long
은 PK에 대한 타입을 지정해준다.
이제 엔티티 정의가 모두 마무리되었으니 서비스 레이어를 작업해보자.
TaskService
클래스를 정의해준다. 서비스 클래스를 담당하고 있으므로 Service
어노테이션을 달아준다. Transactional
어노테이션은 장고의 @transcation.atomic
과 비슷하다고 볼 수 있다.
package com.agiledeveloper.todo
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class TaskService(val repository: TaskRepository) {
fun getAll() = repository.findAll()
fun save(task: Task) = repository.save(task)
fun delete(id: Long): Boolean {
val found = repository.existsById(id)
if (found) {
repository.deleteById(id)
}
return found
}
}
전체적으로 CrudRepository
를 상속받은 CRUD 함수들이므로 많은 추가구현은 필요없다. 각 함수에 대한 요약이다.
getAll()
함수는 CrudRepository
의 findAll()
함수를 호출해 전체 TODO 리스트를 가지고 온다.
save()
함수는 CrudRepository
에서 기본적으로 제공하는 save()
메소드를 사용해준다.
delete()
함수는 매개변수로 들어온 id에 해당하는 로우를 deleteById()
를 통해 제거한다.
이제 모든 CRUD를 구현했으므로 앞서 테스트용으로 구현했던 컨트롤러로 다시 돌아가 완성을 시켜주자.
package com.agiledeveloper.todo
import org.springframework.web.bind.annotation.*
import org.springframework.http.ResponseEntity
@RestController
@RequestMapping("/task")
class TaskController(val service: TaskService) {
@GetMapping
fun tasks() = ResponseEntity.ok(service.getAll())
@PostMapping
fun create(@RequestBody task: Task): ResponseEntity<String> {
val result = service.save(task)
return ResponseEntity.ok(
"added task with description ${result.description}")
}
@DeleteMapping("/{id}")
fun delete(@PathVariable id: Long) = if (service.delete(id)) {
ResponseEntity.ok("Task with id $id deleted")
} else {
ResponseEntity.status(404).body("Task with id $id not found")
}
각각의 메소드에 해당하는 어노테이션 @GetMapping
, @PostMapping
, @DeleteMapping
을 구현한 메소드에 매핑시켜준다.
get()
메소드는 서비스단에서 앞서 구현한 getAll()
함수를 호출해 모든 TODO 리스트를 가져온다.
create()
메소드는 @RequestBody
어노테이션으로 Task
형의 post body를 가져와 save()
메소드를 호출한다.
delete()
메소드는 @PathVariable
어노테이션으로 매핑된 url parameter인 id
를 가지고 해당 id
에 대한 delete()
메소드를 수행한다. 삭제 성공 여부에 따라 200
, 404
응답을 리턴해준다.
이제 구현한 모든 HTTP 메소드를 호출해보자
curl -w "\n" -X GET http://localhost:8080/task
[]
curl -w "\n" -X POST \
-H "Content-Type: application/json" \
-d '{"description": "write code"}' http://localhost:8080/task
curl -w "\n" -X POST \
-H "Content-Type: application/json" \
-d '{"description": "test"}' http://localhost:8080/task
added task with description write code
added task with description test
curl -w "\n" -X DELETE http://localhost:8080/task/1
curl -w "\n" -X DELETE http://localhost:8080/task/10
Task with id 1 deleted
Task with id 10 not found
마지막으로 GET
curl -w "\n" -X GET http://localhost:8080/task
[{"id":2,"description":"test"}]
화이팅