오류 코드와 메시지 처리6

shinyeongwoon·2023년 1월 27일
0

Message

목록 보기
11/11

스프링이 직접 만든 오류 메세지 처리

검증 오류 코드는 다음과 같이 2가지로 나눌 수 있다.
개발자가 직접 설명한 오류 코드 -> rejectValue()를 직접 호출
스프링이 직접 검증 오류에 추가한 경우 (주로 타입 정보가 맞지 않음)

price 필드에 문자 "A"를 입력해보자.
로그를 확인해보면 BindingResultFieldError 가 담겨있고, 다음과 같은 메시지 코드들이 생성된 것을 확인할 수 있다.
codes[typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,typeMismatch]

다음과 같이 4가지 메세지 코드가 입력되어 있다.
typeMismatch.item.price
typeMismatch.price
typeMismatch.java.lang.Integer
typeMismatch

그렇다. 스프링은 타입 오류가 발생하면 typeMismatch라는 오류 코드를 사용한다.
이 오류 코드가 MessageCodesResolver를 통하면서 4가지 메세지 코드가 생성된 것이다.

실행해보자.
아직 errors.properties 에 메시지 코드가 없기 때문에 스프링이 생성한 기본 메시지가 출력된다.

Failed to convert property value of type java.lang.String to required type
java.lang.Integer for property price; nested exception is
java.lang.NumberFormatException: For input string: "A"

error.properties 에 다음 내용을 추가하자

#추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요. typeMismatch=타입 오류입니다.

다시 실행해보자
결과적으로 소스코드를 하나도 건들지 않고, 원하는 메시지를 단계별로 설정할 수 있다.

정리
메시지 코드 생성 전략은 그냥 만들어진 것이 아니다. 조금 뒤에서 Bean Validation을 학습하면 그 진가를 더 확인할 수 있다.

Validator 분리1

목표
복잡한 검증 로직을 별도로 분리하자.

컨트롤러에서 검증 로직이 차지하는 부분은 매우 크다. 이런 경우 별도의 클래스로 역할을 분리하는 것이 좋다. 그리고 이렇게 분리한 검증 로직을 재사용 할 수도 있다.

ItemValidator를 만들어 보자

package hello.itemservice.web.validation;
import hello.itemservice.domain.item.Item;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
  
@Component
public class ItemValidator implements Validator {
	
    @Override
    public boolean supports(Class<?> clazz) {
    	return Item.class.isAssignableFrom(clazz);
	}
      
	@Override
	public void validate(Object target, Errors errors) {
    	Item item = (Item) target;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName","required");
        if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        errors.rejectValue("price", "range", new Object[]{1000, 1000000},null);
        }
        
        if (item.getQuantity() == null || item.getQuantity() > 10000) {
        	errors.rejectValue("quantity", "max", new Object[]{9999}, null);
        }
        
	//특정 필드 예외가 아닌 전체 예외
    if (item.getPrice() != null && item.getQuantity() != null) {
    	int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
        	errors.reject("totalPriceMin", new Object[]{10000,resultPrice}, null);
            } 
		}
	} 
    
}

스프링은 검증을 체계적으로 제공하기 위해 다음 인터페이스를 제공한다.

public interface Validator {
	boolean supports(Class<?> clazz);
    void validate(Object target, Errors errors);
}

supports() {} : 해당 검증기를 지원하는 여부 확인(뒤에서 설명)
validate(Object target, Errors errors) : 검증 대상 객체와 BindingResult

ItemValidator 직접 호출하기

ValidationItemControllerV2 - addItemV5()


private final ItemValidator itemValidator;

@PostMapping("/add")
public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
	itemValidator.validate(item, bindingResult);
    if (bindingResult.hasErrors()) {
    	log.info("errors={}", bindingResult);
        return "validation/v2/addForm";
    }

//성공 로직
Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";
}

코드 변경

addItemV4()@PostMapping 부분 주석 처리
ItemValidator 를 스프링 빈으로 주입 받아서 직접 호출했다.

