[Spring Boot] REST API 게시판 서버 만들기 #3(Response 커스텀 객체, Exception 처리, ExceptionHandler)

프로젝트 구조 설명

configuration 패키지 생성.

 - response 구조를 커스텀할 클래스들 생성.

   1. RestResponse : 200 코드를 반환할 때 사용

   2. ErrorResponse : 에러 발생시 사용

 

configuration.aspect 패키지

 - RestControllerAspect 클래스 

   : RestController 역할의 클래스를 감싸는 역할.

 - ServiceExceptionAspect 

   : 서비스 로직에서 발생하는 에러를 모두 잡는 역할.

 

configuration.controlleradvice

 - RestControllerExceptionAdvice

   :  RestController에서 발생한 에러를 최대한 모두 잡는 역        할.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

파일 설명

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project 
	xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    					https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>co.worker</groupId>
    <artifactId>board</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>board</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.jacoco/org.jacoco.agent -->
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <version>0.8.5</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- jacoco 코드 커버리지 플러그인 -->
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.5</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

 

BoardController

@RestController
@Validated
@RequestMapping("/api/boards")
public class BoardController {

    @Autowired
    private BoardService boardService;

    @GetMapping(
            value = "/all",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    public Object getAll() throws Exception{
        return boardService.getBoard();
    }

    @GetMapping(
            value = "/{seq}",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_VALUE
    )
    public Object get(@PathVariable("seq") @Min(1) Long seq) throws Exception{
        return boardService.getBoard(seq);
    }

    @PostMapping(value = "/add", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Object add(@RequestBody @Valid BoardParam param) throws Exception{
        boardService.add(param);
        return null;
    }

    @PutMapping(value = "/{seq}", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Object edit(@RequestBody @Valid BoardParam param,
                               @PathVariable("seq") @Min(1) Long seq) throws Exception{
        param.setSeq(seq);
        boardService.edit(param);
        return null;
    }

    @DeleteMapping(value = "/{seq}", consumes = MediaType.APPLICATION_JSON_VALUE)
    public Object delete(@PathVariable("seq") @Min(1) Long seq) throws Exception{
        boardService.delete(seq);
        return null;
    }
}

- @Validated 어노테이션은 @Min(1) 어노테이션을 사용하기 위해 클래스단에 먼저 선언해두는 어노테이션이다. Min 어노테이션을 설정해둔 이유는 @PathVariable 어노테이션으로 받은 seq값이 0 이하일 수는 없기 때문이다.

- 또한 한정적인 형태인 ResponseEntity 객체를 사용하지 않고 직접 정의한 객체를 Response로 전달하기 위해 Object 타입으로 리턴하게 하였음.

 

RestControllerAspect

@Component
@Aspect
public class RestControllerAspect {

    @Around("execution(* co.worker.board.*.controller.*.*(..))")
    public RestResponse<Object> restResponseHandler(ProceedingJoinPoint joinPoint) throws Throwable {
        return new RestResponse<>(HttpStatus.OK.value(), "success", joinPoint.proceed());
    }
}

- @Aspect 어노테이션은 해당 클래스에서 AOP를 활용할 수 있도록 합니다.

- @Around 어노테이션은 메서드 앞 뒤에 컨트롤러 메소드를 둔다고 보면 되겠습니다.

- execution 안에있 정규식에 해당하는 메소드들을  잡게됩니다.

- 모든 메소드의 시작은 restResponseHandler 라는 메소드에서 RestResponse 생성자에서 joinPoint.proceed() 메소드를 실행하고 컨트롤러로 향하게 됩니다.

- 모든 로직이 예외없이 실행되면 ReseResponse 객체를 리턴하게 됩니다.

 

ServiceExceptionAspect

@Component
@Aspect
public class ServiceExceptionAspect {

    @Around("execution(* co.worker.board.*.service.*.*(..))")
    public Object serviceExceptionHandler(ProceedingJoinPoint joinPoint) throws Throwable {
        try{
            return joinPoint.proceed();
        }catch(Throwable e){
            //서비스 로직 에러를 RuntimeException으로 컨트롤러에 전달.
            throw new RuntimeException(e);
        }
    }
}

- 서비스 로직에서 에러가 발생할 경우 RuntimeException으로 모두 바꿔서 리턴하도록 함.( 서비스 로직에서만 )

 

 

RestControllerExceptionAdvice

@org.springframework.web.bind.annotation.RestControllerAdvice
@Slf4j
public class RestControllerExceptionAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(RuntimeException.class)
    public ErrorResponse handlerRuntimeException(RuntimeException e, HttpServletRequest req){
        log.error("================= Handler RuntimeException =================");
        return new ErrorResponse(
        	HttpStatus.BAD_REQUEST.value(), 
            "RuntimeException : "+e.getMessage()
        );
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorResponse handlerMethodArgumentNotValidException(MethodArgumentNotValidException e, 
    		HttpServletRequest req){
        log.error("================= Handler MethodArgumentNotValidException =================");
        return new ErrorResponse(
        	HttpStatus.BAD_REQUEST.value(), 
            "MethodArgumentNotValidException : "+e.getMessage()
		);
    }
}

- @ResponseStatus(HttpStatus) : 해당 리스폰스의 타입을 BAD_REQUEST(400)으로 반환하도록 합니다.

- @ExceptionHandler(class) : 해당 메소드가 잡아야할 Exception을 처리합니다. 서비스 로직에서 발생한 RuntimeException은 handlerRuntimeException() 메소드에서 처리하게 됩니다.

- 리턴타입은 ErrorResponse 타입이다.

 

ErrorResponse

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse{
    private Integer code;
    private String message;
}

RestResponse

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RestResponse<T> {
    private int code;
    private String message;
    private T result;
}

code 는 200, 400과 같은 HttpStatus값이 들어가고, message는 반환이나 에러내용이 들어갑니다. result는 json으로 반환될 객체입니다.

 

 

테스트코드
//Bad_Request 테스트
    @Test //add
    public void board_BadRequest_add() throws Exception{
    	//username not null
        BoardParam param = BoardParam.builder().content("test")
                                    .title("test").build(); 
        
        // content not empty
        //BoardParam param = BoardParam.builder().title("title")
                                    .content("")
                                    .username("woo").build(); 
        
        mockMvc.perform(post("/api/boards/add")
            .accept(MediaType.APPLICATION_JSON_VALUE)
            .contentType(MediaType.APPLICATION_JSON_VALUE)
            .content(objectMapper.writeValueAsString(param)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

    @Test
    public void board_BadRequest_getOne() throws Exception{
    	//0 이하인 경우 @Min 어노테이션으로 잡는지 확인.
        mockMvc.perform(get("/api/boards/-1")
                .accept(MediaType.APPLICATION_JSON_VALUE)
                .contentType(MediaType.APPLICATION_JSON_VALUE))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

    @Test
    public void board_BadRequest_edit() throws Exception {
    	//username null
        //BoardParam param = BoardParam.builder().content("test")
                                    .title("test").build();
        
        // seq min 0
        BoardParam param = BoardParam.builder().content("test")
                                    .title("test")
                                    .username("gg").build();
        
        mockMvc.perform(put("/api/boards/0")
                .accept(MediaType.APPLICATION_JSON_VALUE)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(param)))
                .andDo(print())
                .andExpect(status().isBadRequest());
    }

- board_BadRequest_add() : 유저명 null, 게시판내용 null validation. , content "" not empty validation. / insert 테스트

- board_BadRequest_getOne() : @Min 어노테이션으로 seq 값 validation 체크 / 1개 반환 테스트

- board_BadRequest_edit() : 유저명 null, @Min 어노테이션 체크 / 수정 테스트

 

 

 

 

댓글

Designed by JB FACTORY