검증1-validation
- 검증을 하는데 오류가 있으면 바로 오류페이지로 이동한다면 사용자가 너무나 불편하다
- 사용자에게 잘못 입력한 부분을 알려주는 정도만 해야 함
- 따라서, 입력에 대한 검증을 클라이언트 혹은 서버에서 해야 하는데
- 클라이언트에서 하게 되면 보안에 취약하고 서버에서만 하면 반응성이 떨어진다
- 타임리프 사용하면 POST로 전달받은 Item객체를 ModelAttribute로 넘기면 검증 실패 시에도 그대로 다시 재사용가능하게 됨
- prac4에 errors란 이름의 맵에 오류들을 넣어서 모델에 담아 주는데, 그냥 errors.containsKey 호출했을 때 맵이 비어있으면 NullPointerException뜸
- errors?.containsKey를 사용하면 예외 대신 null을 반환하는 문법, th:if가 null을 실패로 여기기에 오류 메세지가 출력되지 않음
- SpringEL에서 제공하는 문법이라 함.
- V1처럼 일일이 if문으로 예외처리하고 맵에다 오류 메세지 넣고 이렇게 힘들게 해도 타입오류 같은걸 잡지 못 함
- V2 : BindingResult사용
- 검증할 대상 애트리뷰트 뒤에 파라미터로 BindingResult선언한다
- @ModelAttribute Item item 바로 뒤에 와야 된다는 뜻
- 객체의 필드에 대한 검증이면 FieldError, global에러같이 복합적 오류는 ObjectError
- 타임리프에서 bindingResult에 접근하는 방법은 #fields로 접근하고 뭐 .globalErrors같은 메소드 지원
- th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다.
- th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.
- BindingResult를 사용하지 않고 핸들링도 하지 않은 오류가 있으면 에러페이지로 가지만
- BindingResult를 사용 하게 되면 핸들링 하지 않은 오류에 대해서도 즉, 필드의 타입오류 같은 경우 해당 필드에 필드에러로 자동으로 추가되어서 넘어간다.
- 우리가 지정한 에러가 발생한 경우 값이 사라지게 됨
- FieldError의 다른 생성자 (rejectedValue가 포함된)
- ObjectError는 기본 필드 값이 존재하지 않으니 해당 부분이 없음
- 또, 타입에러같이 필드에 타입이 잘못들어온 경우 잘못 입력한 값을 rejectedValue에 해당 값을 넣어서 추가해준다.
- th:field의 경우 정상 로직인 경우 선언한 객체에서 값을 꺼내 쓰지만, 오류가 있는 경우 해당 필드의 FieldError에서 값을 꺼내서 사용한다.
- 검증할 대상 애트리뷰트 뒤에 파라미터로 BindingResult선언한다
- 오류코드와 메세지 처리
- 여러 곳에서 오류 메세지를 공통으로 사용하는 경우 이것 또한 메세지를 사용해서 컨트롤 하는 것이 좋다
- 물론 뒤에 언어 붙여놓으면 국제화 가능
- 뭐 기본적으로 messages로 달아주면 자동으로 인식하니 안에 넣어줘도 되긴 하지만, 구분을 위해 따로 설정을 추가해서 이름을 지정해주는 것이 좋다
- FieldError 생성자의 codes와 argument를 이용해서 먼저 오류 메세지를 찾고 없으면 defaultmessage를 사용하는 형식
- 근데, 이렇게 에러 넣어주는 일도 좀 귀찮다
- binding result는 타겟 객체 바로 뒤에 오니 해당 객체를 알고 있다는 의미이다.
- 얘가 들고있는 rejectValue(), reject()를 이용해서 코드를 단순화 할 수 있다 !
- rejectValue는 FieldError, reject는 ObjectError를 만들어내는 것
- 여기서, 에러코드를 맨 앞의 이름만 적어줘도 알아서 찾아서 사용한다 ! 어떻게 ??
- 오류코드를
- required : 필수 값 입니다.
- required.item.itemName : 상품 이름은 필수 입니다.
- 이런식으로 선언할 수 있음
- 만약 이렇게 둘 다 선언되어 있는 경우 required.객체.필드이름이 다 맞는 애가 1순위
- 필드명 맞으면 2순위
- 그냥 코드이름만 같으면 3순위 이렇게 우선순위를 주고 오류메세지를 할당한다.
- 즉, required라는 코드로 메세지를 찾을 때 required.item.itemName을 먼저 찾아본다는 뜻
- 알아서 필드 에러코드 배열을 우선순위에 맞게 만든다
- 오류코드를
- MessageCodesResolver
- rejectValue가 이 리졸버의 resolveMessageCodes메소드를 사용함
- 이 메소드에서 에러코드, 뭐 객체이름, 필드이름 이런걸 넣을 수 있음
- 리턴으로 String code배열을 뱉는데 이게 세밀한 순으로 저장이 됨
- 객체,필드이름 다 주면(Field Error인 경우)
- 코드.객체.필드
- 코드.필드
- 코드.타입
- 코드
- 이 순서대로 저장을 한다.
- Object Error는
- 코드.객체
- 코드
- 이렇게 저장 함
- 이렇게 오류 코드를 단계별로 적용하게 되면 범용성있는 부분은 간단하게 적용하고 구체적으로 필요한 부분은 자세히 적어 오류 메세지 관리를 효율적으로 할 수 있게 된다.
- ValidationUtils
- ValidationUtils.*rejectIfEmptyOrWhitespace*(bindingResult,"itemName","required");
- if(!StringUtils.*hasText*(item.getItemName())) bindingResult.rejectValue("itemName","required");
- 원래 조건문 걸고 추가했던 부분을 한줄로 만들 수 있긴 한데, 뭐 공백같은 간단한 기능만 제공하니 그냥 ㅇㅇ 하고 넘기자
- 개발자가 처리하지 않은 오류(타입 오류같은..) 애들은 스프링이 생성한 기본 메세지가 출력됨
- 근데 이 기본 메세지가 상당히 기니까, 그냥 해당 오류 코드에 맞게 메세지를 파일에 설정해주는게 좋다.
- 여러 곳에서 오류 메세지를 공통으로 사용하는 경우 이것 또한 메세지를 사용해서 컨트롤 하는 것이 좋다
- Validator분리
- 검증로직 때문에 컨트롤러 단에서 하는 일이 뭔지알기 힘들어짐
- Validator를 구현해서 원래 검증 로직을 다 이식하고 뭐, 컴포넌트 달아서 스캔하고 컨트롤러에서 DI받아서 사용하면 간편함
- BindingResult가 Errors의 자식이니 bindingResult와 target인 객체(item)을 넣으면 검증을 수행한다.
- 굳이 컴포넌트 스캔까지 해서 DI를 하고 만들어서 사용하는 것과 그냥 new해서 생성해서 만들어쓰는 것과 큰 차이가 없어 보이지만..!
- 스프링의 도움을 받을 수 있다 .
- @InitBinder라는 애노테이션을 사용하고 WebDataBinder를 파라미터로 받는 메소드를 만들고
- 요청마다 새롭게 만들어짐
- 거기에 방금 만든 validator를 추가한다.
- @Validated 애노테이션이 붙은 객체에 대해 검증기를 실행한다(해당 컨트롤러 내에서만)
- @Valid도 사용가능함
- 근데, 검증기가 여러개라면 ?
- 검증기에서 구현한 supports 메소드를 통해 해당 검증기가 들어온 타겟을 지원하는지 확인하고 지원되는 검증기를 사용한다.
- @InitBinder라는 애노테이션을 사용하고 WebDataBinder를 파라미터로 받는 메소드를 만들고
검증2-Bean Validation
검증로직을 일일이 다 구현하는 것이 생각보다 번거롭다..
그냥 필드에 어노테이션 박아서 처리하는게 좋지 않을까?
이걸 해주는 것이 Bean Validation
- 구현체가 아니라 기술 표준임, 즉, 검증 어노테이션,인터페이스의 모음
- 어노테이션에 속성 설정들로 메세지를 새로 설정하는 등 여러 작업 가능
NotNull같은건 javax.validation이고 Range같은건 hibernate.validator 의 구현체 인데, 사실 실무에서 둘 다 사용해야 하고 스프링에서도 그걸 알아서 validator추가하면 같이 설치하는 것
직접 빈 validation을 사용할 수 도 있지만, 스프링은 이걸 내부적으로 알아서 처리한다.
따로 검증기를 등록하지 않고 그냥 검증대상에 @Valid나 @Validated 달아주면 된다
부트가 LocalValidatorFactoryBean을 글로벌 Validator로 자동으로 등록하고 검증 어노테이션을 보고 검증을 수행함
검증 과정
- Valid달아 놓은 모델 애트리뷰트 객체를 바인딩 시도
- 만약, 바인딩에 성공하면 Validator를 적용
- 실패하면 TypeMismatch로 필드 에러를 추가 하고 Bean Validation하지 않음 (숫자 넣어야 되는데 문자 넣은 경우)
에러코드
- 에러코드를 찍어보면 NotBlank 어노테이션 기준으로
- NotBlank.객체.필드이름, NotBlank.필드이름, NotBlank.타입,NotBlank
- 이렇게 rejectValue할때처럼 메세지 코드를 순서대로 만들어 줌
- 그래서, 해당 에러 코드에 관련된 메세지를 메세지 속성파일에 등록하면 통합관리가 가능
- 에러코드를 찍어보면 NotBlank 어노테이션 기준으로
메세지를 찾는 순서
- 메세지 코드 순서대로 메세지 소스에서 메세지를 찾아봄
- 어노테이션에 메세지 정의되어 있는 지 확인
- 다 없으면 기본 값 사용
오브젝트 오류
- @ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 10000")
- 근데 위 처럼 사용하는 것은 제약 조건이 너무 많고 대응이 어렵다
- 따라서, 오브젝트 관련 오류인 경우는 자바 코드로 작성하는 것이 좋다.
한계점
- 등록 시에는 id가 없지만 수정 시에는 id가 필요함
- 만약 수정을 위해 기존 검증 제약을 변경하면 등록 시에 검증에 문제가 발생함
- 1.groups
- 동일한 모델 객체에 대해 상황에 따라 검증을 가능하게 한다
- 그룹으로 나누는 것
- 구분할 그룹을 인터페이스로 만든다 (그냥 빈 인터페이스 만드는 거임)
- 검증 조건 부분에 groups로 검증 할 대상 인터페이스 설정
- 컨트롤러 단에 @Validated 안에 대상 인터페이스 설정
- @Valid를 사용하면 groups사용 불가능
- 폼 전송 객체 분리
- 사실, groups를 사용하면 복잡도가 올라감, 좀 난잡해보임
- 실무에서는 사실 다른 폼에서 온 데이터는 다른 객체로 다르게 데이터를 받는 경우가 훨씬 많다
- 그래서 그냥 DTO로 분리해서 받는 것, 분리했으니 검증이 중복될 일이 없음
Bean Validation을 @RequestBody에도 달아서 사용가능
- @ModelAttribute는 HTTP 요청 파라미터 (URL 쿼리스트링, POST form)을 다룰 때 사용
- 각각의 필드 단위로 세밀하게 적용되어 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리 가능하다.
- @RequestBody는 HTTP 바디의 데이터를 컨버터를 이용해 객체로 변환할 때 사용한다.
- 전체 객체 단위로 검증이 적용되기에 메세지 컨버터의 작동이 성공해서 원하는 객체로 바꿔져야 검증이 적용이 됨
- 만약 타입 에러가 생기는 경우 JSON을 객체로 바꾸지 못하기에 컨트롤러 호출을 하지 못하고 스프링이 예외를 throw하고 리턴해버린다
- 즉, HttpMessageConverter 단계에서 실패시 예외를 throw한다.
- @ModelAttribute는 HTTP 요청 파라미터 (URL 쿼리스트링, POST form)을 다룰 때 사용
로그인처리1-쿠키,세션
- 쿠키로 로그인 여부 판단하기
- 응답에 setCookie로 로그인 성공 시 멤버아이디나 뭐 그런 값을 넣어줌
- 그러면 만료 전까진 클라이언트에서 쿠키를 요청마다 보내줌
- 뭐, 날짜 지정해주면 지정 날짜까지 유효하고(영속 쿠키), 지정 안하면 페이지 이탈 시 삭제(세션 쿠키) 되고..
- @CookieValue 사용해서 컨트롤러에서 쿠키 값 가져오면 됨
- 근데, 뭐 쿠키는 변경이 가능하고, 도난이 쉬우며, 공격자에게 넘어가면 위험하다.
- 세션으로 처리하기(직접)
- 사용자가 접속하면 서버에서 해당 클라이언트의 세션 ID를 생성하고 저장한다
- 뭐 로그인 성공하면 add해서 값들 넣어주면 됨
- 서버는 클라이언트에게 이 세션 ID를 mySessionId쿠키에 담아 보내주고
- 클라이언트는 이걸 쿠키저장소에 저장함
- 클라이언트는 요청 마다 mySessionId쿠키를 전달
- 서버에선 이걸 보고 값을 사용
- 사용자가 접속하면 서버에서 해당 클라이언트의 세션 ID를 생성하고 저장한다
- 동시에 막 접근하는 경우 Concurrent달린 애들을 써줘야 함
- 서블릿 HTTP 세션
- 동작방식은 같으나 쿠키 이름이 JSESSIONID임
- 이걸 좀 더 편하게 사용하는 방법은 @SessionAttribute 어노테이션을 사용하는 것
- 기존에 Request에서 세션 찾아서 getAttribute로 찾는 것을
- @SessionAttribute(name, required ..)를 받으려던 객체 앞에(파라미터) 사용하면 된다
- 세션을 없다고 생성하진 않음
- 로그인을 완전 처음 하게 되면 url끝에 쿼리 파라미터로 jsessionid가 붙음
- 이 방식은 만약 웹 브라우저가 쿠키를 지원하지 않는 경우를 위해 사용하는 방식인데..
- 다른 페이지로 가거나 하면 사라지는데 이걸 유지하지 않으면 인증이 다시 요구됨
- 맨 처음에만 뜨는 이유는 서버입장에서 웹이 쿠키를 지원하는지를 모르기에 쿠키에도 써서 주고 url에도 주는 것
- 만약, url전달 방식을 끄려면
- server.servlet.session.tracking-modes=cookie
- 이걸 설정 파일에 넣어주면 됨
- 세션 정보 ,타임아웃
- 뭐 여러여러 정보들을 가지고 있음(생성시간, 터치시간, 등등)
- 세션은 서버에서 invalidate하거나 타임아웃 되야 삭제 됨
- 근데 HTTP는 Connectionless니 사용자가 웹을 종료한지 모름
- 그러니, 서버에 세션을 저장하는 메모리가 계속해서 쌓일 수 있다
- 또, JSESSIONID 탈취당해서 악용의 우려가 있음
- 그러므로, 타임아웃 시간은 길지 않게 설정해야 한다.
- server.servlet.session.timeout=60 이렇게 설정파일에 직접 설정할 수 있음
- 기본은 1800(30분)
- 뭐, 특정 세션에게 setMaxInactiveInterval로 따로 타임아웃 시간 설정할 수 도 있음
- 종료 기준은 lastAccessTime 현재 시간을 비교해서 설정한 타임아웃 시간 넘기면 세션 invalidate(WAS내부에서)
로그인처리2-필터,인터셉터
- 로그인 여부 확인은 공통 관심사 ( cross-cutting concern) 이다.
- 뭐 AOP로 해결할 수 있긴 하지만, 서블릿 필터나 인터셉터로 해결할 수 있다 !
- request Flow
- HTTP 요청 ->WAS-> 필터(Chain) -> 서블릿(dispatcher servlet) -> 컨트롤러
- 필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
- 필터 메소드 :
- init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
- doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
- destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
- 필터 구현
- Filter를 구현하면 되는데,
- init, doFilter, destroy 순으로 실행됨
- init은 서버 띄울 때 생성, destroy는 서버 내릴 때 삭제, doFilter는 요청 시 마다.
- doFilter에서 필터의 일을 다하고 다음 필터를 불러야 되는데, 이걸 chain.doFilter로 한다.
- 만약 여기서 다음 필터가 없으면 서블릿을 호출하고 다음필터가 있으면 해당 필터를 호출 함
- 무조건 호출해야 함, 안하면 필터단에서 요청이 멈춰버림
- 필터 설정은 FilterRegistrationBean으로 등록할 수 있음
- 만약, 어떤 요청에 대해서 응답이 나갈 때 까지 남기는 모든 로그를 남기고 싶으면(동일한 id로) logbackMDC 로 할 수 있음
- 인터페이스에 default가 박혀있는 메소드는 구현하지 않아도 됨
- PatternMatchUtils 를 사용하면 설정한 경로 패턴과 타겟 uri가 패턴이 맞는 지 간편하게 확인할 수 있음
- httpResponse.sendRedirect("/login?redirectURL="+requestURI);
- 만약 로그인 안된 상태로 특정 페이지로 접근 한 경우, 로그인 후 해당 페이지로 다시 리다이렉트 시켜주기 위해 저렇게 쿼리파라미터로 경로를 넣어서 줌
- 해당 부분에서 requestParam으로 잡아서 redirect를 해당 주소로 시켜주면 된다.
- 스프링 인터셉터
- 필터같이 Cross-Cutting Concern을 처리하는 기술
- 흐름
- HTTP 요청 ->WAS-> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
- 스프링 MVC가 제공하는 기능이고 MVC의 시작점이 디스패처 서블릿 이기에 인터셉터는 디스패처 서블릿 이후에 등장한다.
- 뭐, 컨트롤러 호출 직전에 호출 됨
- 체인 형식으로 이루어짐(필터처럼)
- 서블릿 필터와 비슷하지만, 더 다양한 기능을 지원함
- 컨트롤러에서 예외가 터지면 postHandler는 호출되지 않고 afterCompletion의 경우 예외를 ex로 받아서 어떤 예외인지 확인할 수 있음 (즉, 얘는 실행됨)
- 필터에선 지역 변수로 로그 id를 남길 수 있었는데, 얘는 싱글톤 처럼 사용되기에 멤버 변수를 사용하면 위험함 그래서 preHandle에서 request에 setAttribute로 담아두고 afterCompletion에서 다시 getAttribute로 꺼내서 사용
- handler는
- @Controller @RequestMapping같은 애들은HandlerMethod
- 정적 리소스 는 ResourceHttpRequestHandler
- 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있다.
- 또, 스프링의 URL 경로를 사용함
- 스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고, 세밀하게 설정할 수 있다.
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/pattern/PathPattern.html
- 코드도 간결하게 URL도 자세하게 지정할 수 있어 앵간해선 필터보단 인터셉터를 사용하는 것이 좋다
- ArgumentResolver활용하기
- 커스텀 어노테이션을 만들고 뭐@Target(ElementType.*PARAMETER*)@Retention(RetentionPolicy.*RUNTIME*)
- 이런거 달아서..
- HandlerMethodArgumentResolver 를 구현한 클래스로 로직을 만들고
- WebMvcConfigurer의 addArgumentResolvers에 add하면 적용 됨
- 커스텀 어노테이션을 만들고 뭐@Target(ElementType.*PARAMETER*)@Retention(RetentionPolicy.*RUNTIME*)
'Inflearn' 카테고리의 다른 글
스프링 MVC 2편 (3) (0) | 2023.06.10 |
---|---|
스프링 MVC 2편 (1) (1) | 2023.06.10 |
JPA 실전 2편 정리 (0) | 2023.04.05 |
JPA 기본 개념 (0) | 2023.04.05 |