Spring Boot: study.diary : RestController 에서 ResponseEntity 를 리턴하자.

diary 프로젝트에서 @RestController 애노테이션을 붙여서 작성한 클래스는 DiaryController 하나뿐이야.

이 클래스에서 정의한 5개의 함수 리턴값을 보면 void 이거나 Diary, List 로 이루어져 있지.

DiaryController.java
@RestController
public class DiaryController {
    private final DiaryService diaryService;

    @Autowired
    public DiaryController(DiaryService diaryService) {
        this.diaryService = diaryService;
    }

    // Create
    @PostMapping("/diary")
    public void CreateDiary(@RequestBody Diary diary) {
        System.out.println(diary);
        diaryService.CreateDiary(diary);
    }

    // Read
    @GetMapping("/diary/{id}")
    public Diary GetDiary(@PathVariable("id") Integer id) {
        return diaryService.GetDiary(id);
    }

    // Update
    @PutMapping(value = "/diary/{id}")
    public void UpdateDiary(@PathVariable("id") Integer id, @RequestBody Diary diary) {
        System.out.println("id=" + id);
        System.out.println(diary);
        diaryService.UpdateDiary(id, diary);
    }

    // Delete
    @DeleteMapping("/diary/{id}")
    public void DeleteDiary(@PathVariable("id") Integer id) {
        diaryService.DeleteDiary(id);
    }

    // all Diary
    @GetMapping("/diary/all")
    public List<Diary> GetAllDiaries() {
        return diaryService.GetAllDiaries();
    }
}

Rest API 를 작성하는 이유는 웹을 통해서 요청을 하고 그 요청에 대한 응답을 하는건데, 응답이 이렇게 천차만별이면 응답처리가 좀 불편하겠지.

물론 @RestController 애노테이션을 붙이면 모든 메소드가 @ResponseBody 가 자동으로 붙어서 동작하기 때문에 HTTP 응답을 처리하게 되기는 하지. 이 HTTP 응답을 처리해주고 있는 것은 swagger UI 화면을 보면 알 수 있어.


아무것도 리턴하지 않는 CreateDiary 함수인데 Response 로 200 (OK) 를 리턴한다고 표현되어 있어. 이것이 바로 @ResponseBody 애노테이션의 기능이지.

Diary 객체를 리턴하는 GetDiary 함수의 경우 swagger 에는 이렇게 표시가 되고 있어.


역시 응답코드는 200 뿐이지.

@RestController 애노테이션을 붙인 클래스가 @ResponseBody 애노테이션에 의해서 HTTP 응답만을 리턴하게 했을 때는 Yes/No 의 값만 받을 수 있다고 생각하면 돼. 즉, 좀 더 세밀한 상태코드나 에러내용 등을 전달하기 어려워.

그래서 ResponseEntity 를 리턴하게 만들면 좀 더 유연한 구조의 Rest API 를 만들 수가 있어.

이번 포스트에서는 기존 Rest API 함수들이 ResponseEntity 를 리턴하도록 변경을 해볼께.

    @PostMapping("/diary")
    public ResponseEntity CreateDiary(@RequestBody Diary diary) {
        System.out.println(diary);
        diaryService.CreateDiary(diary);
        return ResponseEntity.ok().build();
    }

void 형이던 CreateDiary 함수의 리턴값을 ResponseEntity 로 변경하고, ResponseEntity.ok().build() 를 리턴하는 코드로 변경을 해봤어.


리턴값 형식 ResponseEntity 아래에 밑줄이 그어지고 있어. 마우스를 갖다 대니까 파라미터를 붙인 사용방법이 정상적인 사용 방법이래. 아무것도 리턴하지 않고 상태값만 리턴하는 경우에는 ResponseEntity<Objects> 와 같이 사용하는게 좋아.

    @PostMapping("/diary")
    public ResponseEntity<Objects> CreateDiary(@RequestBody Diary diary) {
        System.out.println(diary);
        diaryService.CreateDiary(diary);
        return ResponseEntity.ok().build();
    }

이렇게 수정하고 나면 swagger UI 에는 아래와 같이 응답값이 표시가 돼.


swagger UI 를 통해서 CreateDiary (POST method) 를 테스트해볼께.


예상했던대로 200 응답코드를 받았어.

CreateDiary 함수가 하는 기능은 새로운 일기데이터를 생성하는거야. HttpStatus 코드에는 201 Created 라는게 있어. 이번에는 201 status 코드를 리턴하게 만들어볼께.

    @PostMapping("/diary")
    public ResponseEntity<Objects> CreateDiary(@RequestBody Diary diary) {
        System.out.println(diary);
        diaryService.CreateDiary(diary);
        //return ResponseEntity.ok().build();
        return ResponseEntity.created(URI.create("/")).build();
    }

