스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 섹션4. 검증1 - Validation
CS/김영한 스프링 강의

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 섹션4. 검증1 - Validation

범위나 타입을 제한하고 클라이언트가 제대로 입력했는지 서버에서도 검증해야 한다. 클라이언트에게도 뭐가 잘못되었는지 알려줘야 납득하고 다시 잘 사용한다.

 

취약할 수 밖에 없는 이유는 클라이언트에서 자바스크립트로 실행하는데 누구나 할 수 있기 때문. 서버에서만 검증하려면 실시간 검증하는데 힘들 수 있음.

사용자가 적어 올리고 서버에서 검증했는데 성공했을 경우만을 가정해서 코드를 짰다. 잘 성공했으면 불변성 지키기 위해 GET을 날리는 주소인 상품 주소로 리다이렉트 했다.

실패했을 경우, 성공했을 때 처럼 상품 상세로 리다이렉트 하지 말고 해당 추가 폼을 다시 보여주되 실패하도록 입력한 그 값까지 모델에서 저장하여 다시 표시해줘야 한다. 이게 차이점임.

 

분명 저장 누르면 다시 등록 화면을 보여주는데 값이 그대로인 이유는 저 상태로 컨트롤러 함수 모델 인자로 그대로 들어가서 그대로 나와 타임리프에 의해 입력되기 때문. 헷갈리지 말라고.

 

큰 틀에선 이게 맞지만, 아직 좀 문제가 있는데, 위의 뷰 템플릿을 보면 중복 코드가 좀 있고, 타입 오류 처리가 안된다. 안되는 이유는 컨트로럴에 item으로 알아서 매핑되서 넣어줘야 하는데 타입 오류가 떠서 컨트롤러에 들어가기도 전에 에러가 뜬다. 또 컨트롤러에 들어가 제대로 실행되는게 아니기 때문에 값들도 모델에 저 item이 안들어가게 되고 해서 반환할 게 없어 빈 칸만 남긴다. 결국 고객이 어떤 값을 입력하든 다 저장해서 표시해줘야 한다.

 

사실 이런 검증 문제들도 엄청 많이 있어와서 당연히 스프링에 기능이 있다.

 

BindingResult라는게 있는데, 스프링에서 제공하는 검증 오류를 보관하는 객체이다. 아까같이 error를 따로 담는 Map을 만들어서 저장하는 대신 여기에 에러를 담는다. 왜 굳이 이러냐면 이게 특별한 거라 타임리프의 에러 처리 관련 문법을 사용할 수 있기 때문.

오버로딩된 함수가 2개 있는데, 하나는 (objectName, field, defaultMessage)고, 하나는 (objectName, defaultMessage)임. 전자는 자기가 오류를 담고자하는 객체를 적는데, 특이한 건 그 객체의 키 값을 그대로 적는다. 이유는 타임리프에서 똑같은 변수명으로 해야 편하기 때문. 또 해당 키 값에 에러 값을 넣는 의미도 있다. 그래서 객체 이름을 그대로 적는것도 있음.

 

만약 BindingResult에 에러를 담았다면 BindingResult를 불러오는 문버 #fields를 사용해서 글로벌 에러를 가져오면 되고, 에러로 저장했기 때문에 th:errors를 사용할 수가 있다. 앞 에러에서 객체를 item으로 지정했기 때문에 *를 사용하고 변수 명고 그대로 불러서 사용하므로 그대로 써주면 된다.

또 BindingResult가 애초에 검증용 클래스이기 때문에(Error를 상속함) 만약 컨트롤러 함수에서 매개변수로 BindingResult를 받으면 에러 관련된 걸 BindingResult에 담으면서 컨트롤러가 실행됨. 그래서 에러 페이지가 안뜬다. 담는 방법은 수동으로 new FieldError를 만들어서 담는것과 마찬가지로 new Error를 통해 만들어서 안에 넣는다. 단 검증할 대상 바로 뒤에 매개변수로 되야 함. 에러가 뜨면 th:field에 FieldError에서 보관한 걸 자동으로 가져온다.

 

저런 타입 에러 말고, 내가 만든 안의 비즈니스 로직 에러로 떴을 때, BindingResult에 있는 값을 th:errors를 통해 표시한다. 근데 BindingResult의 item에는 값을 안넣어줘서 값이 자꾸 초기화되는데 rejectedValue를 넣어주면 된다.

생성하는게 FieldError고 타임리프에는 다른 타입이라서 별도 보관하는 역할을 한다.

 

 

