관리 메뉴

bright jazz music

blog12: 예외처리 3 본문

Projects/blog

blog12: 예외처리 3

bright jazz music 2023. 1. 14. 22:43

그런데 원래대로라면 InvalidRequest를 ExceptionController에 추가해 줘야한다.그러나 애플리케이션이 커지면서 예외도 늘어나게 된다. 따라서 다른 방법을 사용해 본다.

 

  • 자바에서 기본 제공해주는 예외들은 기존처럼 따로 만들어준다.
  • 그리고 자바에서 제공되지는 않지만 애플리케이션 운영에서 발생하는 공통적으로 처리해야 하는 예외들은 하나의 클래스를 만들어 대응한다.

 

프로그램 전체에서 사용할 Exception클래스를 하나 만들어서 그것으로 하여금 에러들을 처리하게 하는 것이다. 여기서는 그 BlogException이라는 추상 클래스를 생성하여 그것으로 하여금 모든 에러를 처리하게 할 것이다. 앞선 예제들에서는 각각의 에러 클래스들이 RuntimeException 클래스를 상속하였다. 이제부터는 그 클래스들이 BlogException을 상속한다. 그리고 BlogException만이 RuntimeException을 상속한다.

따라서 PostNotFound 또는 InvalidRequest의 생성자에 들어가는 String 타입의 메시지는 super()를 통해 BlogException을 거쳐 RuntimeException 까지 전달된다.

 

//BlogException.java

package com.endofma.blog.exception;

public abstract class BlogException extends RuntimeException{

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

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

    //추상메서드: 반드시 구현 필요
    public abstract int getStatusCode();

}

 

 

//PostNotFound.java
//status -> 404
package com.endofma.blog.exception;

//public class PostNotFound extends Exception{
//public class PostNotFound extends RuntimeException{ //uncheckedException
public class PostNotFound extends BlogException{ //BlogException은 RuntimeException을 상속했다.
    //생성자 오버로딩을 해서 실제로 발생한 예외에게 메시지를 부여하면 된다.

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

    public PostNotFound(){
        super(MESSAGE); //이 메시지는 BlogException(String message)를 초기화한다.
    }


    public PostNotFound(Throwable cause){
        super(MESSAGE, cause);
    }

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

 

//InvalidRequest.java
//status -> 400
package com.endofma.blog.exception;

//public class InvalidRequest extends RuntimeException {
public class InvalidRequest extends BlogException {
    private static final String MESSAGE ="잘못된 요청입니다.";

    public InvalidRequest() {
        super(MESSAGE);
    }


    //글 작성 등에서 글 작성에서 @NotBlank와 같은 어노테이션으로
    //해결할 수 없을 경우 추가적인 방법이 필요할 때.
    //PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.

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

}

 

//PostController.java

    @PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {

        //PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
        if(request.getTitle().contains("바보")){
            throw new InvalidRequest();
        }

        postService.write(request);
//        return Map.of();
    }

 

 

예외를 처리할 수 있도록 ExceptionController에 등

//ExceptionController.java

package com.endofma.blog.controller;

import com.endofma.blog.exception.BlogException;
import com.endofma.blog.exception.InvalidRequest;
import com.endofma.blog.exception.PostNotFound;
import com.endofma.blog.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

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

@Slf4j
@ControllerAdvice
public class ExceptionController {


    @ResponseStatus(HttpStatus.BAD_REQUEST) //400
    @ResponseBody //응답을 json으로 보내기 위해 추가
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e) {

//        ErrorResponse response = new ErrorResponse("400", "잘못된 요청입니다!");
        ErrorResponse response = ErrorResponse.builder()
                .code("400")
                .message("잘못된 요청입니다!")
                .build();

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

        }

        return response;
    }

    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(PostNotFound.class)
    public ErrorResponse postNotFound(PostNotFound e) {
        ErrorResponse response = ErrorResponse.builder()
                .code("404")
                .message(e.getMessage()) //익셉션으로 받은 에러 메시지
                .build();

        return response;
    }

//    @ResponseBody
//    @ResponseStatus(HttpStatus.NOT_FOUND)
//    @ExceptionHandler(PostNotFound.class)
//    public ErrorResponse invalidRequest(InvalidRequest e) {
//        ErrorResponse response = ErrorResponse.builder()
//                .code("404")
//                .message(e.getMessage()) //익셉션으로 받은 에러 메시지
//                .build();
//
//        return response;
//    }


    /////BlogException을 사용하여 에러처리
    @ResponseBody
//    @ResponseStatus(HttpStatus.NOT_FOUND) //헤더스테이터스. 동적으로 변경하기 어려움.
    //빼면 기본값 200으로 내려감
    @ExceptionHandler(BlogException.class)
    //BlogException을 상속하는 에러클래스가 호출되었을 경우 무조건 이 메소드가 호출된다.
    public ResponseEntity<ErrorResponse> blogException(BlogException e) {
        //ResponseEntity는 스프링에서 기본적으로 제공하는 객체로 반환한다.

        int statusCode = e.getStatusCode();

        ErrorResponse body = ErrorResponse.builder()
                .code(String.valueOf(statusCode)) //String.valuOf()사용해서 int를 String으로 변경
                .message(e.getMessage())
                .build();

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

        return response;
    }


}

 

@ResponseStatus()를 제거한 것을 볼 수 있다. 저기에 헤더 상태가 정해져 있으면 아래 코드에서 변경하더라도 효과가 없다. 그러면 @ResponseStatus() 안의 값을 변경해 주면 되지 않느냐고 반문할 수 있다. 물론 그렇게 할 순 있지만 안에서 로직이 복잡해질 수도 있고 바뀔 수도 있기 때문에 애노테이션을 사용해 고정하는 것은 권장하지 않는다.

 

