'JAVA의 정석'의 저자 남궁성의 Spring 강의를 듣고 정리하였습니다.
source: https://github.com/castello/spring_basic/tree/main
@RequestParam
요청의 파라미터를 연결할 매개변수에 붙이는 애너테이션
참고
- 400번 에러: 클라이언트 에러
- 500번 에러: 서버 에러
@RequstParam(required=false) int year
http://localhost/ch2/requestParam8
파라미터 year은 요청시 필수 입력이 아니다. year을 안적을 경우 내부적으로 null로 값이 보내지는데, null은 int 값으로 변환될 수 없다.
클라이언트는 잘못한 것이 없고, 서버가 잘못한 것이므로(서버에서 에러 발생) 500번 에러이다.
http://localhost/ch2/requestParam8?year
year만 적을 경우 값은 "" 빈 문자열이 입력된다. year의 값은 int 타입이어야하는데 클라이언트가 빈 문자열을 입력했으므로 400번 에러이다.
@RequstParam(required=true) int year
http://localhost/ch2/requestParam8
파라미터 year은 요청시 필수 입력이다. year값을 적지 않고 보내줬으므로 클라이언트 에러(400번 에러)이다.
http://localhost/ch2/requestParam8?year
파라미터 year 입력시 빈 문자열을 입력했으므로 int 타입이 아니기때문에 클라이언트 에러(400번 에러)가 발생한다.
requestParam.java
package com.fastcampus.ch2;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class RequestParamTest {
@ExceptionHandler(Exception.class) //예외 발생시 처리
public String catcher(Exception ex) {
return "yoilError";
}
@RequestMapping("/requestParam")
public String main(HttpServletRequest request) {
String year = request.getParameter("year");
// http://localhost/ch2/requestParam ---->> year=null
// http://localhost/ch2/requestParam?year= ---->> year=""
// http://localhost/ch2/requestParam?year ---->> year=""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam2")
// public String main2(@RequestParam(name="year", required=false) String year) { // 아래와 동일
public String main2(String year) {
// http://localhost/ch2/requestParam2 ---->> year=null
// http://localhost/ch2/requestParam2?year ---->> year=""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam3")
// public String main3(@RequestParam(name="year", required=true) String year) { // 아래와 동일
public String main3(@RequestParam String year) {
// http://localhost/ch2/requestParam3 ---->> year=null 400 Bad Request. required=true라서
// http://localhost/ch2/requestParam3?year ---->> year=""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam4")
public String main4(@RequestParam(required=false) String year) {
// http://localhost/ch2/requestParam4 ---->> year=null
// http://localhost/ch2/requestParam4?year ---->> year=""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam5")
public String main5(@RequestParam(required=false, defaultValue="1") String year) {
// http://localhost/ch2/requestParam5 ---->> year=1
// http://localhost/ch2/requestParam5?year ---->> year=1
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
// =======================================================================
@RequestMapping("/requestParam6")
public String main6(int year) {
// http://localhost/ch2/requestParam6 ---->> 500 java.lang.IllegalStateException: Optional int parameter 'year' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.
// http://localhost/ch2/requestParam6?year ---->> 400 Bad Request, nested exception is java.lang.NumberFormatException: For input string: ""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam7")
public String main7(@RequestParam int year) {
// http://localhost/ch2/requestParam7 ---->> 400 Bad Request, Required int parameter 'year' is not present
// http://localhost/ch2/requestParam7?year ---->> 400 Bad Request, nested exception is java.lang.NumberFormatException: For input string: ""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam8")
public String main8(@RequestParam(required=false) int year) {
// http://localhost/ch2/requestParam8 ---->> 500 java.lang.IllegalStateException: Optional int parameter 'year' is present but cannot be translated into a null value due to being declared as a primitive type. Consider declaring it as object wrapper for the corresponding primitive type.
// http://localhost/ch2/requestParam8?year ---->> 400 Bad Request, nested exception is java.lang.NumberFormatException: For input string: ""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam9")
public String main9(@RequestParam(required=true) int year) {
// http://localhost/ch2/requestParam9 ---->> 400 Bad Request, Required int parameter 'year' is not present
// http://localhost/ch2/requestParam9?year ---->> 400 Bad Request, nested exception is java.lang.NumberFormatException: For input string: ""
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam10")
public String main10(@RequestParam(required=true, defaultValue="1") int year) {
// http://localhost/ch2/requestParam10 ---->> year=1
// http://localhost/ch2/requestParam10?year ---->> year=1
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
@RequestMapping("/requestParam11")
public String main11(@RequestParam(required=false, defaultValue="1") int year) {
// http://localhost/ch2/requestParam11 ---->> year=1
// http://localhost/ch2/requestParam11?year ---->> year=1
System.out.printf("[%s]year=[%s]%n", new Date(), year);
return "yoil";
}
} // class
한글 안깨지게 하는 방법
프로젝트/src/main/webapp/WEB-INF/web.xml 에 붙여넣기
<!-- 한글 변환 필터 시작 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 한글 변환 필터 끝 -->
로그 레벨 info -> trace
log4j.xml
더 자세한 로그를 볼 수 있다.
예외처리
✅ 필수입력 파라미터 존재시 예외처리하기
예외처리시에는 @ExceptionHandler 어노테이션 꼭 붙이기
✅ 필수 입력이 아닐 경우 default Value 지정하기
✅ 여러 변수를 필수 입력으로 지정하기
👍 여러 변수를 하나의 클래스로 묶어서 입력받으면 유지보수에 좋다.
myDate라는 하나의 클래스로 year, month, day 파라미터 입력을 받는다.
myDate.java
getter와 setter가 존재해야 jsp에서 myDate.year로 getter를 불러와 접근가능하다.
❓ 클래스에 요청 파라미터가 어떻게 1대 1로 매칭될 수 있을까?
SetterCall.java
package com.fastcampus.ch2;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.StringUtils;
public class SetterCall {
public static void main(String[] args) throws Exception{
Map<String, String> map = new HashMap<>();
map.put("year", "2021");
map.put("month", "10");
map.put("day", "1");
Class<?> type = Class.forName("com.fastcampus.ch2.MyDate");
// MyDate인스턴스를 생성하고, map의 값으로 초기화한다.
Object obj = dataBind(map, type);
System.out.println("obj="+obj); // obj=[year=2021, month=10, day=1]
} // main
private static Object dataBind(Map<String, String> map, Class<?> clazz) throws Exception {
// 1. MyDate 인스턴스 생성
// Object obj = clazz.newInstance(); // deprecated method
Object obj = clazz.getDeclaredConstructor().newInstance(new Object[0]);
// 2. MyDate인스턴스의 setter를 호출해서, map의 값으로 MyDate를 초기화
// 2-1. MyDate의 모든 iv를 돌면서 map에 있는지 찾는다.
// 2-2. 찾으면, 찾은 값을 setter로 객체에 저장한다.
Field[] ivArr = clazz.getDeclaredFields();
for(int i=0;i<ivArr.length;i++) {
String name = ivArr[i].getName();
Class<?> type = ivArr[i].getType();
// map에 같은 이름의 key가 있으면 가져와서 setter호출
Object value = map.get(name); // 못찾으면 value의 값은 null
Method method = null;
try { // map에 iv와 일치하는 키가 있을 때만, setter를 호출
if(value==null) continue;
method = clazz.getDeclaredMethod(getSetterName(name), type); // setter의 정보 얻기
System.out.println("method="+method);
method.invoke(obj, convertTo(value, type)); // obj의 setter를 호출
} catch(Exception e) {
e.printStackTrace();
}
}
System.out.println(Arrays.toString(ivArr));
return obj;
}
private static Object convertTo(Object value, Class<?> type) {
// value의 타입과 type의 타입이 같으면 그대로 반환
if(value==null || type==null || type.isInstance(value))
return value;
// value의 타입과 type이 다르면, 변환해서 반환
if(String.class.isInstance(value) && type==int.class) // String -> int
return Integer.valueOf(""+value);
return value;
}
// iv의 이름으로 setter의 이름을 만들어서 반환하는 메서드("day" -> "setDay")
private static String getSetterName(String name) {
// return "set"+name.substring(0,1).toUpperCase()+name.substring(1);
return "set" + StringUtils.capitalize(name); // org.springframework.util.StringUtils
}
}
/*
[실행결과]
method=public void com.fastcampus.ch2.MyDate.setYear(int)
method=public void com.fastcampus.ch2.MyDate.setMonth(int)
method=public void com.fastcampus.ch2.MyDate.setDay(int)
[private int com.fastcampus.ch2.MyDate.year, private int com.fastcampus.ch2.MyDate.month, private int com.fastcampus.ch2.MyDate.day]
obj=[year=2021, month=10, day=1]
*/
이런 방식으로 매칭된다.
@ModelAttribute
적용 대상을 Model의 속성으로 자동 추가해주는 애너테이션
반환 타입 또는 컨트롤러 메서드의 매개변수에 적용 가능
1. 컨트롤러 메서드의 매개변수
@ModelAttribute의 myDate 파라미터 이름을 key로, date를 value로 하여 model에 저장된다.
(@ModelAttribute("myDate")를 보면 이름이 명시되어 key값으로 저장된다.
이름을 명시하지 않을 경우 타입의 첫글자를 소문자로 한 key 값으로 저장된다.)
❄️ 사실 @ModelAttribute는 생략 가능하므로 모델에 addAttribute로 매개변수값을 저장하지 않아도 된다.
컨트롤러 매개변수
기본형, String - @RequestParam 생략
참조형 - @ModelAttribute 생략
(MyDate mydate는 참조형이므로 @ModelAttribute 생략)
🐯 참조형은 기본 type(8가지)를 제외한 type
2.반환타입
getYoil 함수를 보면 getYoil 함수 수행 후 key가 yoil이고 반환값이 value로 model에 저장된다.
✅ 모델에 값을 저장하는 코드가 줄어든다.
@WebDataBinder
브라우저를 통해 요청받은 값이 실제 객체에 바인딩될때 중간 다리 역할
1. 타입 변환 후 2. 데이터 검증(Validation)을 진행한다.
타입 변환은 String으로 들어온 value 값을 각각의 type으로 변환하기 위해 필요하다.
데이터 검증은 들어온 value값이 유효한지, 예를 들어 month에서 값이 1과 12 사이인 유효값인지 확인하기위해 필요하다.
타입 변환과 데이터 검증마다 결과와 오류는 BindingResult에 저장된다.
그 결과는 Controller에 넘겨진다.
❕ Controller 메서드에서 BindingResult 파라미터는 바인딩할 객체(MyDate date) 바로 뒤에 위치해야한다.
회원 가입 화면 작성하기 - html
main/webapp/resources/registerForm.html
🫧 url에 resources 적지 않고 싶을때
servlet-context.xml : web 관련 설정파일
root-context.xml : non-web 관련 설정파일
servlet-context.xml
mapping url에 /**로 적기
전송 방식
GET 방식으로 보냈을때
입력값이 url에 공개된다.
POST 방식으로 보냈을때
url에 입력값이 공개되지 않는다. (body에 숨겨진다.)
✅ 대부분의 form은 POST 방식으로 전송한다.
onsubmit 결과
- true일 경우 form 전송
- false일경우 form 전송X
회원 가입 화면 작성하기 - Spring
1. Controller 만들기
2. jsp 파일 생성
아까 만든 회원 등록 html파일을 jsp 파일로 변경하기
registerForm.jsp
<%@ page contentType="text/html;charset=utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css" />
<style>
* { box-sizing:border-box; }
form {
width:400px;
height:600px;
display : flex;
flex-direction: column;
align-items:center;
position : absolute;
top:50%;
left:50%;
transform: translate(-50%, -50%) ;
border: 1px solid rgb(89,117,196);
border-radius: 10px;
}
.input-field {
width: 300px;
height: 40px;
border : 1px solid rgb(89,117,196);
border-radius:5px;
padding: 0 10px;
margin-bottom: 10px;
}
label {
width:300px;
height:30px;
margin-top :4px;
}
button {
background-color: rgb(89,117,196);
color : white;
width:300px;
height:50px;
font-size: 17px;
border : none;
border-radius: 5px;
margin : 20px 0 30px 0;
}
.title {
font-size : 50px;
margin: 40px 0 30px 0;
}
.msg {
height: 30px;
text-align:center;
font-size:16px;
color:red;
margin-bottom: 20px;
}
.sns-chk {
margin-top : 5px;
}
</style>
<title>Register</title>
</head>
<body>
<form action="<c:url value="/register/save"/>" method="POST" onsubmit="return formCheck(this)">
<div class="title">Register</div>
<div id="msg" class="msg"> </div>
<label for="">아이디</label>
<input class="input-field" type="text" name="id" placeholder="8~12자리의 영대소문자와 숫자 조합">
<label for="">비밀번호</label>
<input class="input-field" type="text" name="pwd" placeholder="8~12자리의 영대소문자와 숫자 조합">
<label for="">이름</label>
<input class="input-field" type="text" name="name" placeholder="홍길동">
<label for="">이메일</label>
<input class="input-field" type="text" name="email" placeholder="example@fastcampus.co.kr">
<label for="">생일</label>
<input class="input-field" type="text" name="birth" placeholder="2020/12/31">
<div class="sns-chk">
<label><input type="checkbox" name="sns" value="facebook"/>페이스북</label>
<label><input type="checkbox" name="sns" value="kakaotalk"/>카카오톡</label>
<label><input type="checkbox" name="sns" value="instagram"/>인스타그램</label>
</div>
<button>회원 가입</button>
</form>
<script>
function formCheck(frm) {
var msg ='';
if(frm.id.value.length<3) {
setMessage('id의 길이는 3이상이어야 합니다.', frm.id);
return false;
}
return true;
}
function setMessage(msg, element){
document.getElementById("msg").innerHTML = `<i class="fa fa-exclamation-circle"> ${'${msg}'}</i>`;
if(element) {
element.select();
}
}
</script>
</body>
</html>
위 두 줄 html에 추가하기(jsp 형식으로 만들기)
<c:url>
1. context root 자동 추가
2. session id 자동 추가
registerInfo.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>id=${param.id}</h1>
<h1>pwd=${param.pwd}</h1>
<h1>name=${param.name}</h1>
<h1>email=${param.email}</h1>
<h1>birth=${param.birth}</h1>
<h1>sns=${paramValues.sns}</h1>
<h1>sns=${paramValues.sns[0]}</h1>
<h1>sns=${paramValues.sns[1]}</h1>
<h1>sns=${paramValues.sns[2]}</h1>
</body>
</html>
결과
@GetMapping
@RequestMapping(value="/register/add", method=RequestMethod.GET)
@GetMapping("/register/add")
두 줄은 같은 의미이다.
RegisterController.java
@Controller
public class RegisterController {
@GetMapping("/register/add")
public String register() {
return "registerForm"; //WEB-INF/views/registerForm.jsp
}
}
위 코드를 보면 RegisterController는 view를 보여주는 기능만 한다.
이럴 경우 따로 ViewController로만 등록할 수 있다.
위 코드를 적지 않고,
src/main/webapp/WEB-INF/srping/appServlet/servlet-context.xml
view controller만 등록해놓으면 된다.
❕view controller는 GET 요청만 가능하다.
@PostMapping
@RequestMapping(value="/register/save", method=RequestMethod.POST)
@PostMapping("/register/save")
두 줄은 같은 의미이다.
✅ /register/save에서 get 요청을 막은 이유는 파라미터로 회원가입시 후에 회원가입 자동화등 문제가 생길 수 있기 때문이다.
ex) http://localhost:8080/ch2/register/save?id=sdf&pw=32112!
post 메서드로만 회원가입 허용
@GetMapping 과 @PostMapping
클래스에 붙이는 @RequestMapping
@RequestMapping의 URL 패턴
URL 패턴
Servlet - @WebServlet
Spring - @RequestMapping
RequestMappingTest.java
package com.fastcampus.ch2;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RequestMappingTest {
// @RequestMapping({"/login/hello.do", "/login/hi.do"})
@RequestMapping("/login/hello.do") // http://localhost/ch2/login/hello.do
public void test1(){
System.out.println("urlpattern=/login/hello.do");
}
@RequestMapping("/login/*") // /login/hello, /login/hi
public void test2(){
System.out.println("urlpattern=/login/*");
}
@RequestMapping("/login/**/tmp/*.do") // /login/tmp/hello.do, /login/aaa/tmp/hello.do
public void test3(){
System.out.println("urlpattern=/login/**/tmp/*.do");
}
@RequestMapping("/login/??")
public void test4(){ // /login/hi, /login/my.car
System.out.println("urlpattern=/login/??");
}
@RequestMapping("*.do") // /hello.do, /hi.do, /login/hi.do
public void test5(){
System.out.println("urlpattern=*.do");
}
@RequestMapping("/*.???") // /hello.aaa, /abc.txt
public void test6(){
System.out.println("urlpattern=*.???");
}
}
URL Encoding (퍼센트 인코딩)
Base64 vs URL Encoding
Base64 : 6 bit를 기준으로 쪼갠다. 바이너리 데이터 -> Text로 변환
URL Encoding: 문자코드(숫자)를 문자열로 바꾼다.
request가 UTF-8로 인코딩되어있으면 getParameter시 UTF-8로 디코딩하여 value를 얻는다.
실습
한글을 get 방식으로 보내면 url에 한글이 encoding되어 나온다.
만약 url로 한글 내용을 보내고 싶다면 url encoding을 해주어야 한글이 깨지지않고 보내진다.
메세지 보여줄때 디코딩
유효하지않을때 결과
url에서 한글이 깨지지않는다.
🐬 메세지를 모델에 담아 보내는 방법도 있다.
redirect는 재전송이므로 모델의 값을 다시 보낸다.
redirect 와 forward
redirect(재요청)
요청과 응답이 각각 2번 수행된다.
client가 보낸 첫번째 요청, 재요청으로 자동으로 수행된 두번째 요청
ex) 카드사 분실신고를 하려고했는데 한도조회 페이지로 간다면 재요청 해야한다. (고객은 요청 2번)
✅ 두번째 요청은 GET 요청
❕ 300 Redirect 요청은 응답헤더만 존재하고 body는 존재하지 않는다.
forward(전달)
고객이 잘못 요청했는데 한도조회 부서가 분실신고 부서로 이동시켰다.
고객은 요청 1번만 해도 수행결과를 얻을 수 있다. (요청 2번 안해도 됨)
RedirectView
요청이 오면 Servlet은 Controller가 처리
만약 redirect:/~~를 반환한다면 RedirectView에게 전달
RedirectView는 위이 응답헤더를 만들어서 보낸다.
브라우저는 재요청을 한다.
JstlView
jsp 뷰를 처리
Controller가 registerForm을 보내면 Servlet은 InternalResourceViewResolver에게 보내서 이 view가 어떤 view인지 해석한다.
ex) WEB-INF.views/registerForm.jsp
그 이름을 JstlView에게 넘기고, registerForm.jsp가 응답을 만들어 보낸다.
InternalResourceView
포워딩 후 JstlView와 같은 방식으로 처리된다.
forward 예시
✅ redirect와 달리 forward는 요청이 하나임 - url 안바뀜
redirect 실습
save 후 redirect
결과화면
요청 보낸 후 /url이 register/save에서 /register/add로 바뀌어있다.
302: redirect
200 : OK
forward 실습
✅ /register/save에서 /register/add로 포워드시에 post 요청이 그대로 전달되므로
/register/add가 POST method도 요청받을 수 있도록 설정하기
(view controller는 get 요청만 받을수 있으므로 수정하기)
결과화면
요청 보낸 후에도 url이 /register/save로 같다.
'Spring > 카테캠 - TIL' 카테고리의 다른 글
TIL [0531 - 0604] : 대용량 처리를 위한 MySQL 이해 (0) | 2023.06.03 |
---|---|
TIL [0526-0528] Spring MVC 4 (5) | 2023.05.28 |
TIL [0519-0520] Spring MVC 2 (2) | 2023.05.21 |
TIL [0511- 0514] : Spring Basic + Spring MVC 1 (2) | 2023.05.14 |
TIL [0504-0505] : 객체지향 프로그래밍(예외처리, 로그, IO 스트림, 직렬화) (0) | 2023.05.06 |