Programming/Spring

Spring - 예외처리 복습!!!!!!!

잇(IT) 2023. 10. 5. 04:18

- ExceptionController.java

@Slf4j
@ControllerAdvice
public class ExceptionController {
    @ResponseBody
    @ExceptionHandler(IslogException.class)
    public ResponseEntity<ErrorResponse> islogException(IslogException e) {

        int statusCode = e.getStatuscode();
        ErrorResponse body = ErrorResponse.builder()
                .code(String.valueOf(statusCode))//애초에 받아올 때 String으로 받아오던가 반환값을 뭐 알아서 변경하면 된다.
                .message(e.getMessage())
                .validation(e.getValidation())
                .build();

        ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
                .body(body);

        return response;
    }
}

1. @ControllerAdvice를 사용하게 되면 모든 Controller에서 일어나는 예외가 전부 해당 클래스로 이동한다.

2. @ExceptionHandler의 파라미터에 해당 예외가 존재하면 아래 메서드를 실행하게 된다.

3. 뒤에서 설명하겠지만 우선 IslogException이라는 신규 예외 메서드의 최상위 메서드를 생성해 놓았다. (뒤에 추가로 설명할 것이다.)

4. ResponseEntity를 사용하게 되면 HttpStatus까지 설정할 수 있기 때문에 ResponseEntity의 제네릭 값으로 ErrorResponse를 넣는다. (ErrorResponse는 임의로 새롭게 만든 응답용 클래스이다.)

5. builder를 통해 ErrorReponse 즉, 에러 응답으로 보낼 객체를 생성하고, ResponseEntity의 body에 ErrorResponse객체를 파라미터로 넘긴다.

6. ResponseEntity의 status와 body에 값을 넣어 새로운 response 객체를 만들어 반환한다.

7. 결과적으로 ResponseEntity의 객체가 JSON 형태로 body에 데이터가 담겨져 전달된다.


- ErrorResponse.java

package com.islog.api.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.FieldError;

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

@Getter
public class ErrorResponse {

    private final String code;
    private final String message;
    private final Map<String, String> validation;

    @Builder
    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 fieldName, String errorMessage) {
        this.validation.put(fieldName, errorMessage);
    }
}

- 예외가 발생하면 전달할 데이터를 전달하기 위한 클래스 ErrorResponse이다.

1. 기본적으로 예외가 발생하면 (1)에러코드, (2)에러메세지, (3)validation(원인)을 전달한다.

2. Builder 패턴을 적용할 수 있도록 한다.

3. addValidation을 통해 Map형태 객체인 validation 객체에 값을 넣을 수 있도록 한다.


- 신규 예외 생성

- IslogException.java

@Getter
public abstract class IslogException extends RuntimeException {

    public final Map<String, String> validation = new HashMap<>();

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

    public IslogException(String message, Throwable cause) {
        super(message, cause);
    }

    public abstract int getStatuscode();

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

1. Controller마다 다른 필요한 예외를 생성할 일이 생길 수 있다. 예외를 새롭게 생성 할 때마다 해당 예외 클래스를 @ControllerAdvice에서 구현해주게 되면 추후에 예외 클래스가 수없이 생기면 구분하기 힘들기 때문에 상위 예외 클래스를 하나 생성한다.

2. 상위 예외 클래스는 RuntimeException을 상속 받고, super(message)를 통해 부모 클래스의 기본 생성자를 호출한다.

3. 하위 예외 클래스마다 제공하는 예외 코드가 다를 수 있기 때문에 추상 메서드로 statuscode를 받는 메서드를 생성한다.

4. 예외가 발생한 필드와 메세지를 제공하기 위한 addValidation() 메서드를 생성하고, validation 객체에 넘어온 값을 넣을 수 있도록 한다.

 

- PostNotFound.java (예외 클래스 1)

public class PostNotFound extends IslogException{

    private static final String MESSAGE = "존재하지 않는 글 입니다.";

    public PostNotFound() {
        super(MESSAGE);
    }

    @Override
    public int getStatuscode() {
        return 404;
    }
}

1. 위와 같은 예외 클래스는 여러개 생성될 수 있고, 전부 상위 예외 클래스인 IslogException을 상속 받는다.

2. 예외를 생성할 때 값을 굳이 전달하지 않아도 위의 예외가 발생하면 일정한 MESSAGE가 전달되도록 기본 생성자를 통해 부모 생성자를 호출하도록 한다.

3. 부모 클래스에서 추상 메서드로 생성한 getStatuscode() 메서드를 통해 해당 예외 클래스의 응답 코드를 전달한다.

 

- InvalidRequest.java (예외 클래스 2)

import lombok.Getter;

@Getter
public class InvalidRequest extends IslogException {

    private static final String MESSAGE = "잘못된 요청입니다.";

    public InvalidRequest() {
        super(MESSAGE);
    }

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

    @Override
    public int getStatuscode() {
        return 400;
    }
}

1. PostNotFound 예외 클래스가 유사하지만 위의 예외 클래스는 fieldName, message를 파라미터로 받는 생성자의 경우, 부모 클래스의 addValidation() 메서드를 통해 예외가 발생한 fieldName와 message를 전달 할 수 있다.

2. 그 외에 IslogException 상위 예외 클래스를 상속 받는 것은 동일하고, 부모 생성자를 호출하고 getStatuscode() 메서드를 구현하는 것은 동일하다.


- 예외 발생 예시

- PostController.java

@PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
            request.validate();
            postService.write(request);
    }

