본문 바로가기

Java/spring

[Java/스프링] JPA를 이용한 Todo API 개발 과정 (dependencies: Spring Boot DevTools, Lombok, Spring Web, MySQL Driver, Spring Data JPA)

들어가며

🎯 JPA를 연습하기 위해 CRUD 기능을 구현할 수 있는 Todo API를 만들어 보았습니다.
본 프로젝트는 다음과 같은 순서로 진행하였습니다.
  1. 프로젝트 생성
  2. application.propertices 설정
  3. Entity 및 DB 생성
  4. DTO 생성
  5. Service 생성 및 DTO 사용
  6. Controller 생성 및 DTO로 데이터 송수신

 

기능 요구사항

  • Todo 목록 조회
    • 사용자는 Todo 목록을 조회할 수 있습니다.
    • 각 Todo 항목에는 id, title, description, completed 필드가 포함됩니다.
    • GET / todos 를 통해 모든 Todo 항목을 가져옵니다. - getTodos()
    • GET / todos / {id} 를 통해 id에 해당하는 Todo 항목을 가져옵니다. - getTodoById()
  • Todo 생성
    • 사용자는 새로운 Todo 항목을 추가할 수 있습니다.
    • Todo 항목에는 title, description, completed가 포함되어야 합니다.
    • POST / todos 를 통해 새로운 Todo 항목을 생성할 수 있습니다. - addTodo()
    • 클라이언트: TodoRequest를 통해 title, descriptioin, completed를 전달하고 서버는 이를 저장합니다.
    • 서버: 저장된 항목을 TodoResponse 형식으로 반환합니다.
  • Todo 수정
    • 사용자는 기존 Todo 항목을 수정할 수 있습니다.
    • 수정 시, title, description, completed를 변경할 수 있습니다.
    • PUT / todos / {id} 를 통해 특정 id의 Todo 항목을 수정할 수 있습니다. - updateTodo()
    • 클라이언트: 수정할 데이터는 TodoRequest를 통해 전달됩니다.
    • 서버: 수정된 데이터를 TodoResponse로 반환합니다.
  • Todo 삭제
    • 사용자는 특정 Todo 항목을 삭제할 수 있습니다.
    • DELETE / todos / {id} 를 통해 특정 id의 Todo 항목을 삭제할 수 있습니다. - deleteTodo()
    • 삭제 시, 별도 응답 데이터를 반환하지 않습니다.
  • 완료 상태 관리
    • 각 Todo 항목에는 completed 여부를 나타내는 필드가 있어야 합니다.
    • 사용자는 Todo 항목의 완료 상태를 true 또는 false로 변경할 수 있습니다.

1. 프로젝트 생성

ℹ️ start.spring.io에서 필요한 의존성을 추가합니다.
  • Spring Web (API 개발)
    : Spring MVC를 사용하여 RESTful을 포함한 웹 애플리케이션을 구축합니다. (기본 내장 컨테이너: Apache Tomcat)
  • Spring Boot DevTools (개발 생산성 향상)
    : 자동 재시작, 리로드, 라이브 리로드, 개발용 설정 등을 지원합니다.
  • Lombok (코드 간결화)
    : 코드를 줄이는데 도움이 되는 Java 어노테이션 라이브러리입니다.
  • MySQL Driver (MySQL과 연결)
    : MySQL JDBC 드라이버입니다. JDBC란 Java에서 데이터베이스에 접속할 수 있도록 하는 Java API입니다.
  • Spring Data JPA (JPA 사용)
    : Hibernate와 JPA를 기반으로 JPA를 한 단계 더 추상화 시켜 개발 용이성을 올려주는 인터페이스입니다.


2. application.properties 설정

ℹ️ application.properties에 MySQL 및 JPA를 설정합니다.
  • spring.jpa.hibernate.ddl-auto 설정
설정 종류 상세 내용
create 매번 애플리케이션 시작 시 데이터베이스를 새로 생성합니다.
개발 중에는 유용하지만 실제 배포 환경에서는 데이터 손실 위험이 있으므로 update 또는 validate로 설정하는 것이 좋습니다.
update 기존 테이블을 유지하면서 새로운 엔티티 필드만 추가합니다. (개발 환경에서 자주 사용하는 설정)
validate 애플리케이션 시작 시 데이터베이스 스미카가 엔티티와 일치하는 지 검증합니다.
none 스키마 생성 또는 업데이트를 하지 않습니다.

 

  • createDatabaseIfNotExist=true
    • spring.jpa.hibernate.ddl-auto: create와 spring.datasource.url=...createDatabaseIfNotExist=true... 설정을 추가하면, 데이터베이스가 존재하지 않을 경우 MySQL이 todo_app 데이터베이스를 자동으로 생성할 수 있습니다.
