티스토리 뷰
(참고: Spring Boot와 Thymeleaf를 기준으로 작성된 글입니다.)
(참고: 본 글은 Bean Validation을 살펴보기 전 Validation의 기본적인 동작 구조를 살펴보기 위한 글입니다.)
Validation, 검증이라는 뜻대로 클라이언트가 보낸 값들을 검증하는 것이다
Validation이 필요한 이유를 비롯하여 어떻게 구현하는지 살펴보고자 한다
Validation이 필요한 이유
클라이언트가 보낸 값들을 검증할 때 클라이언트 사이드에서 JS 등을 사용하여 검증할 수도 있다
하지만 보내는 값을 조작하여 뚫을 수 있는 가능성이 있기 때문에 보안에 취약하다
그렇기 때문에 클라이언트 사이드 검증은 물론 반드시 서버 사이드에서도 데이터를 검증하여야 한다.
(적절히 섞어 사용하는것이 제일 좋겠다)
Validation과 UX
클라이언트가 값을 잘못입력하였다고 해서 오류 페이지로 넘어가거나
편의를 위해 본래 페이지로 돌려보냈다고 해도 입력했던 값들이 서버가 받아오지 않고,
어떠한 값들이 잘못입력되었는지 정보를 제공하지 않으면 검증에는 성공했다고 하더라도 클라이언트 입장에서 굉장히 불편하다
Validation의 핵심은 검증 그 자체도 있겠지만, 사용하는 클라이언트에게 편리하게 그 원인과 해결법을 제공하는 것도 포함이 된다
BindingResult
예시를 살펴보기 전 검증의 핵심이자 스프링이 제공하는 BindingResult에 대해서 먼저 살펴보고자 한다
BindingResult는 검증 오류를 보관하는 객체이다, 즉 검증에 오류가 발생하면 여기에 보관되는 것이다
BindingResult가 검증하는 상황을 구분하자면 크게 2가지로 구분된다
1. 클라이언트가 잘못된 타입을 입력하는 경우 -> int 타입으로 받아야 하는데 String으로 입력
2. 클라이언트가 비즈니스 로직을 위배하여 입력하는 경우 -> 9,999 이상 입력이 불가능한데 10,000을 입력
이때 1번 타입 에러 같은 경우는 스프링 MVC에서 컨트롤러에 진입하기도 전에 발생하는 에러이기 때문에 컨트롤러가 호출되지 않고 바로 400 에러가 발생하면서 오류 페이지를 반환한다, 이렇게 되면 클라이언트는 입력한 값 중 어디서 잘못되었는지도 알기 불편할 것이다
하지만 BindingResult를 사용하게 되면 자동으로 오류 메시지를 생성하여 반환하기 때문에 에러 페이지가 아닌 에러에 대한 메시지를 반환한다
이것이 가능한 이유는 BindingResult가 컨트롤러가 호출되기 전에 해당 객체의 값을 검증하여 타입 에러인 경우 에러 메시지를 반환 값에 담은 후에 컨트롤러를 호출하기 때문이다, 또한 에러 메시지와 함께 클라이언트가 입력한 값 또한 반환할 수 있기 때문에 클라이언트는 본인이 어떠한 값을 잘못 입력했었는지 더욱 편리하게 알 수 있다
그렇다면 BindingResult가 어떠한 기능들을 제공하는지 더욱 자세히 살펴보자
@PostMapping("/add")
public String addItem(@ModelAttribute Item item,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
(생략)
}
BindingResult를 사용할 때의 주의점이 있는데 예시에서처럼 반드시 검증하는 대상 뒤에 작성해야 한다는 것이다
이 이유는 BindingResult가 생성하는 FiledError와 ObjectError를 살펴보면 더욱 이해가 쉽다
• FieldError
FieldError는 아래와 같이 두 가지 생성자를 제공한다
public FieldError(String objectName, String field, String defaultMessage);
public FieldError(String objectName, String field, @Nullable Object
rejectedValue, boolean bindingFailure, @Nullable String[] codes,
@NullableObject[] arguments, @Nullable String defaultMessage)
• objectName : 오류가 발생한 객체 이름
• field : 오류 필드
• rejectedValue : 사용자가 입력한 거절 값
• bindingFailure : 타입 오류 같은 바인딩 실패인지, 구분 실패인지를 구분하는 값
• codes : 메시지 코드
• arguments : 메시지에서 사용하는 인자
• defaultMessage : 기본 오류 메세지
파라미터 값에서도 보이듯이 오류가 발생한 객체 이름을 BindingResult가 필요로 하기 때문에 반드시 검증 대상 뒤에 BindingResult 파라미터를 작성해주어야 한다
• ObjectError
ObjectError 또한 아래와 같이 두 개의 생성자를 제공하며 각 파라미터 값은 FieldError의 파라미터와 똑같은 기능을 한다
public ObjectError(String objectName, String defaultMessage) {
}
public ObjectError(String objectName,
@Nullable String[] codes,
@Nullable Object[] arguments,
@Nullable String defaultMessage) {
}
• RejectValue
스프링은 FieldError와 OjbectError 기능을 더욱 편리하게 사용할 수 있도록 RejectValue를 제공한다
void rejectValue(@Nullable String field, String errorCode,
@Nullable Object[] errorArgs, @Nullable String defaultMessage);
• field : 오류 필드명
• errorCode : 오류코드 (스프링 메시지의 오류코드)
• errorArgs : 오류 메시지에서 치환할 Arguments 값
• defaultMessage : 오류 메시지를 찾을 수 없을 때 사용하는 기본 메시지
//FieldError 사용
bindingResult.addError(new FieldError("item", "itemName",
item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
//rejectValue 사용
bindingResult.rejectValue("itemName", "required");
한눈에 봐도 코드량이 훨씬 줄어든 것을 볼 수 있다
Error Message
BindingResult의 FieldError와 ObjectError의 파라미터 값 중 codes, arguments, defaultMessage와 RejectedValue의 errorCode, errorArgss는 에러 메시지를 반환하는 파라미터이다, 스프링 메시지를(스프링 메시지의 글) 활용하면 반환되는 오류 메시지를 효율적으로 관리할 수 있으며 제일 중요한 것은 메시지의 결과물을 달리하기 위해서 애플리케이션의 코드를 수정하지 않아도 된다는 것이다, 예시와 함께 살펴보고자 한다
• Message Properties
스프링 메시지 properties를 작성하듯 properties 파일을 하나 만들고 application.properties에 해당 파일을 추가하여 준다
spring.messages.basename=errors //errors.properties
//errors.properties
required = 필수 값 입니다.
이렇게 메시지 코드로 반환되는 오류 메시지를 한번에 관리 할 수 있어, 해당 오류메시지를 변경하거나 추가하더라도 애플리케이션 코드의 유지보수성이 훨씬 좋아진다
• MessageCodeResolver
bindingResult.rejectValue("itemName", "required");
앞서 살펴봤던 rejectValue에서 field값과 메시지 코드만을 보내어 반환했다
이를 가능케 해주는 것이 바로 MessageCodeResolver인데
우선순위를 주어 messageCode를 생성하여 해당하는 값을 message.properties에서 찾아 반환한다
messageCode를 생성하는 규칙은 아래와 같으며 우선순위를 구분해준다
• FieldError
1: code + "." + object name + "." + field
2: code + "." + field
3: code + "." + field type
4: code
rejectValue에 넣었던 "itemName"과 "required"를 기준으로 4가지가 생성된다면 아래와 같을 것이다
requried.item.itemName
required.itemName
required.java.lang.String
required
• ObjectError
1: code + "." + object name
2: code
객체 오류의 경우 값을 담을 필요가 없기 때문에 위와 같은 규칙으로 생성한다
• TypeMismatch
//타입에러의 경우 스프링이 지정한 code명이 있다
typeMismatch + "." + object name + "." + field
typeMismatch + "." + field
typeMismatch + "." + fieldType
typeMismatch
타입 에러의 경우 스프링이 지정한 code명이 있기 때문에, 해당 objectname과 field값을 조합하여 messages.properties에 정의하면 스프링이 기본적으로 제공하는 난해한 오류 메시지를 커스텀하여 클라이언트에게 제공할 수 있다
• Message Level
각각의 규칙으로 코드를 생성하는 MessageCodeResolver를 활용하면 오류메시지 또한 굉장히 효율적으로 설계할 수 있다
필드 에러의 경우 object name, field, field type 값 등을 활용하여 우선순위를 정하는데 이것을 간단하게 이해해보자면 특정 범위 -> 범용 범위의 개념으로 설명할 수 있겠다
즉 아래와 같이 디테일 한 부분과 범용적인 부분 모두 작성하게 되면, 디테일이 적용될 수 있는 곳에서는 디테일한 오류 메시지가, 해당 디테일 메시지가 존재하지 않는다면 범용적으로 설정한 메시지가 출력이 되게 설계할 수 있는 것이다
#Level1
required.item.itemName=상품 이름은 필수입니다.
#Level2
(생략)
#Level3
required.java.lang.String=필수 문자입니다
#Level4
required=필수 값 입니다.
이로써 각각의 애플리케이션을 수정하지 않아도, 객체 이름과 필드 타입 그리고 가장 범용적인 code를 설계하여, 내가 원하는 곳에서는 디테일한 오류 메시지를, 이러한 디테일이 불필요한 오류 메시지의 경우 범용적인 오류 메시지가 출력되게 할 수 있다
Validator
기본적으로 이런 검증 로직들은 복잡하기도 하고 Controller단에 실제 비즈니스 로직과 같이 작성되어 있다면 가독성이 매우 떨어진다
이럴 경우 별도의 클래스로 검증 로직들을 분리하여 사용하는 것이 가독성도 높이면서 효율적으로 관리할 수 있을 것이다
검증이란 개념은 특정 서비스에서만 사용되는 것이 아니라 전 세계 공용으로 필요한 개념인 만큼 스프링에서 이를 인터페이스로 제공한다
public class ValidatorExample implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return false;
}
@Override
public void validate(Object target, Errors errors) {
}
}
• supports
이름처럼 해당 클래스를 체크하는 역할을 한다, 예를 들어 Validator가 item, member와 같이 여러 검증기가 있다고 가정해보자, 이때 supports는 해당 클래스를 지원하는지 체크하여 아래에 있는 validate를 수행하도록 해준다
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
• validate
Object target은 기존의 object값을 받아주고 Errors errors는 bindingresult역할을 해준다
@Override
public void validate(Object target, Errors errors) {
Item item = (Item) target; //해당 객체의 클래스로 변환시켜주어야 한다
//검증 로직
if (!StringUtils.hasText(item.getItemName())) {
errors.rejectValue("itemName", "required");
}
• WebDataBinder
스프링이 제공하는 Validtor 인터페이스는 WebDataBinder를 사용하면 더욱 간편하게 활용할 수 있다
WebDatabinder는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능 또한 포함해준다
@InitBinder
public void init(WebDataBinder datbinder) {
dataBinder.addValidators();
}
• @InitBinder
InitBinder는 만들어 놓은 검증기(Validator)를 자동으로 동작하게 해주는 역할을 한다
아래와 같이 해당 컨트롤러에 @Validated를 작성해주면 해당 컨트롤러가 호출될 때마다 만들어 놓은 검증기가 작동하게 된다
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
(생략)
}
핵심 정리
클라이언트가 전달하는 값이 정상적인 값인지 확인하기 위하여 Validation(검증)이 반드시 필요하다
클라이언트 사이드에서 하는 검증은 값을 조작할 수 있는 가능성이 있어 보안에 비교적 취약하기 때문에 서버 사이드의 검증이 필수다
검증의 핵심은 값 그 자체를 검증하는 것도 있지만, 클라이언트가 어떠한 값을 잘못 입력했는지에 대한 정보를 제공하는 것도 포함한다
공통적으로 사용되는 검증의 개념을 스프링은 Validator라는 인터페이스로 제공하며
이를 통하여 개발자는 검증 로직을 기존의 컨트롤러 로직과 분리하여 사용할 수 있다
또한 스프링의 메시지를 활용하여 클라이언트에게 제공하여야 하는 오류 메시지를 한 번에 관리할 수 있다
MessageCodeResolver를 활용하여 디테일한 오류 메시지부터 범용적으로 사용할 수 있는 오류 메시지까지
우선순위를 구분하여 관리할 수 있다, 이를 통해 애플리케이션의 코드를 수정하지 않고도 오류 메시지를 수정하거나 추가할 수 있다
개인 학습을 위해 작성되는 글입니다.
제가 잘못 알고 있는 점에 대한 지적 / 더 나은 방향에 대한 댓글을 환영합니다.
참조 링크:
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
'Spring > Spring MVC' 카테고리의 다른 글
[Spring MVC] Login with Cookie & Session (0) | 2021.09.09 |
---|---|
[Spring MVC] Bean Validation (0) | 2021.09.08 |
[Spring MVC] Message & Locale (0) | 2021.09.03 |
[Spring MVC] PRG (Post/Redirect/Get) 패턴 (0) | 2021.08.31 |
[Spring MVC] HTTP Response (HTTP 응답) (0) | 2021.08.30 |
- Total
- Today
- Yesterday
- DefaultHandlerExceptionResolver
- 제이쿼리란
- @ResponseStatus
- 제이쿼리 기본 선택자
- Spring API Error
- 쿠키
- 제이쿼리 직접 선택자
- Cache
- Spring TypeConverter
- jQuery 직접 선택자
- ExceptionHandlerExceptionResolver
- 캐시
- application/x-www-form-urlencoded
- 제이쿼리 인접 관계 선택자
- Session
- http
- @ExceptionHandlere
- Spring Container
- 제이쿼리 위치탐색선택자
- spring
- 맨코
- OOP
- 제이쿼리 탐색선택자
- ResponseStatusExeceptionResolver
- DTO와 VO의 차이
- maenco
- 세션
- cookie
- uri
- Spring MVC
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |