Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2주차과제 - SpringWeb ToDo REST API #115

Merged
merged 11 commits into from
Jul 18, 2023
Merged
3 changes: 3 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ dependencies {

// Spring Web
implementation 'org.springframework.boot:spring-boot-starter-web'

// Spring devtools
developmentOnly "org.springframework.boot:spring-boot-devtools"
}

application {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.codesoom.assignment.application;

import com.codesoom.assignment.exception.custom.TaskNotFound;
import com.codesoom.assignment.models.domain.Task;
import com.codesoom.assignment.models.request.TaskCreate;
import com.codesoom.assignment.models.request.TaskEdit;
import com.codesoom.assignment.repository.TaskRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TaskService {
private final TaskRepository taskRepository;

public TaskService(TaskRepository taskRepository) {
this.taskRepository = taskRepository;
}

public Task createTask(TaskCreate task) {
return taskRepository.save(task.toTask());
}

public List<Task> getTaskList() {
return taskRepository.findAll();
}

public Task getTask(Long id) {
return taskRepository.findById(id).orElseThrow(TaskNotFound::new);
}

public void deleteTask(Long id) {
Task task = taskRepository.findById(id).orElseThrow(TaskNotFound::new);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 만들어진 getTask를 사용할 수도 있겠네요

taskRepository.delete(task);
}

public Task updateTask(TaskEdit task) {
return taskRepository.update(task.toTask());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.codesoom.assignment.controllers;

import com.codesoom.assignment.exception.CustomBaseException;
import com.codesoom.assignment.models.response.ErrorResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* 예외 처리를 담당합니다.
*/
@ControllerAdvice
public class ExceptionController {

@ExceptionHandler(CustomBaseException.class)
@ResponseBody
public ResponseEntity<ErrorResponse> CustomBaseExceptionHandler(CustomBaseException e) {
return sendError(e);
}

private ResponseEntity<ErrorResponse> sendError(CustomBaseException e) {
ErrorResponse errorResponse = parseErrorResponse(e);
return parseErrorResponseEntity(e, errorResponse);
}

private ResponseEntity<ErrorResponse> parseErrorResponseEntity(CustomBaseException e, ErrorResponse errorResponse) {
return ResponseEntity.status(e.getStatusCode()).body(errorResponse);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

에러 객체에서 상태 코드를 결정하는 군요. 저는 예외는 예외만 나타내고 응답을 어떻게 줄지 Advice에서 결정하도록 했었는데 어떤게 더 좋을가요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

커스텀 예외 객체의 status 값을 실제 응답 status 로 쓰는 형태인데
커스터 예외 객체는 예외에 관한 정보만 두고
Advice에서 해당 예외를 체크해서 상태코드를 결정하시라는 말씀이신가여?
분리하는게 더 좋아보이기는 합니다 예외는 예외 응답은 응답은 Advice에서 처리하는게 맞다고 생각은 되는데
그렇게 하면 뭔가 또 if문으로 분기처리하는 게 길어질것 같아서
고민되긴 하네요. (뭔가 분기가 많은 if를 꺼리게되는 느낌)
혹시 좋은 방법이 있을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advice에서 핸들러에 @ResponseStatus(HttpStatus.�NOT_FOUND)를 붙여서 어떻게 응답할지 결정할 수 있어요

See also

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 좋은 기능이 있었네요!
다음부터 일단 공식사이트 먼저 검색해봐야겠습니다.

}

private ErrorResponse parseErrorResponse(CustomBaseException e) {
ErrorResponse errorResponse = new ErrorResponse(String.valueOf(e.getStatusCode()), e.getMessage(), e.validation);
return errorResponse;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.codesoom.assignment.controllers;

import com.codesoom.assignment.application.TaskService;
import com.codesoom.assignment.models.domain.Task;
import com.codesoom.assignment.models.request.TaskCreate;
import com.codesoom.assignment.models.request.TaskEdit;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@CrossOrigin
@RestController
public class TaskController {
private final TaskService taskService;

public TaskController(TaskService taskService) {
this.taskService = taskService;
}

@GetMapping("/tasks")
public List<Task> list() {
return taskService.getTaskList();
}

@GetMapping("/tasks/{id}")
public Task detail(@PathVariable Long id) {
return taskService.getTask(id);
}

@PostMapping("/tasks")
public ResponseEntity<Task> create(@RequestBody TaskCreate taskCreate) {
taskCreate.validate();
Task task = taskService.createTask(taskCreate);
return ResponseEntity.status(HttpStatus.CREATED).body(task);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResponseEntity객체를 이용해서 응답 상태 코드를 변경하셨네요. @ResponseStatus를 사용해서 상태 코드를 지정할 수 있습니다.

아마 이런 어노테이션이 있는지 몰라서 그러셨을 것 같아요. 아주 지극히 정상입니다.

어노테이션이란 코드 외적으로 코드의 힌트를 추가하는 메타 프로그래밍의 일종으로 볼 수 있습니다. 코드에는 영향을 줄 수는 없지만 코드에 대한 메타 정보를 입력할 수 있습니다. 그래서 어노테이션의 장점은 기존 코드에 아무런 영향을 주지 않는다는 것입니다.

그런데 어노테이션 프로세서에게는 의미가 있습니다. 어노테이션 처리자는 어노테이션의 값을 읽어서 다르게 처리합니다. 즉 코드를 읽을 때 우리가 읽는 레이어를 여러개가 있다고 바라볼 수 있어야 합니다.

잘쓰면 편리하지만 아주 치명적인 단점이 있습니다. 발견 가능성이 아주 낮다는 거예요. 무슨 말이냐면 이러한 어노테이션이 있다는 것을 외우지 않는 이상 알 수가 없습니다. 그래서 어노테이션을 지양하는 사람도 있어요.

See also

Copy link
Author

@tmxhsk99 tmxhsk99 Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞는것 같습니다 누가 알려주거나 쓰는것을 보는게 아닌 이상
뭐가 있는지 알기 어렵더라구요...
이런게 있는게 알려면 공식문서를 보는수 밖에 없겠져...?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 ㅎㅎ 공부만이 답입니다~!

}

@RequestMapping(value = "/tasks/{id}", method = {RequestMethod.PUT, RequestMethod.PATCH})
public Task update(@PathVariable Long id,@RequestBody TaskEdit task) {
task.setId(id);
return taskService.updateTask(task);
}

@DeleteMapping("/tasks/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if(id == null) {
return ResponseEntity.notFound().build();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

어떤 요청에서 id가 null이 되나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그냥 http DELETE http://localhost:8080/tasks/ 이런 식으로 요청하면 null로 들어오겠지라고
생각했는데 HttpRequestMethodNotSupportedException 에러로 떨어지네요
수정하겠습니다

taskService.deleteTask(id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.codesoom.assignment.exception;

import java.util.HashMap;
import java.util.Map;

/**
* 사용자정의 예외의 추상클래스 특정 필드의 유효성 검증 실패 정보를 저장하고, HTTP 상태 코드를 반환합니다.
*/
public abstract class CustomBaseException extends RuntimeException {
public final Map<String, String> validation = new HashMap<>();

public CustomBaseException() {
}

public CustomBaseException(String message) {
super(message);
}

public abstract int getStatusCode();

public void addValidation(String fieldName, String message) {
validation.put(fieldName, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.codesoom.assignment.exception.custom;

import com.codesoom.assignment.exception.CustomBaseException;

public class InvalidTaskRequest extends CustomBaseException {
private static final String MESSAGE = "할 일 생성 요청이 올바르지 않습니다.";

public InvalidTaskRequest() {
super(MESSAGE);
}

public InvalidTaskRequest(String fieldName, String message) {
super(MESSAGE);
addValidation(fieldName,message);
}

@Override
public int getStatusCode() {
return 400;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.codesoom.assignment.exception.custom;

import com.codesoom.assignment.exception.CustomBaseException;

public class TaskNotFound extends CustomBaseException {
private static final String MESSAGE = "존재하지 않는 할 일 입니다.";

public TaskNotFound() {
super(MESSAGE);
}

@Override
public int getStatusCode() {
return 404;
}
}
27 changes: 27 additions & 0 deletions app/src/main/java/com/codesoom/assignment/models/domain/Task.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.codesoom.assignment.models.domain;

/**
* 할 일 객체
*/
public class Task {
private Long id;
private String title;

public Task(Long id, String title) {
this.id = id;
this.title = title;
}

public Long getId() {
return id;
}

public String getTitle() {
return title;
}

public void changeTitle(String title) {
this.title = title;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.codesoom.assignment.models.request;

import com.codesoom.assignment.exception.custom.InvalidTaskRequest;
import com.codesoom.assignment.models.domain.Task;
import org.springframework.util.StringUtils;

public class TaskCreate {

private Long id;

private String title;

public TaskCreate(Long id, String title) {
this.id = id;
this.title = title;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public void validate() {
if (!StringUtils.hasText(this.title)) {
throw new InvalidTaskRequest("title","title 은 필수값 입니다.");
}
}
public Task toTask() {
return new Task(this.id,this.title);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.codesoom.assignment.models.request;

import com.codesoom.assignment.models.domain.Task;

public class TaskEdit {
private Long id;

private String title;

public TaskEdit(Long id, String title) {
this.id = id;
this.title = title;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getTitle() {
return title;
}

public void setTitle(String title) {
this.title = title;
}

public Task toTask() {
return new Task(this.id,this.title);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.codesoom.assignment.models.response;

import java.util.HashMap;
import java.util.Map;

/**
* 에러 반환 응답 객체
*/
public class ErrorResponse {
private String code;
private String message;
private Map<String, String> validation;

public ErrorResponse(String code, String message, Map<String, String> validation) {
this.code = code;
this.message = message;
this.validation = validation != null ? validation : new HashMap<>();
}

public void addValidation(String field, String message) {
validation.put(field, message);
}

public String getCode() {
return code;
}

public String getMessage() {
return message;
}

public Map<String, String> getValidation() {
return validation;
}

public void setCode(String code) {
this.code = code;
}

public void setMessage(String message) {
this.message = message;
}

public void setValidation(Map<String, String> validation) {
this.validation = validation;
}

@Override
public String toString() {
return String.format("code: %s, message: %s, validation: %s", code, message, validation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.codesoom.assignment.repository;

import com.codesoom.assignment.models.domain.Task;

import java.util.List;
import java.util.Optional;

public interface TaskRepository {

Task save(Task task);

List<Task> findAll();

Optional<Task> findById(Long id);

void delete(Task task);

Task update(Task task);
}
Loading
Loading