spring.application.name=todo2_practice

# DB 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/todo2_practice?createDatabaseIfNotExist=true&serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234

# JPA 설정
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create

3. Entity 및 DB 생성

ℹ️ model 객체 및 DB를 생성합니다.
  • main > java > dev.todo2_practice > model
    • BaseEntity
      • @Id: DB 테이블의 PK와 객체 필드를 매핑해 주는 어노테이션입니다.
      • @GeneratedValue(strategy = GenerationType.IDENTITY): 기본키를 자동 생성해 주는 어노테이션입니다.
      • @MappedSuperclass: JPA에서 여러 엔티티 클래스 간 공통으로 사용되는 필드를 부모 클래스로 정의하고, 자식 엔티티 클래스에서 이를 상속받아 재사용할 수 있도록 하는 어노테이션입니다.
        package dev.todo2_practice.model;
        
        import jakarta.persistence.GeneratedValue;
        import jakarta.persistence.GenerationType;
        import jakarta.persistence.Id;
        import jakarta.persistence.MappedSuperclass;
        import lombok.AllArgsConstructor;
        import lombok.Getter;
        import lombok.NoArgsConstructor;
        
        @Getter
        @AllArgsConstructor @NoArgsConstructor
        @MappedSuperclass
        public class BaseEntity {
        
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;
        
        }
        
    • Todo
      • @Entity: 클래스를 엔티티로 지정해, 이 클래스가 DB 테이블과 매핑될 대상임을 명시합니다.
      • @Table(name = "todos"): 엔티티 클래스와 매핑될 테이블의 이름을 명시합니다.
      • @Column(nullable = false): 컬럼의 속성을 지정합니다. nullable = false란 해당 컬럼이 null 값을 가질 수 없음을 의미합니다.
      • from 메소드 (TodoRequest → Todo): 클라이언트에서 전달된 요청 데이터를 담은 DTO 클래스(TodoRequest)를 → 엔티티 클래스, 즉 DB에 저장될 데이터(Todo)로 변환합니다.
      • update 메소드: TodoRequest에서 받은 내용으로 Todo의 title, description, completed 값을 업데이트 합니다.
        package dev.todo2_practice.model;
        
        import dev.todo2_practice.dto.TodoRequest;
        import jakarta.persistence.Column;
        import jakarta.persistence.Entity;
        import jakarta.persistence.Table;
        import lombok.AllArgsConstructor;
        import lombok.Builder;
        import lombok.Getter;
        import lombok.NoArgsConstructor;
        
        @Getter
        @AllArgsConstructor @NoArgsConstructor
        @Builder
        
        @Entity
        @Table(name = "todos")
        public class Todo extends BaseEntity{
            @Column(nullable = false)
            private String title;
        
            @Column(nullable = false)
            private String description;
        
            @Column(nullable = false)
            private boolean completed;
        
            public static Todo from(TodoRequest todoRequest) {
                return Todo.builder()
                        .title(todoRequest.getTitle())
                        .description(todoRequest.getDescription())
                        .completed(todoRequest.isCompleted())
                        .build();
            }
        
            public void update(TodoRequest todoRequest) {
                this.title = todoRequest.getTitle();
                this.description = todoRequest.getDescription();
                this.completed = todoRequest.isCompleted();
            }
        }

        테이블 생성 완료
  • main > java > dev.todo2_practice > dto
    • TodoRequest
      package dev.todo2_practice.dto;
      
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Getter
      @AllArgsConstructor @NoArgsConstructor
      public class TodoRequest {
          private String title;
          private String description;
          private boolean completed;
      }
  • main > java > dev.todo2_practice > repository
    • (interface) TodoRepository
      • JpaRepository 상속: 기본적인 CRUD 기능을 자동으로 제공 받아 개발 생산성을 향상시킬 수 있습니다.
        package dev.todo2_practice.repository;
        
        import dev.todo2_practice.model.Todo;
        import org.springframework.data.jpa.repository.JpaRepository;
        
        public interface TodoRepository extends JpaRepository<Todo, Long> {
        }
  • main > java > dev.todo2_practice > data
    • DataInitializer
      • @Component: 해당 클래스가 스프링 빈임을 알려줍니다. 즉, 스프링 컨테이너가 이 클래스의 인스턴스를 관리하고 DI를 통해 다른 빈과 연결할 수 있도록 합니다.
      • @PostContruct: 빈이 생성되고 모든 DI가 주입된 후 실행되어야 하는 메소드를 표시합니다. 이는 빈의 초기화 작업을 수행하는 데 사용됩니다. (ex: DB의 초기 데이터 입력, 외부 시스템과의 연결 설정 등)
      • init 메소드: TodoRepository를 사용하여 DB에 초기 Todo 데이터를 입력합니다.
        package dev.todo2_practice.data;
        
        import dev.todo2_practice.repository.TodoRepository;
        import dev.todo2_practice.model.Todo;
        import jakarta.annotation.PostConstruct;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.stereotype.Component;
        
        @Component
        @Slf4j
        public class DataInitializer {
            private final TodoRepository todoRepository;
        
            public DataInitializer(TodoRepository todoRepository) {
                this.todoRepository = todoRepository;
            }
        
            @PostConstruct
            public void init() {
                if (todoRepository.count() == 0) { // 초기 설정
                    log.info("Initializing data...");
                    todoRepository.save(Todo.builder()
                                    .title("Buy groceries")
                                    .description("Milk, bread, eggs")
                                    .completed(false)
                            .build());
                    todoRepository.save(Todo.builder()
                                    .title("Clean the house")
                                    .description("Vacuum, dust")
                                    .completed(true)
                            .build());
                    todoRepository.save(Todo.builder()
                                    .title("Read a book")
                                    .description("Finish reading 'JPA'")
                                    .completed(false)
                            .build());
                }
            }
        }

        초기 데이터 입력 완료