문제는 @RequestStatus가 없으면 기본으로 200을 내려준다는 것이다. 이를 방지하기 위해 ResponseEntity<>로 ErrorResponse를 감싸서 반환한다.

 

에러를 코드를 매번 수정해 줄 수 없으므로 에러 코드를 에러 클래스로부터 직접 가져와서 넣어주는 로직을 추가하였다.

 

int statusCode = e.getStatusCode(); , String.valueOf(statusCode)

 

이를 추가히 위해 BlogException에 아래와 같은 추상 메서드를 추가하였다.

 

//BlogException.java

//추상메서드: 반드시 구현 필요
public abstract int getStatusCode();

따라서 이 클래스를 상속한 클래스에서는 이 메소드 구현한다

 

//PostNotFound.java

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

 

//InvalidRequest.java

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

 

 

이를 테스트 하면

//PostControllerTest.java

@Test
@DisplayName("게시글 작성 시 제목에 '바보'는 포함될 수 없다.")
void test11() throws Exception{
    //given
    PostCreate request = PostCreate.builder()
            .title("나는 바보입니다.")
            .content("제이드빌.")
            .build();
    String json = objectMapper.writeValueAsString(request);
    
    //when
    mockMvc.perform(post("/posts")
            .contentType(APPLICATION_JSON)
            .content(json))
            .andExpect(status().isBadRequest())
            .andDo(print());
    

}

 

 

 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2023-01-14 22:12:08.180  INFO 38940 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Starting PostControllerTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 38940 (started by user in D:\personal\blog)
2023-01-14 22:12:08.183  INFO 38940 --- [    Test worker] c.e.blog.controller.PostControllerTest   : No active profile set, falling back to 1 default profile: "default"
2023-01-14 22:12:09.194  INFO 38940 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-14 22:12:09.272  INFO 38940 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 67 ms. Found 1 JPA repository interfaces.
2023-01-14 22:12:09.977  INFO 38940 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-01-14 22:12:10.312  INFO 38940 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-01-14 22:12:10.410  INFO 38940 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-14 22:12:10.488  INFO 38940 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-14 22:12:10.756  INFO 38940 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-14 22:12:10.956  INFO 38940 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-14 22:12:11.831  INFO 38940 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-14 22:12:11.846  INFO 38940 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-14 22:12:12.714  WARN 38940 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-14 22:12:13.164  INFO 38940 --- [    Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-14 22:12:13.663  INFO 38940 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-01-14 22:12:13.664  INFO 38940 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2023-01-14 22:12:13.665  INFO 38940 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 1 ms
2023-01-14 22:12:13.698  INFO 38940 --- [    Test worker] c.e.blog.controller.PostControllerTest   : Started PostControllerTest in 6.05 seconds (JVM running for 8.927)

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /posts
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"61"]
             Body = {"title":"나는 바보입니다.","content":"제이드빌."}
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#post(PostCreate)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = com.endofma.blog.exception.InvalidRequest

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 400
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"code":"400","message":"잘못된 요청입니다.","validation":{}}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2

 

 

--

 

지금까지는 타이틀에 "바보"가 있는지 controller에서 체크했다. 그러나 이렇게 꺼내서 로직을 구현하는 것보다는 호출하고 메시지만 받아서 처리하는 것을 권장한다.

 

//PostController.java

	@PostMapping("/posts")
    public void post(@RequestBody @Valid PostCreate request) {

        //PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
//        if(request.getTitle().contains("바보")){
//            throw new InvalidRequest();
//        } 이렇게 데이터를 꺼내서 분리해서 재조립하는 방식보다는 아래처럼 메시지를 통해 가져오는 방식 권장
        
        request.validate(); //검증

        postService.write(request);
//        return Map.of();
    }

 

//PostCreate.java

package com.endofma.blog.request;

import com.endofma.blog.exception.InvalidRequest;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.validation.constraints.NotBlank;

@Setter
@Getter
@ToString
public class PostCreate {
    @NotBlank(message = "타이틀을 입력하세요!")
    //@Valid를 @RequestBody 옆에 @valid를 붙임. 그러면 바인딩 되면서 빈 값이 넘어오면 에러발생시킴.
    private String title;

    @NotBlank(message = "내용을 입력하세요!")
    private String content;

    @Builder //클래스에 달면 다른 생성자와 모순이 발생할 수 있기 때문에 생성자에 다는 것을 추천. builder + NoArgsConstructor 에러
    public PostCreate(String title, String content) {
        this.title = title;
        this.content = content;
    }
    //빌더의 장점
    // - 가독성 up (값 생성에 대한 유연함)
    // - 필요한 값만 받을 수 있다. // -> (오버로딩 가능한 조건 찾아보기)
    // - 객체의 불변성


    public void validate() {
        if(title.contains("바보")){
            throw new InvalidRequest();
        }
        //boolean을 통해 검증 실패 시 false, 성공시 true를 컨트롤러로 반환하고
        //컨트롤러에서 에러를 생성해 줄 수도 있지만 사실 그럴 필요도 없다.
        //여기서 검증하고 예외를 죽이는 게 더 효율적이기 때문이다.
        //이런 역할을 하는 라이브러리를 사용해도 된다.
    }

}

 

당연히 테스트 결과는 같다.

'Projects > blog' 카테고리의 다른 글

blog13: Spring REST Docs 1 -기본설정  (0) 2023.01.15
blog12: 예외처리 4  (0) 2023.01.14
blog12: 예외처리2  (0) 2023.01.13
blog12: 예외처리1  (0) 2023.01.10
blog11: 게시글 삭제  (0) 2023.01.10
Comments