프로젝트 구조 설명
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 어노테이션 체크 / 수정 테스트
'웹 개발 > Spring Boot' 카테고리의 다른 글
[Spring Boot] REST API 게시판 서버 만들기 #5(User쪽 적용, 내가 TDD 활용하는 방법.) (6) | 2020.02.18 |
---|---|
[Spring Boot] REST API 게시판 서버 만들기 #4(ModelMapper를 이용한 객체 매핑) (2) | 2020.02.10 |
[Spring Boot] REST API 게시판 서버 만들기 #2(게시판 기본 CRUD, JPA, H2 Database) (0) | 2020.01.21 |
[Spring Boot] REST API 게시판 서버 만들기 #1(프로젝트 생성 및 기본 예제) (2) | 2020.01.16 |
[Spring Boot] 스프링 @Value 어노테이션으로 properties 값 읽어오기 (2) | 2020.01.07 |