[Spring] Todo 검증기능(1) - 프론트앤드 검증보다 안전한 Spring Boot Starter Validation을 이용한 서버측 검증 (+Spring form tag library)

민지·2024년 1월 25일
0

1. 프론트앤드 검증 - required 속성

input 태그의 required 속성은 폼 데이터(form data)가 서버로 제출되기 전, 반드시 채워져 있어야 할 입력 필드를 지정해준다.

addTodo.jsp의 Description을 공백으로 제출하지 못하게 하기 위해 required 속성을 추가했다.

// addTodo.jsp -> body 태그 일부분
<body>
		<div class = "container">
			<h1>Enter Todo Details</h1>
			<form method = "post">
				Description : <input type = "text" name = "description" required = "required" /> <br>
				<input type = "submit" class = "btn btn-success"/>
			</form>
			<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
			<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
		</div>
	</body>

실행결과


Description을 공란으로 제출하려 하면, 필수란을 작성하지 않았기에 위와 같은 경고메시지가 뜨고, 제출되지 않는다.

다만, HTML, JavaScript로 구현한 프론트앤드 검증은 해커가 아주 쉽게 건너뛸 수 있어, 안정성이 부족한 단점이 있다.

따라서 훨씬 안전한 서버 측 검증, 즉 Java 코드 안에 검증을 하는 것이 좋다.

SpringBoot, Spring MVC에서의 검증 단계 - 4단계

1. Spring Boot Starter Validation

  • pom.xml에 의존성 주입 추가

2. Command Bean or Form Backing Object 개념 사용

  • 2-way binding(양방향 바인딩) 가능 (todo.jsp - TodoController.java끼리의 바인딩 가능)

3. Bean에 검증 추가 - Todo.java에 추가할 것

  • description field의 최소 문자 개수 10개임을 추가하는 것
  • deadline field - 항상 현재 날짜 또는 미래 날짜만 가능하게끔

4. 검증 에러를 뷰에 표시할 것 - todo.jsp에 추가할 것

이번 포스팅에서는 위의 4가지 검증단계 중, 1,2단계까지의 검증에 대해 정리해볼 것이다.

검증 1. spring-boot-starter-validation을 pom.xml에 추가하자

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

검증 2. Command Bean or Form Backing Object 개념 사용

Command Bean or Form Backing Object 뜻

// add-todo - POST
	@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
	public String addNewTodoPage(@RequestParam String description, ModelMap model) {

		String username = (String) model.get("name");
		todoService.AddTodo(username, description, LocalDate.now().plusYears(1), false);
		return "redirect:list-todos";
	}

위 코드를 보면, /add-todo의 웹 request의 쿼리 파라미터컨트롤러 코드를 binding할 방법으로 여태 배운 @RequestParam을 쓴 것을 확인할 수 있었다.

다만, @RequestParam은 만일 내가 description뿐만이 아니라 그외의 10개의 URL payload data값을 받는다고 하면, 코드가 아주아주 복잡해질 것이다...

이때 @RequestParam 대신 등장한 개념이 Command Bean 또는 Form Backing Object이다. 이 용어는 웹 어플리케이션에서 폼 데이터를 전달하고 처리할 때 사용되는 자바 빈(Java Bean)을 말한다. 자세한 용어에 대한 이해는 아래의 폼 태그 라이브러리를 이해하는 과정에서 자연스럽게 이해할 수 있게 된다. (스포 : 아래 코드에서의 Command Bean 또는 Form Backing ObjectTodo todo 객체이다.)

코드 리팩토링 - TodoController, addTodo.jsp 코드 (+스프링 폼 태그 라이브러리란)

1. 스프링 폼 태그 라이브러리란?

결론부터 말하자면, 스프링 폼(form) 태그 라이브러리란, 스프링 폼 태그 라이브러리를 사용하면, 컨트롤러 코드의 자바 객체와 JSP의 폼 항목이 자동으로 데이터 바인딩이 되게 하는 것을 말한다.

