본문 바로가기

카테고리 없음

todo에 TodoErrorCode,GlobalExceptionHandler등 예외처리

package com.navi.nbcampjavatodotask.database.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String content;

    private String username;

    private String password;

    private LocalDateTime createdAt;

    @Builder
    public Todo(String title, String content, String username, String password, LocalDateTime createdAt) {
        this.title = title;
        this.content = content;
        this.username = username;
        this.password = password;
        this.createdAt = createdAt;
    }

    public void update(String title, String content, String username) {
        this.title = title;
        this.content = content;
        this.username = username;
    }
}// 엔티티에 작성과 삭제까지 써놓기

 

이렇게 엔티티에 작성, 수정까지 넣는게 좋다.

public interface TodoRepository extends JpaRepository<Todo, Long> {

    List<Todo> findAllByOrderByCreatedAtDesc();
    
}

 

이거면 내림차순 조회가 가능하다

 

dto는

public record TodoResponseDTO(
        @Schema(description = "일정 ID")
        Long id,
        @Schema(description = "제목")
        String title,
        @Schema(description = "내용")
        String content,
        @Schema(description = "담당자 이름")
        String username,
        @Schema(description = "생성 일시")
        LocalDateTime createdAt
) {

    public static TodoResponseDTO of(Todo entity) {
        return new TodoResponseDTO(
                entity.getId(),
                entity.getTitle(),
                entity.getContent(),
                entity.getUsername(),
                entity.getCreatedAt()
        );
    }
}

 

이렇게 record를 이용한다. 이때, public record엔 class가 붙으면 안된다. public record class 이러면 안된다는것이다.

그리고 리퀘스트는

public record TodoRequestDTO(
        @Schema(description = "제목")
        @Size(min = 1, max = 200)
        @NotBlank
        String title,

        @Schema(description = "내용")
        String content,

        @Schema(description = "유저 이름")
        @NotBlank
        String username,

//        @Schema(description = "담당자 이메일")
//        @NotBlank
//        @Email
//        String username,

        @Schema(description = "비밀번호")
        @NotBlank
        String password
) {

}

 

 

 entity.getId(),
                entity.getTitle(),
                entity.getContent(),
                entity.getUsername(),
                entity.getCreatedAt() 이것이 리스폰스에만 있는건, 그것들을 사용자에게 보여줘야 하기 때문이다.

 

업데이트도 이렇게

public record TodoUpdateDTO(
        @Schema(description = "제목")
        @Size(min = 1, max = 200)
        @NotBlank
        String title,

        @Schema(description = "내용")
        String content,

        @Schema(description = "담당자 이메일")
        @NotBlank
        @Email
        String username,

        @Schema(description = "비밀번호")
        @NotBlank
        String password
) {

}

 

그리고 기존의

@Operation(summary = "할일 삭제", description = "삭제할 할일의 ID를 입력하시오")
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTodo(@PathVariable Long id, @RequestParam String password) {
    todoService.delete(id, password);
    return ResponseEntity.noContent().build();
}

 

이렇게 비밀번호를 보내는게 아니라

public record TodoDeleteRequestDTO (
        @Schema(description = "비밀번호")
        @NotBlank
        String password
) {

}

 

이렇게 처리한다. 그러면 

 

이 방법은 클라이언트에게 더 많은 유연성을 제공하며, 민감한 정보를 안전하게 보낼 수 있다.

 

@Service
@RequiredArgsConstructor
public class TodoService {

    private final TodoRepository todoRepository;

    public Todo createTodo(
            String title,
            String content,
            String username,
            String password,
            LocalDateTime createdAt
    ) {
        Todo todo = Todo.builder()
                .title(title)
                .content(content)
                .username(username)
                .password(password)
                .createdAt(createdAt)
                .build();

        return todoRepository.save(todo);
    }
    //새로운 투두 항목을 생성합니다. 전달받은 인자를 사용하여 Todo 객체를 생성한 후,
    // 이를 데이터베이스에 저장하고 저장된 객체를 반환합니다.

