4. 검증 1 - Validation

2024. 4. 29. 16:37·Spring/MVC 2
목차
  1. 1. 과거 프로젝트 build.gradle 오류
  2. 2. 검증 요구사항
  3. 3. 서버 검증 과정
  4. 4. 검증 V1
  5. 4-1. 검증 V2 [1]
  6. 4-2. 검증 V2 [2]
  7. 4-3. 검증 V2 [3]
  8. 4-4. 검증 V2 [4]
  9. 4-5. 검증 V2 [5]
  10. 4-6. 검증 V2 [6]

1. 과거 프로젝트 build.gradle 오류

1) jdk 17 버전 수정

https://www.inflearn.com/questions/1232895/no-matching-variant-%EC%98%A4%EB%A5%98

 

 

2) gradle 버전 수정

[gradle-wrapper.properties]

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

 

 


 

2. 검증 요구사항

[상품등록]

1) 타입 검증

      → 가격, 수량에 문자가 들어가면 검증 오류 처리 필드 검증

 

2) 필드 검증

      → 상품명: 필수, 공백X
      → 가격: 1000원 이상, 1백만원 이하 수량: 최대 9999

      → 특정 필드의 범위를 넘어서는 검증

 

3) 복합 검증
      → 가격 
* 수량의 합은 10,000원 이상

 

 


 

3. 서버 검증 과정

클라이언트에서는 사용자의 입력을 사전에 검증하여 사용자 경험을 향상시킬 수 있지만,

최종적으로 보안과 데이터 무결성을 위해서 서버 검증은 필수적이다.

 

API의 경우에는, API 스펙을 잘 정의해서 검증 오류를 API 응답 결과에 남겨주어야 한다.

 

상품 저장 검증 : 성공 로직(좌) / 실패 로직(우)

 

 


 

4. 검증 V1

1) Map<String, String> errors = new HashMap<>();

검증 시 오류가 발생하면, 어떤 검증에서 오류가 발생했는지 정보를 담아둔다.

이 때, 어떤 필드에서 오류가 발생했는지 구분하기 위해 오류가 발생한 필드명을 Key로 사용한다.

 

2) StringUtils.hasText()

상품 이름이 입력되었는지 확인한다.

 

 

남은 문제점.

- Type 오류 처리가 안 된다.

      → Spring MVC에서 컨트롤러에 진입하기도 전에 예외가 발생하기 때문에, 400 예외가 발생하면서 오류 페이지가 나타난다.

 

- 사용자가 입력한 문자가 화면에 남지 않는다.

      → 고객이 입력한 값도 별도로 관리가 되어야 한다.

 

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

    // 검증 오류 결과를 보관
    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)) {
        log.info("errors = {} ", errors); // 로그
        model.addAttribute("errors", errors);
        return "validation/v1/addForm";
    }

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

 

 


 

4-1. 검증 V2 [1]

- 오류 정보( `FieldError` )를 `BindingResult` 에 담아서 컨트롤러를 정상 호출한다.

 

1) BindingResult

검증 시 오류가 발생하면, 어떤 검증에서 오류가 발생했는지 정보를 담아둔다. (V1, HashMap 대체)

 

BindingResult bindingResult 파라미터 위치는 @ModelAttribute Item item 다음에 와야 한다.

public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model)

 

 

2) 필드 오류 - FieldError

: 필드에 오류가 있으면 FieldError 객체를 생성해서, bindingResult에 담으면 된다.

 

[FieldError 생성자]

- objectName : @ModelAttribute 이름

- field : 오류가 발생한 필드 이름

- defaultMessage : 오류 기본 메시지

public FieldError(String objectName, String field, String defaultMessage) {}
// 필드 오류 → new FieldError()
if (!StringUtils.hasText(item.getItemName())) {
    bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}

 

 

 

3) 복합 오류 - ObjectError

: 특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서, bindingResult에 담으면 된다.

 

[ObjectError 생성자]

- objectName : @ModelAttribute 이름