스프링 폼 태그 라이브러리

  • 스프링 폼을 이용하면, HTML form과 자바 객체를 쉽게 바인딩할 수 있다.
  • path : HTML form 항목과 바인딩될 자바 객체(Controller에 있음)의 프로퍼티를 지정
    -> 스프링 폼을 사용하면, 컨트롤러 코드의 자바 객체(Todo.java의 Todo 인스턴스)와 JSP(JavaServer Pages)의 폼 항목(description)이 자동으로 데이터 바인딩!

form tag 목록

  • <form:password> : 패스워드 필드 요소
  • <form:form> : 폼 요소
  • <form:input path="text"> : 테스트 필드. id="test" name="test"
  • <form:textarea> : 텍스트 영역
  • <form:checkbox> : 체크박스
  • <form:checkboxes> : 여러개 체크박스
  • <form:radiobutton> : 라디오 버튼
  • <form:radiobuttons> : 여러 개의 라디오 버튼
  • <form:select> : 셀렉트 박스
  • <form:hidden> : 숨겨진 필드
  • <form:label> : 라벨
  • <form:button> :버튼

예제 코드

1. 컨트롤러 클래스

@Controller
public class TodoController {
    
    @GetMapping("/add-todo")
    public String showAddTodoForm(Model model) {
        model.addAttribute("todo", new Todo());
        return "addTodo";
    }

    @PostMapping("/add-todo")
    public String processAddTodoForm(@ModelAttribute("todo") Todo todo) {
        // todo 객체에는 JSP 폼에서 입력한 데이터가 자동으로 바인딩됨
        // 처리 로직 수행...
        return "redirect:/todos";
    }
}

이때, @ModelAttribute("todo")를 굳이 명시적으로 선언해주지 않아도, 스프링이 알아서 jsp와 바인딩된 todo객체를 알아서 컨트롤러 코드로 넘겨주기에, 굳이 강박적으로 명시할 필요 없다.

2. Todo 클래스

public class Todo {
    private String description;

    // getter, setter 등 필요한 메서드...
}

3. JSP 파일 ('addTodo.jsp')

아래 코드는 앞선 포스팅 JSTL Core Tag library 문법과 비슷하게 선언문과 form:태그를 따로 명시해, 해당 Spring의 form 태그 라이브러리를 사용했음을 알려주었다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
	<head>
	    <title>Add Todo</title>
	</head>
	<body>
	    <h2>Add Todo</h2>
	    <form:form method="post" modelAttribute="todo">
	        <label for="description">Description:</label>
	        <form:input path="description" id="description" required="required" />
	        <br>
	        <input type="submit" value="Add Todo" />
	    </form:form>
	</body>
</html>
  • Controller에서 @ModelAttribute로 지정한 속성명과 스프링 폼의 modelAttribute값이 동일해야, 바인딩이 된다.

  • 사용자가 JSP에서 스프링 폼 태그 라이브러리를 통해 입력한 데이터가, 스프링에 의해, Todo 객체의 description 속성에 자동으로 바인딩되어, 컨트롤러로 전달된다.

    1. 사용자가 /add-todo URL에 요청을 보냄.

    2. <form:form> 태그를 사용한 JSP에서 입력한 데이터(description)는 모델 속성인 todo에 자동으로 바인딩됨.(modelAttribute = "todo")

    3. modelAttribute="todo"로 설정된 모델 속성명과 일치하는 이름을 가진 자바 객체(Todo 클래스의 인스턴스)가 생성되고, 입력된 데이터가 해당 인스턴스의 필드에 자동으로 매핑됨.

      (form에서 입력한 todo의 description이 Todo.java에 해당 description 값이 들어간 인스턴스와 매핑)

    4. 컨트롤러@ModelAttribute("todo") 어노테이션이 적용된 메서드에 바인딩된 객체가 자동으로 전달된다.

      하지만, 컨트롤러 코드에 @ModelAttribute("todo")를 굳이 명시적으로 선언해주지 않아도, 스프링이 알아서 jsp와 바인딩된 todo객체를 알아서 컨트롤러 코드로 넘겨주기에, 굳이 강박적으로 명시할 필요 없다.