4. DTO 생성

  • main > java > dev.todo2_practice > dto
    • TodoRequest
      package dev.todo2_practice.dto;
      
      import lombok.AllArgsConstructor;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      
      @Getter
      @AllArgsConstructor @NoArgsConstructor
      public class TodoRequest {
          private String title;
          private String description;
          private boolean completed;
      }


    • TodoResponse
      • from 메소드 (Todo→ TodoResponse): 엔티티 클래스를 →  클라이언트에 보여줄 데이터를 담은 DTO 클래스(TodoResponse)로 변환합니다.
        package dev.todo2_practice.dto;
        
        import dev.todo2_practice.model.Todo;
        import lombok.AllArgsConstructor;
        import lombok.Builder;
        import lombok.Getter;
        import lombok.NoArgsConstructor;
        
        @Getter
        @NoArgsConstructor @AllArgsConstructor
        @Builder
        public class TodoResponse {
            private Long id;
            private String title;
            private String description;
            private Boolean completed;
        
            public static TodoResponse from(Todo todo) {
                return TodoResponse.builder()
                        .id(todo.getId())
                        .title(todo.getTitle())
                        .description(todo.getDescription())
                        .completed(todo.isCompleted())
                        .build();
            }
        }




5. Service 생성 및 DTO 사용

  • main > java > dev.todo2_practice > service
    • (interface) TodoService
      package dev.todo2_practice.service;
      
      import dev.todo2_practice.dto.TodoResponse;
      
      import java.util.List;
      
      // R: TodoResponse, D: TodoRequest, ID: id
      public interface TodoService<R, D, ID> {
          // TODO: 목록 조회, 생성, 수정, 삭제
          List<R> findAll();
          TodoResponse findById(ID id);
          TodoResponse save(D TodoRequest);
          TodoResponse update(ID id, D TodoRequest);
          void delete(ID id);
      }


    • TodoServiceImpl
ℹ️ Service 구현 방법이 헷갈린다면 아래 주석처럼 구현 순서를 먼저 작성한 뒤, 코드를 작성합니다.
package dev.todo2_practice.service;

import dev.todo2_practice.dto.TodoRequest;
import dev.todo2_practice.dto.TodoResponse;
import dev.todo2_practice.model.Todo;
import dev.todo2_practice.repository.TodoRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Transactional
public class TodoServiceImpl implements TodoService<TodoResponse, TodoRequest, Long>{

    private final TodoRepository todoRepository;