- defaultMessage : 오류 기본 메시지

// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10000) {
        bindingResult.addError(new ObjectError("item", "가격*수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
    }
}

 

 


 

4-2. 검증 V2 [2]

- FiledError에 더 많은 정보를 정의함으써, 사용자가 입력한 내용이 사라지지 않게 한다.

 

1) 필드 오류 - FieldError

: 필드에 오류가 있으면 FieldError 객체를 생성해서, bindingResult에 담으면 된다.

 

[FieldError 또 다른 생성자]

- objectName : @ModelAttribute 이름

- field : 오류가 발생한 필드 이름

- rejectedValue : 사용자가 입력한 값(거절된 값)

- bindingFailure : 타입 오류 같은 바인딩 실패인지, 검증 실패인지 구분 값

- codes : 메시지 코드

- arguments : 메시지에서 사용하는 인자

- defaultMessage : 오류 기본 메시지

public FieldError(String objectName, String field, String defaultMessage) {}
// 필드 오류 → new FieldError()
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
    bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, null, null, "가격은 1,000 ~ 1,000,000 까지 허용합니다."));
}

 

 

 

3) 복합 오류 - ObjectError

: 특정 필드를 넘어서는 오류가 있으면 ObjectError 객체를 생성해서, bindingResult에 담으면 된다.

 

[ObjectError 또 다른 생성자]

- objectName : @ModelAttribute 이름

- codes : 메시지 코드

- arguments : 메시지에서 사용하는 인자

- defaultMessage : 오류 기본 메시지

// 복합 오류 → new ObjectError()
if (item.getPrice() != null & item.getQuantity() != null) {
    int resultPrice = item.getPrice() * item.getQuantity();
    if (resultPrice < 10000) {
        bindingResult.addError(new ObjectError("item", null, null, "가격*수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
    }
}

 

 


 

4-3. 검증 V2 [3]

codes에 String[]을 넣어, resorce/errors.properties과 바인딩 될 수 있도록 메시지화 한다.

 

→ codes(메시지 코드)에 있는 값과 errors.properties 파일의 키 값과 맞는 value를 오류 메시지로 리턴한다.

required.item.itemName = 상품 이름은 필수입니다.
range.item.price = 가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity = 수량은 최대 {0} 까지 허용합니다.
totalPriceMin = 가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

    // 검증 오류 결과를 보관 → BindingResult

    // 필드 오류 → new FieldError()
    if (!StringUtils.hasText(item.getItemName())) {
        bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.addError(new FieldError("item", "price", item.getPrice(), false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));
    }

    if (item.getQuantity() == null || item.getQuantity() >= 9999) {
        bindingResult.addError(new FieldError("item", "quantity", item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]{9999}, null));
    }

    // 복합 오류 → new ObjectError()
    if (item.getPrice() != null & item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.addError(new ObjectError("item", new String[]{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));
        }
    }

    ...
}

 

 


 

4-4. 검증 V2 [4]

BindingResult 메소드인 rejectValue로 FieldError / ObjectError 생성 없이, 메시지 호출을 단순화하여 깔끔하게 검증 실행

 

[rejectValue 매개변수]

- field : 오류가 발생한 필드 이름

- errorCode : 루트 오류 코드(messageResolver 위함)

   → errorCode.target_object.field 메시지 코드를 탐색함

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

    // 검증 오류 결과를 보관 → BindingResult

    // 필드 오류 → new FieldError()
    if (!StringUtils.hasText(item.getItemName())) {
        // bindingResult.addError(new FieldError("item", "itemName", item.getItemName(), false, new String[]{"required.item.itemName"}, null, null));
        bindingResult.rejectValue("itemName", "required");
    }

    if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
        bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
    }

    if (item.getQuantity() == null || item.getQuantity() >= 9999) {
        bindingResult.rejectValue("quantity,", "max", new Object[]{9999}, null);
    }

    // 복합 오류 → new ObjectError()
    if (item.getPrice() != null & item.getQuantity() != null) {
        int resultPrice = item.getPrice() * item.getQuantity();
        if (resultPrice < 10000) {
            bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
        }
    }

    ...
}

 

 


 