2. TodoController의 addNewTodoPage() 코드 리팩토링

@Controller
@SessionAttributes("name"){

// add-todo - GET
	@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
	public String showNewTodoPage() {

		return "addTodo";
	}

	// add-todo - POST
	@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
	public String addNewTodoPage(ModelMap model, Todo todo) {

		String username = (String) model.get("name");
		todoService.AddTodo(username, todo.getDescription(), LocalDate.now().plusYears(1), false);
        
		return "redirect:list-todos";
	}
}

ModelMap이 첫번째 파라미터가 되어야 하고, 두번째 파라미터가 바인딩될 Command Bean이 되어야 한다.

3. addTodo.jsp 코드 리팩토링

<html>
	<head>
		<link href = "webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel = "stylesheet">
		<title>Add Todo Page</title>
	</head>
	<body>
		<div class = "container">
			<h1>Enter Todo Details</h1>
			<form:form method = "post" modelAttribute="todo">
				Description : <form:input type = "text" path="description" name = "description" required = "required" /> <br>
				<input type = "submit" class = "btn btn-success"/>
			</form:form>
			<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
			<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
		</div>
	</body>
</html>

JSTL Core Tag Library의 문법과 거의 비슷하지만, 해당 스프링 tag library에서는 접두어로 form을 사용했다.

또한, <form:form>을 추가해 modelAttribute="todo"를 추가했다. 해당 코드로 사용자가 input태그를 통해 입력받은 description 필드를 (modelAttribute="todo" 코드를 보고), 모델 속성인 todo에 자동으로 바인딩하게 된다.

또한, <form:input>을 추가하면 path를 추가할 수 있게 된다. 해당 path를 참고해, HTML form 항목과 바인딩될 자바 객체의 properties를 지정할 수 있게 된다. 즉, 모델 속성명 todo와 일치하는 이름을 가진 자바 인스턴스(Todo 클래스의 인스턴스)가 생성되고, 입력한 데이터(description)이 해당 객체의 필드(Todo.java의 todo 객체의 description값)이 자동으로 매핑된다. -> 해당 과정은 앞서 정리한 단계별 과정을 살피면 더 이해가 쉽다.

그러나, /add-todo를 실행하고 나니,

에러가 났다.

해당 에러메세지의 “Neither BindingResult nor plain target object for bean name ‘todo’ available as request attribute”에 주목해보면, Bean이름 todo객체가 request 속성과 다르다는 에러메세지가 떴다.

그런데, 이 에러는 /add-todo URL에서 난 것이니, TodoController의 @RequestMapping /add-todo이자 GET 메서드를 사용하는 함수인 showNewTodoPage()메서드에서 에러가 났을 거라 추측할 수 있다.

addTodo.jsp에서 form 태그로 설정한 model todo가, showNewTodoPage()에 없다! 따라서, 해당 모델을 추가해준다. (앞서 정리한 단계에 살펴보면, 애초에 modelAttribute = "todo"에 해당하는 모델 todo를 컨트롤러 코드인 showNewTodoPage()에 존재하지 않으니, 처음부터 어긋난 것임)

에러 수정한 최종 코드 및 실행결과

TodoController의 수정된 showNewTodoPage(ModelMap model) 과 addTodo.jsp 코드

// TodoController.java
@Controller
@SessionAttributes("name")
public class TodoController {

	// add-todo - GET
	@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
	public String showNewTodoPage(ModelMap model) {

		String username = (String) model.get("name");
		Todo todo = new Todo(0, username, "", LocalDate.now().plusYears(1), false); // 새로운 Todo를 생성할 때마다의 디폴트 todo
		model.put("todo", todo);
		return "addTodo";
	}

