투두 앱을 작동시키는데 필요한 백엔드 서버 만들기.
투두 앱은 아래와 같이 총 5개의 기본 요구사항을 갖고 있다.
TodoApp 설계
요구 사항 (필수)
요구사항 | 설명 |
할일 카드 작성 기능 | 할 일 제목, 할일 내용, 작성일, 작성자 이름을 저장할 수 있다. 저장된 할일의 정보를 반환받아 확인할 수 있다. |
선택한 할 일 조회 기능 | 선택한 할 일의 정보를 조회할 수 있다. 반환 받은 할 일 정보에는 할일 제목, 할일 내용, 작성일 ,작성자 이름 정보가 있다. |
할일 카드 목록 조회 기능 | 등록된 할 일 전체를 조회할 수 있다. 조회한 할 일 목록은 작성일 기준 내림차순으로 정렬되어있다. |
선택한 할일 수정 기능 | 선택한 할일의 할일 제목, 작성자명, 작성 내용을 수정할 수 있다. 수정된 할 일의 정보를 반환받아 확인 가능. |
선택한 할 일 삭제 기능 | 선택한 할일 정보를 삭제할 수 있다. |
패키지 구조
처음에는 계층형 구조를 사용할까 했다.
하지만, 아직 파일들의 관계 등을 제대로 확인하고, 흐름을 이해햐는데에는 시각적으로 보기 어려운 점이 있어(지극히 개인적인 문제) 도메인형 구조로 작성하기로 했다.
domain
⎿ todo
⎿ controller
⎿ service
⎿ repository
⎿ dto
⎿ model
exception
infra
⎿ swagger
추후에 댓글 기능, 로그인 기능 등이 추가되면, 계층형으로 파일을 관리하는 것 보단 확실히 도메인형으로 파일을 관리하는 것이 수정하는 것 등에 용이할 것이라 생각했다.
API 명세서
우선 필수 요구사항 들을 기준으로 API 명세서를 작성해보았다.
Command | Method | API Path | Response |
할일 작성 | POST | /todo | 201 |
할일 목록 조회 | GET | /todo | 200 |
할일 상세 조회 | GET | /todo/{todoId} | 200 |
할일 수정 | PUT | /todo/{todoId} | 200 |
할일 삭제 | DELETE | /todo/{todoId} | 204 |
할일 수정의 경우에는 PUT, PATCH 두 가지 Method 중 어떤걸 쓸지 고민을 좀 많이 했었다.
DB
Todo | |
id | Long ( auto generated ) |
title | String |
author | String |
date | LocalDate |
description | String |
구현
실제로 구현하기에 앞서, 순서를 정했다.
Step1. Controller와 DTO 설정하기
Step2. Service 작성 및 Controller와 연결
Step3. Repository 작성 및 Service와 연결
우선 DB 명세에 따라, TodoResponse data class를 생성했다.
data class TodoResponse (
val id:Long,
val title:String,
val author:String,
val date:LocalDate,
val body:String,
)
//TodoResponse.kt
그리고, 할일 추가, 할일 수정시 필요한 요청들에 대한 data class도 구현했다.
data class CreateTodoRequest (
val title: String,
val author:String,
val body: String?,
val date: LocalDate,
)
//CreateRequest.kt
data class UpdateTodoRequest (
val title: String,
val author: String,
val body: String?,
val date: LocalDate,
)
//UpdateTodoRequest.kt
그리고 Controller를 작성했다. 각각의 항목들은 API명세서를 바탕으로 작성되었다.
@RequestMapping("/todos")
@RestController
class TodoController (private val todoService: TodoService) {
@GetMapping
fun getTodoList(): ResponseEntity<List<TodoResponse>> {
}
@GetMapping("/{todoId}")
fun getTodoById(@PathVariable todoId: Long) : ResponseEntity<TodoResponse> {
}
@PostMapping
fun createTodo(@RequestBody createTodoRequest: CreateTodoRequest):ResponseEntity<TodoResponse> {
}
@PutMapping("/{todoId}")
fun updateTodo(@PathVariable todoId: Long, @RequestBody updateTodoRequest: UpdateTodoRequest): ResponseEntity<TodoResponse> {
}
@DeleteMapping("/{todoId}")
fun deleteTodo(@PathVariable todoId: Long) : ResponseEntity<TodoResponse> {
}
}
//TodoController.kt
다음으로 실질적으로 해당 컨트롤러가 작동할 수 있도록 서비스 레이어를 작성하겠다.
Service의 경우에는, Controller가 굳이 Service가 어떻게 동작을 해야하는지 알 필요가 없다. 단순히, 해당 fun들을 활용해 response 하면 된다!
그렇기 때문에, Service를 interface로 만들고 서비스 구현부를 따로 만들었다.
interface TodoService {
fun getAllTodos():List<TodoResponse>
fun getTodoById(todoId:Long): TodoResponse
fun createTodo(createTodoRequest: CreateTodoRequest): TodoResponse
fun updateTodo(todoId:Long, updateTodoRequest: UpdateTodoRequest): TodoResponse
fun deleteTodo(todoId:Long)
}
Service구현부와 인터페이스의 연결을 위해 @Service 어노테이션을 사용했다.
@Service
class TodoServiceImpl(private val todoRepository: TodoRepository) :TodoService{
override fun getAllTodos(): List<TodoResponse> {
}
override fun getTodoById(todoId: Long): TodoResponse {
}
@Transactional
override fun createTodo(createTodoRequest: CreateTodoRequest): TodoResponse {
}
@Transactional
override fun updateTodo(todoId: Long, updateTodoRequest: UpdateTodoRequest): TodoResponse {
}
@Transactional
override fun deleteTodo(todoId: Long) {
}
}
이제, Service와 Controller를 연결하자. TodoService를 불러와 각각의 기능에 맞는 funtion들을 넣어주었다.
@RequestMapping("/todos")
@RestController
class TodoController (private val todoService: TodoService) {
@GetMapping
fun getTodoList(): ResponseEntity<List<TodoResponse>> {
return ResponseEntity
.status(HttpStatus.OK)
.body(todoService.getAllTodos())
}
@GetMapping("/{todoId}")
fun getTodoById(@PathVariable todoId: Long) : ResponseEntity<TodoResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(todoService.getTodoById(todoId))
}
@PostMapping
fun createTodo(@RequestBody createTodoRequest: CreateTodoRequest):ResponseEntity<TodoResponse> {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(todoService.createTodo(createTodoRequest))
}
@PutMapping("/{todoId}")
fun updateTodo(@PathVariable todoId: Long, @RequestBody updateTodoRequest: UpdateTodoRequest): ResponseEntity<TodoResponse> {
return ResponseEntity
.status(HttpStatus.OK)
.body(todoService.updateTodo(todoId, updateTodoRequest))
}
@DeleteMapping("/{todoId}")
fun deleteTodo(@PathVariable todoId: Long) : ResponseEntity<TodoResponse> {
return ResponseEntity
.status(HttpStatus.NO_CONTENT)
.build()
}
}
//TodoController.kt
다음으로 Entity를 작성했다. Entity는 만들어놓은 DB테이블과 맵핑이 된다.
@Entity
@Table(name = "todo")
class Todo(
@Column(name = "title", nullable = false)
var title: String,
@Column(name = "author", nullable = false)
var author: String,
@Column(name = "body")
var body: String?,
@Column(name = "created_at", nullable = false)
var createdAt: LocalDate,
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
}
//매번 TodoResponse()를 생성하기엔 너무 길어 단순화하기 위해 만든 함수.
fun Todo.toResponse(): TodoResponse {
return TodoResponse(
id=id!!,
title=title,
author=author,
date=createdAt,
body = body!!,
)
}
//Todo.kt
Repository의 경우에는 JpaRepository를 사용한다. 제공하는 Method들은 다음 링크를 들어가면 알 수 있다.
interface TodoRepository : JpaRepository<Todo, Long> {
//커스텀할 함수들이 있으면 해당 공간에 작성한다.
}
//TodoRepository.kt
이제 Repository와 Service를 연결한다. 참고로 전체 Todo를 불러올 경우 작성시간 기준 내림차순 정렬을 해야한다는 조건이 있었다.
@Service
class TodoServiceImpl(private val todoRepository: TodoRepository) :TodoService{
override fun getAllTodos(): List<TodoResponse> {
return todoRepository.findAll().sortedByDescending { it.createdAt }.map { it.toResponse() }
}
override fun getTodoById(todoId: Long): TodoResponse {
val todo = todoRepository.findByIdOrNull(todoId) ?: throw ModelNotFoundException("Todo", todoId)
return todo.toResponse()
}
@Transactional
override fun createTodo(createTodoRequest: CreateTodoRequest): TodoResponse {
return todoRepository.save(
Todo(
title = createTodoRequest.title,
body = createTodoRequest.body,
createdAt = createTodoRequest.date,
author = createTodoRequest.author
)
).toResponse()
}
@Transactional
override fun updateTodo(todoId: Long, updateTodoRequest: UpdateTodoRequest): TodoResponse {
val todo = todoRepository.findByIdOrNull(todoId) ?: throw ModelNotFoundException("Todo", todoId)
val (title, author, body, createdAt) = updateTodoRequest
todo.title = title
todo.author = author
todo.body = body
todo.createdAt = createdAt
return todoRepository.save(todo).toResponse()
}
@Transactional
override fun deleteTodo(todoId: Long) {
val todo = todoRepository.findByIdOrNull(todoId) ?: throw ModelNotFoundException("Todo", todoId)
todoRepository.delete(todo)
}
}
//TodoServiceImpl.kt
이렇게 작성해두면, 기본적으로 Todo CRUD의 구현이 완료된다.
전체 필수 요구사항 구현 코드는 GitRepo에 잘 정리되어 있다! 글이 너무 길다면 가서 전체 코드를 보는것도 좋다.
Git Repository : https://github.com/DEVxMOON/TodoServer
아마 내일은 개인 프로젝트 작업을 들어가면서, 선택 구현 3개중 첫번째 부분에 관련된 내용을 정리할 것 같다.
'PROJECT > Spring Todo 프로젝트' 카테고리의 다른 글
TodoApp 백엔드 서버 만들기 (3) (0) | 2024.05.17 |
---|---|
TodoApp 백엔드 서버 만들기 (2) (0) | 2024.05.16 |