๐Ÿ’ก ๊ฒŒ์‹œํŒ - ๊ฒ€์ฆ(Bean Validation)

๋ฐ•์ƒ๋ฏผยท2023๋…„ 9์›” 23์ผ
0

๋‚˜์˜ ์ž‘์€ ํ”„๋กœ์ ํŠธ

๋ชฉ๋ก ๋ณด๊ธฐ
10/10

โœ๏ธ ๊ฒ€์ฆ1 - Validation์—์„œ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ์†Œ๊ฐœํ–ˆ๋‹ค.(์™œ์ธ์ง€ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ ๊ธ€์ด ๋‚ ๋ผ๊ฐ”๋‹ค.. ์ถ”ํ›„์— ๋‹ค์‹œ ์ž‘์„ฑํ•ด์•ผํ•  ๊ฒƒ ๊ฐ™๋‹ค.)
๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ์ง€๋‚œ ๊ธ€์—์„œ ์†Œ๊ฐœํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ๋งค๋ฒˆ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์€ ์ƒ๋‹นํžˆ ๋ฒˆ๊ฒ๋กญ๋‹ค. ํŠนํžˆ ํŠน์ • ํ•„๋“œ์— ๋Œ€ํ•œ ๊ฒ€์ฆ ๋กœ์ง์€ ๋Œ€๋ถ€๋ถ„ ๋นˆ ๊ฐ’์ธ์ง€ ์•„๋‹Œ์ง€, ํŠน์ • ํฌ๊ธฐ๋ฅผ ๋„˜๋Š”์ง€ ์•„๋‹Œ์ง€์™€ ๊ฐ™์ด ๋งค์šฐ ์ผ๋ฐ˜์ ์ธ ๋กœ์ง์ด๋‹ค.

@Entity
@Data
public class Board extends TimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotBlank(message = "๊ณต๋ฐฑx", groups = {SaveCheck.class, UpdateCheck.class})
    @Size(min=2, max=40, groups = {SaveCheck.class, UpdateCheck.class})
    private String title;


    private String content;

    @NotBlank(groups = {SaveCheck.class})
    @Size(max=10, groups = {SaveCheck.class})
    private String author;

    private String filename; //์‹œ์šฉ์ž๊ฐ€ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ ์ด๋ฆ„
    //...
}

์ด๋Ÿฐ ๊ฒ€์ฆ ๋กœ์ง์„ ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ณตํ†ตํ™”ํ•˜๊ณ , ํ‘œ์ค€ํ™” ํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ Bean Validation์ด๋‹ค.
Bean Validation์„ ์ž˜ ํ™œ์šฉํ•˜๋ฉด, ์• ๋…ธํ…Œ์ด์…˜ ํ•˜๋‚˜๋กœ ๊ฒ€์ฆ ๋กœ์ง์„ ๋งค์šฐ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์˜์กด๊ด€๊ณ„ ์ถ”๊ฐ€
build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

Bean Validation์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ์œ„์˜ ์˜์กด๊ด€๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ

public class BeanValidationTest {

    @Test
    void beanValidation() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Board board = new Board();  
        board.setTitle(" "); //๊ณต๋ฐฑ
        board.setAuthor(" ");
        board.setContent("ํ…Œ์ŠคํŠธ");

        Set<ConstraintViolation<Board>> violations = validator.validate(board);
        for (ConstraintViolation<Board> violation : violations) {
            System.out.println("violation = " + violation);
            System.out.println("violation.getMessage() = " + violation.getMessage());
        }
    }
}

์‹คํ–‰ ๊ฒฐ๊ณผ

