- HTTP method
- GET : 메서드 GET는 지정된 리소스의 표현을 요청합니다. 를 사용하는 요청은 GET데이터 검색만 해야 합니다.
- HEAD : 이 메서드는 요청과 동일하지만 응답 본문이 없는 HEAD응답을 요청합니다 .GET
- POST : 이 POST메서드는 엔터티를 지정된 리소스에 제출하며, 종종 서버의 상태 변경이나 부작용을 유발합니다.
- PUT : 이 PUT메서드는 대상 리소스의 모든 현재 표현을 요청 페이로드로 바꿉니다.
- Delete : 이 DELETE메서드는 지정된 리소스를 삭제합니다.
- Connect : 이 CONNECT방법은 대상 리소스로 식별된 서버에 대한 터널을 설정합니다.
- Options : 이 OPTIONS방법은 대상 리소스에 대한 통신 옵션을 설명합니다.
- Trace : 이 TRACE메서드는 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행합니다.
- Patch : 이 PATCH방법은 리소스에 부분 수정을 적용합니다.
- PostCreate.java
@Getter @Setter
@ToString
public class PostCreate {
private String title;
private String content;
}
- PostController.java
@RestController
@Slf4j
public class PostController {
@PostMapping("/posts")
public String post(PostCreate params) {
log.info("params={}", params.toString());
return "Hello World";
}
}
- post를 통해 아무런 값이 넘어오지 않았기 때문에 PostCreate의 필드인 title과 cotent에는 null 값이 들어가 있는 상태인 것을 확인 할 수 있다.
- PostController.java
@RestController
@Slf4j
public class PostController {
@PostMapping("/posts")
public Map<String, String> post(PostCreate params) {
log.info("params={}", params.toString());
return Map.of();
}
}
- return 값으로 Map으로 변경한다.
- Test 코드 추가
- PostControllerTest.java
@WebMvcTest
//MockMvc를 주입받기 위해서 사용한다.
class PostControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DisplayName("/posts 요청시 Hello World를 출력한다.")
void test() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"제목입니다.\", \"content\":\"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().string("{}"))
.andDo(MockMvcResultHandlers.print());
}
}
- MockMvc를 이용하여 요청 화면을 직접 생성하여 데이터를 보내지 않아도 테스트 용도로 만들어 볼 수 있다.
1. JSON 형태의 POST 방식으로 요청을 보낸다.
2. 그에 대한 응답에 대한 기대값을 작성한다.
- request
- response
- 데이터 검증
- build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
// 검증을 위해 gradle에 추가해야 하는 부분
implementation 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- PostCreate.java
@Getter @Setter
@ToString
public class PostCreate {
@NotBlank(message = "title을 입력해주세요")
private String title;
@NotBlank(message = "content를 입력해주세요")
private String content;
}
- 직접 검증을 위한 코드를 작성해도 되지만, 검증에는 많은 요소들이 있기 때문에 Spring에서 편의를 제공해준다.
- 위의 코드처럼 @NotBlank와 같이 어노테이션을 지정해주면 스프링에서 검증을 해준다.
- PostController.java
@RestController
@Slf4j
public class PostController {
@PostMapping("/posts")
public Map<String, String> post(@RequestBody @Valid PostCreate params) {
log.info("params={}", params.toString());
return Map.of();
}
}
- PostControllerTest.java
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcResultHandlers.print());
}
}
- 검증에 실패했기 때문에 400오류가 발생하는 것을 확인 할 수 있다.
- BindingResult
- BindingResult를 사용하게 되면 검증에서 발생한 오든 에러들이 담기게 된다.
- PostController.java
@RestController
@Slf4j
public class PostController {
@PostMapping("/posts")
public Map<String, String> post(@RequestBody @Valid PostCreate params, BindingResult result) {
log.info("params={}", params.toString());
if (result.hasErrors()) {
List<FieldError> fieldErrors = result.getFieldErrors();
FieldError firstFieldError = fieldErrors.get(0);
String fieldName = firstFieldError.getField(); // title에 검증 오류 값이 있기 때문에 title이 넘어온다.
String errorMessage = firstFieldError.getDefaultMessage();// 에러메시지가 넘어간다.
Map<String, String> error = new HashMap<>();
error.put(fieldName, errorMessage);
return error; // 반환값도 맞춰줘야 한다.
}
return Map.of();
}
}
- BindingResult를 파라미터로 넣어준 다음 bindingResult에 저장된 에러들을 error라는 객체에 넣어 반환한다.
- PostControllerTest.java
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.title").value("title을 입력해주세요"))
.andDo(MockMvcResultHandlers.print());
}
}
- PostCreate.java
@Getter @Setter
@ToString
public class PostCreate {
@NotBlank(message = "title을 입력해주세요")
private String title;
@NotBlank(message = "content를 입력해주세요")
private String content;
}
- title 필드에서 에러가 발생했기 때문에 body에 error 객체에 저장된 key, value 값이 반환되는 것을 확인 할 수 있다.
.andExpect(jsonPath("$.title").value("title을 입력해주세요"))
- 추가로 jsonPath를 통해 json에 저장된 key에 대한 value 값을 검증해 볼 수 있다.
- ControllerAdvice
- bindingResult를 통해 매번 에러에 대한 조건을 작성하기는 비효율적이기 때문에 @ControllerAdvice를 사용한다.
- @ControllerAdvice를 사용하게 되면 전역 컨트롤러에서 발생하는 예외를 처리하고 관리하는 데 사용된다.
- BindingResult가 있으면 에러가 발생하게 되면 bindingResult에 담겨 넘어가게 되기 때문에 ControllerAdvice를 사용하게 되면, BindingResult는 지워준다.
- ExceptionController.java
@Slf4j
@ControllerAdvice
public class ExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
// controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
// body로 넘겨준다.
public Map<String, String> exceptionHandler(MethodArgumentNotValidException e) {
FieldError fieldError = e.getFieldError();
String field = fieldError.getField();
String message = fieldError.getDefaultMessage();
Map<String, String> response = new HashMap<>();
response.put(field, message);
return response;
}
}
- 예외의 최고 조상인 Exception으로 받아도 되지만 Exception으로 받을 경우 특정 에러의 세부사항을 알 수 없기 때문에 현재 발생하는 MethodArgumentNotValidException 클래스를 사용하여 에러의 field와 message를 받아서 body에 json 형태로 넘길 수 있다.
- PostControllerTest.java
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.title").value("title을 입력해주세요"))
.andDo(MockMvcResultHandlers.print());
}
}
- ExceptionController에서 보낸 json형태의 데이터가 넘어온 것을 확인 할 수 있다.
* MockMvc에서 요청을 생성 -> body에 json형태로 넘어온 값에 title 값이 비어있는 상태 -> valid에 의해 에러 발생 -> 에러 발생 시 ControllerAdvice로 해당 에러 이동 -> body에 json 형태로 에러에 대한 정보 전달 -> HttpResponse에 담아서 전달
- ExceptionController에서 Map 형태로 넘기는 것이 아닌 응답 전용 클래스를 생성해서 전달 할 수 있다. 에러 클래스, 에러 json을 만드는 규칙은 전부 다르다.
- ErrorResponse.java
@Getter
@RequiredArgsConstructor
public class ErrorResponse {
private final String code;
private final String message;
}
- 응답 전용 클래스를 생성하게 되면 에러가 발생했을 때 원하는 필드를 작성하여 json 형태로 값을 전달 할 수 있다.
- ExceptionController.java
@Slf4j
@ControllerAdvice
public class ExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
// controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
// body로 넘겨준다.
public ErrorResponse exceptionHandler(MethodArgumentNotValidException e) {
return new ErrorResponse("400", "잘못된 요청입니다.");
}
}
- PostControllerTest.java
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
.andDo(MockMvcResultHandlers.print());
}
}
- ExceptionController.java에 작성된 메서드에 의해 반환된 ErrorResponse가 @ResponseBody에 의해 json 형태로 변환되어 응답에 전달된다.
- 위 경우에는 에러가 발생한 경우 해당 사실을 전달 할 수 있지만 세부사항에 대한 정보를 보내기는 어려운 상황이다.
- ErrorResponse.java
@Getter
@RequiredArgsConstructor
public class ErrorResponse {
private final String code;
private final String message;
private final Map<String, String> validation = new HashMap<>();
public void addValidation(String field, String errorMessage) {
this.validation.put(field, errorMessage);
}
}
- Map 대신 생성한 클래스에 원하는 필드를 생성하여 넘김과 동시에 어떤 구체적인 에러가 발생했는지 전달하기 위해 bindingResult로 넘어온 값을 넣을 Map을 하나 생성한다.
- addValidation 메서드는 에러가 발생한 field와, errorMessage를 필드로 받아서 validation Map 객체에 값을 넣는다.
- ExceptionController.java
@Slf4j
@ControllerAdvice
public class ExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody // controller는 rest이기 때문에 body로 넘어가는데
// controllerAdvice는 viewResolver로 넘어가기 때문에 body 어노테이션을 달아서
// body로 넘겨준다.
public ErrorResponse exceptionHandler(MethodArgumentNotValidException e) {
ErrorResponse response = new ErrorResponse("400", "잘못된 요청입니다.");
for (FieldError fieldError : e.getFieldErrors()) {
response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
}
return response;
}
}
- bindingResult에 여러개의 검증 에러가 발생하게 되면 getFieldErrors List에 담기게 되고, 해당 List를 for문으로 돌리면서 해당 에러의 field와 message를 받아와 validation 객체에 순차적으로 넣는다.
- PostControllerTest.java
@Test
@DisplayName("/posts 요청시 title값은 필수다.")
void test2() throws Exception {
//expected
mockMvc.perform(MockMvcRequestBuilders.post("/posts")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\" : \"\", \"content\" : \"내용입니다.\"}")
)
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(jsonPath("$.code").value("400"))
.andExpect(jsonPath("$.message").value("잘못된 요청입니다."))
.andExpect(jsonPath("$.validation.title").value("title을 입력해주세요"))
.andDo(MockMvcResultHandlers.print());
}
}
- 마찬가지로 Test 코드를 실행하게 되면 title에 빈 값이 들어가 검증 에러가 발생하게 되고, ErrorResponse를 통해 개발자가 임의로 생성한 에러 필드들과 bindingResult를 통해 넘어온 에러들을 함께 body에 json 형태로 넣어서 전달할 수 있다.
'Programming > Spring' 카테고리의 다른 글
Spring - 게시글 조회, 클래스 분리 (단건) (0) | 2023.08.29 |
---|---|
Spring - 클래스 분리, 데이터 저장 (Builder, ObjectMapper, 상황 별Controller 반환 값 등...) (0) | 2023.08.29 |
JPA - 기본 동작 방식, 아키텍처 (0) | 2023.08.01 |
Spring Data JPA - 새로운 엔티티 구별하기 (0) | 2023.07.26 |
Spring Data JPA - Web 확장 (도메인 클래스 컨버터, 페이징과 정렬) (0) | 2023.07.26 |