    public List<Todo> getTodos() {
        return todoRepository.findAllByOrderByCreatedAtDesc();
    }
//모든 투두 항목을 조회하여 생성일자 내림차순으로 정렬된 리스트를 반환
    public Todo getTodoById(Long id) {
        return todoRepository.findById(id).orElseThrow(TodoNotFoundException::new);
    }
//주어진 ID를 사용하여 특정 투두 항목을 조회합니다. 해당 ID가 존재하지 않으면
// TodoNotFoundException을 던집니다.
    public Todo updateTodo(
            Long id,
            String title,
            String content,
            String username,
            String password
    ) {
        Todo todo = getTodoById(id);

        if (!todo.getPassword().equals(password)) {
            throw new TodoPasswordNotMatchedException();
        }

        todo.update(title, content, username);

        return todoRepository.save(todo);
    }
//주어진 ID를 사용하여 특정 투두 항목을 업데이트합니다. 먼저 투두 항목을 조회한 후,
// 비밀번호가 일치하지 않으면 TodoPasswordNotMatchedException을 던집니다.
// 비밀번호가 일치하면 제목, 내용, 사용자명을 업데이트하고 변경된 투두 항목을 저장합니다.
    public void deleteTodo(
            Long id,
            String password
    ) {

        Todo todo = getTodoById(id);

        if (!todo.getPassword().equals(password)) {
            throw new TodoPasswordNotMatchedException();
        }

        todoRepository.delete(todo);
    }//주어진 ID를 사용하여 특정 투두 항목을 삭제합니다. 먼저 투두 항목을 조회한 후, 
    // 비밀번호가 일치하지 않으면 TodoPasswordNotMatchedException을 던집니다. 
    // 비밀번호가 일치하면 해당 투두 항목을 삭제합니다.
}

 

 

 

 

 

 

그리고

public record ErrorResponse(
        String errorCode,
        String errorMessage
) {

}

 

ErrorResponse는 예외 발생 시 반환될 오류 응답을 담는 레코드입니다. 두 개의 필드 errorCode와 errorMessage를 가지고 있습니다.

 

package com.teamsparta.task.exception;

import com.teamsparta.task.exception.ErrorResponse;
import java.util.stream.Collectors;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<ErrorResponse> handleTodoException(
            TodoException exception
    ) {
        var status = exception.getErrorCode().getStatus();
        var errorResponse = new ErrorResponse(
                exception.getErrorCode().name(),
                exception.getErrorCode().getMessage()
        );
        return ResponseEntity.status(status).body(errorResponse);
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException exception
    ) {
        var message = exception.getBindingResult().getFieldErrors()
                .stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.joining());
        var errorResponse = new ErrorResponse(
                "ARGUMENT_NOT_VALID",
                message
        );

        return ResponseEntity.badRequest().body(errorResponse);
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException exception) {
        var errorResponse = new ErrorResponse(
                "INTERNAL_SERVER_ERROR",
                "서버 에러입니다"
        );
        return ResponseEntity.internalServerError().body(errorResponse);
    }
}

 

 

  • handleTodoException 메서드는 TodoException을 처리하여, 예외의 상태 코드와 메시지를 ErrorResponse에 담아 반환합니다.
  • handleMethodArgumentNotValidException 메서드는 유효성 검사 실패 예외(MethodArgumentNotValidException)를 처리하여, 필드 오류 메시지를 조합하여 ErrorResponse에 담아 반환합니다.
  • handleRuntimeException 메서드는 기타 런타임 예외(RuntimeException)를 처리하여, 내부 서버 오류 메시지를 ErrorResponse에 담아 반환합니다.

 

 

 

 

@RequiredArgsConstructor
@Getter
public enum TodoErrorCode {
    PASSWORD_NOT_MATCHED(HttpStatus.UNAUTHORIZED, "패스워드가 불일치합니다."),
    TODO_NOT_FOUND(HttpStatus.NOT_FOUND, "투두를 찾을 수 없습니다.");

    private final HttpStatus status;
    private final String message;

}

 

TodoErrorCode는 특정 예외 상황에 대한 오류 코드와 메시지를 정의한 열거형(enum). 각 항목은 HTTP 상태 코드와 메시지를 포함. 밑과 연결됨.

@Getter
public abstract class TodoException extends RuntimeException {

    private final TodoErrorCode errorCode;

