12 Bean Validation(빈검증)
p210 - build.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
@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
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
<!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
<!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
@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
typeMismatch=올바른 형식의 값을 입력해주세요.
typeMismatch.int=숫자를 입력해주세요.
NotBlank.book.title = 책 제목은 공백일 수 없습니다.
NotBlank={0} 공백일 수 없습니다
Min={0}, 최소 {1}
p218, 219 - src/main/resources/templates/validation/bookFormBinding.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
package spring.mvc.domain;
public interface BookCreate {
}
package spring.mvc.domain;
public interface BookUpdate {
}
p221, 222 - src/main/java/mvc/domain/Book
@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
@Data
public class BookCreateForm {
// 도서 이름
@NotBlank
private String title;
// 출판사
@NotBlank
private String publisher;
// 판매 가격
@NotNull
@Min(0)
private int salePrice;
}
@Data
public class BookUpdateForm {
// 도서 이름
@NotBlank
private String title;
// 출판사
@NotBlank
private String publisher;
}
p224, 225 - src/main/java/mvc/controller/ValidationBeanFormSeperateController
@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";
}
}
Last updated