	// add-todo - POST
	@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
	public String addNewTodoPage(ModelMap model, Todo todo) {

		String username = (String) model.get("name");
		todoService.AddTodo(username, todo.getDescription(), LocalDate.now().plusYears(1), false);
		return "redirect:list-todos";
	}
}

// addTodo.jsp
<html>
	<head>
		<meta charset="UTF-8">
		<link href = "webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel = "stylesheet">
		<title>Add Todo Page</title>
	</head>
	<body>
		<div class = "container">
			<h1>Enter Todo Details</h1>
			<form:form method = "post" modelAttribute="todo">
				Description : <form:input type = "text" path="description" name = "description" required = "required" /> <br>
				<input type = "submit" class = "btn btn-success"/>
			</form:form>
			<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
			<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
		</div>
	</body>
</html>

/add-todo URL request -> showNewTodoPage()메서드 실행 -> 새롭게 추가될 todo 인스턴스를 모델에 넣기 -> 모델에 넣은 todo 인스턴스를 addTodo.jsp로 보낸다.

addTodo.jsp -> 스프링 form tag library를 통해, 앞선 showNewTodoPage()에서 모델에 추가한 todo 객체addTodo.jsp에서 input 태그로 description 넣은 todo 객체를 바인딩한다. 즉, model 속성명 "todo"에 showNewTodoPage()의 디폴트 todo 객체에 addTodo.jsp에서 추가한 description이 들어가게 된다.

그러고 나면 스프링은, 해당 모델 속성 "todo"와 동일한 이름을 가진 컨트롤러의 메서드에 바인딩된 객체가 자동으로 전달된다.



실행결과, 몇가지 기본 검증이 실행된 것을 확인할 수 있었다.

"Validation failed for object = 'todo', Error count : 1"을 보니, id가 null로 들어오기에 에러가 난 것을 확인할 수 있었다.

해당 에러를 고치기 위해, 에러가 난 id에 대한 hidden변수를 만들어주었다. /add-todo에서 난 에러이니, addTodo.jsp를 수정했다.
하다보니, done 변수에 대해서도 해당 에러가 나서, done에 대한 hidden 변수 또한 만들어주었다.

hidden변수는 브라우저 화면에 id와 done이 보이지 않게 해준다.

// addTodo.jsp 수정 부분
<body>
		<div class = "container">
			<h1>Enter Todo Details</h1>
			<form:form method = "post" modelAttribute="todo">
				Description : <form:input type = "text" path="description" name = "description" required = "required" /> <br>
				<form:input type = "hidden" path="id"/> <br>
				<form:input type = "hidden" path="done"/> <br>
				<input type = "submit" class = "btn btn-success"/>
			</form:form>
			<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
			<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
		</div>
	</body>

form에서 hidden필드를 사용해, id와 form을 전송하게끔 한다. hidden 필드를 통해 id, done값을 전달하게 하면, id와 done필드는 폼에서 전송한 값으로 설정되어, null이 아닌 유효한 값이 되어, 검증 규칙을 통과할 수 있게 된다.

실행결과,


성공적으로 들어갔다!

마치며..

SpringBoot를 이용한 검증 4단계

1단계 : Spring Boot Starter Validation
2단계 : Command Bean (or Form Backing Object)

  • form tag library를 통해, 자바 객체 - HTML form 객체를 바인딩시켰다!

위의 4가지 검증단계 중, 2가지를 배웠다. 다음 포스팅에서는, 검증의 3,4단계를 마저 배울 것이다.




출처 : https://www.tcpschool.com/html-tag-attrs/input-required

이 시리즈는 Udemy 강의의 내용을 정리한 것입니다.
https://www.udemy.com/course/spring-boot-and-spring-framework-korean/

profile
배운 내용을 바로바로 기록하자!

0개의 댓글