실행

실행해보면 기존과 완전히 동일하게 동작하는 것을 확인할 수 있다. 검증과 관련된 부분이 깔끔하게 분리되었다.

Validator 분리2

스프링이 Validator 인터페이스를 별도로 제공하는 이유는 체계적으로 검증 기능을 도입하기 위해서다. 그런데 앞에서는 검증기를 직접 불러서 사용했고, 이렇게 사용해도 된다. 그런데 Validator 인터페이스를 사용해서 검증기를 만들면 스프링의 추가적인 도움을 받을 수 있다.

WebDataBinder를 통해서 사용하기

WebDataBinder 는 스프링의 파라미터 바인딩의 역할을 해주고 검증 기능도 내부에 포함한다.

ValidationItemControllerV2에 다음 코드를 추가하자

@InitBinder
public void init(WebDataBinder dataBinder) {
	log.info("init binder {}", dataBinder);
    dataBinder.addValidators(itemValidator);
}

이렇게 WebDataBinder 에 검증기를 추가하면 해당 컨트롤러에서는 검증기를 자동으로 적용할 수 있다.
@InitBinder -> 해당 컨트롤러에만 영향을 준다. 글로벌 설정은 별도로 해야한다. (마지막에 설명)

@Validated 적용

ValidationItemControllerV2 - addItemV6()

@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item,BindingResult bindingResult, RedirectAttributes redirectAttributes) {
	if (bindingResult.hasErrors()) {
	log.info("errors={}", bindingResult);
	return "validation/v2/addForm";
}

//성공 로직
Item savedItem = itemRepository.save(item); redirectAttributes.addAttribute("itemId", savedItem.getId()); redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v2/items/{itemId}";

}

코드 변경

addItemV5()@PostMapping 부분 주석 처리

validator를 직접 호출하는 부분이 사라지고, 대신에 검증 대상 앞에 @Validated 가 붙었다.

실행

기존과 동일하게 잘 동작하는 것을 확인할 수 있다.

동작 방식

@Validated 는 검증기를 실행하라는 애노테이션이다.
이 애노테이션이 붙으면 앞서 WebDataBinder 에 등록한 검증기를 찾아서 실행한다. 그런데 여러 검증기를 등록한다면 그 중에 어떤 검증기가 실행되어야 할지 구분이 필요하다. 이때 supports() 가 사용된다

여기서는 supports(Item.class) 호출되고, 결과가 true 이므로 ItemValidatorvalidate() 가 호출된다.

@Component
public class ItemValidator implements Validator {

	@Override
    public boolean supports(Class<?> clazz) {
    	return Item.class.isAssignableFrom(clazz);
	}
	
    @Override
		public void validate(Object target, Errors errors) {...}
	}
  

글로벌 설정 - 모든 컨트롤러에 다 적용


@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
	
    public static void main(String[] args) {
	SpringApplication.run(ItemServiceApplication.class, args);
	}
      
	@Override
	public Validator getValidator() {
		return new ItemValidator();
	}

}

이렇게 글로벌 설정을 추가할 수 있다. 기존 컨트롤러의 @InitBinder 를 제거해도 글로벌 설정으로 정상 동작하는 것을 확인할 수 있다.

글로벌 설정을 하면 다음에 설명할 BeanValidator가 자동 등록되지 않는다. 글로벌 설정 부분은 주석처리 해두자. 참고로 글로벌 설정을 직접 사용하는 경우는 드물다.

🤞 참고

검증시 @Validated @Valid 둘다 사용가능하다.
javax.validation.@Valid 를 사용하려면 build.gradle 의존관계 추가가 필요하다.
implementation 'org.springframework.boot:spring-boot-starter-validation' > @Validated 는 스프링 전용 검증 애노테이션이고, @Valid 는 자바 표준 검증 애노테이션이다.
자세한 내용은 다음 Bean Validation에서 설명하겠다.

0개의 댓글