    public TodoException(TodoErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

 

TodoException은 사용자 정의 예외의 기본 클래스입니다. TodoErrorCode를 받아서 초기화하며, 예외 메시지는 TodoErrorCode의 메시지를 사용(패스워드가 불일치합니다,투두를 찾을 수 없습니다)

public class TodoNotFoundException extends TodoException {

    public TodoNotFoundException() {
        super(TodoErrorCode.TODO_NOT_FOUND);
    }
}

 

  • TodoNotFoundException은 TodoException을 상속받아 특정 예외(TODO_NOT_FOUND)를 처리하는 클래스입니다.
  • TodoPasswordNotMatchedException은 TodoException을 상속받아 다른 특정 예외(PASSWORD_NOT_MATCHED)를 처리하는 클래스입니다.

 

 

 

public class TodoPasswordNotMatchedException extends TodoException {

    public TodoPasswordNotMatchedException() {
        super(TodoErrorCode.PASSWORD_NOT_MATCHED);
    }
}

 

 

  • 이 클래스는 TodoException 클래스를 상속받습니다. TodoException은 사용자 정의 예외의 기본 클래스입니다.
  • TodoPasswordNotMatchedException 생성자는 TodoErrorCode.PASSWORD_NOT_MATCHED를 인자로 받는 TodoException 생성자를 호출합니다. 이를 통해 예외가 발생했을 때, "패스워드가 불일치합니다"라는 메시지와 함께 HTTP 상태 코드 401 (Unauthorized)을 반환하게 됩니다.

참고로 getall용으로 dto도 만들어주는것이 좋다. 전체조회는 간략하게 일부만 보여주는게 좋기 때문이다.

 

public record SimplifiedTodoResponseDTO(
        @Schema(description = "일정 ID")
        Long id,
        @Schema(description = "제목")
        String title,
        @Schema(description = "생성 일시")
        LocalDateTime createdAt
) {

}

 

일반조회가 아이디, 제목, 내용, 날짜를 보여주나 전체조회는 아이디, 제목 날짜로 간소화해서 보여준다

 

그리고 

@RestController
@RequestMapping("/api/todos")
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService;

        @Operation(summary = "일정 생성")
        @ApiResponses(value = {
                @ApiResponse(responseCode = "200", description = "Created the todo.")
        })
        @PostMapping
        public TodoResponseDTO createTodo(@RequestBody @Valid TodoRequestDTO request) {
            Todo todo = todoService.createTodo(
                    request.title(),
                    request.content(),
                    request.username(),
                    request.password(),
                    LocalDateTime.now()
            );

            return TodoResponseDTO.of(todo);
        }

        @Operation(summary = "일정 목록 조회")
        @ApiResponses(value = {
                @ApiResponse(responseCode = "200", description = "Fetched the todo.")
        })
        @GetMapping
        public List<SimplifiedTodoResponseDTO> getTodos() {
            return todoService.getTodos()
                    .stream()
                    .map(todo -> new SimplifiedTodoResponseDTO(todo.getId(), todo.getTitle(), todo.getCreatedAt()))
                    .toList();
        }

        @Operation(summary = "일정 단일 조회")
        @ApiResponses(value = {
                @ApiResponse(responseCode = "200", description = "Fetched the todo."),
                @ApiResponse(responseCode = "400", description = "Todo not found."),
        })
        @GetMapping("/{id}")
        public TodoResponseDTO getTodo(@PathVariable("id") Long id) {
            Todo todo = todoService.getTodoById(id);
            return TodoResponseDTO.of(todo);
        }

        @Operation(summary = "일정 수정")
        @ApiResponses(value = {
                @ApiResponse(responseCode = "200", description = "Updated the todo."),
                @ApiResponse(responseCode = "400", description = "Todo not found."),
                @ApiResponse(responseCode = "401", description = "Password not matched.")
        })
        @PatchMapping("/{id}")
        public TodoResponseDTO updateTodo(
                @PathVariable("id") Long id,
                @RequestBody @Valid TodoUpdateDTO request
        ) {
            Todo todo = todoService.updateTodo(
                    id,
                    request.title(),
                    request.content(),
                    request.username(),
                    request.password()
            );
            return TodoResponseDTO.of(todo);
        }

        @Operation(summary = "일정 삭제")
        @ApiResponses(value = {
                @ApiResponse(responseCode = "200", description = "Deleted the todo."),
                @ApiResponse(responseCode = "400", description = "Todo not found."),
                @ApiResponse(responseCode = "401", description = "Password not matched.")
        })
        @DeleteMapping("/{id}")
        public void deleteTodo(
                @PathVariable("id") Long id,
                @RequestBody @Valid TodoDeleteRequestDTO request
        ) {
            todoService.deleteTodo(id, request.password());
        }
    }

 

 

이렇세 완성해준다