    public TodoServiceImpl(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @Override
    public List<TodoResponse> findAll() {
        // DB에서 전체 할일 목록 조회, Entity type으로 응답 받기
        List<Todo> todos = todoRepository.findAll();
        // Entity를 TodoResponse로 변환하기
        List<TodoResponse> todoResponses = todos.stream().map(TodoResponse::from).collect(Collectors.toList());
        return todoResponses;
    }

    @Override
    public TodoResponse findById(Long id) {
        // DB에서 id별 할일 목록 조회, Entity type으로 응답 받기
        Todo todo = todoRepository.findById(id).orElseThrow(() -> new RuntimeException(id + "에 해당하는 할 일이 없습니다."));
        // Entity를 TodoResponse로 변환하기
        TodoResponse todoResponse = TodoResponse.from(todo);
        return todoResponse;
    }

    @Override
    public TodoResponse save(TodoRequest todoRequest) {
        // 파라미터로 전달받은 TodoRequest를 Entity로 변환
        Todo todo = Todo.from(todoRequest);
        // 실제 DB에 저장하는 처리
        Todo saveTodo = todoRepository.save(todo);
        // 응답할 때는 TodoResponse 형태로 변환하여 응답
        TodoResponse todoResponse = TodoResponse.from(saveTodo);
        return todoResponse;
    }

    @Override
    public TodoResponse update(Long id, TodoRequest todoRequest) {
        // 전달받은 id에 해당하는 Entity 조회
        Todo todo = todoRepository.findById(id).orElseThrow(() -> new RuntimeException(id + "에 해당하는 할 일이 없습니다."));
        // Entity를 update 처리
        todo.update(todoRequest);
        // 응답할 때는 TodoResponse 형태로 변환하여 응답
        TodoResponse todoResponse = TodoResponse.from(todo);
        return todoResponse;
    }

    @Override
    public void delete(Long id) {
        // 전달받은 id에 해당하는 Entity 조회
        Todo todo = todoRepository.findById(id).orElseThrow(() -> new RuntimeException(id + "에 해당하는 할 일이 없습니다."));
        // Entity를 삭제 처리
        todoRepository.delete(todo);
    }
}

 



6. Controller 생성 및 DTO로 데이터 송수신

  • main > java > dev.todo2_practice > controller
    • TodoController
ℹ️ PutMapping과 DeleteMapping으로 구현한 update, delete는 PostMapping으로도 동작 가능합니다.
package dev.todo2_practice.controller;

import dev.todo2_practice.dto.TodoRequest;
import dev.todo2_practice.dto.TodoResponse;
import dev.todo2_practice.service.TodoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/todos")
@RequiredArgsConstructor
@Slf4j
public class TodoController {
    private final TodoService todoService;

    @GetMapping
    public ResponseEntity<List<TodoResponse>> getTodos() {
        log.info("getTodos()...");
        List<TodoResponse> todos = todoService.findAll();
        return new ResponseEntity<>(todos, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<TodoResponse> getTodoById(@PathVariable Long id) {
        log.info("getTodoById()...");
        TodoResponse todo = todoService.findById(id);
        return new ResponseEntity<>(todo, HttpStatus.OK);
    }

    @PostMapping
    public ResponseEntity<TodoResponse> addTodo(@RequestBody TodoRequest todoRequest) {
        log.info("addTodo()...");
        TodoResponse todo = todoService.save(todoRequest);
        return new ResponseEntity<>(todo, HttpStatus.CREATED);
    }

    @PutMapping("/{id}") // @PostMapping으로도 동작
    public ResponseEntity<TodoResponse> updateTodo(@PathVariable Long id, @RequestBody TodoRequest todoRequest) {
        log.info("updateTodo()...");
        TodoResponse todo = todoService.update(id, todoRequest);
        return new ResponseEntity<>(todo, HttpStatus.OK);
    }

    @DeleteMapping("/{id}") // @PostMapping으로도 동작
    public ResponseEntity<TodoResponse> deleteTodo(@PathVariable Long id) {
        log.info("deleteTodo()...");
        todoService.delete(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

추가적인 학습 및 기능 확장 방향

  1. ManyToOne / OneToMany 매핑 연습: User, Category Entity 추가합니다.
  2. AOP 적용 연습 (로깅)
  3. 테스트 케이스 작성
  4. 검색 및 필터링 기능: Todo 항목을 제목, 완료 여부, 날짜 등으로 검색하고 필터링 하는 기능을 추가합니다.
  5. 보안: Spring Security를 사용하여 인증 및 권한 관리를 추가합니다.
  6. 파일 업로드: Todo 항목에 파일 첨부하는 기능을 추가합니다.

Todo API

https://documenter.getpostman.com/view/38203469/2sAXqqd3FX

 

todo2_practice

The Postman Documenter generates and maintains beautiful, live documentation for your collections. Never worry about maintaining API documentation again.

documenter.getpostman.com

반응형