다시 swagger UI 를 이용해서 테스트해보면 응답코드가 201 인 것을 확인할 수가 있어.


이처럼 상황에 맞게 응답코드값을 변경시킬 수 있으려면 ResponseEntity 를 사용해야 한다는거야.

그럼 이번에는 무언가 리턴하는 값이 있는 경우에 ResponseEntity 를 사용하는 방법을 알아볼께.

GetDiary 가 Diary 객체를 리턴하고 있었지.

    // Read
    @GetMapping("/diary/{id}")
    public Diary GetDiary(@PathVariable("id") Integer id) {
        return diaryService.GetDiary(id);
    }

이 함수 역시 ResponseEntity 를 리턴하는 것으로 수정해볼께.

    @GetMapping("/diary/{id}")
    public ResponseEntity<Diary> GetDiary(@PathVariable("id") Integer id) {
        //return diaryService.GetDiary(id);
        return ResponseEntity.ok().body(diaryService.GetDiary(id));
    }

리턴하는 객체 타입을 ResponseEntity<T> 의 T 자리에 넣고, ResponseEntity 의 body 함수의 파라미터로 리턴할 객체를 담아주면 돼.

객체를 리턴하지 않고 응답코드만을 리턴하는 경우에는 build() 함수로 마무리를 했는데, body() 함수를 사용해서 객체를 담아 리턴하는 경우에는 build() 함수를 사용할 필요가 없어.

swagger UI 를 이용해서 테스트해본 결과 200 응답코드와 함께 Diary 객체정보가 json 형식으로 내려온 걸 확인할 수가 있어.


만약에 DB 에 존재하지 않는 고유값 33 을 달라고 했을 때는 어떤 결과가 나올까?


500 응답코드와 함께 timestamp, status, error, trace, message, path 등의 key 로 구성된 json 데이터가 body 로 내려왔어. 이 구조의 body 는 예외가 발생했을 때Spring Boot 가 자동으로 만들어준 구조야.

이러한 구조의 데이터를 예외 핸들링을 통해서 필요한 내용만 전달하도록 변경을 해볼께.

우선 DiaryMapper 의 GetDiary 가 적절한 값을 찾지 못한 경우에 null 을 리턴할 수도 있는 상황에 대해서 처리를 해줘야겠지.

DiaryMapper.java
@Mapper
public interface DiaryMapper {
    ...
    //public Diary GetDiary(Integer id);
    public Optional<Diary> GetDiary(Integer id);
    ...
}

DiaryService 의 GetDiary 함수에서는 Optional<Diary> 를 처리할 수 있도록 변경해줘야지.

    public Diary GetDiary(Integer id) {
        String email = SecurityContextHolder.getContext().getAuthentication().getName();
        //Diary diary = diaryMapper.GetDiary(id);
        Optional<Diary> diary = diaryMapper.GetDiary(id);
        if (diary.isEmpty()) {
            throw new DiaryException("일기데이터가 존재하지 않습니다.");
        }
        if (!diary.get().getEmail().equals(email)) {
            throw new DiaryException("작성자가 아닙니다.");
        }
        return diary.get();
    }

마지막으로 DiaryController 의 GetDiary 함수에서는 DiaryException 에 대한 핸들링이 필요해.

    @GetMapping("/diary/{id}")
    public ResponseEntity<Diary> GetDiary(@PathVariable("id") Integer id) {
        //return diaryService.GetDiary(id);
        try {
            Diary diary = diaryService.GetDiary(id);
            return ResponseEntity.ok().body(diary);
        } catch (DiaryException ex) {
            return ResponseEntity.badRequest().build();
        } catch (Exception ex) {
            return ResponseEntity.internalServerError().build();
        }
    }

DiaryException 예외를 처리해서 badRequest() 함수를 호출하면 400 응답코드가 내려가게 돼. DiaryException 이 아닌 나머지 예외시에는 internalServerError() 함수를 호출해서 500 응답코드가 내려가게 수정했어.

swagger UI 를 통해서 확인해볼께.

응답데이터는 단순하게 응답코드만 내려오기 때문에 조금 더 자세한 내용의 응답데이터가 필요하겠는걸.

DiaryController.java
    @GetMapping("/diary/{id}")
    //public ResponseEntity<Diary> GetDiary(@PathVariable("id") Integer id) {
    public ResponseEntity<Object> GetDiary(@PathVariable("id") Integer id) {
        //return diaryService.GetDiary(id);
        try {
            Diary diary = diaryService.GetDiary(id);
            return ResponseEntity.ok().body(diary);
        } catch (DiaryException ex) {
            //return ResponseEntity.badRequest().build();
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
        } catch (Exception ex) {
            //return ResponseEntity.internalServerError().build();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
        }
    }

