Spring - 김영한 MVC 2편 정리 (1)

choko's avatar
Jun 30, 2024
Spring - 김영한 MVC 2편 정리 (1)
 
 

Validation

  • 컨트롤러의 중요한 역할중 하나는 HTTP 요청이 정상인지 검증하는 것이다.
  • 클라이언트, 서버 사이드에서 적절히 검증을 섞어 사용하되, 최종적으로 서버 사이드에서의 검증 작업은 필수적이다.
 

BindingResult

  • 스프링에서 제공하는 Validation 처리 객체이다.
  • 다음과 같이 BindingResult.addError() 로 에러 객체를 담은 후 처리한다.
    • @PostMapping("/add") public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) { ... if (!StringUtils.hasText(item.getItemName())) { bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입 니다.")); if (item.getPrice() != null && item.getQuantity() != null) { int resultPrice = item.getPrice() * item.getQuantity(); if (resultPrice < 10000) { bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice)); } } if (bindingResult.hasErrors()) { log.info("errors={}", bindingResult); return "validation/v2/addForm"; } }
       
  • @ModelAttribute, @RequestBody, @RequestParam
    • @ModelAttribute
      • 클라이언트로부터 일반 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 사용하고 싶을 때 이용
      • @BindingResult@ModelAttribute 이후에 위치시켜야 한다. (바인딩을 먼저 하고 검증을 하기 때문)
    • @RequestBody
      • HTTP Body를 자바 객체로 변환한다. JSON 데이터를 처리할 때 사용한다.
    • @RequestParam
      • URL 파라미터나 HTML form-data를 받아온다.
       
  • @BindingResult가 있으면, @ModelAttribute 에 바인딩 오류가 발생해도 컨트롤러가 호출된다.
    • BindingResult 이 없으면 400에러 발생
    • BindingResult 이 있으면 Error 정보를 BindingResult에 담아 컨트롤러가 정상 호출됨
  • 하지만 @RequestBody는 바인딩 오류가 발생하면 바로 예외를 발생시킨다.

Validation 분리

  • 스프링에서 제공하는 Validator 인터페이스
    • public interface Validator { boolean supports(Class<?> clazz); // 해당 검증기를 지원하는 여부 확인 void validate(Object target, Errors errors); // 검증 대상 객체와 BindingResult }
  • Validator 호출 예시 코드
    • @Component // Validator을 스프링 빈으로 주입받아 처리 public class ItemValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Item.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // ... Validator 로직 ... } } // 호출부 @PostMapping("/add") public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) { itemValidator.validate(item, bindingResult); // Validate if (bindingResult.hasErrors()) { log.info("errors={}", bindingResult); return "validation/v2/addForm"; } // 성공 로직 .. }
 
 
 

Bean Validator

  • 검증 기능을 매번 코드로 작성하는것은 번거롭다. 스프링은 Bean Validation을 통해 어노테이션으로 간단히 검증을 진행할 수 있도록 지원한다.
    • 예시
      • @NotBlank : 빈값 + 공백만 있는 경우를 허용하지 않는다.
      • @NotNull: null 을 허용하지 않는다.
      • @Range(min = 1000, max = 1000000) : 범위 안의 값이어야 한다.
      • @Max(9999)
  • 스프링 부트는 Bean Validator를 자동으로 글로벌 Validator로 등록한다.
  • @ModelAttribute의 타입 변환을 시도하고, 성공하면 Validator을 적용한다.
  • @Valid 어노테이션을 통해 검증
    • @PostMapping("/user/add") public ResponseEntity<Void> addUser(@RequestBody @Valid AddUserRequest addUserRequest) { ... }
  • message 속성을 사용하여 에러 메세지를 변경시킬 수 있다.
    • @NotBlank(message = "공백은 입력할 수 없습니다.")
 
 

@Valid vs @Validated

  • @Valid
    • Bean Validator을 통해 컨트롤러단에서 검증 진행
    • MethodArgumentNotValidException 예외를 발생시킨다.
  • @Validated
    • 컨트롤러단이 아닌 다른 곳에서 파라미터를 검증해야 하는 경우 사용한다.
    • AOP 기반으로 메서드 요청을 가로채 유효성 검증을 진행한다.
    • ConstraintViolationException 예외를 발생시킨다.
    • @Validated는 빈으로 등록된 클래스에 붙여 사용한다. 그 안의 유효성을 검증할 메서드 파라미터에 @Valid 를 붙여 검증을 진행한다.
      • @Service @Validated public class UserService { public void createUser(@Valid UserDto userDto) { // ... }
    • Validated는 Group을 통해 검증 항목을 그룹으로 나눠 검증할 수 있다.
      • 하지만 보통 데이터를 다룰 때 전용 클래스를 생성하여 처리하기 때문에, Group은 현업에서 많이 사용되지 않는다.
      • // 1. 그룹용 인터페이스 생성 public interface SaveCheck { } // 2. 그룹 적용 @Data public class Item { @NotNull(groups = UpdateCheck.class) //수정시에만 적용 private Long id; @NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) private String itemName; @NotNull(groups = {SaveCheck.class, UpdateCheck.class}) @Range(min = 1000, max = 1000000, groups = {SaveCheck.class, UpdateCheck.class}) private Integer price; @NotNull(groups = {SaveCheck.class, UpdateCheck.class}) @Max(value = 9999, groups = SaveCheck.class) //등록시에만 적용 private Integer quantity; } // 3. 로직에 사용 @PostMapping("/add") public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) { //... }
         
  • 참고
    • 스프링 부터 3.2부터 @Constraint 어노테이션들이 @Valid, @Validated 없이 기본적으로 유효성 검증을 지원한다고 한다.
    •  
 
 

쿠키와 세션

쿠키(보안 이슈가 있음)

notion image
  • 쿠키 생성
    • Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId())); response.addCookie(idCookie);
  • 쿠키 조회(@CookieValue)
    • @GetMapping("/") public String homeLogin( @CookieValue(name = "memberId", required = false) Long memberId, Model model) { // memberId을 이용하여 로그인 로직 .. }
  • 쿠키 삭제(로그아웃)
    • Cookie cookie = new Cookie(cookieName, null); cookie.setMaxAge(0); response.addCookie(cookie);
       

세션(서버 측 부하가 생김)

notion image
notion image
  • 서버 세션 저장소에 UUID 형태로 세션 ID을 저장한 후, 클라이언트에게 세션ID만 쿠키에 담아 전달한다.
    • 따라서, 세션 방식도 결국 쿠키로 연결되어야 한다.
    • 하지만 쿠키엔 세션ID만 들어가기 때문에 유저의 정보는 보호된다.
  • 클라이언트는 요청마다 쿠키에 세션ID를 담고, 서버는 세션 저장소를 통해 해당 유저의 정보를 확인한다.
  • HttpSession
    • 서블릿의 HttpSession를 사용하여 세션을 생성할 수 있다.
      • //세션이 있으면 있는 세션 반환, 없으면 신규 세션 생성 HttpSession session = request.getSession(); //세션에 로그인 회원 정보 보관 session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
  • @SessionAttribute
    • 세션을 편하게 사용할 수 있도록 스프링에서는 @SessionAttribute를 지원한다.
    • @SessionAttribute(name = "loginMember", required = false) Member loginMember
 
 

필터와 인터셉터

필터와 인터셉터를 다루기 위해, HTTP 헤더나 URL 정보등이 필요할 수 있다. 이런 경우 HttpServletRequest를 사용하여 Http Request 가져올 수 있다.
  • HttpServletRequest
    • http request 정보를 서블릿에게 전달하기 위한 목적
    • 헤더정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어드리는 메서드를 가지고 있음.
    • Body의 stream을 읽어 들이는 메서드를 가지고 있음.
  • HttpServletResponse
    • WAS는 어떤 클라이언트가 요청을 보냈는지 알고 있고, 해당 클라이언트에게 응답을 보내기 위한 객체
    • Content type, 응답 코드, 응답 메시지 등을 전송
    •  

서블릿 필터

  • 필터의 흐름
    • HTTP 요청 → WAS → 필터 → 서블릿 → 컨트롤러
  • 필터 체인(여러개의 필터
    • HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러
  • 필터 인터페이스
    • 필터 인터페이스는 싱글톤 객체로 생성되고 관리된다.
    • 하단 인터페이스를 implements 하고, 메서드들을 @Override하여 구현해 사용한다.
    • public interface Filter { public default void init(FilterConfig filterConfig) throws ServletException{} public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException; public default void destroy() {} }
    • init()
      • 필터 초기화 작업
    • doFilter()
      • 필터가 걸릴 때마다 doFilter 메서드가 호출됨
      • doFilter안에 chain.doFilter(request, response); 가 있으면, 필터 체인으로 다음 필터로 이동한다.
    • destroy()
      • 서블릿 컨테이너가 종료될 때 호출됨
  • 필터 등록
    • 스프링 부트에서는 FilterRegistrationBean를 사용하여 필터 등록이 가능하다.
    • @Configuration public class WebConfig { @Bean public FilterRegistrationBean logFilter() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setFilter(new LogFilter()); // 등록할 필터 지정 filterRegistrationBean.setOrder(1); // 필터 체인의 순서 지정 filterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 return filterRegistrationBean; } }
 
 

스프링 인터셉터

notion image
  • 인터셉터의 흐름
    • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
    • 인터셉터는 스프링 MVC가 제공하는 기능이다.
  • 인터셉터 체인
    • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러
  • 인터셉터 인터페이스
    • preHandle
      • 컨트롤러 호출 전
    • postHandle
      • 컨트롤러 호출 후
    • afterCompletion
      • 컨트롤러 요청 완료 이후(View 랜더링 이후)
      • 예외가 발생해도 항상 호출된다
        • 예외와 무관하게 공통 처리를 할 경우 afterCompletion에서 처리한다.
        public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {} default void postHandle(HttpServletRequest request, HttpServletResponseresponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {} default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {} }
  • 인터페이스 등록
    • WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.
      • @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록 .order(1) // 인터셉터 호출 순서 .addPathPatterns("/**") // 패턴 처리 .excludePathPatterns("/css/**", "/*.ico", "/error"); } //... }
 
Share article

Tom의 TIL 정리방