SPRING/Spring MVC

[SpringMVC] 검증(Validation)(1) - HashMap

IT록흐 2023. 8. 22. 23:20
반응형

 

 

 

 

클라이언트로부터 요청(Request)이 들어오면, 요청 데이터는 가장 먼저 Controller에 도착한다. 그러므로 Controller에서 적합한 데이터인지 검사해야 한다. 적합하지 않은 데이터가 서버 내부로직으로 들어오는 것을 방지하기 위해서다. 

 

실무에서는 주로 Bean Validation을 사용한다. 

 

 

 

 

 

요청이 들어오면 요청에 맞는 DTO 객체에 요청데이터가 담긴다. DTO 필드에는 @NotNull, @NotEmpty 같은 어노테이션이 선언되어 있는데, BeanValidator는 선언되어 있는 어노테이션을 토대로 필드에 담긴 데이터를 검증한다. 그리고 검증결과를 Controller에 넘긴다. Controller는 검증결과에 따라 클라이언트에 적절히 응답하면 된다. 실무에서는 주로 이런 방식으로 요청데이터를 검증한다.

 

위 방식이 나오기 전까지 여러 변천과정을 겪었다. 그 과정을 이번 포스팅부터 하나씩 정리해볼까 한다. 

 

 

HashMap 

 

가장 원시적인 방법이다.

 

Controller로 들어온 요청데이터를 Controller가 직접 검사하는 것이다. 그리고 검증에 오류가 있으면 오류 내용을 HashMap 자료구조에 저장하고, 이를 Model에 넘겨 화면에 출력한다.

 

 

클라이언트는 상품명, 가격, 수량을 입력하여 서버로 전달한다. 요청에 매핑되는 컨트롤러는 요청으로 들어온 데이터를 검증한다. 이때 검증에 실패하면, 오류 내용을 HashMap에 담아 Model로 전달한다. 

 

 

@Slf4j
@Controller
@RequestMapping("/validation/v1/items")
@RequiredArgsConstructor
public class ValidationItemControllerV1 {

    private final ItemRepository itemRepository;

    @PostMapping("/add")
    public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes, Model model) {

        // 검증 오류 결과를 보관하는 HashMap
        Map<String,String> errors = new HashMap<>();

        // 상품명이 공백인 경우
        if(!StringUtils.hasText(item.getItemName())){ 
            errors.put("itemName","상품 이름은 필수입니다.");
        }
        
        // 가격이 허용범위 밖인 경우
        if(item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000){ 
            errors.put("price","가격은 1,000 ~ 1,000,000 까지 허용합니다. ");
        }
        
        // 수량이 허용범위 밖인 경우
        if(item.getQuantity() == null || item.getQuantity() >= 9999){ 
            errors.put("quantity", "수량은 최대 9,999까지 혀용합니다. ");
        }

        // 두 개 이상의 복합 룰 검증
        if(item.getPrice() != null && item.getQuantity() != null){
            int resultPrice = item.getPrice()*item.getQuantity();
            if(resultPrice < 10000){
                errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재값 = " + resultPrice);
            }
        }

        // 검증에 실패한 경우
        if(hasError(errors)){ // HashMap 자료구조에 오류내용이 담겨있는 경우
            log.info("errors={}",errors);
            model.addAttribute("errors",errors); // Model에 오류 데이터 담기
            return "validation/v1/addForm"; // VIEW로 반환
        }

        // 검증에 성공한 경우
        // 비즈니스 로직 수행
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
        return "redirect:/validation/v1/items/{itemId}";
    }

    private boolean hasError(Map<String, String> errors) {
        return !errors.isEmpty();
    }

}

 

요청으로 들어온 데이터는 @ModelAttribute가 선언된 객체에 담긴다. 객체에 담긴 데이터를 검증하기 위해, Controller에 HashMap을 만들고 if문 로직을 구현한다. 데이터 검증에서 오류를 발견하면 오류 내용을 HashMap에 담고 Model로 전달한다. 이 방식의 가장 큰 문제는 복잡함도 있겠지만, 만약 @ModelAttribute로 선언된 객체에 데이터가 담길때, 타입이 맞지 않는다면 오류를 핸들링 할 수 없다는 점이다.

 

 

아무런 데이터도 넣지 않고 저장 버튼을 클릭해보았다.  Model에 담긴 오류 데이터는 Thymeleaf로 전달되고 Thymeleaf는 화면에 오류 데이터를 동적으로 생성하여 HTML을 만든다. HTML은 클라이언트에 전달되어 위 사진처럼 화면에 그려진다. 

 

그럼 가격에 문자열을 넣어보자.

 

 

 

 

@ModelAttribute가 선언되어 있으면 요청데이터를 특정객체로 바인딩한다. 그런데 객체 필드와 데이터 사이의 타입이 맞지 않으면 Exception이 발생한다. 가격은 int형인데 String 데이터가 들어오는 것처럼 말이다.  이는 단순히 입력 실수이다. 그럼에도 Exception이 발생하며 페이지가 바뀌어 버리는 것이다.  이렇듯 HashMap은 Controller 내부의 유효성 검사 내용을 담을 수는 있지만, Controller 외부에서 발생하는 오류를 핸들링하지 못한다. Spring은 이런 단점을 해결하고자 BindingResult라는 객체를 제공한다. 

 

이는 다음 포스팅에서 다루어 보겠다. 

 

 

 


 

 

 

참고자료

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

 

 

 

반응형