🦁멋쟁이사자처럼 백엔드 부트캠프 13기 🦁
TIL 회고 - [38]일차
🚀38일차에는 LocalDate 클래스와 GET/POST 방식에 대한 실습을 진행하고, 포워딩와 리다이렉팅, 쿠키까지 배울 수 있었다.
특히 GET/POST방식과 포워딩/리다이렉팅 부분을 이해하는 것에서 공부가 필요할 것 같아 회고를 통해 더 이해해봐야겠다.
Thymeleaf - 조건표현식의 status
- status : 인덱스로 참조 가능
ExampleController에 (/list) URL 추가
@GetMapping("/list")
public String showList(Model model){
List<String> items = Arrays.asList(
"Item 1", "Item 2", "Item 3", "Item 4", "Item 5",
"Item 6", "Item 7", "Item 8", "Item 9", "Item 10"
);
model.addAttribute("itemList", items);
return "list";
}
<h1>List Example</h1>
<ul>
<li th:each="item, status : ${itemList}"
th:if="${status.index >= 2 and status.index < 7}"
th:text="'아이템 : ' + ${item}">
</li>
</ul>
- th:each=”item, status : ${items}”
➡️item : 객체를 꺼내서 넣어줄 것
➡️status : 상태를 추상화한 변수로 (인덱스)를 꺼내서 넣어줄 것
- status.index >= 2 and status.index < 7
➡️index가 2부터 6까지 출력
아이템은 총 10개이지만 리스트의 인덱스 규칙에 따라 Item 1 = index 0, Item 2 = index 1… Item 10 = index 9 이므로
status.index ≥ 2 : Item 3부터 출력
status.index < 7 : Index가 6인 Item 7까지 출력
LocalDateTime
- java.time 패키지의 클래스 사용 (날짜/시간 사용 패키지 제공)
- LocalDate.now()
➡️현재 날짜만 출력 가능 (ex. 2025-01-24) - LocalTime.now() ➡️현재 시간만 출력 가능
- LocalDateTime.now() ➡️현재 날짜/시간 출력 가능
- LocalDate firstDate = LocalDate.of(2025,1,1);
➡️ LocalDate 타입의 firstDate 변수로 LocalDate.of()를 넣음 - System.out.println(firstDate.plusDays(100));
➡️firstDate날짜로부터 100일 후의 값을 출력 가능 - ZonedDateTime now = ZonedDateTime.now(); ➡️Asia/Seoul 출력
- ZonedDateTime.now(ZoneId.of(""));
➡️사용할 수 있는 ZoneId 중 골라서 지역의 시간을 가져올 수 있음
다만 ZoneID는 상수로 제공하고 있지 않으므로 getAvailableZoneIds()로 가져올 수 있음
for(String zoneId : zoneIds){
System.out.println(zoneId);
}
- Set<String> zoneIds = ZonedId.getAvailableZoneIds()
➡️얻을 수 있는 모든 ZonedId를 Set 형태로 리턴하고 있으므로 Set형태의 문자열로 받아서 출력 가능
Thymeleaf + 날짜 출력
ExampleController에 (/datetime) URL 추가
@GetMapping("/datetime")
public String showDatetime(Model model){
model.addAttribute("currentDate", LocalDate.now());
model.addAttribute("currentDateTime", LocalDateTime.now());
model.addAttribute("currentTime", LocalTime.now());
model.addAttribute("currentZonedDateTime", ZonedDateTime.now(ZoneId.of("Asia/Seoul")));
return "datetime";
}
<body>
<h1>날짜 출력기</h1>
<p>
current Date : <span th:text="${currentDate}"></span><br>
current DateTime : <span th:text="${currentDateTime}"></span><br>
current Time : <span th:text="${currentTime}"></span><br>
current ZonedDateTime : <span th:text="${currentZonedDateTime}"></span>
</p>
</body>
Thymeleaf + 날짜 출력 (Format 사용)
${#temporals.format(currentDateTime, ‘yyyy-MM-dd HH:mm:ss’)}
- currentDateTime의 형식을 지정 가능
- 공식문서에서 알려주고 있는 표현방식
- “현재 날짜/시간(포맷버전)” 을 추가하여 지정한 포맷의 출력을 확인
Validation 유효성 검사
implementation 'org.springframework.boot:spring-boot-starter-validation'
- Validation사용을 위해 build.gradle에 의존성 추가
<form action="" method="post">
<input type="text">
<button type="submit">제출하기</button>
</form>
- method는 GET이 default이다.
- 폼데이터가 서버에 전송될때 이 form안에 들어있는 값들을 모두 전송할 것 (type이 submit이어야함)
domain/UserForm 클래스
@Getter
@Setter
public class UserForm {
@NotEmpty(message = "username은 공백을 허용하지 않습니다.")
@Size(min = 5, max = 20, message = "username 은 5-20자까지만 허용합니다.")
private String username;
@NotEmpty(message = "비밀번호는 공백을 허용하지 않습니다.")
@Size(min = 6, message = "비밀번호는 최소 6자 이상 입력해야합니다.")
private String password;
}
- @NotEmpty(message = "...")
➡️공백을 허용하지 않음. 공백값이 들어온다면 들어올 메시지를 설정
@NotEmpty는 문자열 중간이나 앞뒤의 공백을 포함한 입력값을 찾아내지 않고 공백만으로 이루어진 문자열을 찾게 된다.
만약 중간 공백을 찾아내고 싶으면 @Pattern 어노테이션을 사용하는 방법도 있다. - @Size(min = 5, max = 20, message = "...")
➡️아이디의 길이를 최소 5글자, 최대 20글자까지 허용할 것, 조건에 맞지 않으면 출력될 메시지를 설정 - if - else문으로 각 ID와 비밀번호의 유효성을 검사하는 부분을 @NotEmpty, @Size 어노테이션들이 대체
➡️validation 의존성 추가로 인한 추가된 어노테이션들
🚀실습 - @NotEmpty + @Pattern사용으로 공백 검사
@Getter
@Setter
public class UserForm {
@NotEmpty(message = "username은 공백을 허용하지 않습니다.")
@Size(min = 5, max = 20, message = "username은 5-20자까지만 허용합니다.")
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "username은 공백 없이 영문자와 숫자만 허용합니다.")
private String username;
@NotEmpty(message = "비밀번호는 공백을 허용하지 않습니다.")
@Size(min = 6, message = "비밀번호는 최소 6자 이상 입력해야합니다.")
private String password;
}
폼 요청 전달 과정
첫번째 요청 - 아이디, 패스워드 폼 전송 요청
@GetMapping("/form")
public String showForm(Model model){
model.addAttribute("userForm", new UserForm());
return "form";
}
- 아이디와 패스워드의 폼을 보여달라는 요청에 대한 URL을 만든다.
두번째 요청 - 아이디, 패스워드 데이터를 담아 전송하는 요청
@PostMapping("/submitForm")
public String submitForm(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult result){
if(result.hasErrors()){ // 만약 result가 에러를 가졌으면 다시 "form"으로
return "form"; // 유효성 검사 실패 시 다시 Form View로 리턴하는 것
}
return "result"; // 유효성 검사 성공 시 "결과 페이지"로 리다이렉트
}
- @PostMapping("/submitForm")
Post방식으로 받아오는데, form은 <form action="" method="POST"> 방식으로 요청 방식을 일치시켜야함 - @Valid
➡️유효성을 검사하는 부분 - @ModelAttribute("userForm")
➡️userForm을 받아와서 UserForm userForm에 넣어준다. - BindingResult result
➡️오류가 발생했을 경우 result 객체에 오류 내용이 담긴다.
❓@PostMapping과 @GetMapping의 차이점
➡️@PostMapping
form이 들어있는 HTML에서 method="post" 작성 후 컨트롤러의 @PostMapping과 연결
method="post" : HTML 표준으로, 폼 데이터를 HTTP POST 메소드로 전송
@PostMapping("/submitForm") : 스프링에서는 컨트롤러에 POST 요청에 대한 처리를 명시
이 요청 방식이 일치해야 폼 데이터가 정상적으로 컨트롤러에 전달됨
장점 :
- 요청 본문에 데이터를 포함하므로 URL에는 아이디, 비밀번호 등이 노출되지 않음
- 로그인, 회원가입, 데이터 생성 등처럼 데이터를 변경하는 작업에 적합
➡️@GetMapping
@GetMapping은 HTTP GET 메서드 요청만 처리하므로
컨트롤러에서 @GetMapping으로 지정하면 폼 태그에서 method="post"로 설정했기 때문에
POST 요청이 GET 요청만 처리하는 컨트롤러에 전달되지 않아서 매핑되지않는 오류 발생
단점 :
- 모든 데이터가 URL에 포함되므로 아이디, 비밀번호 등 전달에는 적합하지 않음
- GET 요청은 브라우저에서 캐싱될 가능성으로 데이터를 변경하는 작업보다는 (검색, 조회)와 같은 작업에 적합
아이디, 패스워드 폼 HTML
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>회원가입 기초 세팅</title>
</head>
<body>
<form action="submitForm" method="post">
<input type="text" name="username"/>
<input type="password" name="password"/>
<button type="submit">제출하기</button>
</form>
</body>
</html>
- form태그의 action부분에 “submitForm”을 추가해주어 연결
➡️@GetMapping("/submitForm)을 사용하는 컨트롤러와 매핑
result.html - 결과 페이지
<head>
<meta charset="UTF-8">
<title>로그인 성공 결과 페이지</title>
</head>
<body>
<h1>성공적으로 입력하였습니다.</h1>
</body>
- 결과가 출력되었을때 표시될 페이지
아이디, 패스워드 폼 HTML + Thymeleaf
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>회원가입 기초 세팅</title>
</head>
<body>
<form th:action="@{/exam/submitForm}" th:object="${userForm}" method="post">
<label for="username">username</label>
<input type="text" th:field="*{username}" id="username"/>
<div th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>
<label for="password">password</label>
<input type="password" th:field="*{password}" id="password"/>
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>
<button type="submit">제출하기</button>
</form>
</body>
</html>
- <label의 for>와 <input의 id>로 두 태그를 연결한다.
이렇게 연결하게되면 사용자가 <label>을 클릭 시 자동으로 해당 입력창이 활성화됨 - th:object="${userForm}
➡️모델 객체를 폼과 연결하여 입력 데이터를 바인딩 가능
➡️th:object : Thymeleaf의 바인딩 객체를 지정하는 기능
➡️${userForm} : 컨트롤러에서 제공된 모델 객체 - method ="post"
➡️POST방식으로 보안성이 뛰어나 로그인과 같은 작업에 적합
➡️따라서 주로 폼 데이터를 안전하게 제출할 때 사용 - th:action="@{/exam/submitForm}"
➡️Thymeleaf 문법을 사용하여 컨트롤러의 URL 경로를 동적으로 매핑
➡️애플리케이션의 경로가 변경되거나 컨텍스트 루트가 추가되더라도 자동으로 업데이트하여 유지보수가 쉬움
➡️"@{/exam/submitForm}" : 절대경로로 URL을 지정한 것
/exam/submitForm URL 경로로 인해 브라우저에서 폼 데이터 제출 시 이 URL로 요청이 전송 - th:field="*{username}"
➡️기존 HTML 코드에서 name 속성을 사용해 데이터를 전송했지만
➡️Thymeleaf를 통해 모델 객체(userForm)와 폼 데이터를 자동으로 바인딩
➡️Thymeleaf의 field 사용으로 컨트롤러에서 해당 모델 객체를 자동으로 매핑할 수 있어
데이터 매핑 시 오류를 줄일 수 있음
➡️*{username} : userForm 객체의 속성과 바인딩하는 것
사용자가 필드에 "Hello"를 입력하면 userForm.getUsername()를 통해 값을 얻어올 때 "Hello"로 설정
이 field를 통한 바인딩을 통해 양방향 데이터 바인딩을 수행
- 방향 1) 입력 필드에 값을 표시 (userForm.username 값을 읽어서 표시)
- 방향 2) 제출 시 값을 모델 객체에 저장 - th:errors="*{username}"
➡️ 기존 HTML 코드에서 폼 데이터를 수동으로 검증해야했다면
➡️Thymeleaf를 통해 th:errors 와 @Valid (=유효성 검증 어노테이션)을 통해 에러 메시지를 UI에 쉽게 출력합니다. - th:if="${#fields.hasErrors('username')}"
➡️에러 발생 시 메시지 출력을 보여줄 공간(div태그)를 만듦
➡️#fields: Thymeleaf에서 제공하는 객체로, 현재 바인딩된 필드의 유효성 검사 상태를 확인함
➡️컨트롤러의 BindingResult로부터 오류 정보를 꺼내왔을때
⚠️오류가 발생했으면 true가 리턴되고 th:errors="*{username}" 문구를 실행할 것이다.
(그 결과 가져온 오류 정보를 th:errors를 이용해 *{username}에 해당하는 에러를 출력하는 것)
⚠️오류가 발생하지 않았으면 false가 리턴되어 th:errors="*{username}" 문구가 실행되지 않는다. - button type="submit"
➡️ 버튼 클릭 시 폼 데이터가 전송됨
th:action으로 설정한 /exam/submitForm URL로 POST 요청이 발생하는 것
정리하자면
유효성 검사의 흐름은
➡️입력값 -> 컨트롤러 -> 모델 검증 (@Valid) -> 결과 (BindingResult) -> UI 표시
❓th:action에서 상대경로와 절대경로의 차이점
➡️상대경로 = th:action="@{submitForm}"
URL의 상대경로를 지정하는 경우이며, 현재 페이지의 경로를 기준으로 submitForm에 접근하므로 사용중인 경로에 따라
URL을 찾는 과정이 달라질 수 있다.
ex. 현재 경로가 /exam이면 /exam/submitForm으로 전달되겠지만
/newexam 이면 /newexam/submitForm으로 전달되기때문에 단순한 구조의 프로젝트나 URL 변경 가능성이 낮을때 사용
➡️절대경로 = th:action="@{/exam/submitForm}"
URL의 절대경로를 지정하는 경우이며, 애플리케이션의 루트 경로(/)로부터 시작하므로
항상 정확한 URL로 요청이 전송된다.
따라서 URL충돌을 방지할 수 있고 프로젝트 구조 변경 시에도 안정적으로 URL을 찾을 수 있게된다.
@ModelAttribute
- Spring MVC에서 Model 객체의 데이터를 메소드 파라미터나 메소드 레벨에 바인딩 하는데 사용되는 어노테이션
- 이 어노테이션은 폼 데이터를 객체에 바인딩하거나 모델에 데이터를 추가하는데 유용
➡️즉 Spring MVC에서 사용자가 전달한 요청 데이터를 폼 객체(Form Object)에 매핑해주는 역할 - 이 어노테이션은 명시적으로 사용할 수도 있고, 생략도 가능.
생략하게되면 스프링이 자동으로 처리하게됨
⚠️하지만 가독성과 일관성을 위해 @ModelAttribute를 명시적으로 추가하는 것이 권장됨
@ModelAttribute 활용방법
첫번째 방법 - 메소드의 파라미터로 사용
@PostMapping("/register")
public String registerUser(@ModelAttribute User user){
// user 객체는 폼 데이터에 자동으로 바인딩
// userService.save(user);
return "redirect:/users";
}
두번째 방법 - 메소드 레벨에서 사용
- 이 방법은 모델에 데이터를 추가하기 위해 사용하며, 이 @ModelAttribute 어노테이션이 적용된 메소드는
해당 컨트롤러의 다른 모든 요청 메소드가 호출되기 전에 실행된다. - 활용 : 모든 요청에 공통적으로 모델 데이터를 초기화하고자할때 사용
@ModelAttribute
public void addAttributes(Model model){
model.addAttribute("msg", "Welcome to the Application!");
}
- 다른 곳에서 @ModelAttribute 를 매개변수로 사용할때마다 출력할 메시지를 설정할 수 있다는 것
1. 기존코드 @RequestParam 어노테이션 사용
@PostMapping("/submitForm")
public String submitForm(@RequestParam("username") String username, @RequestParam("password") String password){
{
System.out.println(username + " ::::: " + password);
return "result"; // 유효성 검사 성공 시 "결과 페이지"로 리다이렉트
}
- @RequestParam(”username”)
➡️username 속성을 꺼내어 String username에 넣음
2. @ModelAttribute만 사용
@PostMapping("/submitForm")
public String submitForm(@ModelAttribute("userForm") UserForm userForm){
System.out.println(userForm.getPassword() + " :::: " + userForm.getUsername());
return "result"; // 유효성 검사 성공 시 "결과 페이지"로 리다이렉트
}
- @ModelAttribute(”userForm”)
➡️@RequestParam처럼 속성을 꺼내는 것이 아닌 객체 자체를 꺼내와서 UserForm타입의 userForm에 넣음
➡️객체 자체를 꺼내왔으므로 userForm.getUsername()처럼 username 속성에 접근할 수 있음
3. @ModelAttribute + @Valid (유효성 검사)사용
@PostMapping("/submitForm")
public String submitForm(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult result){
if(result.hasErrors()){ // 만약 result가 에러를 가졌으면 다시 "form"으로
return "form"; // 유효성 검사 실패 시 다시 Form View로 리턴하는 것
}
return "result"; // 유효성 검사 성공 시 "결과 페이지" 출력
}
- BindingResult result
➡️유효성 검사(@Valid)와 함께 사용하여 오류가 발생했을 시에 result에 오류 정보가 담김 - if(result.hasErrors())
➡️만약 result 객체에 오류 정보가 담겨있으면 유효성 검사가 실패한 것이므로 form 뷰를 다시 보여주게함 - return "result";
➡️유효성 검사가 성공했으면 result 뷰를 보여줌으로써 결과페이지를 출력
@ModelAttribute의 주요 목적
- 요청 데이터 바인딩 : 요청 파라미터를 객체에 바인딩하고, 해당 객체를 컨트롤러 메소드에서 사용할 수 있도록 함
- 모델 초기화 : 공통 모델 데이터를 모든 뷰에서 사용할 수 있도록 초기화함
- 장점 : 개발자는 모델 데이터 관리를 더 효과적으로 하고 코드의 반복을 줄이며, 요청 데이터 처리를 간소화 가능
포워딩 / 리다이렉팅
포워딩
- 포워딩으로 반환하면 "페이지가 바뀐다 하더라도 URL은 바뀌지 않을 수 있다"
➡️서버 내부에서 뷰를 렌더링하기 때문에 사용자가 보는 URL은 바뀌지 않아 사용자 입장에선 내부 처리 과정을 모름
@PostMapping("/register")
public String registerUser(@ModelAttribute User user){
userService.save(user);
return "users" // 포워딩
}
- @ModelAttribute User user
➡️HTML 폼에서 전송된 데이터를 User 객체로 바인딩
ex. 사용자가 name, email 등의 정보를 폼에 입력하면 이를 User 객체에 자동으로 매핑 - userService.save(user);
userService를 통해 전달된 user 객체를 데이터베이스에 저장
userService : 비즈니스 로직을 처리하는 서비스 계층의 객체로, 저장 로직이 포함됨 - return "users";
뷰 리졸버(View Resolver)를 통해 return "users"값을 실제 HTML 파일로 매핑
Thymeleaf 사용 시 resources/templates/users.html 파일을 찾음
redirect:라는 접두어가 없으면 기본적으로 포워딩 - 총 요청이 1번 발생
➡️이 요청은 특정 뷰 페이지나 다른 컨트롤러로 요청을 넘겨줌
즉 클라이언트는 URL이 변경되지 않은 상태로 동일한 요청안에서 응답을 받게됨
정리하자면 포워딩은 서버내부에서 처리되어 요청이 1번인 것 - 활용 : 데이터를 숨기고 간단히 처리가 가능할때 사용, 요청 간 데이터를 공유할때 사용
리다이렉팅
@PostMapping("/register")
public String registerUser(@ModelAttribute User user){
userService.save(user);
return "redirect:/users"; // 리다이렉팅
}
- return "redirect:/users";
➡️요청 처리가 완료된 후 클라이언트를 /users 경로로 리다이렉팅(redirect)
리다이렉팅 : 서버가 클라이언트에게 새로운 URL로 다시 요청하라는 명령을 내리는 것
뷰 리졸버를 거치지 않고 클라이언트에게 /users 경로로 리다이렉트 명령을 다시 보냄
그 결과 /users 경로에 매핑된 컨트롤러 메소드가 실행되어 렌더링할 HTML 파일을 결정 - 브라우저는 서버의 응답을 받고 /users URL로 다시 요청을 보냄
- 총 요청이 2번 발생
➡️1번째 요청: /register로 폼 데이터 제출
➡️2번째 요청: /users로 브라우저가 새롭게 요청
정리하자면 리다이렉팅는 클라이언트가 URL을 새로 요청하면서 요청이 2번인 것 - 클라이언트가 서버의 명령을 받아 새로운 URL로 이동하게되어 브라우저의 URL이 변경됨
- 활용 : 명시적으로 경로를 보여줄 때 사용, 요청 처리를 완료 후 안전하게 새 요청을 유도하고자할때 사용
다시 정리
포워딩과 리다이렉팅는 users.html이라는 뷰 페이지를 렌더링하고 사용자에게 출력하는 것처럼
출력 결과가 같더라도 내부 처리 방식이 다르다.
리다이렉팅 (Redirect)
동작 방식
1. 사용자가 /register 경로로 POST 요청을 보냄
2. 서버에서 요청 처리 후 클라이언트에게 "다시 /users로 요청하라"고 응답
3. 사용자(브라우저)가 /users 경로로 새롭게 GET 요청을 보냄 (기존 요청 데이터는 사라짐)
4. 서버는 /users 요청에 따라 적절한 HTML(ex. users.html)을 반환
요청이 2번 발생(POST → GET)하므로
브라우저의 URL이 변경됨 (/register -> /users)
새로고침 시 마지막 요청(/users)이 다시 실행
포워딩 (Forward)
동작 방식
1. 사용자가 /register 경로로 POST 요청을 보냄
2. 서버에서 요청 처리 후 users.html 파일을 찾아 바로 렌더링 (같은 요청 내에서 데이터 유지 가능)
3. 서버가 렌더링한 HTML을 클라이언트에 응답으로 반환
요청이 1번만 발생 (POST 요청만)하므로
브라우저의 URL이 변경되지 않음 (계속 /register로 표시)
새로고침 시 동일 POST 요청이 다시 실행될 가능성이 있음
@Valid
- Java Bean Validation API와 통합하여 모델 객체에 선언된 제약조건을 기반으로 데이터 검증을 수행
- 컨트롤러 메소드의 파라미터에 이 어노테이션을 사용하면, 스프링이 해당 객체가 바인딩될때 자동으로 검증을 수행
BindingResult
- @Valid 어노테이션 or @Validated 어노테이션과 함께 사용하여 데이터 바인딩과 검증 후 발생하는
오류를 포착하고 관리하는데 사용 - 검증 과정에서 오류가 발생하면 이 객체에 오류 정보가 저장됨.
BindingResult의 목적
- 오류 확인 : hasErrors() 또는 hasFieldErrors() 메소드를 사용하여 오류의 존재 여부를 확인 가능
- 오류 세부 정보 가져오기 : getFieldError() 또는 getAllErrors() 등을 사용하여 구체적인 오류 정보를 얻어올 수 있음
▶️실습 - 유효성 검사와 리다이렉팅의 로그인 페이지
- redirect와 @Valid(=validation) 활용
form.html을 개선한 registerForm.html 작성
<head>
<meta charset="UTF-8">
<title>회원가입 리다이렉트</title>
</head>
<body>
<form th:action="@{/exam/register}" th:object="${userRegisterForm}" method="post">
<label for="name">이름</label>
<input type="text" th:field="*{name}" id="name"/>
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
<label for="email">이메일</label>
<input type="email" th:field="*{email}" id="email"/>
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
<button type="submit">제출하기</button>
</form>
</body>
forward 방식과 redirect 방식
- forward와 redirect를 수행하는 간단한 @Controller클래스
- 클라이언트로부터 요청을 각각 다른 방식으로 처리하며
1. 요청을 내부적으로 전달하거나(forward)
2. 새 위치로 리다이렉트(redirect)하는 방법
forward
@Controller
public class RoutingController {
@GetMapping("/start")
public String startProcess(Model model){
System.out.println("Process Start!!!");
model.addAttribute("forwardTest", "Jonny");
return "forward:/forward";
// 포워딩이되었을때는
}
@GetMapping("/forward")
public String forward(Model model){
System.out.println("forward Start!!!");
System.out.println("forward Test ::::: " + model.getAttribute("forwardTest"));
return "forwardPage";
}
}
- forward페이지에서 Jonny라는 값을 가지고온 것을 확인할 수 있다.
- 동작 방식
1. @GetMapping("/start") ➡️HTTP GET 요청이 "/start" 경로로 들어올 때의 메소드 처리
2. model.addAttribute("forwardTest", "Jonny"); ➡️Model 객체에 "forwardTest" Key로 "Jonny" Value값 추가
3. return "forward:/forward"; ➡️"/forward" 경로로 서버 내부에서 요청을 전달(=포워딩)
4. @GetMapping("/forward") ➡️HTTP GET 요청이 "/forward" 경로로 들어올 때의 메소드 처리
5. return "forwardPage"; ➡️"forwardPage"라는 이름의 뷰를 반환
6. forwardPage.HTML ➡️Thymeleaf를 사용하여 Model에서 "forwardTest" 속성의 값을 가져와 표시
forward + HttpServletRequest 추가
@GetMapping("/start")
public String startProcess(Model model){
System.out.println("Process Start!!!");
model.addAttribute("forwardTest", "[Forward] Jonny");
return "forward:/forward";
}
@GetMapping("/forward")
public String forward(Model model, HttpServletRequest request){
System.out.println("forward Start!!!");
System.out.println("forward Test ::::: " + model.getAttribute("forwardTest"));
System.out.println(request.getAttribute("forwardTest"));
return "forwardPage";
}
- HttpServletRequest를 추가하기 전에는 값을 출력해보면 null이 발생하였다.
➡️이유 : forward를 사용할 때 Model 객체가 새로 생성되어 기존의 속성들이 유지되지 않기 때문인데
forward는 내부적으로 요청이 전달되지만 새로운 요청 객체를 생성하게되므로 Model의 속성이 유지되지 않는 것 - HttpServletRequest를 사용하게되면 request 객체에 직접 속성을 설정하고 가져올 수 있어
forward 시에도 데이터를 유지할 수 있음 - /start : 이 경로로 요청이 오면 . startProcess()메소드가 실행되어 요청이 /forward 경로로 “포워드”된다.
- 이는 사용자가 URL변경을 인지하지 못하는 상태에서 내부적으로만 처리가 이동하는 것이다.
- 즉 포워드 하게 되면 기존 모델에 있던 정보는 포워딩 요청 객체에게 넘겨주고 모델은 새로 만들어진다.
redirect
@GetMapping("redirect")
public String redirect(Model model){
System.out.println("redirect");
model.addAttribute("redirectTest", "[Redirect] Jonny");
return "redirect:/finalDestination";
}
@GetMapping("/finalDestination")
public String finalDestination(Model model, HttpServletRequest request){
System.out.println("redirect Start!!!");
System.out.println("redirect Test ::::: " + model.getAttribute("redirectTest"));
System.out.println(request.getAttribute("redirectTest"));
return "redirectPage";
}
- [Redirect] Jonny라는 값이 전달되지 않았음을 알 수 있다.
(리다이렉팅은 새로운 요청을 발생시켜서 기존의 요청 객체가 유지되지 않았기때문이다) - /redirect : 이 경로로 요청이 오면, redirect() 메소드가 실행되어요청이 /finalDestination 경로로 리다이렉트 된다.
- 이 경우 클라이언트는 브라우저의 URL을 /finalDestination으로 업데이트하게되어
forward가 URL변경을 인지하지 못하는것과는 달리
redirect는 URL변경을 인지하게된다. 즉 새롭게 요청이 들어오게되어 모델, 요청 객체가 초기화된다
쿠키와 세션
쿠키
- 서버가 사용자의 웹 브라우저에 저장하는 작은 데이터 조각
- key-value 문자열 형태
- 유지할 정보를 클라이언트에게 맡겨놓는 정책을 말함
- 정보가 클라이언트에게 있다보니 (=브라우저에게 있다보니) 쿠키의 정보는 언제든지 노출될 가능성이 있다.\
- 서버는 쿠키를 통해 사용자의 상태정보를 유지할 수 있음
- 장점 : 사용자가 사이트에 대한 설정을 저장하는데 도움이 되며 브라우저 간 세션 유지에 유용
- 단점 : 보안에 취약할 수 있어 중요 정보는 저장하지 말아야한다.
세션
- "HttpSession"을 의미함
- 서버 측에서 사용자와 관련된 데이터를 저장하는 방법
➡️ 웹 브라우저에 사용자정보를 저장하는 쿠키와는 다르다
➡️데이터 저장 시 Object타입으로 저장하게되어 List, 객체 등 다양한 타입을 저장할 수 있다. - 유지할 정보를 클라이언트가 아닌 “서버”가 가진다.
- 세션은 서버에 생성되고 세션 ID를 통해 각 클라이언트를 식별
- 세션 ID는 보통 쿠키를 사용하여 클라이언트에 저장되고 각 요청마다 서버로 전송
- 장점 :
- 객체 저장, 사용자 로그인 상태 유지 등 복잡한 데이터 관리에 적합
- 서버메모리에 데이터를 저장하기때문에 쿠키보다 보안이 강화 - 단점 : 많은 사용자가 접속하는 경우 서버 자원 사용이 증가할 수 있음
▶️실습 - Cookie
// 쿠키에 대한 컨트롤러
@Controller
public class VisitController {
@GetMapping("/visit")
public String showVisit(
@CookieValue(name ="lastVisit", defaultValue = "N/A") String lastVisit,
HttpServletResponse response,
Model model){
model.addAttribute("lastVisit", lastVisit);
return "visit"; // 뷰는 visit 뷰에서 출력하도록 함
}
}
- @CookieValue(name = “lastVisit”, defaultValue = “N/A”) String lastVisit,
➡️쿠키에서 값을 꺼내서 lastVisit 변수에 넣음
➡️처음에는 쿠키에 값이 없으므로 defaultValue로 기본값을 지정
➡️name : 쿠키의 이름을 의미 (=lastVisit)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>Cookie Test</title>
</head>
<body>
<h1>쿠키 실습</h1>
<p>쿠키에서 얻은 값 ::: <span th:text="${lastVisit}"></span></p>
</body>
</html>
- ${lastVisit} ➡️쿠키의 이름으로 접근하여 값을 가져옴
- 하지만 쿠키의 값이 N/A로 없는 것을 확인 가능
➡️해결방법 : Cookie를 먼저 만들어야한다.
▶️실습 - Cookie 생성/등록
// 쿠키에 대한 컨트롤러
@Controller
public class VisitController {
@GetMapping("/visit")
public String showVisit(
@CookieValue(name ="lastVisit", defaultValue = "N/A") String lastVisit,
HttpServletResponse response,
Model model){
Cookie cookie = new Cookie("lastVisit", "oreoCookie");
// cookie.setDomain("/");
cookie.setMaxAge(60*60*24);
response.addCookie(cookie);
model.addAttribute("lastVisit", lastVisit);
return "visit"; // 뷰는 visit 뷰에서 출력하도록 함
}
}
- Cookie cookie = new Cookie("lastVisit", "oreoCookie");
➡️lastVisit이름으로 쿠키를 만들고 oreoCookie라는 값을 넣음 - cookie.setDomain("/");
➡️setDomain()을 생략하면 "/"가 default 경로로 지정된다.
➡️("/") : 애플리케이션 전체에서 이 쿠키를 꺼내올 수 있다는 것을 의미 - cookie.setMaxAge(60*60*24);
➡️쿠키의 유지시간을 초 단위 값으로 받으므로 60*60*24이면 하루를 의미 (1 day)
(60*60은 1시간을 의미) - response.addCookie(cookie);
➡️쿠키정보들은 반드시 응답에 포함시켜야함
➡️response는 addCookie()메소드를 통해 여러 개의 쿠키를 저장할 수 있고
쿠키를 얻어올때도 "배열"로 얻어오는 메소드를 가진다.
- 쿠키에 넣은 값을 확인해볼 수 있다.
- 만약 브라우저의 시크릿모드에서 실행하게되면 "oreoCookie"라는 쿠키 값을 확인할 수 없다
- 다시 기존 브라우저에서 새 탭을 열면서 실행하여도 쿠키가 유지되어 "oreoCookie"라는 쿠키 값을 확인 가능하다.
(이 쿠키는 설정에 따라 하루동안은 쿠키가 유지될 것) >> 60*60*24
🚀 회고를 작성하다보니 수업을 들으면서 이해못하는 내용들을 여러번 메모하고 있었다.
블로그에 이론을 정리하면서 실습을 다시한번 보게되고, 여러번 적혔던 중복된 의문들을 검색과 공부를 통해 해결해나갈 수 있었다.
여전히 메소드 요청 전달 부분이 어려운 상태이지만 블로그에 정리했던 회고 내용들을 계속해서 읽어보며
익숙해질 수 있도록 해야할 것 같다.🦁
'Recording > 멋쟁이사자처럼 BE 13기' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_40일차_"Spring JDBC" (1) | 2025.02.04 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_39일차_"쿠키 / 세션" (1) | 2025.02.03 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_37일차_"스프링 Thymeleaf" (1) | 2025.01.23 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_36일차_"스프링 MVC" (1) | 2025.01.22 |
[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_35일차_"스프링 AOP" (1) | 2025.01.21 |