1. 기본 설정
ToDo List 프로젝트의 기본 설정을 위한 작업들이다.
// build.gradle
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.h2database:h2'
compileOnly 'org.projectlombok:lombok:1.18.24'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
JPA, h2 DB, lombok 등을 사용하기 위해 dependencies들을 추가해주었다.
// application.config
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.open-in-view=false
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=create
h2 DB, JPA, Hibernate 등과 관련된 여러 설정들을 진행하였다.
2. TodoEntity 구현
Todo 관련 내용을 담을 객체를 구현하기 위한 코드이다. JPA를 통해 자동으로 해당 테이블을 생성할 것이다.
//TodoEntity.java
@Builder // 자동으로 빌더를 생성해줌
@NoArgsConstructor // 파라미터가 없는 기본 생성자를 생성해줌
@AllArgsConstructor // 모든 필드 값을 파라미터로 받는 생성자를 생성해줌
@Data // getter, setter, toString을 자동 생성해줌
@Entity // DB에서의 Entity로써 테이블의 스키마
@Table(name = "Todo") // Table 이름을 지정, 미지정 시에는 클래스 이름과 동일
public class TodoEntity {
@Id //Primary Key로 설정
@GenericGenerator(name="uuid2", strategy="org.hibernate.id.UUIDGenerator") // 자동 임의값 지정
@GeneratedValue(generator="uuid2")
private String id;
private String userId; // 차후 로그인 지원시 user의 구분을 위함
private String title; // Todo의 내용
private LocalDate date; // 날짜
private boolean done; // Todo 완료 여부
}
3. CORS - 교차 출처 리소스 공유를 위한 설정
CORS란 추가 HTTP 헤더를 사용하여 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제다.
백엔드에서는 프론트 쪽에서의 요청들을 처리해야 하므로 이 과정이 필요하다.
//WebMVCConfig.java
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
private final long MAX_AGE_SECS = 3600;
@Override
public void addCorsMappings(CorsRegistry registry) {
// 모든 경로에 대해
registry.addMapping("/**") // CORS를 적용할 URL 패턴을 정의 (여기서는 와일드카드를 사용)
.allowedOrigins("http://localhost:3000") // Origin(http:localhost:3000)에 대해
// GET, POST, PUT, PATCH, DELETE, OPTIONS 메서드를 허용한다.
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*") // 모든 헤더 허용
// MAX_AGE_SECS만큼 pre-flight 리퀘스트(사전에 서버에서 어떤 origin과 어떤 method를 허용하는지 브라우저에게 알려주는 역할)를 캐싱
.maxAge(MAX_AGE_SECS);
}
}
4. DTO (Data Transfer Object) 구현
DTO는 컨트롤러, 서비스, 리포지토리 계층간의 데이터 교환을 위한 객체이다.
프로젝트를 위해서 Todo와 Response를 위한 DTO 객체를 생성하였다.
// ResponseDTO.java
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseDTO<T> {
private String error; // 에러가 발생하면 에러 정보를 저장
private List<T> data; // 넘겨줄 데이터를 저장
}
// TodoDTO.java
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
private String id;
private String title;
private LocalDate date;
private boolean done;
public TodoDTO(TodoEntity todo) { // 주입되는 todo의 정보에 맞춰 TodoDTO를 생성
this.id = todo.getId();
this.title = todo.getTitle();
this.date = todo.getDate();
this.done = todo.isDone();
}
public static TodoEntity toTodo(final TodoDTO todoDTO) { // 주입되는 todoDTO의 정보에 맞춰 todo를 생성한 후 반환
return TodoEntity.builder()
.id(todoDTO.getId())
.title(todoDTO.getTitle())
.date(todoDTO.getDate())
.done(todoDTO.isDone())
.build();
}
}
5. 컨트롤러 구현
유저가 요청하는 다양한 기능들을 처리해줄 컨트롤러이다.
//TodoController.java
@RestController
@RequestMapping("todo") // 아래 메서드들을 localhost:8080/todo로 Mapping
@RequiredArgsConstructor
public class TodoController {
private final TodoService service; // 서비스 계층의 기능을 사용하기 위함
@GetMapping("/test") // localhost:8080/todo/test로 Mapping
public ResponseEntity<?> testTodo() {
String str = service.testService();
List<String> list = new ArrayList<>();
list.add(str);
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return ResponseEntity.ok().body(response);
}
@PostMapping("/create") // localhost:8080/todo/create로 Mapping
public ResponseEntity<?> createTodo(@RequestBody TodoDTO todoDTO){
try{
String tempUserId = "temp-user"; // User ID
LocalDate date = LocalDate.now();
TodoEntity todo = TodoDTO.toTodo(todoDTO); // DB로 넘길 todo를 만들기
todo.setId(null); // 명시
todo.setUserId(tempUserId);
todo.setDate(date); // 날짜 저장
// todo가 올바른 상태인지 확인하고 올바르다면 저장 후에 그대로 반환
List<TodoEntity> todos = service.create(todo);
// todos에 들어가 있는 Todo들을 하나씩 순회하면서 TodoDTO로 변경 후에 모두 toDoDTOs에 리스트 형태로 저장
List<TodoDTO> todoDTOs = todos.stream().map(TodoDTO::new).collect(Collectors.toList());
// todoDTOs을 가지는 ResponseDTO<TodoDTO>인 response 생성
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(todoDTOs).build();
return ResponseEntity.ok().body(response); // response를 응답 메시지에 포함하여 전달함으로써 todo 생성 완료
} catch (Exception e){
String error = e.getMessage();
// 에러 정보를 담은 ResponseDTO인 response를 생성
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
return ResponseEntity.badRequest().body(response); // 에러 정보를 응답 메시지에 포함하여 badRequest로 전달
}
}
@GetMapping("/find") // localhost:8080/todo/find로 Mapping
public ResponseEntity<?> findTodoList(){
try{
String tempUserId = "temp-user"; // User ID
List<TodoEntity> todos = service.retrieve(tempUserId);
List<TodoDTO> todoDTOs = todos.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(todoDTOs).build();
return ResponseEntity.ok().body(response);
}catch (Exception e){
String error = e.getMessage();
return ResponseEntity.badRequest().body(error);
}
}
@PutMapping("/update") // localhost:8080/todo/update로 Mapping
public ResponseEntity<?> updateTodo(@RequestBody TodoDTO todoDTO){
try {
String tempUserId = "temp-user";
TodoEntity todo = todoDTO.toTodo(todoDTO);
todo.setUserId(tempUserId);
List<TodoEntity> todos = service.update(todo);
List<TodoDTO> todoDTOs = todos.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(todoDTOs).build();
return ResponseEntity.ok().body(response);
} catch (Exception e){
String error = e.getMessage();
return ResponseEntity.badRequest().body(error);
}
}
@DeleteMapping("/delete") //localhost:8080/todo/delete로 Mapping
public ResponseEntity<?> deleteTodo(@RequestBody TodoDTO todoDTO){
try {
String tempUserId = "temp-user";
TodoEntity todo = todoDTO.toTodo(todoDTO);
todo.setUserId(tempUserId);
List<TodoEntity> todos = service.delete(todo);
List<TodoDTO> todoDTOs = todos.stream().map(TodoDTO::new).collect(Collectors.toList());
ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(todoDTOs).build();
return ResponseEntity.ok().body(response);
}catch (Exception e){
String error = e.getMessage();
return ResponseEntity.badRequest().body(error);
}
}
}
6. 서비스 계층 구현
서비스 계층은 컨트롤러와 데이터 엑세스 계층(Repository)을 연결해주는 역할을 한다.
CRUD 기능뿐만 아니라 Transaction 관리나 Todo의 적합성 검증(validation)도 수행할 것이다.
@Slf4j // 다양한 로깅 프레임 워크에 대한 추상화(인터페이스) 역할을 하는 라이브러리
@Service
@RequiredArgsConstructor
public class TodoService {
private final TodoRepository todoRepository;
public String testService() {
// TodoEntity 생성 (Create)
TodoEntity todo = TodoEntity.builder().title("My first todo item").build();
// TodoEntity 저장 (Create)
todoRepository.save(todo);
// TodoEntity 검색 (Read)
// TODO: Optional 값을 어떻게 처리해주는 것이 좋은 것인가?
Optional<TodoEntity> optionalTodoEntity = todoRepository.findById(todo.getId());
if (optionalTodoEntity.isPresent()) {
TodoEntity savedEntity = optionalTodoEntity.get();
return savedEntity.getTitle();
}
return "";
}
@Transactional
public List<TodoEntity> create(TodoEntity todo) { // todo 검증 후 저장을 한 후에 로그 작성하고 저장한 todo를 반환
validate(todo);
todoRepository.save(todo);
log.info("Todo Id : {} is saved", todo.getId());
return retrieve(todo.getUserId());
}
public List<TodoEntity> retrieve(String userId) { // 해당 uid로 작성된 todo들 반환
return todoRepository.findByUserId(userId);
}
private void validate(TodoEntity todo) { // 올바른 todo인지 검증
if(todo == null) {
log.warn("Entity can not be NULL.");
throw new RuntimeException("Entity can not be NULL.");
}
if(todo.getUserId() == null) {
log.warn("Unknown User.");
throw new RuntimeException("Unknown User.");
}
}
@Transactional
public List<TodoEntity> update(TodoEntity todo) {
validate(todo);
final Optional<TodoEntity> optionalTodo = todoRepository.findById(todo.getId());
optionalTodo.ifPresent((offeredTodo) -> {
// spring JPA의 더티체킹 기능 덕에 todoRepository.save(entity)를 해주지 않아도 된다.
offeredTodo.setTitle(todo.getTitle()); // todo 내용 수정 후 저장
offeredTodo.setDone(todo.isDone()); // todo 완료 여부 수정 후 저장
// offeredTodo.setDate(backupDate); // todo 날짜 저장
// todoRepository.save(offeredTodo);
});
return retrieve(todo.getUserId());
}
@Transactional
public List<TodoEntity> delete(TodoEntity todo) {
validate(todo);
try{
todoRepository.deleteById(todo.getId());
}catch (Exception e){
log.error("error deleting entity {} not exists!", todo.getId());
throw new RuntimeException("error deleting entity " + todo.getId());
}
return retrieve(todo.getUserId());
}
}
7. 리포지토리 계층 구현
JpaRepository를 사용하여, DB에 CRUD 명령들을 실행할 수 있게 하는 계층이다. 복잡한 쿼리문을 쉽게 실행할 수 있게 해 준다.
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
List<TodoEntity> findByUserId(String userId); // 해당 uid를 가지는 todo들을 리스트 형태로 반환
}
8. 실행 결과
(1) ToDo 테이블 생성
JPA를 사용하여서 자동적으로 테이블을 생성하게 하였는데, 의도대로 잘 실행되었음을 콘솔 창에서 확인할 수 있다.
H2에서 show문을 사용해 Todo 테이블의 column들을 확인하였을 때, 역시 의도한 대로 잘 생성됐음을 확인할 수 있다.
(2) ToDo 입력
이제 웹 화면에 ToDo를 직접 입력을 해보자.
위와 같이 ToDo 입력 창에 해야 할 일을 입력한다.
(3) ToDo 생성
엔터키를 입력하거나 + 버튼을 클릭하게 되면 위와 같이 ToDo가 생성되어 화면에 나타남을 확인할 수 있다.
콘솔 창에도 마찬가지로 자동으로 SQL문이 실행되어서 데이터가 DB에 입력되었음을 확인할 수 있다.
H2에서 select문을 사용해 입력한 ToDo가 DB에 저장되었는지 확인할 수 있다.
(4) ToDo 수정
생성된 ToDo의 내용을 마우스로 클릭하면 내용을 수정할 수 있게 된다.
위처럼 수정을 한 후에 엔터키를 눌러 수정을 완료한다.
콘솔 창에서 자동으로 SQL문이 실행되어서 데이터가 수정되어 DB에 입력되었음을 확인할 수 있다.
H2에서 select문을 사용해 Todo 테이블의 모든 데이터들을 확인하였을 때, 잘 삭제되었음을 확인할 수 있다.
(5) ToDo 삭제
생성된 ToDo의 우측에 위치한 휴지통 버튼을 클릭하면 그 ToDo는 삭제가 된다.
콘솔창에서 자동으로 SQL문이 실행되어서 데이터가 DB에서 삭제되었음을 확인할 수 있다.
H2에서 select문을 사용해 Todo 테이블의 모든 데이터들을 확인하였을 때, 잘 삭제되었음을 확인할 수 있다.
9. 다음으로
다음에는 ToDo List를 사용하기 위한 회원가입을 구현해보도록 하겠다.
※ 참고
프론트엔드 구현은 같이 스프링 스터디를 진행했던 팀원(leejh7)이 제공해준 프론트엔드 코드를 사용하였다.
https://github.com/leejh7/Co_TodoList
'개발 > ToDo List using JPA' 카테고리의 다른 글
[ToDo List using JPA] 1. CRUD와 JPA란? (0) | 2022.11.15 |
---|