Recording/멋쟁이사자처럼 BE 13기

[멋쟁이사자처럼 부트캠프 TIL 회고] BE 13기_38일차_"스프링 포워딩/리다이렉팅"

LEFT 2025. 1. 24. 18:46

🦁멋쟁이사자처럼 백엔드 부트캠프 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형태의 문자열로 받아서 출력 가능

zoneId 출력 결과


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

🚀 회고를 작성하다보니 수업을 들으면서 이해못하는 내용들을 여러번 메모하고 있었다.

블로그에 이론을 정리하면서 실습을 다시한번 보게되고, 여러번 적혔던 중복된 의문들을 검색과 공부를 통해 해결해나갈 수 있었다.

여전히 메소드 요청 전달 부분이 어려운 상태이지만 블로그에 정리했던 회고 내용들을 계속해서 읽어보며

익숙해질 수 있도록 해야할 것 같다.🦁