예외가 발생했을 때 String 타입의 예외 메시지를 body 에 담아서 내려보내기 위해서 ResponseEntity body 타입을 Diary에서 Object 로 수정했어. Object 대신에 와일드카드 문자 ? 를 사용해도 돼.

그런데, 아무래도 try… catch… 블록을 사용한게 지저분하고, 리턴값 타입이 Diary 가 아닌 Object 로 불명확한게 걸리네.

Spring Boot 에는 @RestControllerAdvice 애노테이션이 있는데, 이걸 이용해서 예외 처리를 집중화시켜볼께.

exception 패키지에 GlobalExceptionHandler 클래스를 만들고 @RestControllerAdvice 애노테이션을 붙여보자.

@RestControllerAdvice
public class GlobalExceptionHandler {

}

이 안에 각각의 예외 클래스에 대해서 핸들러를 작성해주면 되는데 형식은 아래와 같아.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DiaryException.class)
    protected ResponseEntity<ErrorResponse> handleDiaryException(DiaryException e) {
        final ErrorResponse errorResponse = ErrorResponse.create(e, HttpStatus.BAD_REQUEST, e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
}

핸들러가 리턴하는 값은 ResponseEntity 이고 body 는 ErrorResponse 타입이야. ErrorResponse 는 static 함수인 create 를 사용해서 예외객체와, 응답코드, 메시지를 설정해주지. 이렇게 작성된 ErrorResponse 를 ResponseEntity 에 담아서 리턴하는거야.

DiaryException 예외에 대한 핸들러가 만들어졌으니까 DiaryController 클래스의 GetDiary 함수는 아래와 같이 단순화시킬 수가 있게 돼.

    @GetMapping("/diary/{id}")
    public ResponseEntity<Diary> GetDiary(@PathVariable("id") Integer id) {
        Diary diary = diaryService.GetDiary(id);
        return ResponseEntity.ok().body(diary);
    }

예외처리를 위한 try…catch 블록을 모두 제거했어. 그럼에도 불구하고 @RestControllerAdvice 애노테이션을 붙인 GlobalExceptionHandler 의 handleDiaryException 핸들러 함수에 의해서 DiaryException 이 처리가 되지.

이제 swagger UI 를 통해서 결과를 확인해볼께.


존재하지 않는 일기 데이터 고유값을 요청했을 때 응답받은 결과야. 예외에 대한 처리를 하지 않은 초기와 비교했을 때와 비교해서 훨씬 간결해졌고, 필요로 하는 응답코드와 함께 메시지를 함께 받을 수 있는 구조가 되었어.

이제 무언가 예외처리가 추가적으로 필요해졌을 때 예외클래스와 이 예외클래스에 대한 핸들러만 추가해주면 다른 코드는 수정할 필요가 없어서 매우 간편해졌네.

지금 현재는 DiaryException 예외를 여러가지 경우에 공통적으로 사용하고 있는 상태야.


총 3가지 경우인데, 각각 별개의 예외 클래스를 만들어서 처리하는 코드로 변경을 해볼께.

  • 일기정보 부족 : DiaryDataNotValidException
  • 일기데이터 존재하지 않음 : DiaryDataNotFoundException
  • 일기작성자 다름 : DiaryUserNotMatchException

위에 열거한 예외 클래스를 다음과 같이 만들었어.


GlobalExceptionHandler 에 새로 추가한 예외 클래스에 대한 핸들러를 다음과 같이 작성했지.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(DiaryException.class)
    protected ResponseEntity<ErrorResponse> handleDiaryException(DiaryException e) {
        final ErrorResponse errorResponse = ErrorResponse.create(e, HttpStatus.BAD_REQUEST, e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }

    @ExceptionHandler(DiaryDataNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleDiaryDataNotValidException(DiaryDataNotValidException e) {
        final ErrorResponse errorResponse = ErrorResponse.create(e, HttpStatus.BAD_REQUEST, e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }

    @ExceptionHandler(DiaryDataNotFoundException.class)
    protected ResponseEntity<ErrorResponse> handleDiaryDataNotFoundException(DiaryDataNotFoundException e) {
        final ErrorResponse errorResponse = ErrorResponse.create(e, HttpStatus.NOT_FOUND, e.getMessage());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
    }

    @ExceptionHandler(DiaryUserNotMatchException.class)
    protected ResponseEntity<ErrorResponse> handleDiaryUserNotMatchException(DiaryUserNotMatchException e) {
        final ErrorResponse errorResponse = ErrorResponse.create(e, HttpStatus.BAD_REQUEST, e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
}

기존에 DiaryException 으로만 예외를 생성시킨 코드를 찾아서 유형에 따라 새로 추가한 예외클래스를 사용하는 코드로 변경을 했어.


이제 아래와 같이 예외 상황에 대해서 각각 다른 응답코드와 메시지를 받을 수 있게 되었어.

Leave a Comment