1. API 형식 정의
먼저 서버와 클라이언트 간에 주고받는 데이터 형식을 통일하기 위해 API 형식을 정의합니다. Api 클래스는 다음과 같이 구성됩니다:
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {
private String statusCode;
private String message;
private T data;
private Error error;
public static class Error {
private List<String> errorList;
}
}
2. Controller API 형식으로 변경
다음으로, StudentValidationController 컨트롤러에서 API 형식을 적용합니다:
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {
@PostMapping("")
public Api<Student> studentInfo(
@Valid
@RequestBody
Api<Student> student
) {
log.info("info : {}", student);
return student;
}
}
3. API 형식으로 클라이언트가 서버에 보내기
클라이언트가 다음과 같은 형식으로 서버에 데이터를 보냅니다:
{
"status_code": "",
"message": "",
"data": {
"id": "",
"password": " ",
"email": "hong.com",
"name": "홍길동",
"score": 1000
},
"error": {
"error_list": []
}
}
4. 결과 화면
서버에서 반환되는 결과는 다음과 같습니다:
{
"status_code": "",
"message": "",
"data": {
"id": "",
"password": " ",
"email": "hong.com",
"name": "홍길동",
"score": 1000
},
"error": {
"error_list": []
}
}
유효성 검사가 동작하지 않는 이유
Student 모델에서 유효성 검사 어노테이션을 사용했음에도 불구하고 유효성 검사가 동작하지 않는 이유는 Api 클래스의 data 필드에 @Valid 어노테이션이 없기 때문입니다.
- Api 클래스의 data 필드는 유효성 검사가 필요하지만, @Valid 어노테이션이 적용되지 않았습니다.
- 유효성 검사는 @Valid 어노테이션이 있는 필드에 대해 적용됩니다. 따라서, data 필드에 @Valid 어노테이션을 추가해야 합니다.
5. @Valid 어노테이션 추가
Api 클래스의 data 필드에 @Valid 어노테이션을 추가하여 유효성 검사를 활성화합니다:
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {
private String statusCode;
private String message;
@Valid // data 필드를 검사한다
private T data;
private Error error;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public static class Error {
private List<String> errorList;
}
}
400번 Bad Request가 나온 것을 확인 할 수 있습니다
BindingResult 설명
- BindingResult란?
BindingResult는 스프링 프레임워크에서 제공하는 인터페이스로, 유효성 검사 후 발생한 오류 정보를 저장하는 역할을 합니다. 컨트롤러 메서드에서 유효성 검사가 수행된 후, 유효성 검사 오류가 있으면 BindingResult 객체에 오류 정보가 저장됩니다. - BindingResult의 역할:
- 유효성 검사 결과를 저장하고, 유효성 검사 실패 시 오류 정보를 제공.
- 오류 메시지를 처리하고, 클라이언트에게 적절한 오류 메시지를 전송하는 데 사용.
6. 오류 메시지 전송
유효성 검사 오류가 발생했을 때 이를 처리하기 위해 BindingResult를 사용합니다:
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {
@PostMapping("")
public Api<Student> studentInfo(
@Valid
@RequestBody
Api<Student> student,
BindingResult bindingResult // 빈 주입
) {
log.info("info : {}", student);
return student;
}
}
7. 오류 검사
오류가 발생했을 때 오류 메시지를 수집하는 로직을 추가합니다:
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {
@PostMapping("")
public Api<Student> studentInfo(
@Valid
@RequestBody
Api<Student> student,
BindingResult bindingResult
) {
// error 를 가지고 있는지 확인한다 반환값은 true, false
if (bindingResult.hasErrors()) {
// bindingResult에 존재하는 에러들을 전부 반환하여 stream map을 사용
var errorList = bindingResult.getFieldErrors().stream()
.map(it -> {
// errorList에 담을 값의 format
var format = "%s : { %s } 은 %s";
// getField : 오류 필드 명
// getRejectedValue : 삽입된 오류 값
// getDefaultMessage : 기본 오류 메세지
var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
return errorFormat;
// 타입을 List로 반환
}).collect(Collectors.toList());
}
log.info("info : {}", student);
return student;
}
}
8. 응답하는 변수에 값을 넣어서 반환하기
오류 메시지를 API 형식으로 반환합니다:
if (bindingResult.hasErrors()) {
var errorList = bindingResult.getFieldErrors().stream()
.map(it -> {
var format = "%s : { %s } 은 %s";
var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
return errorFormat;
}).collect(Collectors.toList());
// api error에 담을 body 생성
var body = Api.Error.builder()
.errorList(errorList)
.build();
// 반환하기
var response = Api.builder()
.statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.message(HttpStatus.BAD_REQUEST.getReasonPhrase())
.error(body)
.build();
return response;
}
메서드 반환 값 오류 발생
public Api<Student> studentInfo() {...}
반환 값이 Api <Student>이기 때문에 반환 값을 바꾸어야합니다
public Api<? extends Object> studentInfo() {...}
- 모든 클래스는 Object를 상속받습니다.
- ?는 와일드카드로, 어떤 특정 타입이 올지 모른다는 의미입니다.
- ? extends Object는 Object를 상속받는 모든 클래스 타입이 올 수 있다는 의미입니다.
자세한 설명
이 선언은 메서드의 반환 타입으로 Api 클래스의 인스턴스를 사용하지만, 그 내부 데이터 타입이 무엇이 될지는 컴파일 시점에서 알 수 없음을 나타냅니다.
- 모든 클래스는 Object를 상속받습니다. 자바에서 최상위 클래스는 Object이므로, 모든 클래스는 Object를 상속받습니다.
- ? 와일드카드: ?는 특정 타입을 명시하지 않고, 어떤 타입이든 가능하다는 것을 나타냅니다.
- extends Object 제약조건: ? extends Object는 Object를 상속하는 어떤 클래스든 올 수 있다는 의미입니다. 따라서, 메서드는 Api 클래스의 인스턴스를 반환하지만, 그 데이터 타입이 무엇인지는 미리 정의되어 있지 않습니다.
이를 통해 메서드가 반환할 수 있는 타입의 유연성을 높일 수 있습니다. 다음과 같은 여러 가지 타입을 반환할 수 있습니다:
9. 최종 결과
다음은 최종적으로 클라이언트에게 반환되는 오류 메시지의 예입니다:
{
"status_code": "400",
"message": "Bad Request",
"data": null,
"error": {
"error_list": [
"data.password : { } 은 크기가 4에서 12 사이여야 합니다",
"data.email : { hong.com } 은 올바른 형식의 이메일 주소여야 합니다",
"data.id : { } 은 크기가 3에서 10 사이여야 합니다",
"data.score : { 1000 } 은 100 이하여야 합니다",
"data.password : { } 은 공백일 수 없습니다",
"data.id : { } 은 공백일 수 없습니다"
]
}
}
전체 코드
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.example.model.Api;
import org.example.model.Student;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.stream.Collectors;
@RestController
@Slf4j
@RequestMapping("/student/info")
public class StudentValidationController {
@PostMapping("")
public Api<? extends Object> studentInfo(
@Valid
@RequestBody
Api<Student> student,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
var errorList = bindingResult.getFieldErrors().stream()
.map(it -> {
var format = "%s : { %s } 은 %s";
var errorFormat = String.format(format, it.getField(), it.getRejectedValue(), it.getDefaultMessage());
return errorFormat;
}).collect(Collectors.toList());
var body = Api.Error.builder()
.errorList(errorList)
.build();
var response = Api.builder()
.statusCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.message(HttpStatus.BAD_REQUEST.getReasonPhrase())
.error(body)
.build();
return response;
}
log.info("info : {}", student);
return student;
}
}
전체 코드를 보면 서비스 로직이 무엇인지 잘 보이지 않습니다.
이는 서비스 로직 안에서 오류 처리를 하기 때문입니다.
다음 글에서는 서비스 로직에서 오류 처리를 하지 않고, 이를 따로 다른 클래스에서 처리하는 방법을 설명하겠습니다.
- 오류가 있는지 확인: bindingResult.hasErrors()를 사용하여 요청에 오류가 있는지 확인합니다.
- 오류 메시지 생성: bindingResult에서 오류 목록을 추출하여 각 필드에 대한 오류 메시지를 생성합니다.
- API 오류 응답 생성: 오류 메시지를 Api.Error 객체에 담고, 이를 포함한 Api 응답을 생성하여 반환합니다.
- 정상 처리: 오류가 없는 경우, 입력받은 student 객체를 그대로 반환합니다.
'Spring Boot' 카테고리의 다른 글
[Spring Boot] IoC/DI (0) | 2024.07.10 |
---|---|
[Spring Boot] 유효성 검사와 예외 처리를 통한 API 구현 (0) | 2024.07.05 |
[Spring Boot] Validation 유효성 검사 (0) | 2024.07.04 |
[Spring Boot] Web에서 응답 만드는 방법 - Response Entity (0) | 2024.07.02 |
[Spring Boot] Rest API Put 메서드 + boolean is 변수명의 문제점 (0) | 2024.07.01 |