1. Controller에서 body를 통해 전달 받은 JSON 객체를 PostCreate 클래스에 매핑하여 값을 전달 할 때, 전달 받은 값을 검증하기 위한 validate() 검증 메서드를 실행한다.

2. 검증에서 예외가 발생하지 않아야만 postService의 write의 메서드를 사용할 수 있다.

 

- PostCreate.java

@ToString
@Getter
@Setter
public class PostCreate {

    @NotBlank(message = "title을 입력해주세요")
    private String title;

    @NotBlank(message = "content를 입력해주세요")
    private String content;

    @Builder
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }

    public void validate() {
        if (title.contains("바보")) {
            throw new InvalidRequest("title", "제목에 바보를 포함할 수 없습니다.");
        }
    }
}

1. PostCreate에 생성해 놓은 validate() 메서드를 보게되면, body를 통해 전달 받은 title 속성의 값에 "바보"가 포함되어 있을 경우 InvalidRequest("title", "제목에 바보를 포함할 수 없습니다.") 파라미터 2개를 전달하는 예외 객체를 생성하게 된다.

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

2. InvalidRequest 클래스에서 생성한 생성자에 의해 addValidation 메서드가 실행되고

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

3. 예외 상위 클래스의 addValidation 메서드에 의해 validation 객체에 map 형태로 값이 전달된다.

@ResponseBody
    @ExceptionHandler(IslogException.class)
    public ResponseEntity<ErrorResponse> islogException(IslogException e) {
        int statusCode = e.getStatuscode();

        ErrorResponse body = ErrorResponse.builder()
                .code(String.valueOf(statusCode))//애초에 받아올 때 String으로 받아오던가 반환값을 뭐 알아서 변경하면 된다.
                .message(e.getMessage())
                .validation(e.getValidation())
                .build();

4. 상위 예외 클래스에서 builder를 통해 validation을 가져오게 되면 body 값에 전달 받은 validation 객체가 전달되어 표시된다.


- Test 코드
1. title이 null이 전달되어 @Valid에 의해 MethodArgumentNotValidException이 발생한 경우
 @Test
    @DisplayName("글 작성 요청시 title값은 필수다.")
    void test2() throws Exception {

        PostCreate request = PostCreate.builder()
                .content("내용입니다.")
                .build();

        String json = objectMapper.writeValueAsString(request);

//        expectd
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                                .contentType(APPLICATION_JSON)
//                {"title": ""} 검증이 정상적으로 되었다.
//                                .content("{\"title\": \"\", \"content\": \"내용입니다.\"}")
                                .content(json)
//                {"title": null}일 때
                )
//                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(status().isBadRequest())
//                .andExpect(MockMvcResultMatchers.content().string("Hello World"))
//                json 검증 방법에 좋은 것
                .andExpect(jsonPath("$.code").value("400"))
                .andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
                .andExpect(jsonPath("$.validation.title").value("title을 입력해주세요"))
                .andDo(print());
    }
@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody

    public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e) {
        ErrorResponse response = ErrorResponse.builder()
                .code("400")
                .message("잘못된 요청입니다.")
                .build();

        for (FieldError fieldError : e.getFieldErrors()) {
            response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
        }
        return response;
    }

1. @NotBlank 변수인 title에 null값이 전달되면 MethodArgumentNotValidException이 발생하고 해당 예외가 클래스의 메서드가 실행된다. ErrorResponse를 생성하고, 해당 예외 클래스는 기본적으로 제공하는 클래스이기 때문에 속성에 filed와 message를 기본적으로 가지고 있기 때문에, ErrorResponse에서 생성한 addValidation의 파라미터로 넘겨 validation 객체에 key, value 형태로 값을 넣고 ErrorResponse를 반환한다.

 

2. title에 금지된 단어가 들어간 경우
@Test
    @DisplayName("게시글 작성시 제목에 '바보'는 포함될 수 없다.")
    void test11() throws Exception {

        PostCreate request = PostCreate.builder()
                .title("나는 바보입니다.")
                .content("내용입니다.")
                .build();

        String json = objectMapper.writeValueAsString(request);

        //when
        mockMvc.perform(MockMvcRequestBuilders.post("/posts")
                                .contentType(APPLICATION_JSON)
                                .content(json)
                )
                .andExpect(status().isBadRequest())
                .andDo(print());
    }
}

1. title과 content가 null이 아닌 값이 JSON 형태로 변환되어 post 요청으로 전달된다.

@PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {
            request.validate();
            postService.write(request);
    }

2. JSON으로 넘어온 값은 PostCreate 필드에 매핑되어 전달된다.

3. title과 content가 매핑되어 새롭게 생성된 request 객체의 validate() 메서드가 실행된다.

public void validate() {
        if (title.contains("바보")) {
            throw new InvalidRequest("title", "제목에 바보를 포함할 수 없습니다.");
        }
    }

4. title을 검증하여 "바보"가 포함되어 있으면 InvalidRequest() 메서드에 파라미터 2개를 전달하는 예외 객체를 생성한다.

public InvalidRequest(String fieldName, String message) {
        super(MESSAGE);
//        this.fieldName = fieldName;
//        this.message = message;
        addValidation(fieldName, message);
    }

5. 파라미터 2개를 받는 예외 생성자가 생성되고, 최종적으로 예외 title과 message를 validation 객체에 값을 넣어 ErrorResponse 객체를 body에 담아 전달한다.

728x90