contents
chapter 12.다국어 처리: 도서 등록 페이지에서 다국어 페이지 만들기
3.LocaleResolver와 LocaleChangeInterceptor를 이용한 다국어 변경
chapter 13.유효성 검사: 도서 등록 페이지의 오류 메시지 출력하기
1.유효성 검사 개요
2.JSR-380으로 유효성 검사
3.사용자 정의 애너테이션으로 유효성 검사
4.Validator 인터페이스로 유효성 검사
---------------------------------------------------------------------------------------
chapter 12.다국어 처리: 도서 등록 페이지에서 다국어 페이지 만들기
3.LocaleResolver와 LocaleChangeInterceptor를 이용한 다국어 변경
1)LocaleResolver 환경 설정
-스프링 MVC는 LocaleResolver로 웹 브라우저의 로케일을 추출해서 알맞은 언어를 선택하여 메시지를 출력함. 즉, 디스패처 서블릿은 웹 요철이 들어오면 LocaleResolver를 검색함
-로케일 객체가 검색된다면 이를 이용하여 로케일을 설정함
◎LocaleResolver 구현체의 유형
유형 | 설명 |
AcceptHeaderLocaleResolver | 웹 브라우저에 설정된 기본 로케일 정보를 사용함. HTTP 요청의 accept-language 헤더에 지정된 기본 로케일을 사용함 |
CookieLocaleResolver | 쿠키를 이용한 로케일 정보를 사용함. 사용자 지정 로케일, 표준 시간대 정보를 브라우저 쿠키로 유지함 |
SessionLocaleResolver | 세션을 이용한 로케일 정보를 사용함. 사용자 세션에서 locale 속서을 사용하여 지정된 로케일 또는 요청의 accept-header 로케일로 대체함 |
FixedLocaleResolver | 특정 로케일을 지정함. 항상 고정된 기본 로케일을 반환하고 선택적으로 시간대를 반환함 |
-CookieLocaleResolver와 SessionLocaleResolver는 웹 브라우저의 로케일에 따라 원하는 언어를 선택하여 서비스할 수 있음. 하지만 FixedLocaleResolver는 웹 브라우저의 로케일과는 상관 없이 지정된 언어만 서비스함
2)LocaleChangeInterceptor를 이용한 로케일 변경
-LocaleChangeInterceptor 클래스를 사용하면 로케일을 변경하는 별도의 컨트롤러 클래스를 구현할 필요 없이 메시지를 해당 언어로 변경할 수 있음. 즉, 웹 요청의 매개변수를 사용하여 손 쉽게 로케일을 바꿀 수 있음
-LocaleChangeInterceptor 클래스는 HandlerInterceptor로 다음과 같이 <interceptors> 요소에 등록만 하면 디스패처 서블릿이 컨트롤러에 접근할 때 응답을 가로채서 LocaleChangeInterceptor를 적용할 수 있음
LocaleChangeInterceptor 빈 등록 설정: servler-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans...> ... <interceptors> <beans:bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <beans:property name="paramName" value="language"/> </beans:bean> </interceptors> |
-웹 요청 경로가 http://localhost:8080/SpringBookMarket_9.0/books/add라면 '?language=언어 값'을 붙여서 사용함. '?language=언어 값'이라는 요청 매개변수를 포함해서 요청이 들어오면 언어 값이 ko일 때는 한국어로, en일 때는 영어로, ja일 때는 일본어로 변환하는 처리를 하게 됨
3) 실습) LocaleChangeInterceptor LocaleResolver와 LocaleChangeInterceptor를 이용하여 다국어 변경하기
-MessageSource를 기반으로 언어별로 쉡게 변경할 수 있도록 LocaleResolver와 LocaleChangeInterceptor를 이용하여 도서 등록 페이지의 폼 필드 이름을 출력하는 것을 구현함
http://localhost:8080/SpringBookMarket_9.0/books/add
servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 핸들러매핑 객체를 자동으로 생성한다. --> <annotation-driven /> <!-- 뷰리졸브 객체를 설정 --> <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="/WEB-INF/views/" /> <beans:property name="suffix" value=".jsp" /> </beans:bean> <!-- 자동으로 객체를 스캔하여 생성 --> <context:component-scan base-package="com.springmvc.*" /> <annotation-driven enable-matrix-variables="true"/> <!-- 리소스파일은 MVC 처리하지 않음 --> <resources mapping="/resources/**" location="/resources/" /> <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <beans:property name="maxUploadSize" value="10240000" /> </beans:bean> <!-- interceptor --> <interceptors> <beans:bean class="com.springmvc.interceptor.MonitoringInterceptor"/> <beans:bean class="com.springmvc.interceptor.AuditingInterceptor"/> ------------------------------------ 추가 -------------------------------------------- <beans:bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <beans:property name="paramName" value="language"/> </beans:bean> </interceptors> ------------------------------------ 추가 -------------------------------------------- <beans:bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"> <beans:property name="defaultLocale" value="ko"/> </beans:bean> <!-- MessageSource 다국어 빈 객채 등록 --> <beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <beans:property name="basename" value="messages"/> <beans:property name="defaultEncoding" value="UTF-8"/> </beans:bean> </beans:beans> |
addBook.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <title>도서 등록</title> </head> <body> <nav class="navbar navbar-expand navbar-dark bg-dark"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="./home">Home</a> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1 class="display-3"> <spring:message code="addBook.form.title.label"/> </h1> </div> </div> <div class="float-right" style="padding-right:30px"> <a href="?language=ko">Korean</a>|<a href="?language=en">English</a> </div> <div class="container"> <form:form modelAttribute="NewBook" class="form-horizontal" method="post" action="?${_csrf.parameterName}=${_csrf.token}" enctype="multipart/form-data" > <fieldset> <legend><spring:message code="addBook.form.subtitle.label"/></legend> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookId.label"/> </label> <div class="col-sm-3"> <form:input path="bookId" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.name.label"/> </label> <div class="col-sm-3"> <form:input path="name" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitPrice.label"/> </label> <div class="col-sm-3"> <form:input path="unitPrice" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.author.label"/> </label> <div class="col-sm-3"> <form:input path="author" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.description.label"/> </label> <div class="col-sm-5"> <form:textarea path="description" cols="50" rows="2" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.publisher.label"/> </label> <div class="col-sm-3"> <form:input path="publisher" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.category.label"/> </label> <div class="col-sm-3"> <form:input path="category" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitsInStock.label"/> </label> <div class="col-sm-3"> <form:input path="unitsInStock" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.releaseDate.label"/> </label> <div class="col-sm-3"> <form:input path="releaseDate" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.condition.label"/> </label> <div class="col-sm-3"> <form:radiobutton path="condition" value="New"/>New <form:radiobutton path="condition" value="Old"/>Old <form:radiobutton path="condition" value="E-Book"/>E-Book </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookImage.label"/> </label> <div class="col-sm-7"> <form:input path="bookImage" type="file" class="form-control"/> </div> </div> <div class="form-group row"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" class="btn btn-primary" value="<spring:message code="addBook.form.button.label"/>"/> </div> </div> </fieldset> </form:form> <hr> <footer> <p>© BookMarket</p> </footer> </div> </body> </html> |
실행 결과
http://localhost:8080/SpringBookMarket_9.0/books/add?language=ko
![]() |
http://localhost:8080/SpringBookMarket_9.0/books/add?language=en
![]() |
chapter 13.유효성 검사: 도서 등록 페이지의 오류 메시지 출력하기
1.유효성 검사 개요
-웹 애플리케이션의 폼 페이지에서 입력 항목의 데이터 값을 입력하고 [전송] 버튼을 누르면 폼 데이터 값이 서버로 전송됨. 이때 사용자 실수로 유효하지 않은 폼 데이터 값이 서버로 전송될 수도 있음. 그래서 이런 유효하지 않른 폼 데이터 값이 서버로 전송되지 않도록 해당 값을 전송하기 전에 검사해서 부적합하다고 판단되면 폼 페이지로 다시 되돌려 폼 페이지 값의 오류를 알림
-유효성 검사는 폼 페이지에서 입력 항목의 데이터 값이 서버로 전송되기 전에 정해진 규정으로 정확히 입력되었는지, 입력된 데이터를 이용한 계산 결과가 타당한지 검사하는 것임. 예를 들어 폼 페이지에서 입력 항목에 나이를 입력할 때 하는 숫자 검사, 회원 가입할 때 하는 아이디 중복 검사, 로그인 인증할 때 하는 아이디나 패스워드 검사, IP 패킷 검사 등이 이에 해당함
1)유효성 검사의 유형
-폼 데이터 값에 대한 유효성 검사는 JSR-380 Validation(Bean Validation)이나 스프링이 제공하는 Validator 인터페이스를 사용하여 구현할 수 있음
·JSR-380 Validation(Java Bean Validation 2.0) 방식: 웹 애플리케이션을 구성하는 특정 도메인 클래스의 멤버 변수, 즉 필드에 대한 유효성 검사 제약 사항(constraints) 애너테이션을 선언하여 해당 값이 올바른지 검증하는 방식임
·Validator 인터페이스의 구현체 방식: 웹 애플리케이션을 구성하는 특정 도메인 클래스의 멤버 변수에는 제약 사항 애너테이션을 선언하지 않음. 그 대신 스프링에서 제공하는 Validator 인터페이스로 구현하고 이를 Validator 인스턴스로 사용하여 해당 속성 값의 유효성 검사를 수행함. 스프링 Validator 인터페이스는 애플리케이션의 모든 계층에서 유효성 검증을 위해 사용할 수 있음
*객체 생성
(1)new
(2)singleton ex)@autowired 의존성 주입
(3)xml
-web.xml
-servlet-context-xml
-root-context.xml
sevlet
2.<interceptor>(S)/(R) servlet(3,4)
<bean> (S)/(R)
req ----> 1.Dispatcher ----> 3.Contorller ----> 4.Model
resp <---- ↑ ↓ ↘
5.view
new, S
◎유효성 검사를 해야 하는 폼 데이터 항목
-웹 애플리케이션에서 입력 데이터의 유효성 검사가 필요한 가장 큰 이유는 보안 공격이나 잘못된 데이터 또는 사용자 실수로 예상할 수 있는 오류를 방지할 수 있기 때문임. 다음은 입력 항목이 있는 폼 페이지를 만들 때 꼭 점검해야 할 유효성 검사 항목 들임
·입력 데이터가 null인지에 대한 유효성 검사 -> HTML, JS
·날짜나 이메일을 입력할 때 형식에 맞는지에 대한 유효성 검사 -> HTML, JS
·나이를 입력할 때 숫자인지에 대한 유효성 검사 -> HTML, JS ex)number
·입력 데이터의 제한 길이를 초과했는지에 대한 유효성 검사 -> HTML, JS ex)min, max
·로그인 인증을 할 때 아이디와 패스워드에 대한 유효성 검사 -> Java
·회원 가입을 할 때 아이디 중복 여부에 대한 유효성 검사 -> Java
-도서 쇼핑몰에서 도서 가격과 도서 주문 개수를 사용하여 총 금액을 계산하려고 하는데, 입력된 가격이 올바른 값이 아니라면 분명히 오류가 발생할 것임. 이렇게 사용자 입장에서 발생할 수 있는 오류를 방지하려면 데이터 유효성을 검사하는 기능이 필여함. 입력 데이터의 유효성 검사 기능을 이용한다면 다양한 사용자가 작성하는 페이지임에도 폼 페이지는 동일한 양식과 형태를 유지할 수 있음
2)@valid를 이용한 유효성 검사
-스프링 MVC에서는 사용자가 폼 페이지에서 입력한 데이터의 유효성을 검사하기 위해 코드를 작성할 필요 없이 간단한 방법으로 @valid 애너테이션을 제공함. @valid를 이용하는 데 필요한 환경 설정과 오류 메시지 출력을 살펴봄
(1)pom.xml 파일에 의존 라이브러리 등록하기
-@valid를 이용하여 폼 데이터 값에 대한 유효성 검사를 하려면 pom.xml 파일에 validation-api.jar과 hibernate-validator.jar 의존 라이브러리를 등록해야 함
<!-- Validation-->
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.2.Final</version> </dependency> |
(2)요청 처리 메서드의 매개변수에 @valid 선언하기-@valid를 이용하면 컨트롤러 내 요청 처리 메서드의 매개변수에 전달되는 폼 데이터 값에 대한 유효성 검사를 실행할 수 있음
@PostMapping("/...")
public String 메서드 이름(@Valid 매개변수, ..., BindingResult result) { if(result.hasErrors()){ //오류 메시지 저장 } return "뷰 이름"; } |
(3)뷰 페이지에 오류 메시지 출력하기
-유효성을 검사하여 발생한 오류 메시지를 JSP 뷰 페이지에 쉽게 출력하려면 폼 태그 라이브러리 중 <form:errors> 태그를 사용하면 됨
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> ... <form:errors path="커맨드 객체의 멤버 변수 이름"/> |
2.JSR-380으로 유효성 검사
-JSR-380 애너테이션을 이용한 유효성 검사는 웹 애플리케이션을 구성하는 특정 도메인 클래스의 프로퍼티(멤버 변수), 즉 필드에 대해 유효성 검사의 제약 사항 애너테이션을 선언하여 해당 멤버 변수 값이 올바른지 검사하는 것으로, Bean Validation 2.0이라고도 함
◎Bean Validation
-Bean Validation은 자바빈(JavaBean)의 유효성 검사를 위한 메타데이터 모델과 API를 정의함. 여기에서 자바빈은 매개변수가 없는 생성자를 가지며, Setter()와 Getter() 메서드를 사용하여 프로퍼티에 접근이 가능한 객체임
-Bean Validation 1.0은 JSR-303, Bean Validation 1.1은 JSR-349, Bean Validation 2.0은 JSR-380임
◎JSR-380을 이용한 처리 과정
(1)JSR-380 제약 사항의 애너테이션 선언
(2)@Valid를 이용한 유효성 검사 실행
(3)<form:errors> 태그로 오류 메시지 출력
1)JSR-380 애너테이션 선언
-JSR-380 애너테이션은 Hibernate Validator가 제공하는 애너테이션을 그대로 따름. 이와 마찬가지로 JSR-380도 유효성 검사가 필요한 도메인 클래스의 프로퍼티, 즉 필드에 제약 사항을 설정할 수 있음. 형식은 다음과 같음
public class 클래스 이름 {
@JSR-380 제약 사항 애너테이션(속성[, message="오류 메시지 또는 [오류 코드]") private String 멤버 변수 ... // Setter()와 Getter() 메서드 구현 생략 |
*DTO: 변수 위에 작성
◎JSR-380 제약 사항 애너테이션을 사용하려면 다음과 같이 스프링 MVC 설정 파일에 반드시 <annotation-driven/> 요소를 등록해야 함
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 핸들러매핑 객체를 자동으로 생성한다. --> <annotation-driven /> |
◎JSR-380 애너테이션 유형
유형 | 설명 | 속성 |
@AssertFalse | 프로퍼티 값이 거짓(false)인지 검사함 | |
@AssertTrue | 프로퍼티 값이 거짓(true)인지 검사함 | |
@DecimalMax | 프로퍼티 값이 가질 수 있는 최대 실수 값을 검사함 | ·value: 값 ·inclusive: true/false |
@DecimalMin | 프로퍼티 값이 가질 수 있는 최소 실수 값을 검사함 | ·value: 값 ·inclusive: true/false |
@Digits | 프로퍼티가 가질 수 있는 지정된 범위(정수 부분의 자릿수와 소수 부분의 자릿수)를 검사함 | ·integer: 정수의 자릿수 ·fraction: 소수의 자릿수 |
@Future | 프로퍼티 값이 미래 날짜(현재일 이후)인지 검사함 | |
@Max | 프로퍼티 값이 가질 수 있는 최대 길이를 검사함 | value: 값 |
@Min | 프로퍼티 값이 가질 수 있는 최소 길이를 검사함 | value: 값 |
@NotNull | 프로퍼티 값이 Null이 아닌지 검사함 | |
@Null | 프로퍼티 값이 Null인지 검사함 | |
@Past | 프로퍼티 값이 과거 날짜(현재일 이전)인지 검사함 | |
@Pattern | 프로퍼티 값이 정의된 정규 표현식에 일치하는지 검사함 | regexp: 정규 표현식 |
@Size | 프포퍼티 값이 가질 수 있는 최대, 최소 길이를 검사함 | ·min: 최소 길이 ·max: 최대 길이 |
@Valid | 객체에 대해 유효성 검사를 함 |
(1)JSR-380의 기본 메시지 사용하기
-유효성 검사를 할 때 JSR-380이 선언된 클래스의 멤버 변수가 제약 사항을 위반하여 오류가 발생하면 다음과 같이 hibernate-validator-xxx.jar 라이브러리에서 제공하는 기본 메시지를 출력함. 기본 메시지가 아닌 사용자 정의 오류 메시지를 출력하려면 message 속성 값에 출력할 오류 메시지를 직접 설정해야 함
◎JSR-380의 기본 메시지
애너테이션 | 기본 메시지 |
@AssertFalse | 반드시 거짓(false)이어야 함 |
@AssertTrue | 반드시 참(true)이어야 함 |
@DecimalMax | 반드시 {value}보다 같거나 작아야 함 |
@DecimalMin | 반드시 {value}보다 같거나 커야 함 |
@Digits | 숫자 값이 허용 범위를 벗어남(허용 범위: <{integer} 자리>, <{fraction} 자리>) |
@Future | 반드시 미래 날짜이어야 함 |
@Max | 반드시 {value} 보다 같거나 작아야 함 |
@Min |
반드시 {value}보다 같거나 커야 함 |
@NotNull | 반드시 값이 있어야 함 |
@Null | 반드시 값이 없어야 함 |
@Past |
반드시 과거 날짜이어야 함 |
@Pattern |
정규 표현식 "{regexp}" 패턴과 일치해야 함 |
@Size @Size |
반드시 최솟값 {min}과(와) 최댓값 {max} 사이의 크기이어야 함 |
(2)사용자 정의 오류 메시지 설정하기
-유효성 검사를 할 때 제약 사항을 위반하여 오류가 발생하는 경우 JSR-380 애너테이션의 기본 메시지가 아닌 사용자 정의 메시지를 출력하고 싶으면 mesaage 속성을 사용함. message 속성 값에 출력할 오류 메시지를 직접 정의하거나 메시지 리소스 파일(*.properties)을 만들어 '오류 코드=출력할 오류 메시지' 형식으로 다음과 같이 정의하여 message 속성 값에 오류 코드를 설정함
JSR-380 애너테이션.커맨드 객체 이름.필드 이름=출력할 오류 메시지 규약 클래스 변수 // 또는 JSR-380 애너테이션 = 출력할 오류 메시지 |
-메시지 리소스 파일을 사용하려면 servlet-context.xml 파일에 다음과 같이 MessageSource의 환경 설정을 해야 함
sevlet-context.xml
<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<beans:property name="basename" value="messages"/> <beans:property name="defaultEncoding" value="UTF-8"/> </beans:bean> |
-클래스에 멤버 변수 값에 대한 유효성 검사를 위해 JSR-380 제약 사항의 애너테이션과 사용자 정의 오류 메시지를 선언한 예임
public class Book {
@NotNull @Size(min=4, max=50, message="4자~1자 이내로 입력해 주세요") --- 메시지 속성 private String name; |
*@NotNull은 빈 문자열이 검증되지 않는다는 점을 주의해야 함. 즉, 폼 페이지에 있는 텍스트 타입의 입력 항목에 값을 입력하지 않고 전송할 경우 빈 문자열이 들어와도 @NotNull은 검증하지 못함. 따라서 빈 문자열까지 확인하려면 @Size(min=1)을 사용하거나 하이버네이트 유효성 검사에서 제공하는 @NotEmpty(org.hibernate.validator.constraints.NotEmpty(임포트해야 함))를 사용해야 함
2)@Valid를 이용한 유효성 검사
-스프링 MVC에서는 컨트롤러의 요청 처리 메서드 내 바인딩되는 데이터의 유효성 검사를 위해 코드를 작성할 필요 없이 간단한 방법으로 요청 처리 메서드의 매개변수에 선언하는 @Valid를 제공함. @Valid를 이용하면 컨트롤러 내 요청 처리 메서드의 매개변수에 전달되는 폼 데이터 값에 대한 유효성 검사를 실행할 수 있음
3)<form:errors> 태그로 오류 메시지 출력
-유효성 검사할 때 발생한 오류 메시지를 JSP 뷰 페이지에 출력하려면 폼 태그 라이브러리 중 <form:errors> 태그를 사용해야 함
◎<form:errors> 태그는 와일드카드 기능으로 오류 메시지를 출력할 수 있음
-<form:errors> 태그는 필드 이름을 지정하는 것 이외에 path 속성에서 와일드카드(*) 기능을 이용하여 오류 메시지를 출력할 수 있음
·path="*": 모든 오류를 출력함
·path="name": name 필드와 연관된 모든 오류를 출력함
·path 생략: 객체 오류만 출력함
4) 실습) JSR-380을 이용하여 유효성 검사하기
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.springmvc</groupId> <artifactId>SpringBookMarket_9.0</artifactId> <name>BookMarket</name> <packaging>war</packaging> <version>1.0.0-BUILD-SNAPSHOT</version> <properties> <java-version>17</java-version> <org.springframework-version>5.3.19</org.springframework-version> <org.aspectj-version>1.9.9.1</org.aspectj-version> <org.slf4j-version>1.7.36</org.slf4j-version> <security.version>5.6.3</security.version> <commons-fileupload-version>1.4</commons-fileupload-version> <commons-io-version>2.11.0</commons-io-version> </properties> <dependencies> ----------------------------- 추가 --------------------------------- <!-- Validation--> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.4.2.Final</version> </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework-version}</version> <exclusions> <!-- Exclude Commons Logging in favor of SLF4j --> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${org.springframework-version}</version> </dependency> <!-- Spring Security --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>${security.version}</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>${security.version}</version> </dependency> <!-- File Upload --> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>${commons-fileupload-version}</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons-io-version}</version> </dependency> <!-- AspectJ --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${org.aspectj-version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${org.slf4j-version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${org.slf4j-version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${org.slf4j-version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.15</version> <exclusions> <exclusion> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> </exclusion> <exclusion> <groupId>javax.jms</groupId> <artifactId>jms</artifactId> </exclusion> <exclusion> <groupId>com.sun.jdmk</groupId> <artifactId>jmxtools</artifactId> </exclusion> <exclusion> <groupId>com.sun.jmx</groupId> <artifactId>jmxri</artifactId> </exclusion> </exclusions> <scope>runtime</scope> </dependency> <!-- @Inject --> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <!-- Servlet --> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- Test --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-eclipse-plugin</artifactId> <version>2.9</version> <configuration> <additionalProjectnatures> <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature> </additionalProjectnatures> <additionalBuildcommands> <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand> </additionalBuildcommands> <downloadSources>true</downloadSources> <downloadJavadocs>true</downloadJavadocs> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5.1</version> <configuration> <source>17</source> <target>17</target> <compilerArgument>-Xlint:all</compilerArgument> <showWarnings>true</showWarnings> <showDeprecation>true</showDeprecation> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.2.1</version> <configuration> <mainClass>org.test.int1.Main</mainClass> </configuration> </plugin> </plugins> </build> </project> |
SpringBookmarket_9.0/src/main/resources/messages.properties
Pattern.NewBook.bookId = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11CID\uC785\uB2C8\uB2E4(\uC22B\uC790\uB85C \uC870\uD569\uD558\uACE0 ISBN\uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694).
Size.NewBook.name = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11C\uBA85\uC785\uB2C8\uB2E4(\uCD5C\uC18C 4\uC790\uC5D0\uC11C \uCD5C\uB300 50\uC790\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). Min.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694). Digits.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uC18C\uC218\uC810 2\uC790\uB9AC\uAE4C\uC9C0, 8\uC790\uB9AC\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). NotNull.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694). |
Book.java
package com.springmvc.domain;
import javax.validation.constraints.Digits; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import org.springframework.web.multipart.MultipartFile; ----------------------- 추가 ------------------------------ public class Book { @Pattern(regexp = "ISBN[1-9]+") private String bookId; //도서ID @Size(min=4, max=50) private String name; //도서명 @Min(value=0) @Digits(integer=8, fraction=2) @NotNull private int unitPrice; //가격 private String author; //저자 private String description; //설명 private String publisher; //출판사 private String category; //분류 private long unitsInStock; //재고 수 private String releaseDate; //출판일(월/년) private String condition; //신규 도서 또는 중고 도서 또는 전자책 private MultipartFile bookImage; private String saveName; public Book() { super(); } public Book(String bookId, String name, int unitPrice) { super(); this.bookId = bookId; this.name = name; this.unitPrice = unitPrice; } public String getBookId() { return bookId; } public void setBookId(String bookId) { this.bookId = bookId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getUnitPrice() { return unitPrice; } public void setUnitPrice(int unitPrice) { this.unitPrice = unitPrice; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher = publisher; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public long getUnitsInStock() { return unitsInStock; } public void setUnitsInStock(long unitsInStock) { this.unitsInStock = unitsInStock; } public String getReleaseDate() { return releaseDate; } public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } public String getCondition() { return condition; } public void setCondition(String condition) { this.condition = condition; } public MultipartFile getBookImage() { return bookImage; } public void setBookImage(MultipartFile bookImage) { this.bookImage = bookImage; } public String getSaveName() { return saveName; } public void setSaveName(String saveName) { this.saveName = saveName; } @Override public String toString() { return "Book [bookId=" + bookId + ", name=" + name + ", unitPrice=" + unitPrice + ", author=" + author + ", description=" + description + ", publisher=" + publisher + ", category=" + category + ", unitsInStock=" + unitsInStock + ", releaseDate=" + releaseDate + ", condition=" + condition + "]"; } } |
BookController.java
package com.springmvc.controller;
import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; import com.springmvc.domain.Book; import com.springmvc.exception.BookIdException; import com.springmvc.exception.CategoryException; import com.springmvc.service.BookService; @Controller @RequestMapping("/books") //추가 public class BookController { /*객체를 생성하는 방법 3가지 1 : 직접생성 private BookService bookService = new BookService();; 2 : 싱글톤 방식 private BookService bookService = BookService.getInstance(); private static BookService bs= new BookService(); private BookService() {} public static BookService getInstance() { return bs; } */ private static final Logger logger = LoggerFactory.getLogger(BookController.class); @Autowired private BookService bookService; @GetMapping public String requesteBookList(HttpServletRequest request, Model model) { //System.out.println("컨트롤러:books"); // 1. 함수 입장 //logger.info("컨트롤러:books"); //전처리: 데이터 안 가져옴 (5) /*System.out.println("주소: "); String path = request.getRealPath("resources/images"); System.out.println(path); */ //webapp\resources\images //D:\이름\jsp\.metadata\.plugins\org.eclipse.wst.server.core\tmp3\wtpwebapps\SpringBookMarket_9.0\resources\images //모델 이동 (5) ArrayList<Book> list = bookService.getAllBookList(); // 전달받은 값 없음, 서비스 호출 for(int i=0; i<list.size(); i++) { Book book =list.get(i); //System.out.println("전달받은 책: " + book.toString()); // 2. 전달받은 값 } //뷰이동 model.addAttribute("bookList", list); // 모델에 추가 return "books"; } @GetMapping("/all") public ModelAndView requesteAllBooks() { logger.info("컨트롤러:requesteAllBooks"); ModelAndView modelAndView = new ModelAndView(); ArrayList<Book> list = bookService.getAllBookList(); // 2. 서비스 호출 (전달받은 값 없음) modelAndView.addObject("bookList", list); // 모델에 추가 modelAndView.setViewName("books"); // 뷰 이름 설정 //System.out.println("전달받은 모든 책: " + list.toString()); // 2. 전달받은 값 return modelAndView; } @GetMapping("/{category}") //URL:서버/프로젝트/books/IT전문서 public String requestBooksByCategory(@PathVariable("category") String bookCategory, Model model) { logger.info("컨트롤러:requestBooksByCategory"); System.out.println("IT 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 카테고리: " + bookCategory.toString()); // 2. 전달받은 값 //전처리 //모델이동 IT전문서 ArrayList<Book> booksByCategory = bookService.getBookListByCategory(bookCategory); // 서비스 호출 if(booksByCategory == null || booksByCategory.isEmpty()) { throw new CategoryException(); } /*if(bookByCategry) return "redirect:/Exception/noCategory";*/ //뷰이동 model.addAttribute("bookList", booksByCategory); // 모델에 추가 System.out.println("카테고리별 책: " + booksByCategory.toString()); // 2. 전달받은 값 return "books"; } // .books/filter/bookFilter;publisher=길벗;category=IT전문서 // .books/filter/bookFilter;publisher=길벗;category=IT전문서,IT활용서 @GetMapping("/filter/{bookFilter}") public String requestBooksByFilter( @MatrixVariable(pathVar="bookFilter") Map<String, List<String>> bookFilter, Model model) { System.out.println("Filter 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 필터: " + bookFilter); // 2. 전달받은 값 //변수: bookFiler Set<Book> booksByFilter = bookService.getBookListByFilter(bookFilter); // 서비스 호출 model.addAttribute("bookList", booksByFilter); // 모델에 추가 //System.out.println("필터링된 책: " + booksByFilter.toString()); // 2. 전달받은 값 return "books"; } //http://localhost:8080/SpringBookMarket/books/book?id=ISBN1234 @GetMapping("/book") public String requestBookById(@RequestParam("id") String bookId, Model model) { System.out.println("requestBookById 커트롤러 입장"); // 1. 함수 입장 //System.out.println("전달받은 책 ID: " + bookId); // 2. 전달받은 값 Book bookById = bookService.getBookById(bookId); // 서비스 호출 //System.out.println("bookById 서비스 전달 받은 값: " + bookById.toString()); // 2. 전달받은 값 model.addAttribute("book", bookById); // 모델에 추가 //System.out.println("확인용" + model.addAttribute("book", bookById)); return "book"; } @GetMapping("/add") public String requestAddBookForm(@ModelAttribute("NewBook") Book book) { return "addBook"; } --------------------------- 추가 ------------------------------ @PostMapping("/add") public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest req) { if(result.hasErrors()) return "addBook"; MultipartFile bookImage = book.getBookImage(); //파일이름 겹치지 않게 만드는 방법 String now = String.valueOf(System.currentTimeMillis()); //숫자를 문자로 cf)문자를 숫자로: Integer.parsaInt(); System.out.println("현재 시간 : " + now); String saveName = bookImage.getOriginalFilename(); System.out.println("저장되는 파일이름: " + saveName); String[] format = saveName.split("\\."); for(int i = 0; i < format.length; i++) { //이름 + 확장자 (2) System.out.println(format[i]); } String newFileName = now + "." + format[1]; System.out.println(newFileName); // --------------------------------------- String path = req.getRealPath("resources/images"); File saveFile = new File(path, newFileName); //어디에, 무슨 이름으로 try { bookImage.transferTo(saveFile); } catch(Exception e){} book.setSaveName(newFileName); bookService.setNewBook(book); return "redirect:/books"; } @ModelAttribute public void addAttributes(Model model) { model.addAttribute("addTitle", "신규 도서 등록"); } @GetMapping("/login") // 로그인 세션 public String login(HttpServletRequest request) { HttpSession session=request.getSession(true); return "redirect:/books"; } @ExceptionHandler(value={BookIdException.class}) public ModelAndView handleError(HttpServletRequest req, BookIdException exception) { ModelAndView mav = new ModelAndView(); mav.addObject("invalidBookId", exception.getBookId()); mav.addObject("exception", exception); mav.addObject("url", req.getRequestURL() + "?" + req.getQueryString()); mav.setViewName("errorBook"); return mav; } } |
addBook.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <title>도서 등록</title> </head> <body> <nav class="navbar navbar-expand navbar-dark bg-dark"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="./home">Home</a> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1 class="display-3"> <spring:message code="addBook.form.title.label"/> </h1> </div> </div> <div class="float-right" style="padding-right:30px"> <a href="?language=ko">Korean</a>|<a href="?language=en">English</a> </div> <div class="container"> <form:form modelAttribute="NewBook" class="form-horizontal" method="post" action="?${_csrf.parameterName}=${_csrf.token}" enctype="multipart/form-data" > <fieldset> <legend><spring:message code="addBook.form.subtitle.label"/></legend> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookId.label"/> </label> <div class="col-sm-3"> <form:input path="bookId" class="form-control"/> </div> ------------------------- 추가 -------------------------- <div class="col-sm-6"> <form:errors path="bookId" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.name.label"/> </label> <div class="col-sm-3"> <form:input path="name" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="name" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitPrice.label"/> </label> <div class="col-sm-3"> <form:input path="unitPrice" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="unitPrice" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.author.label"/> </label> <div class="col-sm-3"> <form:input path="author" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.description.label"/> </label> <div class="col-sm-5"> <form:textarea path="description" cols="50" rows="2" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.publisher.label"/> </label> <div class="col-sm-3"> <form:input path="publisher" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.category.label"/> </label> <div class="col-sm-3"> <form:input path="category" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitsInStock.label"/> </label> <div class="col-sm-3"> <form:input path="unitsInStock" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.releaseDate.label"/> </label> <div class="col-sm-3"> <form:input path="releaseDate" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.condition.label"/> </label> <div class="col-sm-3"> <form:radiobutton path="condition" value="New"/>New <form:radiobutton path="condition" value="Old"/>Old <form:radiobutton path="condition" value="E-Book"/>E-Book </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookImage.label"/> </label> <div class="col-sm-7"> <form:input path="bookImage" type="file" class="form-control"/> </div> </div> <div class="form-group row"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" class="btn btn-primary" value="<spring:message code="addBook.form.button.label"/>"/> </div> </div> </fieldset> </form:form> <hr> <footer> <p>© BookMarket</p> </footer> </div> </body> </html> |
http://localhost:8080/SpringBookMarket_9.0/books/add
실행 결과
![]() |
3.사용자 정의 애너테이션으로 유효성 검사
-JSR-380 제약 사항의 애너테이션으로 유효성 검사를 할 때는 중복 여부를 체크할 수 없다는 점에서 한계가 있음. 이런 문제를 해결하도록 JSR-380 Bean Validation API는 사용자 정의 제약 사항(custom constraints)을 선언할 수 있는 인터페이스를 제공함
-보통 웹 애플리케이션에서 회원 관리를 위해 회원을 식별할 수 있는 속성이 ID임. 회원 가입을 할 때 아이디에 대해 반드시 중복 여부를 체크함. 이처럼 속성 값의 중복 여부를 체크하는 유효성 검사는 JSR-380 제약 사항으로는 불가능하므로 제약 사항을 사용자가 정의하여 사용하면 됨. 사용자 정의 제약 사항을 이용한 유효성 검사는 속성 값의 중복 여부를 비롯하여 다양한 제약 사항을 만들어 사용할 수 있어 유용함
◎사용자 정의 애너테이션을 이용한 처리 과정
(1)사용자 정의 애너테이션 선언
(1)-1.사용자 정의 애너테이션 생성: 제약 사항 및 구성 속성에 설정하는 @interface를 사용하여 정의 애너테이션을 생성함
(1)-2.ConstraintValidator 구현체 생성: 생성한 사용자 정의 애너테이션의 유효성 검사 클래스는 javax.validation.ConstraintValidator 인터페이스의 구현체로 생성함
(2)@Valid를 이용한 유효성 검사
(3)<form:errors> 태그로 오류 메시지 출력
-사용자 정의 애너테이션을 이용한 유효성 검사는 사용자 정의 애너테이션을 만들어 사용하는 것을 제외하고는 JSR-380 애너테이션을 이용한 처리 과정과 같음
1)사용자 정의 애너테이션 생성
-사용자 정의 애너테이션은 JSR-380처럼 도메인 클래스의 멤버 변수에 선언할 수 있는 제약 사항임. 또한 내장된 제약 사항이 아니므로 사용자가 직접 생성하여 사용할 수 있음. 사용자 정의 애너테이션을 생성하는 형식은 다음과 같음
@Constraint(validatedBy=유효성 검사 클래스.class) @Target(속성) @Retention(속성) @Documented public @interface 사용자 정의 애너테이션 이름{ String message() default "출력할 오류 메시지"; Class<?>[] groups() default {}; Class<?>[] payload() default {}; } |
-유효성 검사를 위한 사용자 정의 애너테이션을 만들 때 반드시 있어야 하는 필수 속성은 message, groups, payload임
◎사용자 정의 애너테이션의 필수 속성
속성 | 설명 |
message | 유효성 검사에서 오류가 발생하면 반환되는 기본 메시지임 |
groups | 특정 유효성 검사를 그룹으로 설정함 |
payload | 사용자가 추가한 정보를 전달하는 값임 |
◎Retention의 속성
속성 | 설명 |
Source | 소스 코드까지만 유지함. 즉, 컴파일하면 해당 애너테이션 정보는 사라짐 |
Class | 컴파일한 .class 파일에 유지함. 즉, 런타임을 할 때 클래스를 메모리로 읽어 오면 해당 정보는 사라짐 |
Runtime | 런타임을 할 때도 .class 파일에 유지함. 사용자 정의 애너테이션을 만들 때 주로 사용함 |
◎Target의 속성
속성 | 애너테이션 적용 시점 |
TYPE | class, interface, enum |
FIELD | 클래스의 멤버 변수 |
METHOD | 메서드 |
PARAMETER | 메서드 인자 |
CONSTRUCTOR | 생성자 |
LOCAL_VARIABLE | 로컬 변수 |
ANNOTATION_TYPE | 애너테이션 타입에만 적용 |
PACKAGE | 패키지 |
TYPE_PARAMETER | 제네릭 타입 변수(예)MyClass<T> |
TYPE_USE | 어떤 타입에도 적용(예) extends, implements, 객체 생성할 때 등 |
2)ConstraintValidator 인터페이스의 구현체 생성
-사용자 정의 애너테이션의 유효성 검사 클래스는 javax.validation.ConstraintValidator 인터페이스의 구현체를 생성함. 해당 구현체를 생성하려면 initialize()와 isValid() 메서드를 구현해야 함
◎ConstraintValidator 인터페이스의 메서드
유형 | 설명 |
void initialize(A constraintAnnotation) | 사용자 정의 애너테이션과 관련 정보를 읽어 초기화함. 이때 A는 사용자 정의 제약 사항을 설정함 |
boolean isValid(T value, ConstraintValidatorContext context) | 유효성 검사 로직을 수행함. value는 유효성 검사를 위한 도메인 클래스의 변수 값이고, context는 제약 사항을 평가하는 컨텍스트임 |
3) 실습) 사용자 정의 애너테이션을 이용하여 유효성 검사하기
message.properties
Pattern.NewBook.bookId = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11CID\uC785\uB2C8\uB2E4(\uC22B\uC790\uB85C \uC870\uD569\uD558\uACE0 ISBN\uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694).
Size.NewBook.name = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11C\uBA85\uC785\uB2C8\uB2E4(\uCD5C\uC18C 4\uC790\uC5D0\uC11C \uCD5C\uB300 50\uC790\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). Min.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694). Digits.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uC18C\uC218\uC810 2\uC790\uB9AC\uAE4C\uC9C0, 8\uC790\uB9AC\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). NotNull.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694). ---------------------------------- 추가 -------------------------------------- BookId.NewBook.bookId = \uB3C4\uC11CID\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. |
Book.java
package com.springmvc.domain;
import javax.validation.constraints.Digits; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; import org.springframework.web.multipart.MultipartFile; import com.springmvc.validator.BookId; public class Book { ---------------------------- 추가 ------------------------------------- @BookId @Pattern(regexp = "ISBN[1-9]+") private String bookId; //도서ID @Size(min=4, max=50) private String name; //도서명 @Min(value=0) @Digits(integer=8, fraction=2) @NotNull private int unitPrice; //가격 private String author; //저자 private String description; //설명 private String publisher; //출판사 private String category; //분류 private long unitsInStock; //재고 수 private String releaseDate; //출판일(월/년) private String condition; //신규 도서 또는 중고 도서 또는 전자책 private MultipartFile bookImage; private String saveName; public Book() { super(); } public Book(String bookId, String name, int unitPrice) { super(); this.bookId = bookId; this.name = name; this.unitPrice = unitPrice; } public String getBookId() { return bookId; } public void setBookId(String bookId) { this.bookId = bookId; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getUnitPrice() { return unitPrice; } public void setUnitPrice(int unitPrice) { this.unitPrice = unitPrice; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getPublisher() { return publisher; } public void setPublisher(String publisher) { this.publisher = publisher; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public long getUnitsInStock() { return unitsInStock; } public void setUnitsInStock(long unitsInStock) { this.unitsInStock = unitsInStock; } public String getReleaseDate() { return releaseDate; } public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } public String getCondition() { return condition; } public void setCondition(String condition) { this.condition = condition; } public MultipartFile getBookImage() { return bookImage; } public void setBookImage(MultipartFile bookImage) { this.bookImage = bookImage; } public String getSaveName() { return saveName; } public void setSaveName(String saveName) { this.saveName = saveName; } @Override public String toString() { return "Book [bookId=" + bookId + ", name=" + name + ", unitPrice=" + unitPrice + ", author=" + author + ", description=" + description + ", publisher=" + publisher + ", category=" + category + ", unitsInStock=" + unitsInStock + ", releaseDate=" + releaseDate + ", condition=" + condition + "]"; } } |
BookId.java
package com.springmvc.validator;
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.validation.Constraint; @Constraint(validatedBy=BookIdValidator.class) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface BookId { String message() default""; Class<?>[] groups() default {}; Class<?>[] payload() default {}; } |
BookIdValidator.java
package com.springmvc.validator;
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import org.springframework.beans.factory.annotation.Autowired; import com.springmvc.domain.Book; import com.springmvc.exception.BookIdException; import com.springmvc.service.BookService; public class BookIdValidator implements ConstraintValidator<BookId, String>{ @Autowired private BookService bookservice; public void initialize(BookId constraintAnnotation) { //@BookId 정보 초기화 메서드 } //유효성 검사 메서드 public boolean isValid(String value, ConstraintValidatorContext context) { System.out.println("BookIdValidator 확인"); Book book; try{ book = bookservice.getBookById(value); }catch(BookIdException e) { return true; } if(book!=null) { return false; } return true; } } |
BookController.java
package com.springmvc.controller;
import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; import com.springmvc.domain.Book; import com.springmvc.exception.BookIdException; import com.springmvc.exception.CategoryException; import com.springmvc.service.BookService; @Controller @RequestMapping("/books") //추가 public class BookController { /*객체를 생성하는 방법 3가지 1 : 직접생성 private BookService bookService = new BookService();; 2 : 싱글톤 방식 private BookService bookService = BookService.getInstance(); private static BookService bs= new BookService(); private BookService() {} public static BookService getInstance() { return bs; } */ private static final Logger logger = LoggerFactory.getLogger(BookController.class); @Autowired private BookService bookService; @GetMapping public String requesteBookList(HttpServletRequest request, Model model) { //System.out.println("컨트롤러:books"); // 1. 함수 입장 //logger.info("컨트롤러:books"); //전처리: 데이터 안 가져옴 (5) /*System.out.println("주소: "); String path = request.getRealPath("resources/images"); System.out.println(path); */ //webapp\resources\images //D:\이름\jsp\.metadata\.plugins\org.eclipse.wst.server.core\tmp3\wtpwebapps\SpringBookMarket_9.0\resources\images //모델 이동 (5) ArrayList<Book> list = bookService.getAllBookList(); // 전달받은 값 없음, 서비스 호출 for(int i=0; i<list.size(); i++) { Book book =list.get(i); //System.out.println("전달받은 책: " + book.toString()); // 2. 전달받은 값 } //뷰이동 model.addAttribute("bookList", list); // 모델에 추가 return "books"; } @GetMapping("/all") public ModelAndView requesteAllBooks() { logger.info("컨트롤러:requesteAllBooks"); ModelAndView modelAndView = new ModelAndView(); ArrayList<Book> list = bookService.getAllBookList(); // 2. 서비스 호출 (전달받은 값 없음) modelAndView.addObject("bookList", list); // 모델에 추가 modelAndView.setViewName("books"); // 뷰 이름 설정 //System.out.println("전달받은 모든 책: " + list.toString()); // 2. 전달받은 값 return modelAndView; } @GetMapping("/{category}") //URL:서버/프로젝트/books/IT전문서 public String requestBooksByCategory(@PathVariable("category") String bookCategory, Model model) { logger.info("컨트롤러:requestBooksByCategory"); System.out.println("IT 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 카테고리: " + bookCategory.toString()); // 2. 전달받은 값 //전처리 //모델이동 IT전문서 ArrayList<Book> booksByCategory = bookService.getBookListByCategory(bookCategory); // 서비스 호출 if(booksByCategory == null || booksByCategory.isEmpty()) { throw new CategoryException(); } /*if(bookByCategry) return "redirect:/Exception/noCategory";*/ //뷰이동 model.addAttribute("bookList", booksByCategory); // 모델에 추가 System.out.println("카테고리별 책: " + booksByCategory.toString()); // 2. 전달받은 값 return "books"; } // .books/filter/bookFilter;publisher=길벗;category=IT전문서 // .books/filter/bookFilter;publisher=길벗;category=IT전문서,IT활용서 @GetMapping("/filter/{bookFilter}") public String requestBooksByFilter( @MatrixVariable(pathVar="bookFilter") Map<String, List<String>> bookFilter, Model model) { System.out.println("Filter 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 필터: " + bookFilter); // 2. 전달받은 값 //변수: bookFiler Set<Book> booksByFilter = bookService.getBookListByFilter(bookFilter); // 서비스 호출 model.addAttribute("bookList", booksByFilter); // 모델에 추가 //System.out.println("필터링된 책: " + booksByFilter.toString()); // 2. 전달받은 값 return "books"; } //http://localhost:8080/SpringBookMarket/books/book?id=ISBN1234 @GetMapping("/book") public String requestBookById(@RequestParam("id") String bookId, Model model) { System.out.println("requestBookById 커트롤러 입장"); // 1. 함수 입장 //System.out.println("전달받은 책 ID: " + bookId); // 2. 전달받은 값 Book bookById = bookService.getBookById(bookId); // 서비스 호출 //System.out.println("bookById 서비스 전달 받은 값: " + bookById.toString()); // 2. 전달받은 값 model.addAttribute("book", bookById); // 모델에 추가 //System.out.println("확인용" + model.addAttribute("book", bookById)); return "book"; } @GetMapping("/add") public String requestAddBookForm(@ModelAttribute("NewBook") Book book) { return "addBook"; } @PostMapping("/add") public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest req) { System.out.println("@Valid 확인"); if(result.hasErrors()) return "addBook"; MultipartFile bookImage = book.getBookImage(); //파일이름 겹치지 않게 만드는 방법 String now = String.valueOf(System.currentTimeMillis()); //숫자를 문자로 cf)문자를 숫자로: Integer.parsaInt(); System.out.println("현재 시간 : " + now); String saveName = bookImage.getOriginalFilename(); System.out.println("저장되는 파일이름: " + saveName); String[] format = saveName.split("\\."); for(int i = 0; i < format.length; i++) { //이름 + 확장자 (2) System.out.println(format[i]); } String newFileName = now + "." + format[1]; System.out.println(newFileName); // --------------------------------------- String path = req.getRealPath("resources/images"); File saveFile = new File(path, newFileName); //어디에, 무슨 이름으로 try { bookImage.transferTo(saveFile); } catch(Exception e){} book.setSaveName(newFileName); bookService.setNewBook(book); return "redirect:/books"; } @ModelAttribute public void addAttributes(Model model) { model.addAttribute("addTitle", "신규 도서 등록"); } @GetMapping("/login") // 로그인 세션 public String login(HttpServletRequest request) { HttpSession session=request.getSession(true); return "redirect:/books"; } @ExceptionHandler(value={BookIdException.class}) public ModelAndView handleError(HttpServletRequest req, BookIdException exception) { ModelAndView mav = new ModelAndView(); mav.addObject("invalidBookId", exception.getBookId()); mav.addObject("exception", exception); mav.addObject("url", req.getRequestURL() + "?" + req.getQueryString()); mav.setViewName("errorBook"); return mav; } } |
addBook.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <title>도서 등록</title> </head> <body> <nav class="navbar navbar-expand navbar-dark bg-dark"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="./home">Home</a> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1 class="display-3"> <spring:message code="addBook.form.title.label"/> </h1> </div> </div> <div class="float-right" style="padding-right:30px"> <a href="?language=ko">Korean</a>|<a href="?language=en">English</a> </div> <div class="container"> <form:form modelAttribute="NewBook" class="form-horizontal" method="post" action="?${_csrf.parameterName}=${_csrf.token}" enctype="multipart/form-data" > <fieldset> <legend><spring:message code="addBook.form.subtitle.label"/></legend> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookId.label"/> </label> <div class="col-sm-3"> <form:input path="bookId" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="bookId" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.name.label"/> </label> <div class="col-sm-3"> <form:input path="name" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="name" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitPrice.label"/> </label> <div class="col-sm-3"> <form:input path="unitPrice" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="unitPrice" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.author.label"/> </label> <div class="col-sm-3"> <form:input path="author" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.description.label"/> </label> <div class="col-sm-5"> <form:textarea path="description" cols="50" rows="2" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.publisher.label"/> </label> <div class="col-sm-3"> <form:input path="publisher" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.category.label"/> </label> <div class="col-sm-3"> <form:input path="category" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitsInStock.label"/> </label> <div class="col-sm-3"> <form:input path="unitsInStock" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.releaseDate.label"/> </label> <div class="col-sm-3"> <form:input path="releaseDate" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.condition.label"/> </label> <div class="col-sm-3"> <form:radiobutton path="condition" value="New"/>New <form:radiobutton path="condition" value="Old"/>Old <form:radiobutton path="condition" value="E-Book"/>E-Book </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookImage.label"/> </label> <div class="col-sm-7"> <form:input path="bookImage" type="file" class="form-control"/> </div> </div> <div class="form-group row"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" class="btn btn-primary" value="<spring:message code="addBook.form.button.label"/>"/> </div> </div> </fieldset> </form:form> <hr> <footer> <p>© BookMarket</p> </footer> </div> </body> </html> |
http://localhost:8080/SpringBookMarket_9.0/books/add
실행 결과
![]() |
4.Validator 인터페이스로 유효성 검사
-스프링에서 제공하는 Validator 인터페이스를 사용하여 도메인 클래스의 속성 값이 올바른지 유효성을 검사하는 방법을 다음과 같이 구분하여 알아봄
1.도메인 클래스의 속성에 애너테이션을 선언하지 않고 스프링에서 제공하는 Validator 인터페이스의 구현체를 생성하고, 이를 이용하여 속성 값의 유효성을 검사하는 방법
2.스프링의 Validator 인터페이스와 JSR-380 Bean Validation과 연동하여 유효성을 검사하는 방법
1)유효성 검사 과정
-스프링 Validator 인터페이스를 활용하여 유효성을 검사할 때는 크게 네 단계를 거침
(1)Validator 인터페이스의 구현체 생성
(2)@InitBinder 선언 메서드 추가
(3)@Valid를 이용한 유효성 검사
(4)<form:erroes> 태그로 오류 메시지 출력
2)Validator 인터페이스의 구현체 생성
-Validator 인터페이스의 구현체는 유효성 검사를 위한 클래스로 다음 두 가지 메서드를 구현해야 함
◎Validator 인터페이스의 메서드
메서드 | 설명 |
void rejectValue(String field, String errorCode, String dafaultMessage) | 설정된 field가 유효성 검사를 할 때 오류를 발생시키면 설정된 errorCode와 함께 거부함 |
void reject(String errorCode, String defaultMessage) | 유효성 검사를 할 때 오류가 발생하면 설정된 errorCode를 사용하여 도메인 객체에 대한 전역 오류로 사용함 |
3)@InitBinder를 선언한 메서드 추가
-컨트롤러의 메서드 내에 바인딩되는 데이터의 유효성 검사를 하려면 매개변수에 @Valid를 선언하여 커맨드 객체의 속성 값을 전달받음. 그리고 @InitBinder 메서드를 정의하고 해당 메서드의 입력 매개변수로 전달된 WebDataBinder 객체를 사용하여 해당 Validator 인터페이스의 구현체를 설정함. @InitBinder가 선언된 메서드에 Validator를 미리 등록하면 해당 컨트롤러의 모든 메서드에서 유효성 검사 기능을 사용할 수 있기 때문에 편리함
4) 실습) Validator 인터페이스를 사용하여 유효성 검사하기
messages.properies
Pattern.NewBook.bookId = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11CID\uC785\uB2C8\uB2E4(\uC22B\uC790\uB85C \uC870\uD569\uD558\uACE0 ISBN\uC73C\uB85C \uC2DC\uC791\uD558\uC138\uC694).
Size.NewBook.name = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uB3C4\uC11C\uBA85\uC785\uB2C8\uB2E4(\uCD5C\uC18C 4\uC790\uC5D0\uC11C \uCD5C\uB300 50\uC790\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). Min.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(0\uC774\uC0C1\uC758 \uC218\uB97C \uC785\uB825\uD558\uC138\uC694). Digits.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uC18C\uC218\uC810 2\uC790\uB9AC\uAE4C\uC9C0, 8\uC790\uB9AC\uAE4C\uC9C0 \uC785\uB825\uD558\uC138\uC694). NotNull.NewBook.unitPrice = \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uAC00\uACA9\uC785\uB2C8\uB2E4(\uAC00\uACA9\uC744 \uC785\uB825\uD558\uC138\uC694). BookId.NewBook.bookId = \uB3C4\uC11CID\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. ------------------------------- 추가 ---------------------------------- UnitsInStockValidator.message = \uAC00\uACA9\uC774 10000\uC6D0 \uC774\uC0C1\uC778 \uACBD\uC6B0\uC5D0\uB294 99\uAC1C \uC774\uC0C1\uC744 \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. |
UnitsInStockValidator.java
package com.springmvc.validator;
import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import com.springmvc.domain.Book; @Component public class UnitsInStockValidator implements Validator{ public boolean supports(Class<?> clazz) { // Book 클래스의 유효성 검사 여부를 위한 메서드 return Book.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { // Book 클래스의 유효성 검사 메서드 Book book = (Book) target; if(book.getUnitPrice() >= 10000 && book.getUnitsInStock() > 99) { System.out.println("validate 도착"); //오류 객체의 속성과 메시지 저장 errors.rejectValue("unitsInStock", "UnitsInStockValidator.message"); } } } |
BookController.java
package com.springmvc.controller;
import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.MatrixVariable; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.ModelAndView; import com.springmvc.domain.Book; import com.springmvc.exception.BookIdException; import com.springmvc.exception.CategoryException; import com.springmvc.service.BookService; import com.springmvc.validator.UnitsInStockValidator; @Controller @RequestMapping("/books") //추가 public class BookController { /*객체를 생성하는 방법 3가지 1 : 직접생성 private BookService bookService = new BookService();; 2 : 싱글톤 방식 private BookService bookService = BookService.getInstance(); private static BookService bs= new BookService(); private BookService() {} public static BookService getInstance() { return bs; } */ private static final Logger logger = LoggerFactory.getLogger(BookController.class); @Autowired private BookService bookService; ------------------------------- 추가 ------------------------------- @Autowired // UnitsInStockValidator의 인스턴스 선언 private UnitsInStockValidator unitsInStockValidator; @GetMapping public String requesteBookList(HttpServletRequest request, Model model) { //System.out.println("컨트롤러:books"); // 1. 함수 입장 //logger.info("컨트롤러:books"); //전처리: 데이터 안 가져옴 (5) /*System.out.println("주소: "); String path = request.getRealPath("resources/images"); System.out.println(path); */ //webapp\resources\images //D:\이름\jsp\.metadata\.plugins\org.eclipse.wst.server.core\tmp3\wtpwebapps\SpringBookMarket_9.0\resources\images //모델 이동 (5) ArrayList<Book> list = bookService.getAllBookList(); // 전달받은 값 없음, 서비스 호출 for(int i=0; i<list.size(); i++) { Book book =list.get(i); //System.out.println("전달받은 책: " + book.toString()); // 2. 전달받은 값 } //뷰이동 model.addAttribute("bookList", list); // 모델에 추가 return "books"; } @GetMapping("/all") public ModelAndView requesteAllBooks() { logger.info("컨트롤러:requesteAllBooks"); ModelAndView modelAndView = new ModelAndView(); ArrayList<Book> list = bookService.getAllBookList(); // 2. 서비스 호출 (전달받은 값 없음) modelAndView.addObject("bookList", list); // 모델에 추가 modelAndView.setViewName("books"); // 뷰 이름 설정 //System.out.println("전달받은 모든 책: " + list.toString()); // 2. 전달받은 값 return modelAndView; } @GetMapping("/{category}") //URL:서버/프로젝트/books/IT전문서 public String requestBooksByCategory(@PathVariable("category") String bookCategory, Model model) { logger.info("컨트롤러:requestBooksByCategory"); System.out.println("IT 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 카테고리: " + bookCategory.toString()); // 2. 전달받은 값 //전처리 //모델이동 IT전문서 ArrayList<Book> booksByCategory = bookService.getBookListByCategory(bookCategory); // 서비스 호출 if(booksByCategory == null || booksByCategory.isEmpty()) { throw new CategoryException(); } /*if(bookByCategry) return "redirect:/Exception/noCategory";*/ //뷰이동 model.addAttribute("bookList", booksByCategory); // 모델에 추가 System.out.println("카테고리별 책: " + booksByCategory.toString()); // 2. 전달받은 값 return "books"; } // .books/filter/bookFilter;publisher=길벗;category=IT전문서 // .books/filter/bookFilter;publisher=길벗;category=IT전문서,IT활용서 @GetMapping("/filter/{bookFilter}") public String requestBooksByFilter( @MatrixVariable(pathVar="bookFilter") Map<String, List<String>> bookFilter, Model model) { System.out.println("Filter 컨트롤러:books"); // 1. 함수 입장 //System.out.println("전달받은 필터: " + bookFilter); // 2. 전달받은 값 //변수: bookFiler Set<Book> booksByFilter = bookService.getBookListByFilter(bookFilter); // 서비스 호출 model.addAttribute("bookList", booksByFilter); // 모델에 추가 //System.out.println("필터링된 책: " + booksByFilter.toString()); // 2. 전달받은 값 return "books"; } //http://localhost:8080/SpringBookMarket/books/book?id=ISBN1234 @GetMapping("/book") public String requestBookById(@RequestParam("id") String bookId, Model model) { System.out.println("requestBookById 커트롤러 입장"); // 1. 함수 입장 //System.out.println("전달받은 책 ID: " + bookId); // 2. 전달받은 값 Book bookById = bookService.getBookById(bookId); // 서비스 호출 //System.out.println("bookById 서비스 전달 받은 값: " + bookById.toString()); // 2. 전달받은 값 model.addAttribute("book", bookById); // 모델에 추가 //System.out.println("확인용" + model.addAttribute("book", bookById)); return "book"; } @GetMapping("/add") public String requestAddBookForm(@ModelAttribute("NewBook") Book book) { return "addBook"; } ---------------------------------- 확인 -------------------------------- @PostMapping("/add") public String submitAddNewBook(@Valid @ModelAttribute("NewBook") Book book, BindingResult result, HttpServletRequest req) { System.out.println("@Valid 확인"); if(result.hasErrors()) return "addBook"; MultipartFile bookImage = book.getBookImage(); //파일이름 겹치지 않게 만드는 방법 String now = String.valueOf(System.currentTimeMillis()); //숫자를 문자로 cf)문자를 숫자로: Integer.parsaInt(); System.out.println("현재 시간 : " + now); String saveName = bookImage.getOriginalFilename(); System.out.println("저장되는 파일이름: " + saveName); String[] format = saveName.split("\\."); for(int i = 0; i < format.length; i++) { //이름 + 확장자 (2) System.out.println(format[i]); } String newFileName = now + "." + format[1]; System.out.println(newFileName); // --------------------------------------- String path = req.getRealPath("resources/images"); File saveFile = new File(path, newFileName); //어디에, 무슨 이름으로 try { bookImage.transferTo(saveFile); } catch(Exception e){} book.setSaveName(newFileName); bookService.setNewBook(book); return "redirect:/books"; } ------------------------------- 추가 ------------------------------- @InitBinder
public void initBinder(WebDataBinder binder) { System.out.println("initBinder 도착"); binder.setValidator(unitsInStockValidator); //생성한 unitsInStockValidator 설정 binder.setAllowedFields("bookId", "name", "unitPrice", "author", "description", "publisher", "category", "unitsInStock", "totalPages", "releaseDate", "condition", "bookImage"); } @ModelAttribute public void addAttributes(Model model) { model.addAttribute("addTitle", "신규 도서 등록"); } @GetMapping("/login") // 로그인 세션 public String login(HttpServletRequest request) { HttpSession session=request.getSession(true); return "redirect:/books"; } @ExceptionHandler(value={BookIdException.class}) public ModelAndView handleError(HttpServletRequest req, BookIdException exception) { ModelAndView mav = new ModelAndView(); mav.addObject("invalidBookId", exception.getBookId()); mav.addObject("exception", exception); mav.addObject("url", req.getRequestURL() + "?" + req.getQueryString()); mav.setViewName("errorBook"); return mav; } } |
addBook.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet"> <title>도서 등록</title> </head> <body> <nav class="navbar navbar-expand navbar-dark bg-dark"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="./home">Home</a> </div> </div> </nav> <div class="jumbotron"> <div class="container"> <h1 class="display-3"> <spring:message code="addBook.form.title.label"/> </h1> </div> </div> <div class="float-right" style="padding-right:30px"> <a href="?language=ko">Korean</a>|<a href="?language=en">English</a> </div> <div class="container"> <form:form modelAttribute="NewBook" class="form-horizontal" method="post" action="?${_csrf.parameterName}=${_csrf.token}" enctype="multipart/form-data" > <fieldset> <legend><spring:message code="addBook.form.subtitle.label"/></legend> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookId.label"/> </label> <div class="col-sm-3"> <form:input path="bookId" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="bookId" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.name.label"/> </label> <div class="col-sm-3"> <form:input path="name" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="name" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitPrice.label"/> </label> <div class="col-sm-3"> <form:input path="unitPrice" class="form-control"/> </div> <div class="col-sm-6"> <form:errors path="unitPrice" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.author.label"/> </label> <div class="col-sm-3"> <form:input path="author" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.description.label"/> </label> <div class="col-sm-5"> <form:textarea path="description" cols="50" rows="2" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.publisher.label"/> </label> <div class="col-sm-3"> <form:input path="publisher" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.category.label"/> </label> <div class="col-sm-3"> <form:input path="category" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.unitsInStock.label"/> </label> <div class="col-sm-3"> <form:input path="unitsInStock" class="form-control"/> </div> -------------------------- 추가 -------------------------------- <div class="col-sm-6"> <form:errors path="unitsInStock" cssClass="text-danger"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.releaseDate.label"/> </label> <div class="col-sm-3"> <form:input path="releaseDate" class="form-control"/> </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.condition.label"/> </label> <div class="col-sm-3"> <form:radiobutton path="condition" value="New"/>New <form:radiobutton path="condition" value="Old"/>Old <form:radiobutton path="condition" value="E-Book"/>E-Book </div> </div> <div class="form-group row"> <label class="col-sm-2 control-label"> <spring:message code="addBook.form.bookImage.label"/> </label> <div class="col-sm-7"> <form:input path="bookImage" type="file" class="form-control"/> </div> </div> <div class="form-group row"> <div class="col-sm-offset-2 col-sm-10"> <input type="submit" class="btn btn-primary" value="<spring:message code="addBook.form.button.label"/>"/> </div> </div> </fieldset> </form:form> <hr> <footer> <p>© BookMarket</p> </footer> </div> </body> </html> |
http://localhost:8080/SpringBookMarket_9.0/books/add
실행 결과
![]() |
'벡엔드 웹프로그래밍 > Spring' 카테고리의 다른 글
Spring 웹 프로그래밍 93일차 (25/3/31) (0) | 2025.03.31 |
---|---|
Spring 웹 프로그래밍 89일차 (25/3/25) (0) | 2025.03.25 |
Spring 웹 프로그래밍 87일차 (25/3/21) (0) | 2025.03.21 |
Spring 웹 프로그래밍 85일차 (25/3/19) (0) | 2025.03.19 |
Spring 웹 프로그래밍 77일차 (25/3/7) (0) | 2025.03.07 |