> For the complete documentation index, see [llms.txt](https://team-everywhere.gitbook.io/backend/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://team-everywhere.gitbook.io/backend/spring/spring-boot/12-bean-validation.md).

# 12 Bean Validation(빈검증)

p210 - build.gradle

```gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.3'
	id 'io.spring.dependency-management' version '1.1.6'
}

group = 'spring'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
	useJUnitPlatform()
}

```

p211 - src/main/java/spring/mvc/domain/Book

```java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    // 인덱스 번호
    private Long bookIdx;
    // 도서 이름
    @NotBlank(groups = {BookCreate.class})
    private String title;
    // 출판사
    @NotBlank(groups = {BookCreate.class})
    private String publisher;
    // 판매 가격
    @NotNull(groups = {BookCreate.class, BookUpdate.class})
    @Min(0)
    private int salePrice;
    // 대여 비용
    private int rentalPrice;
    // 대여자
    private String renter;
    // 등록일자
    private LocalDate registrationDate;
    // 수정일자
    private LocalDate updateDate;
}


```

p212 - src/test/java/spring/mvc/domain/BookBeanTest

```java
class BookBeanTest {

    private Validator validator;

    @BeforeEach
    void setUp() {
        // Validator를 초기화합니다.
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }

    @Test
    void testValidBookBean() {
        // 모든 필드가 올바른 경우
        Book book = new Book(1L, "Effective Java", "Addison-Wesley", 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertTrue(violations.isEmpty(), "유효한 Book 객체여야 합니다.");
    }

    @Test
    void testInvalidBookBean_NullBookIdx() {
        // bookIdx가 null인 경우
        Book book = new Book(null, "Effective Java", "Addison-Wesley", 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "bookIdx가 null일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("bookIdx", violation.getPropertyPath().toString(), "오류가 발생한 필드는 bookIdx이어야 합니다.");
    }

    @Test
    void testInvalidBookBean_NullTitle() {
        // title이 null인 경우
        Book book = new Book(1L, null, "Addison-Wesley", 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "title이 null일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("title", violation.getPropertyPath().toString(), "오류가 발생한 필드는 title이어야 합니다.");
    }

    @Test
    void testInvalidBookBean_BlankTitle() {
        // title이 공백인 경우
        Book book = new Book(1L, "   ", "Addison-Wesley", 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "title이 공백일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("title", violation.getPropertyPath().toString(), "오류가 발생한 필드는 title이어야 합니다.");
    }

    @Test
    void testInvalidBookBean_NullPublisher() {
        // publisher가 null인 경우
        Book book = new Book(1L, "Effective Java", null, 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "publisher가 null일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("publisher", violation.getPropertyPath().toString(), "오류가 발생한 필드는 publisher이어야 합니다.");
    }

    @Test
    void testInvalidBookBean_BlankPublisher() {
        // publisher가 공백인 경우
        Book book = new Book(1L, "Effective Java", "   ", 50000, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "publisher가 공백일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("publisher", violation.getPropertyPath().toString(), "오류가 발생한 필드는 publisher이어야 합니다.");
    }

    @Test
    void testInvalidBookBean_NegativeSalePrice() {
        // salePrice가 음수인 경우
        Book book = new Book(1L, "Effective Java", "Addison-Wesley", -100, 3000, "John Doe", LocalDate.now(), LocalDate.now());

        Set<ConstraintViolation<Book>> violations = validator.validate(book);
        assertEquals(1, violations.size(), "salePrice가 음수일 때 검증 오류가 발생해야 합니다.");

        ConstraintViolation<Book> violation = violations.iterator().next();
        assertEquals("salePrice", violation.getPropertyPath().toString(), "오류가 발생한 필드는 salePrice이어야 합니다.");
    }
}

```

p213 - src/main/resources/templates/validation/bookBean.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>책 관리 폼</title>
</head>
<body>
<h1>책 관리</h1>

<!-- 에러 메시지 표시 -->
<div th:if="${error}" style="color: red;">
  <p th:text="${error}"></p>
</div>

<!-- 성공 메시지 표시 -->
<div th:if="${success}" style="color: green;">
  <p th:text="${success}"></p>
</div>

<form th:action="@{/validation/bean/books}" th:object="${book}" method="post">
  <label for="title">책 제목:</label>
  <input type="text" th:field="*{title}" id="title"><br>
  <div th:if="${#fields.hasErrors('title')}" style="color: red;">
    <p th:errors="*{title}"></p>
  </div>

  <label for="publisher">저자:</label>
  <input type="text" th:field="*{publisher}" id="publisher"><br>
  <div th:if="${#fields.hasErrors('publisher')}" style="color: red;">
    <p th:errors="*{publisher}"></p>
  </div>

  <label for="salePrice">가격:</label>
  <input type="text" th:field="*{salePrice}" id="salePrice"><br>
  <div th:if="${#fields.hasErrors('salePrice')}" style="color: red;">
    <p th:errors="*{salePrice}"></p>
  </div>

  <button type="submit">제출</button>
</form>
</body>
</html>

```

p213

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>책 상세 정보</title>
</head>
<body>
<h1>책 상세 정보</h1>

<!-- 에러 메시지 표시 -->
<div th:if="${error}" style="color: red;">
  <p th:text="${error}"></p>
</div>

<!-- 성공 메시지 표시 -->
<div th:if="${success}" style="color: green;">
  <p th:text="${success}"></p>
</div>

<!-- 책 상세 정보 표시 -->
<div th:if="${book}">
  <p><strong>책 제목:</strong> <span th:text="${book.title}"></span></p>
  <p><strong>저자:</strong> <span th:text="${book.publisher}"></span></p>
  <p><strong>가격:</strong> <span th:text="${book.salePrice}"></span></p>
</div>

<!-- 책 수정 폼 -->
<h2>책 정보 수정</h2>
<div th:if="${book}">
  <form th:action="@{/validation/bean/books/{bookIdx}/update(bookIdx=${book.bookIdx})}" th:object="${book}" method="post">
    <label for="title">책 제목:</label>
    <input type="text" id="title" name="title" th:value="*{title}"><br>

    <label for="publisher">저자:</label>
    <input type="text" id="publisher" name="publisher" th:value="*{publisher}"><br>

    <label for="salePrice">가격:</label>
    <input type="number" id="salePrice" name="salePrice" th:value="*{salePrice}"><br>

    <button type="submit">수정</button>
  </form>
</div>

</body>
</html>

```

p214,215 - src/main/java/spring/mvc/controller/ValidationBeanController

```java
@Slf4j
@Controller
@RequestMapping("/validation/bean")
public class ValidationBeanController {

    private final BookService bookService;

    public ValidationBeanController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/books")
    public String showBookForm(Model model) {
        model.addAttribute("book", new Book());  // 빈 Book 객체를 모델에 추가
        return "validation/bookBean";
    }

    // CREATE: 책 생성 - BindingResult 사용
    @PostMapping("/books")
    public String createBook(@Validated(BookCreate.class) @ModelAttribute Book book, BindingResult bindingResult, Model model) {
        // 검증 실패 시, 다시 폼 페이지로 이동
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "validation/bookBean";
        }

        // 검증이 통과된 경우 책 생성
        Book createdBook = bookService.createBook(book);
        model.addAttribute("book", createdBook);
        model.addAttribute("success", "책이 성공적으로 생성되었습니다!");
        return "validation/bookBean";
    }

    // READ: ID로 책 검색 - 수동 검증
    @GetMapping("/books/{bookIdx}")
    public String getBookById(@Validated @PathVariable Long bookIdx, Model model) {
        if (bookIdx <= 0) {
            model.addAttribute("error", "책 ID는 0보다 큰 값이어야 합니다.");
            return "validation/bookDetailBean";
        }

        Optional<Book> book = bookService.getBookById(bookIdx);
        if (book.isPresent()) {
            model.addAttribute("book", book.get());
        } else {
            model.addAttribute("error", "책을 찾을 수 없습니다.");
        }
        return "validation/bookDetailBean";
    }

    // UPDATE: 책 정보 수정 - BindingResult 사용
    @PostMapping("/books/{bookIdx}/update")
    public String updateBook(@PathVariable Long bookIdx, @Validated(BookUpdate.class) @ModelAttribute Book updatedBook, BindingResult bindingResult, Model model) {
        // 검증 실패 시, 다시 폼 페이지로 이동
        if (bindingResult.hasErrors()) {
            return "validation/bookBean";
        }
        Optional<Book> book = bookService.updateBook(bookIdx, updatedBook);
        if (book.isPresent()) {
            model.addAttribute("book", book.get());
            model.addAttribute("success", "책 정보가 성공적으로 수정되었습니다!");
        } else {
            model.addAttribute("error", "책을 찾을 수 없습니다.");
        }
        return "validation/bookBean";
    }
}
```

p217, 219 - src/main/resources/messages.properties

```properties
typeMismatch=올바른 형식의 값을 입력해주세요.
typeMismatch.int=숫자를 입력해주세요.
NotBlank.book.title = 책 제목은 공백일 수 없습니다.
NotBlank={0} 공백일 수 없습니다
Min={0}, 최소 {1}
```

p218, 219 - src/main/resources/templates/validation/bookFormBinding.html

```html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>책 관리 폼</title>
</head>
<body>
<h1>책 관리</h1>

<!-- 에러 메시지 표시 -->
<div th:if="${error}" style="color: red;">
  <p th:text="${error}"></p>
</div>

<!-- 성공 메시지 표시 -->
<div th:if="${success}" style="color: green;">
  <p th:text="${success}"></p>
</div>

<form th:action="@{/validation/form/binding/books}" th:object="${book}" method="post">
  <label for="title">책 제목:</label>
  <input type="text" th:field="*{title}" id="title"><br>
  <div th:if="${#fields.hasErrors('title')}" style="color: red;">
    <p th:errors="*{title}"></p>
  </div>

  <label for="publisher">저자:</label>
  <input type="text" th:field="*{publisher}" id="publisher"><br>
  <div th:if="${#fields.hasErrors('publisher')}" style="color: red;">
    <p th:errors="*{publisher}"></p>
  </div>

  <label for="salePrice">가격:</label>
  <input type="text" th:field="*{salePrice}" id="salePrice"><br>
  <div th:if="${#fields.hasErrors('salePrice')}" style="color: red;">
    <p th:errors="*{salePrice}"></p>
  </div>

  <button type="submit">제출</button>
</form>
</body>
</html>

```

p221, 222 - src/main/java/spring/mvc/domain/BookCreate, BookUpdate

```java
package spring.mvc.domain;

public interface BookCreate {
}

```

```
package spring.mvc.domain;

public interface BookUpdate {
}

```

p221, 222 - src/main/java/mvc/domain/Book

```java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Book {
    // 인덱스 번호
    private Long bookIdx;
    // 도서 이름
    @NotBlank(groups = {BookCreate.class})
    private String title;
    // 출판사
    @NotBlank(groups = {BookCreate.class})
    private String publisher;
    // 판매 가격
    @NotNull(groups = {BookCreate.class, BookUpdate.class})
    @Min(0)
    private int salePrice;
    // 대여 비용
    private int rentalPrice;
    // 대여자
    private String renter;
    // 등록일자
    private LocalDate registrationDate;
    // 수정일자
    private LocalDate updateDate;
}


```

p223 - src/main/java/mvc/dto/BookCreateForm, BookUpdateForm

```java
@Data
public class BookCreateForm {
    // 도서 이름
    @NotBlank
    private String title;
    // 출판사
    @NotBlank
    private String publisher;
    // 판매 가격
    @NotNull
    @Min(0)
    private int salePrice;
}

```

```java
@Data
public class BookUpdateForm {
    // 도서 이름
    @NotBlank
    private String title;
    // 출판사
    @NotBlank
    private String publisher;
}

```

p224, 225 - src/main/java/mvc/controller/ValidationBeanFormSeperateController

```java
@Slf4j
@Controller
@RequestMapping("/validation/bean/seperate")
public class ValidationBeanFormSeperateController {

    private final BookService bookService;

    public ValidationBeanFormSeperateController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping("/books")
    public String showBookForm(Model model) {
        model.addAttribute("book", new Book());  // 빈 Book 객체를 모델에 추가
        return "validation/bookBean";
    }

    // CREATE: 책 생성 - BindingResult 사용
    @PostMapping("/books")
    public String createBook(@Validated @ModelAttribute("book") BookCreateForm book, BindingResult bindingResult, Model model) {
        // 검증 실패 시, 다시 폼 페이지로 이동
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "validation/bookBean";
        }

        Book newBook = new Book();
        newBook.setTitle(book.getTitle());
        newBook.setPublisher(book.getPublisher());
        newBook.setSalePrice(book.getSalePrice());

        // 검증이 통과된 경우 책 생성
        Book createdBook = bookService.createBook(newBook);
        model.addAttribute("book", createdBook);
        model.addAttribute("success", "책이 성공적으로 생성되었습니다!");
        return "validation/bookBean";
    }

    // READ: ID로 책 검색 - 수동 검증
    @GetMapping("/books/{bookIdx}")
    public String getBookById(@Validated @PathVariable Long bookIdx, Model model) {
        if (bookIdx <= 0) {
            model.addAttribute("error", "책 ID는 0보다 큰 값이어야 합니다.");
            return "validation/bookDetailBean";
        }

        Optional<Book> book = bookService.getBookById(bookIdx);
        if (book.isPresent()) {
            model.addAttribute("book", book.get());
        } else {
            model.addAttribute("error", "책을 찾을 수 없습니다.");
        }
        return "validation/bookDetailBean";
    }

    // UPDATE: 책 정보 수정 - BindingResult 사용
    @PostMapping("/books/{bookIdx}/update")
    public String updateBook(@PathVariable Long bookIdx, @Validated @ModelAttribute("book") BookUpdateForm bookUpdateForm, BindingResult bindingResult, Model model) {
        // 검증 실패 시, 다시 폼 페이지로 이동
        if (bindingResult.hasErrors()) {
            return "validation/bookBean";
        }

        // 기존 책 정보 가져오기
        Optional<Book> existingBook = bookService.getBookById(bookIdx);
        if (existingBook.isEmpty()) {
            model.addAttribute("error", "책을 찾을 수 없습니다.");
            return "validation/bookBean";
        }

        // 기존 책 정보 업데이트
        Book bookToUpdate = existingBook.get();
        bookToUpdate.setTitle(bookUpdateForm.getTitle());
        bookToUpdate.setPublisher(bookUpdateForm.getPublisher());

        // 업데이트된 책 정보 저장
        Optional<Book> updatedBook = bookService.updateBook(bookIdx, bookToUpdate);
        if (updatedBook.isPresent()) {
            model.addAttribute("book", updatedBook.get());
            model.addAttribute("success", "책 정보가 성공적으로 수정되었습니다!");
        } else {
            model.addAttribute("error", "책 정보 업데이트에 실패했습니다.");
        }

        return "validation/bookBean";
    }
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://team-everywhere.gitbook.io/backend/spring/spring-boot/12-bean-validation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