이 다음은 오류 메세지를 어떻게 내 식에 맞게 표시하는지.

 

중요한 설정 맞추기. errors해서 기존처럼 메시지를 추가하지만, 새로운 파일로 만들어서 보관하는게 좋기 때문에 파일을 만든다. 하지만 기본값이 messages이므로 errors를 스프링 설정 파일에 추가한다.

그럼 아까 무시했던 code에 배열 형식으로 넣고, argument에도 원하는 걸 넣는다. 배열 형식으로 받는 이유는 뒤에 앞의 것을 못찾았을 시 뒤에껄 순차적으로 실행시키게 할 수 있기 때문.

여기서부터 자바의 똥냄새가 남을 알 수 있다. 그래서 더 개선시킬려 했다고 한다.

 

위에선 new FieldError로 새로운 필드 에러를 만들며 어떤 객체에 대해 만들어야 할지 'item'으로 일일히 지정해줬지만 사실 argument에서 내가 원하는 객체 뒤에 BindingResult를 받으면(순서) 그걸 감지한다. 그래서 objectName으로 일일히 할 필요가 없어 코드를 좀 줄일 수 있다.

 

원래 필요한건 객체 이름, 필드 명, 에러 코드였는데 이제 객체 이름은 안써도 되니 필드 이름과 코드만 입력하면 된다. 그데 rejectValue는 쌩 코드 대신 errorCode를 받아서 string으로 에러 이름을 적으면 된다. 이렇게 하면 저 안에서는 결국 위에서 했던 new Error를 생성해준다.

에러 이름 순서를 왜 저렇게 해야되는지는 나중에.

아까 메세지 순서대로 표시할 수 있다고 했는데, 이렇게 디테일한 범위를 정해놓고 없으면 점점 더 광범위하게끔 구현하면 많은 에러를 커버 가능.

 

위 같이 디테일하게 레벨별로 에러 메세지를 만들어 놓으면 그걸 차례대로 출력해주는 기능이 이미 있다. 스프링의 MessageCodesResolver고 테스트를 보며 어떻게 사용되는지 보자.

 

위 같이 나는 그냥 required, item만 했을 뿐인데 resolver가 알아서 구체적인 것 부터 넓은 범위까지 순서대로 넣어서 구현해준다. 즉 밑과 같은 객체를 알아서 만들어서 해준다는 것이다.

new ObjectError("item", new String[]{"required.item", "required"});

 

new FieldError("item", "itemName", null, false, messageCodes, null, null);

인자를 필드와 타입까지 넣어주면 그것도 구체적인 것부터 순서대로 알아서 만든다.

사실 이걸 이미 BindingResult에서 알아서 하고 있었다. 그래서 저렇게 설렁설렁 적어도 알아서 잘 되었었던것.

 

이걸 실제로 적용해보고 주석처리 했다 말았다 하면서 테스트 해보면 진짜 된다.

 

 

간단한 ValidationUtils도 있다.

 

이제 스프링의 기본 메세지를 우리 환경에 맞게 바꿔보자. 타입을 다르게 하면 스프링의 기본 처리 메세지가 뜬다.

잘 보면 앞에서 많이 했던 codes의 앞에 typeMismatch.item.price, ... 해서 적혀있다. 이게 우리 에러 메세지 정의에 없기 때문에 스프링의 기본 값으로 출력한 것. 그렇다면 저 메세지를 정의해주면 우리 것이 출력될 것이다.

 

 

 

이제 코드 줄이기임. Validator를 다른 클래스의 함수로 따로 뺄 건데, 그냥도 만들 수 있지만 스프링의 Validator 인터페이스를 활용해서 해보자.

 

빈에 등록해서 주입자로 쓸려고 @Component에 등록

BindingResult의 부모가 Error이기 때문에 그대로 사용할 수 있는 모습이다.

함수가 하는것만 보면 굳이 스프링의 Validator 인터페이스를 상속하지 않아도 만들 수 있는데 왜 상속했냐면 스프링의 도움을 받으면 어노테이션 한 단어면 끝나기 때문

 

스프링에는 WebBinder라는게 있는데 저걸 하면 저 클래스가 실행될 때마다 실행되서 클래스 안의 모든 함수에서 사용할 수 있음. 스프링의 Validator를 상속했기 때문에 @Validated를 하면 앞에서 했던 itemValidator.validate(item, bindReulst); 한 걸 그대로 해준다. supports에 해당하면 그거에 대해 실행하는 거임. 이걸 글로벌로도 적용할 수 있는데 굳이 잘 안한다.