들어가며
🎯 JPA를 연습하기 위해 CRUD 기능을 구현할 수 있는 Todo API를 만들어 보았습니다.
본 프로젝트는 다음과 같은 순서로 진행하였습니다.
- 프로젝트 생성
- application.propertices 설정
- Entity 및 DB 생성
- DTO 생성
- Service 생성 및 DTO 사용
- 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(); } }
테이블 생성 완료
- BaseEntity
- 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; }
- TodoRequest
- 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> { }
- JpaRepository 상속: 기본적인 CRUD 기능을 자동으로 제공 받아 개발 생산성을 향상시킬 수 있습니다.
- (interface) TodoRepository
- 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()); } } }
초기 데이터 입력 완료
- DataInitializer
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(); } }
- from 메소드 (Todo→ TodoResponse): 엔티티 클래스를 → 클라이언트에 보여줄 데이터를 담은 DTO 클래스(TodoResponse)로 변환합니다.
- TodoRequest
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
- (interface) TodoService
ℹ️ 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);
}
}
추가적인 학습 및 기능 확장 방향
- ManyToOne / OneToMany 매핑 연습: User, Category Entity 추가합니다.
- AOP 적용 연습 (로깅)
- 테스트 케이스 작성
- 검색 및 필터링 기능: Todo 항목을 제목, 완료 여부, 날짜 등으로 검색하고 필터링 하는 기능을 추가합니다.
- 보안: Spring Security를 사용하여 인증 및 권한 관리를 추가합니다.
- 파일 업로드: 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
반응형