4-5. 검증 V2 [5]

컨트롤러와 검증 로직을 ItemValidator로 따로 분리하여 실행한다.

public class ValidationItemControllerV2 {

    private final ItemValidator itemValidator;

    @InitBinder
    public void init(WebDataBinder dataBinder) {
        dataBinder.addValidators(itemValidator);
    }

    @PostMapping("/add")
    public String addItemV5(@ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {
        
        itemValidator.validate(item, bindingResult);

        ...
    }
}

 

 

 


 

4-6. 검증 V2 [6]

별도의 검증 로직 호출 없이, @Validate 어노테이션으로 검증 로직 호출 자동화

// @Validated: 자동으로 검증기가 실행이 되어, 에러는 BindingResult에 담겨진다.
@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes, Model model) {

    // 검증에 실패하면 다시 입력 폼으로
    // BindingResult는 Spring에서 자동으로 View에 넘어가므로, model에 담지 않아도 된다.
    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}";
}

 

저작자표시 (새창열림)

'Spring > MVC 2' 카테고리의 다른 글

6. 로그인 처리1 - 쿠키, 세션  (1) 2024.05.02
5. 검증 2 - Bean Validation  (0) 2024.05.01
3. 메시지, 국제화  (0) 2024.04.29
2. 타임리프 - 스프링 통합과 폼  (0) 2024.04.29
1. 타임리프 - 기본 기능  (0) 2024.04.28
  1. 1. 과거 프로젝트 build.gradle 오류
  2. 2. 검증 요구사항
  3. 3. 서버 검증 과정
  4. 4. 검증 V1
  5. 4-1. 검증 V2 [1]
  6. 4-2. 검증 V2 [2]
  7. 4-3. 검증 V2 [3]
  8. 4-4. 검증 V2 [4]
  9. 4-5. 검증 V2 [5]
  10. 4-6. 검증 V2 [6]
'Spring/MVC 2' 카테고리의 다른 글
  • 6. 로그인 처리1 - 쿠키, 세션
  • 5. 검증 2 - Bean Validation
  • 3. 메시지, 국제화
  • 2. 타임리프 - 스프링 통합과 폼
wch_t
wch_t
끄적끄적(TIL)wch_t 님의 블로그입니다.
  • wch_t
    끄적끄적(TIL)
    wch_t
  • 글쓰기 관리
  • 전체
    오늘
    어제
    • 분류 전체보기 (170)
      • Architecture (0)
      • Algorithm (67)
        • Math (5)
        • Simulation (1)
        • Data Structure (4)
        • DP (7)
        • Brute Fource (10)
        • Binary Search (6)
        • Greedy (2)
        • Graph (11)
        • Mst (1)
        • Shortest path (10)
        • Two Pointer (1)
        • Tsp (3)
        • Union Find (2)
        • Mitm (1)
      • CS (2)
        • 데이터베이스 (5)
        • 네트워크 (5)
      • DB (6)
      • DevOps (17)
        • AWS (9)
        • Docker (1)
        • CI-CD (5)
      • Error (1)
      • Project (0)
        • kotrip (0)
      • Spring (59)
        • 끄적끄적 (5)
        • 기본 (9)
        • MVC 1 (7)
        • MVC 2 (11)
        • ORM (8)
        • JPA 1 (7)
        • JPA 2 (5)
        • Spring Data Jpa (7)
      • Test (2)
      • TIL (5)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    백준 17289 파이썬
    docker
    Sxssf
    Merge
    response_mode
    docker: not found
    spring-cloud-starter-aws-secrets-manager-config
    백준 3015 파이썬
    scope
    aws secrets manager
    백준 17299 파이썬
    form_post
    애플
    Jenkins
    TempTable
    spring-cloud-starter-bootstrap
    apache poi
    view algorithm
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.3
wch_t
4. 검증 1 - Validation
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.