violation = ConstraintViolationImpl{interpolatedMessage='๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค', propertyPath=title, rootBeanClass=class com.studyweb.webboard.service.domain.Board, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation.getMessage() = ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค
violation = ConstraintViolationImpl{interpolatedMessage='๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค', propertyPath=author, rootBeanClass=class com.studyweb.webboard.service.domain.Board, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation.getMessage() = ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค

์ถœ๋ ฅ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ฉด, ๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฐ์ฒด, ํ•„๋“œ, ๋ฉ”์‹œ์ง€ ์ •๋ณด ๋“ฑ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€ Bean Validation์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ดค๋‹ค. ์ด์ œ ๊ฒŒ์‹œํŒ์— ์ ์šฉ์„ ํ•ด๋ณด์ž.

๊ฒŒ์‹œํŒ์— Bean Validation ์ ์šฉ

Board

package com.studyweb.webboard.service.domain.board;

import com.studyweb.webboard.service.time.TimeEntity;
import lombok.Data;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
@Data
public class Board extends TimeEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @NotBlank(message = "๊ณต๋ฐฑx", groups = {SaveCheck.class, UpdateCheck.class})
    @Size(min=2, max=40, groups = {SaveCheck.class, UpdateCheck.class})
    private String title;


    private String content;

    @NotBlank(groups = {SaveCheck.class})
    @Size(max=10, groups = {SaveCheck.class})
    private String author;
    
    private String filename; //์‹œ์šฉ์ž๊ฐ€ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ ์ด๋ฆ„
    private String filepath; // DB์—  ์ €์žฅ๋˜๋Š” ํŒŒ์ผ ์ด๋ฆ„(UUID ์ถ”๊ฐ€)

    public void update(String title, String content) {
        this.title = title;
        this.content = content;
    }

    //๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์ถ”๊ฐ€!
    public Board() {

    }
}

Entity ํด๋ž˜์Šค์ธ Board ํด๋ž˜์Šค์ด๋‹ค. title์— @Notblank๋ฅผ ์ ์šฉํ•ด์„œ ๊ณต๋ฐฑ์ด ์ž…๋ ฅ๋˜์ง€ ์•Š๋„๋ก ํ–ˆ๋‹ค. ์ด๋•Œ, message๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ๋‚˜ํƒ€๋‚ด์ค„ ๋ฉ”์‹œ์ง€๋ฅผ ์ ์–ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.
๋งˆ์ฐฌ๊ฐ€์ง€๋กœ author์—๋„ Bean Validation์„ ์ ์šฉํ–ˆ๋‹ค.

groups์— ๋Œ€ํ•ด์„œ๋Š” ๋’ค์—์„œ ์†Œ๊ฐœํ•˜๊ฒ ๋‹ค.

BoardController

 @PostMapping("/board/write")
public String boardWrite(@Validated(SaveCheck.class) @ModelAttribute Board board, BindingResult bindingResult, Model model, MultipartFile file) throws Exception {
    if (bindingResult.hasErrors()) {
        log.info("errors={}", bindingResult);
        return "boardWrite";
    }

    //์„ฑ๊ณต ๋กœ์ง
    boardService.save(board, file);
    Integer id = board.getId();

    model.addAttribute("message", "๊ธ€ ์ž‘์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
    model.addAttribute("searchUrl", "/board/post/" + id);

    return "message";
}

BoardController ์ค‘ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ๋ฉ”์„œ๋“œ์ด๋‹ค.
Bean Validation์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” @ModelAttribute๊ณผ ํ•จ๊ป˜ @Validated๋ฅผ ์ž‘์„ฑํ•ด์•ผํ•œ๋‹ค.

@Valid, @Validated๋Š” ์œ ์‚ฌํ•œ ๊ธฐ๋Šฅ์„ ํ•˜๊ธฐ์— ๋‘˜ ์ค‘ ํ•˜๋‚˜๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ @Validated๊ฐ€ ๋’ค์—์„œ ์‚ฌ์šฉํ•  groups๋ฅผ ์ง€์›ํ•˜๊ธฐ์— @Validated๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.

boardWrite

 <div class="form-group">
     <label for="title" th:text="#{label.title}">์ œ๋ชฉ</label>
     <input type="text" id="title" th:field="*{title}" th:errorclass="field-error" placeholder="์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”.">
     <div class="field-error" th:errors="*{title}">
          ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ ์˜ค๋ฅ˜
     </div>
 </div>

๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑํผ ์ค‘ ์ œ๋ชฉ ๋ถ€๋ถ„๋งŒ ๊ฐ€์ ธ์™”๋‹ค.
th:errorclass="field-error"

  • field์— ๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ํฌํ•จ๋˜์„œ ์˜จ๋‹ค๋ฉด class๋ฅผ "field-error"๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

๊ฒ€์ฆ ์˜ค๋ฅ˜๊ฐ€ ๋„˜์–ด์˜จ๋‹ค๋ฉด th:errors="*{title}"๋กœ ์ธํ•ด ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋„˜์–ด์˜ค๊ณ  ์ถœ๋ ฅ๋œ๋‹ค.

์ด๋•Œ error.properties๋ฅผ ์ด์šฉํ•ด์„œ ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

Bean Validation - ์—๋Ÿฌ ์ฝ”๋“œ

Bean Validation์ด ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ข€ ๋” ์ž์„ธํžˆ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ๋ ๊นŒ?
Bean Validation์„ ์ ์šฉํ•˜๊ณ  bindResult์— ๋“ฑ๋ก๋œ ๊ฒ€์ฆ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๋ณด์ž. ์˜ค๋ฅ˜ ์ฝ”๋“œ๊ฐ€ ์• ๋…ธํ…Œ์ด์…˜ ์ด๋ฆ„์œผ๋กœ ๋“ฑ๋ก๋œ๋‹ค. ๋งˆ์น˜ typeMismatch์™€ ์œ ์‚ฌํ•˜๋‹ค.

์•„๋ž˜๋Š” NotBlank๋ผ๋Š” ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ MessageCodesResolver๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ๋ฉ”์‹œ์ง€ ์ฝ”๋“œ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ์ƒ์„ฑ๋œ๊ฒƒ์ด๋‹ค.

[NotBlank.board.title, NotBlank.title, 
NotBlank.java.lang.String, NotBlank];

์•„๋ž˜๋Š” ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๊ฒ€์ฆ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์ด๋‹ค.

#์ถ”๊ฐ€
typeMismatch.java.lang.String=๋ฌธ์ž๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.
typeMismatch=ํƒ€์ž… ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค.

#Bean Validation ์ถ”๊ฐ€
NotBlank.board.title=๊ฒŒ์‹œ๊ธ€์˜ ์ œ๋ชฉ์„ ์ ์–ด์ฃผ์„ธ์š”.
NotBlank.board.author=์ž‘์„ฑ์ž ์ด๋ฆ„์„ ์ ์–ด์ฃผ์„ธ์š”. 
NotBlank={0} ๊ณต๋ฐฑX
Size.board.title=๊ฒŒ์‹œ๊ธ€์˜ ์ œ๋ชฉ์€ 2~40๊ธ€์ž๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
Size.board.author=์ž‘์„ฑ์ž ์ด๋ฆ„์€ 10์ž ์ดํ•˜๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
Size = {0} ๊ธ€์ž์ˆ˜ ์ œํ•œ

์ด๋•Œ ๋ฉ”์‹œ์ง€์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ.
์œ„์—์„œ NotBlank ๋ณด๋‹ค NotBlank.board.title๊ฐ€ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’๋‹ค. ๋” ๊ตฌ์ฒด์ ์ผ์ˆ˜๋ก ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’๋‹ค.


์ด์ œ ํ•œ ๋ฒˆ ์‹คํ–‰ํ•ด์„œ ํ™•์ธํ•ด๋ณด์ž

๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํŽ˜์ด์ง€์—์„œ ์ œ๋ชฉ๊ณผ ์ž‘์„ฑ์ž๋ฅผ ๊ณต๋ฐฑ์œผ๋กœ ํ•˜๊ณ  '๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ' ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €๋‹ค. 'error.properties'์— ์ €์žฅํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋นจ๊ฐ„ ๊ธ€์”จ๋กœ ๋‚˜ํƒ€๋‚œ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.


๋” ์ž์„ธํ•œ ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!
Github: https://github.com/pp8817/ToyProjectBoard


์ถœ์ฒ˜
(๊ฐ•์˜)์Šคํ”„๋ง MVC 2ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ 

profile
์Šคํ”„๋ง ๋ฐฑ์—”๋“œ๋ฅผ ๊ณต๋ถ€์ค‘์ธ ๋Œ€ํ•™์ƒ์ž…๋‹ˆ๋‹ค!

0๊ฐœ์˜ ๋Œ“๊ธ€