본문 바로가기
PROJECT/Spring Todo 프로젝트

TodoApp 백엔드 서버 만들기 (1)

by HR_J 2024. 5. 14.

투두 앱을 작동시키는데 필요한 백엔드 서버 만들기.

투두 앱은 아래와 같이 총 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들은 다음 링크를 들어가면 알 수 있다.

https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html

 

JpaRepository (Spring Data JPA Parent 3.2.5 API)

All Superinterfaces: CrudRepository , ListCrudRepository , ListPagingAndSortingRepository , PagingAndSortingRepository , QueryByExampleExecutor , Repository All Known Subinterfaces: EnversRevisionRepository , JpaRepositoryImplementation All Known Implement

docs.spring.io

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

 

GitHub - DEVxMOON/TodoServer

Contribute to DEVxMOON/TodoServer development by creating an account on GitHub.

github.com

 

아마 내일은 개인 프로젝트 작업을 들어가면서, 선택 구현 3개중 첫번째 부분에 관련된 내용을 정리할 것 같다.

'PROJECT > Spring Todo 프로젝트' 카테고리의 다른 글

TodoApp 백엔드 서버 만들기 (3)  (0) 2024.05.17
TodoApp 백엔드 서버 만들기 (2)  (0) 2024.05.16