์ธํ๋ฐ - ์คํ๋ง MVC 2ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํ์ฉ ๊ธฐ์ ์ ๋ณด๊ณ ๊ณต๋ถํ๋ฉด์ ์ ๋ฆฌํ ๊ธ์ ๋๋ค.
์คํ๋ง ๋ถํธ์์ ์ฟ ํค์ ์ธ์ ์ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํด ๋ณด์.
ํ๋ก์ ํธ๋ ์คํ๋ง ๋ถํธ 2.4.9์ spring-boot-starter-web, spring-boot-starter-thymeleaf, spring-boot-starter-validation, lombok ์์กด์ฑ์ ์ถ๊ฐํ๋ค.
์์กด์ฑ๊ณผ ๊ฐ์ด ๋ทฐ ํ ํ๋ฆฟ์ thymeleaf๋ฅผ ์ฌ์ฉํ๋ค.
Bootstrap
์ด๊ฑด ๊ผญ ํ์ํ๊ฑด ์๋๋ฏ๋ก ์คํตํด๋ ๋๋ค.
https://getbootstrap.com/ ์์ bootstrap.min.css ํ์ผ์ ๋ฐ์ resources/static/css์ ์ถ๊ฐํ๋ค.
์ด์ ํ์ ๋๋ฉ์ธ ๊ฐ๋ฐ๋ถํฐ ์์ํด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํด๋ณผ ๊ฒ์ด๋ค.
์์ํ๊ธฐ ์ ์, ์ ์ฒด ํ๋ก์ ํธ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ๋ค.
โโโ src
โโโ main
โ โโโ java
โ โ โโโ com
โ โ โโโ atozdevelop
โ โ โโโ loginexample
โ โ โโโ LoginExampleApplication.java
โ โ โโโ domain
โ โ โ โโโ login
โ โ โ โ โโโ LoginService.java
โ โ โ โโโ member
โ โ โ โโโ Member.java
โ โ โ โโโ MemberRepository.java
โ โ โโโ web
โ โ โโโ HomeController.java
โ โ โโโ SessionConstants.java
โ โ โโโ login
โ โ โโโ LoginController.java
โ โ โโโ LoginForm.java
โ โโโ resources
โ โโโ application.yml
โ โโโ static
โ โ โโโ css
โ โ โโโ bootstrap.min.css
โ โโโ templates
โ โโโ home.html
โ โโโ login
โ โ โโโ loginForm.html
โ โโโ loginHome.html
1. ํ์ ๋๋ฉ์ธ ๊ฐ๋ฐ
Member
package com.atozdevelop.loginexample.domain.member;
import lombok.Data;
@Data
public class Member {
private Long id;
private String loginId;
private String name;
private String password;
}
MemberRepository
package com.atozdevelop.loginexample.domain.member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new ConcurrentHashMap<>();
private static long sequence = 0L;
public Member save(Member member) {
member.setId(++sequence);
log.info("save: member={}", member);
store.put(member.getId(), member);
return member;
}
public Member findById(Long id) {
return store.get(id);
}
public Optional<Member> findByLoginId(String loginId) {
return this.findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
/**
* ํ
์คํธ์ฉ ๋ฐ์ดํฐ ์ถ๊ฐ
*/
@PostConstruct
public void init() {
Member member = new Member();
member.setLoginId("test");
member.setPassword("test!");
member.setName("ํ
์คํฐ");
save(member);
}
}
ํ์ ์ ๋ณด๋ static ConcorrentHashMap์ ์ฌ์ฉํด์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํ๋๋ก ํ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ findByLoginId()๊ฐ loginId๋ฅผ ๋ฐ์ ํ์ ์ ์ฅ์์์ ํ์ ์ธ์คํด์ค๋ฅผ ์ฐพ๋ ๋ฉ์๋์ด๋ค.
์ ์ฅ์์ loginId์ ํด๋นํ๋ ํ์์ด ์์์๋ ์์ผ๋ฏ๋ก ๋ฆฌํด ํ์ ์ Optional๋ก ๊ฐ์ผ๋ค.
ํ์ ๊ฐ์ ์ ํ๋ฉด์ ๋ณ๋๋ก ๋ง๋ค์ง ์์ ๊ฒ์ด๋ฏ๋ก @PostConstruct๋ฅผ ์ฌ์ฉํด ํ ์คํธ์ฉ ํ์์ ๋ง๋ค๋๋ก ํ์๋ค.
2. ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ฐ๋ฐ
LoginService
package com.atozdevelop.loginexample.domain.login;
import com.atozdevelop.loginexample.domain.member.Member;
import com.atozdevelop.loginexample.domain.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class LoginService {
private final MemberRepository memberRepository;
/**
* @return null์ด๋ฉด ๋ก๊ทธ์ธ ์คํจ
*/
public Member login(String loginId, String password) {
return memberRepository.findByLoginId(loginId)
.filter(m -> m.getPassword().equals(password))
.orElse(null);
}
}
์ด ๋ก๊ทธ์ธ ๋ก์ง์ ์์ ๊ตฌํํ MemberRepository#findByLoginId๋ก ํ์์ ์กฐํํ ๋ค์์ ํ๋ผ๋ฏธํฐ๋ก ๋์ด์จ password์ ๋น๊ตํด์ ๊ฐ์ผ๋ฉด Member ์ธ์คํด์ค๋ฅผ ๋ฐํํ๊ณ , password๊ฐ ๋ค๋ฅด๋ฉด null์ ๋ฐํํ๋ค.
LoginForm
package com.atozdevelop.loginexample.web.login;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class LoginForm {
@NotBlank
private String loginId;
@NotBlank
private String password;
}
LoginForm์ ์์ฒญ form ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉ ๋ฐ๊ธฐ ์ํ DTO ํด๋์ค์ด๋ค.
validation์ ์ํด @NotBlank๋ฅผ ์ถ๊ฐํ์๋ค.
LoginController
LoginController์๋ @GetMapping์ผ๋ก ๋ก๊ทธ์ธ ํผ์ ๋ณด์ฌ์ฃผ๋ ํธ๋ค๋ฌ, @PostMapping์ผ๋ก ๋ก๊ทธ์ธ ์์ฒญ์ ๋ฐ์ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ๋ฅผ ๋ง๋ ๋ค.
package com.atozdevelop.loginexample.web.login;
import com.atozdevelop.loginexample.domain.login.LoginService;
import com.atozdevelop.loginexample.domain.member.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Slf4j
@RequiredArgsConstructor
@Controller
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm loginForm) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@ModelAttribute @Validated LoginForm loginForm,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ๋ง์ง ์์ต๋๋ค.");
return "login/loginForm";
}
// ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฒ๋ฆฌ
return "redirect:" + redirectURL;
}
}
๋ก๊ทธ์ธ ํธ๋ค๋ฌ๋ ์ธ์๋ก ์์ฒญ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉ ๋ฐ์ LoginForm, ๋ฐ์ธ๋ฉ ๊ฒฐ๊ณผ๋ฅผ ๋ด๋ BindingResult ๊ทธ๋ฆฌ๊ณ @RequestParam์ผ๋ก ์์ฒญ ํ๋ผ๋ฏธํฐ๋ก redirectURL์ ๋ฐ๋๋ก ํ์๋ค.
redirectURL์ ์ฉ๋๋ ์๋ธ๋ฆฟ ํํฐ์ ์ธํฐ์ ํฐ๋ฅผ ์ด์ฉํด ๋ก๊ทธ์ธ ์ธ์ฆ ์ฒดํฌ๋ฅผ ํ๋ ๋ถ๋ถ์์ ๋ค์ ์ค๋ช ํ๋๋ก ํ๊ฒ ๋ค.
bindingResult์ TypeMissMatch๊ฐ ๋ฐ์ํ๊ฑฐ๋ ์์ฒญ์ผ๋ก ๋์ด์จ login id, password๋ก ํ์์ด ์กฐํ๋์ง ์์ผ๋ฉด "login/loginForm"์ ๋ฆฌํดํ์ฌ ๋ก๊ทธ์ธ ํผ ์ ๋ ฅ ํ๋ฉด์ผ๋ก ๊ฐ๊ฒ ํ๋ค.
ํ์์ด ์กฐํ๋์ง ์๋ ๊ฒฝ์ฐ์๋ BindingResult#reject๋ก ์๋ฌ๋ฅผ ๋ด์ ์๋์ ๊ฐ์ด ํ๋ฉด์์ ์ ์ ํ ๋ฉ์์ง๋ฅผ ๋ ธ์ถํ ์ ์๋๋ก ํ์๋ค.
๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด ํน์ url๋ก redirect ํ๋๋ก ํ๋ค.
@RequestParam์ defaultValue์ ์ํด ํ์ฌ๋ "/"๋ก redirect ๋ ๊ฒ์ด๋ค.
loginForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
.field-error {
border-color: #dc3545;
color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>๋ก๊ทธ์ธ</h2>
</div>
<form action="item.html" th:action th:object="${loginForm}" method="post">
<div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}" th:text="${err}">์ ์ฒด ์ค๋ฅ ๋ฉ์์ง</p>
</div>
<div>
<label for="loginId">๋ก๊ทธ์ธ ID</label>
<input type="text" id="loginId" th:field="*{loginId}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{loginId}" />
</div>
<div>
<label for="password">๋น๋ฐ๋ฒํธ</label>
<input type="password" id="password" th:field="*{password}" class="form-control"
th:errorclass="field-error">
<div class="field-error" th:errors="*{password}" />
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">๋ก๊ทธ์ธ</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg" onclick="location.href='items.html'"
th:onclick="|location.href='@{/}'|"
type="button">์ทจ์</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
์คํ
์คํ ํ localhost:8080/login์ ์ ์ํ๋ฉด ์์ ๊ฐ์ ํ๋ฉด์ ๋ณผ ์ ์๋ค.
์์ @PostConstruct๋ฅผ ํตํด ํ ์คํธ ํ์์ ๋ง๋ค์ด๋์์ผ๋ฏ๋ก test/test!๋ก ๋ก๊ทธ์ธ์ ํ ์ ์๋ค.
ํ์ฌ๋ ๋ก๊ทธ์ธ์ ํด๋ ๋ณ๋ค๋ฅธ ๊ธฐ๋ฅ์ด ์๊ณ "/"๋ก redirect ๋๋๋ฐ ํด๋น ์์ฒญ์ ์ฒ๋ฆฌํ๋ ํธ๋ค๋ฌ๊ฐ ์์ผ๋ฏ๋ก ๊ธฐ๋ณธ ์๋ฌํ์ด์ง๊ฐ ๋ ธ์ถ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๋ก๊ทธ์ธ์ ์ ์ ๋์ ํ๋, ์ฟ ํค๋ ์ธ์ ์ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ๋ก๊ทธ์ธ ์ํ๊ฐ ์ ์ง๋์ง ์๋๋ค.
3. HttpSession ์ ์ฉ
HTTP๋ ๊ธฐ๋ณธ์ ์ผ๋ก statelessํ ํ๋กํ ์ฝ์ด๋ฏ๋ก ํด๋ผ์ด์ธํธ, ์๋ฒ๊ฐ์ ์ฐ๊ฒฐ์ ์ ์งํ๊ธฐ ์ํด ์ฟ ํค ๋๋ ์ธ์ ์ ์ฌ์ฉํ ์ ์๋ค.
๊ทธ๋ฐ๋ฐ ์ฟ ํค๋ง์ผ๋ก ๋ก๊ทธ์ธ์ ๊ตฌํํ๋ฉด ์ฒซ์งธ, ์ฟ ํค ๊ฐ์ ํด๋ผ์ด์ธํธ์์ ์์๋ก ๋ณ๊ฒฝํ์ฌ ์๋ฒ์ ์ ์กํ ์ ์๊ณ ๋์งธ, ์ฟ ํค์ ๋ณด๊ด๋ ์ ๋ณด๋ ์ค๊ฐ์ ํ์ทจ๋ ์ ์์ผ๋ฏ๋ก ๋ณด์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค. ๋ฐ๋ผ์ ์ฟ ํค์๋ ์ค์ํ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ฉด ์๋๊ณ ์ฟ ํค ๊ฐ์ ์ถ์ ๋ถ๊ฐ๋ฅํ ์์์ ๊ฐ์ด์ด์ผ ํ๋ค. HttpSession์ ์ฌ์ฉํ์ฌ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
์ด์ HttpSession์ ํตํด ๋ก๊ทธ์ธ์ ๊ตฌํํด๋ณด์.
SessionConstants
package com.atozdevelop.loginexample.web;
public interface SessionConstants {
String LOGIN_MEMBER = "loginMember";
}
๋จผ์ HttpSession์์ ๋ก๊ทธ์ธ์ฉ์ผ๋ก ์ฌ์ฉํ ์ธ์ id๋ ์ฌ๊ธฐ์ ๊ธฐ์ ์ฌ์ฉ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์์๋ก ๋บ๋ค.
์ด ๋ ์์ ํด๋์ค๋ฅผ interface ๋๋ abstract class๋ก ๋ง๋ค๋ฉด ๊ฐ์ฒด ์์ฑ์ ๋ง์ ์ ์๋ค.
LoginController
@PostMapping("/login")
public String login(@ModelAttribute @Validated LoginForm loginForm,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ๋ง์ง ์์ต๋๋ค.");
return "login/loginForm";
}
// ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฒ๋ฆฌ
HttpSession session = request.getSession(); // ์ธ์
์ด ์์ผ๋ฉด ์๋ ์ธ์
๋ฐํ, ์์ผ๋ฉด ์ ๊ท ์ธ์
์ ์์ฑํ์ฌ ๋ฐํ
session.setAttribute(SessionConstants.LOGIN_MEMBER, loginMember); // ์ธ์
์ ๋ก๊ทธ์ธ ํ์ ์ ๋ณด ๋ณด๊ด
return "redirect:" + redirectURL;
}
@PostMapping("/logout")
public String logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); // ์ธ์
๋ ๋ฆผ
}
return "redirect:/";
}
์๊น ์์ฑํ LoginController#login์ HttpServletRequest ์ธ์๋ฅผ ์ถ๊ฐํ๋ค.
๊ทธ๋ฆฌ๊ณ // ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฒ๋ฆฌ ์ฃผ์ ๋ค์์ ๋ ๋ผ์ธ์ ์ถ๊ฐํ๋ค.
HttpSession session = request.getSession(); // ์ธ์ ์ด ์์ผ๋ฉด ์๋ ์ธ์ ๋ฐํ, ์์ผ๋ฉด ์ ๊ท ์ธ์ ์ ์์ฑํ์ฌ ๋ฐํsession.setAttribute(SessionConstants.LOGIN_MEMBER, loginMember); // ์ธ์ ์ ๋ก๊ทธ์ธ ํ์ ์ ๋ณด ๋ณด๊ด
์์ฒญ์์ ๋์ด์จ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ก ํ์์ด ์ ์ ์กฐํ๋๋ค๋ฉด HttpServletRequest์์ ์ธ์ ์ ๊ฐ์ ธ์ setAttribute()๋ฅผ ํตํด ์์์ ๋ง๋ ์์๊ฐ SessionConstants.LOGIN_MEMBER๋ฅผ session attribute์ name์ผ๋ก, ๋ก๊ทธ์ธ ํ์ ์ธ์คํด์ค๋ฅผ ๊ฐ์ผ๋ก ๋ณด๊ดํ๋ค.
HttpServletRequest#getSession์๋ boolean ํ์ ์ ์ธ์๋ฅผ ๋๊ธธ ์ ์๋๋ฐ, true๋ฅผ ๋๊ธธ ๊ฒฝ์ฐ ์ธ์ ์ด ์์ผ๋ฉด ์ ๊ท ์ธ์ ์ ์์ฑํ์ฌ ๋ฐํํ๋ค. false๋ฅผ ๋๊ธฐ๋ฉด ์ธ์ ์ด ์์ผ๋ฉด ๊ทธ๋ฅ null์ ๋ฐํํ๋ค. ๊ธฐ๋ณธ๊ฐ์ true์ด๋ค.
๋ค์์ผ๋ก logout ํธ๋ค๋ฌ๋ฅผ ์ถ๊ฐํ๋ค.
HttpSession์ ์ด์ฉํ ๋ก๊ทธ์ธ์ ๋ก๊ทธ์์์ ํธ๋ค๋ฌ์์ HttpServletRequest๋ฅผ ์ธ์๋ก ๋ฐ์ HttpSession์ ์ ๊ทผํ์ฌ invalidate()๋ฅผ ํธ์ถํ์ฌ ๊ตฌํํ๋ฉด ๋๋ค.
HomeController
package com.atozdevelop.loginexample.web;
import com.atozdevelop.loginexample.domain.member.Member;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttribute;
@Controller
public class HomeController {
@GetMapping("/")
public String home(@SessionAttribute(name = SessionConstants.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
// ์ธ์
์ ํ์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ํ์ผ๋ก ์ด๋
if (loginMember == null) {
return "home";
}
// ์ธ์
์ด ์ ์ง๋๋ฉด ๋ก๊ทธ์ธ ํ์ผ๋ก ์ด๋
model.addAttribute("member", loginMember);
return "loginHome";
}
}
ํํ์ด์ง ์ปจํธ๋กค๋ฌ์ด๋ค.
ํํ์ด์ง ์ ์ ์ ๋ฏธ๋ก๊ทธ์ธ์ด๋ฉด ํํ์ด์ง๋ฅผ ๋ณด์ฌ์ฃผ๊ณ ๋ก๊ทธ์ธ์ด๋ฉด ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ด๋ฆ์ ๋ณด์ฌ์ค ๊ฒ์ด๋ค.
์ด๋ฅผ ์ํด ์ปจํธ๋กค๋ฌ์์ ๋ก๊ทธ์ธ ์ฌ๋ถ๋ฅผ ํ๋จํด์ผ ํ๋๋ฐ ์คํ๋ง์ ์ธ์ ์ ๋ ํธ๋ฆฌํ๊ฒ ์ฌ์ฉํ ์ ์๋๋ก @SessionAttribute๋ฅผ ์ง์ํ๋ค.
์์ ๊ฐ์ด ํ๋ฉด HttpSession์ด ์กด์ฌํ๋ฉด session attribute์์ name์ด SessionConstants.LOGIN_MEMBER์ธ ๊ฐ์ ๊ฐ์ ธ์ loginMember์ ๋ฐ์ธ๋ฉ๋ฐ์ ์ ์๋ค.
๊ทธ๋์ ์ธ์ ์ ํ์์ด ์์ผ๋ฉด home์, ์ธ์ ์ ํ์์ด ์์ผ๋ฉด ํ์ ์ธ์คํด์ค๋ฅผ model์ ๋ด์ loginHome ๋ทฐ๋ฅผ ๋ฆฌํดํ๋ค.
์ ์ฝ๋๋ ์๋์ ๊ฐ๋ค. ๋ค์์ @SessionAttribute๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋์ผํ ๋ก์ง์ ์ํํ๋ ์ฝ๋์ด๋ค.
@GetMapping("/")
public String home(HttpServletRequest request, Model model) {
// ์ธ์
์ด ์์ผ๋ฉด ํ์ผ๋ก ์ด๋
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
// ์ธ์
์ ์ ์ฅ๋ ํ์ ์กฐํ
Member loginMember = (Member) session.getAttribute(SessionConstants.LOGIN_MEMBER);
// ์ธ์
์ ํ์ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ํ์ผ๋ก ์ด๋
if (loginMember == null) {
return "home";
}
// ์ธ์
์ด ์ ์ง๋๋ฉด ๋ก๊ทธ์ธ์ผ๋ก ์ด๋
model.addAttribute("member", loginMember);
return "loginHome";
}
home.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>ํ ํ๋ฉด</h2>
</div>
<div class="row">
<div class="col">
<button class="w-100 btn btn-dark btn-lg" onclick="location.href='items.html'"
th:onclick="|location.href='@{/login}'|" type="button">
๋ก๊ทธ์ธ
</button>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
loginHome.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>ํ ํ๋ฉด</h2>
</div>
<h4 class="mb-3" th:text="|๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ด๋ฆ: ${member.name}|">๋ก๊ทธ์ธ ์ฌ์ฉ์ ์ด๋ฆ</h4>
<hr class="my-4">
<div class="row">
<div class="col">
<form th:action="@{/logout}" method="post">
<button class="w-100 btn btn-dark btn-lg" onclick="location.href='items.html'" type="submit">
๋ก๊ทธ์์
</button>
</form>
</div>
</div>
<hr class="my-4">
</div> <!-- /container -->
</body>
</html>
loginHome ๋ทฐ์์๋ ์ปจํธ๋กค๋ฌ์์ model์ ๋ด์ Member ์ธ์คํด์ค์์ name์ ๊ฐ์ ธ์์ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ด๋ฆ์ ์ถ๋ ฅํ๋๋ก ํ์๋ค.
์ฌ๊ธฐ๊น์ง ์คํ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์.
์คํ
์คํํ์ฌ ๋ฃจํธ๋ก ์ ์ํ ํ๋ฉด์ด๋ค.
ํ ์คํธ ํ์์ผ๋ก ๋ก๊ทธ์ธ
url ๋ค์ jsessionid ํ๋ผ๋ฏธํฐ๊ฐ ๋ถ๊ณ ์๋ต ํค๋์ Set-Cookie๋ก JSSESSIONID๋ผ๋ ์ด๋ฆ์ ์ฟ ํค์ ๊ฐ์ด ๋ด๊ธด ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด JSESSIONID ์ฟ ํค๊ฐ ํด๋ผ์ด์ธํธ - ์๋ฒ ์ฐ๊ฒฐ ์ ์ง์ ํต์ฌ์ด๋ค.
url ๋ค์ jsessionid๋ ์๋ฒ ์ ์ฅ์์ ํด๋ผ์ด์ธํธ์ ์ฟ ํค๋ฅผ ์ง์ ์ฌ๋ถ๋ฅผ ํ๋จํ์ง ๋ชปํ๋ฏ๋ก ์ฟ ํค๋ฅผ ์ง์ํ์ง ์์ ๊ฒฝ์ฐ ๋์ URL์ ํตํด ์ธ์ ์ ์ ์งํ ์ ์๋๋ก ๋ถ์ฌ์ฃผ๋ ๊ฒ์ด๋ค. ์ด ์ต์ ์ ๋๊ณ ํญ์ ์ฟ ํค๋ฅผ ํตํด์๋ง ์ธ์ ์ ์ ์งํ๋ ค๋ฉด ๋ค์ ์ค์ ์ ์ถ๊ฐํ๋ฉด ๋๋ค.
application.yml
server:
servlet:
session:
tracking-modes: cookie
HttpSession์ ์ธ์ ์์ฑ ์ ์์์ ํ ํฐ ๊ฐ์ ์์ฑํ์ฌ ํด๋น ํ ํฐ ๊ฐ์ JSESSIONID ์ฟ ํค๋ก ํด๋ผ์ด์ธํธ์ ์ ๋ฌํ๋ค.
"/"๋ก ๋ค์ ์ ์ํด๋ณด๋ฉด ์ด๋ฒ์๋ ์์ฒญ Cookie ํค๋์ ์ด์ ์ ์๋ฒ์์ ์๋ต์ผ๋ก ๋ฐ์ JSESSIONID๋ฅผ ๋ด์์ ๋ณด๋ด๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์๋ฒ์์๋ ํด๋ผ์ด์ธํธ๊ฐ ์ ๋ฌํ ์ด JSESSIONID๋ก ์๋ฒ์ชฝ ์ธ์ ์ ์ฅ์๋ฅผ ์กฐํํ์ฌ ๋ณด๊ด๋ ์ธ์ ์ ๋ณด๋ฅผ ์ฌ์ฉํ๋ค.
์ฆ, HttpSession์ JSESSIONID์ด๋ผ๋ ์ด๋ฆ์ ์ฟ ํค๋ก ํด๋ผ์ด์ธํธ์ ์๋ฒ์ ์ฐ๊ฒฐ์ ์ ์งํ๋ฉฐ ์ฟ ํค ๊ฐ์ ์์์ ํ ํฐ ๊ฐ์ด๊ณ , ์ค์ ๋ก ์ค์ํ ๋ฐ์ดํฐ๋ ์๋ฒ ์ชฝ HttpSession์ ์ธ์ ์ ์ฅ์์ ๋ณด๊ดํ๋ค.
๊ฐ๋ฐ์ ๋๊ตฌ์ Application ํญ > Storage > Cookies์์ ์ฟ ํค์ ๋ํ ์์ธํ ์ ๋ณด๋ฅผ ๋ณผ ์ ์๋๋ฐ, JSESSIONID๋ Expires / Max-Age๊ฐ Session์ผ๋ก, ์ด๋ฌํ ์ฟ ํค๋ฅผ ์ธ์ ์ฟ ํค๋ผ๊ณ ํ๋ค.
์ธ์ ์ฟ ํค๋ ๋ธ๋ผ์ฐ์ ์ข ๋ฃ ์ ๊น์ง๋ง ์ ์ง๋๋ ์ฟ ํค๋ก, ๋ธ๋ผ์ฐ์ ๋ฅผ ์ข ๋ฃํ๋ฉด ์ฟ ํค๊ฐ ์ญ์ ๋๋ค.
๋ก๊ทธ์์ํ๋ฉด ๋ค์ ๊ธฐ๋ณธ ํํ๋ฉด์ด ๋ณด์ฌ์ง ๊ฒ์ด๋ค.
๋ก๊ทธ์์์ ํด๋ ์์ฒญ ํค๋์ JSESSIONID ์ฟ ํค๋ฅผ ๊ณ์ ๋ณด๋ด์ง๋ง ์๋ฒ ์ธก์์๋ ์ด๋ฏธ ๋ก๊ทธ์์ ํธ๋ค๋ฌ์์ HttpSession์ invalidate() ํ์์ผ๋ฏ๋ก ์ ์์ ์ผ๋ก ๋ก๊ทธ์์์ด ๋ ๊ฒ์ด๋ค.
JSESSIONID๋ ๋ธ๋ผ์ฐ์ ์ข ๋ฃ ์ ์ญ์ ๋๋ ์ฟ ํค๊ฐ ๋จ์์๋ ๊ฒ์ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋๋ค.
4. HttpSession ํ์์์ ์ค์
์ธ์ ์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์๋ชจํ๋ฏ๋ก ํ์์์์ ์ค์ ํด์ผ ํ๋ค.
HttpSession์ LastAccessedTime์ด๋ผ๋ ์ํ๊ฐ์ ๊ธฐ์ค์ผ๋ก ํ์์์์ด ๋์ํ๋ค.
LastAccessedTime๋ ํด๋ผ์ด์ธํธ์์ ์๋ฒ๋ก session id๋ฅผ ์ ์กํด ์ธ์ ์ ์ ๊ทผํ ๋ ๋ง๋ค ์๋ก ์ด๊ธฐํ๋๋ค.
์คํ๋ง๋ถํธ์์๋ ๋ค์๊ณผ ๊ฐ์ด HttpSession์ ํ์์์์ ์ค์ ํ ์ ์๋ค.
์ด ์ค์ ์ ๊ธ๋ก๋ฒํ๊ฒ ํ์ฌ ์น ์ดํ๋ฆฌ์ผ์ด์ ์ ๋ชจ๋ ์ธ์ ์ ์ ์ฉ๋๋ค.
application.yml
server:
servlet:
session:
timeout: 30m
๊ธฐ๋ณธ๊ฐ์ด 30m(30๋ถ)์ด๊ณ 60๊ณผ ๊ฐ์ด m์ ๋ถ์ด์ง ์์ผ๋ฉด ์ด ๋จ์๋ก ์ค์ ๋๋ค.
์ด ๋จ์๋ก ์ค์ ํ ๊ฒฝ์ฐ ์ต์ 60(1๋ถ) ์ด์์ด์ด์ผ ํ๋ค.
HttpSession ์ธ์คํด์ค๋ฅผ ํตํด์๋ ํ์์์์ ์ค์ ํ ์ ์๋ค.
session.setMaxInactiveInterval(1800); // 1800์ด
HttpSession์ LastAccessedTime ์ดํ๋ก ์ค์ ๋ ํ์์์์ด ์ง๋๋ฉด WAS๊ฐ ๋ด๋ถ์์ ํด๋น ์ธ์ ์ ์ ๊ฑฐํ๋ค.
์ฌ๊ธฐ๊น์ง HttpSession์ผ๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํด ๋ณด์๋ค.
์ค๋ฌด์์ ์ฃผ์ํ ์ ์ ์ธ์ ์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ๋๋ฏ๋ก ์ต์ํ์ ๋ฐ์ดํฐ๋ง ๋ณด๊ดํด์ผ ํ๊ณ ์ ์ ํ ํ์์์์ ์ค์ ํด์ผ ํ๋ค. ์ธ์ ์ ๋ณด๊ดํ ๋ฐ์ดํฐ์ ์ฉ๋ * ์ฌ์ฉ์ ์๋งํผ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์์ํ๋ฏ๋ก ์ธ์ ์ผ๋ก ์ธํด ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ๊ธ๊ฒฉํ๊ฒ ๋์ด๋์ ์ฅ์ ๋ก ์ด์ด์ง ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ค์ ๊ธ์์๋ ์๋ธ๋ฆฟ ํํฐ์ ์คํ๋ง ์ธํฐ์ ํฐ๋ฅผ ์ถ๊ฐํ์ฌ ๋ก๊ทธ์ธ ์ธ์ฆ ๊ธฐ๋ฅ์ ๊ตฌํํด๋ณผ ๊ฒ์ด๋ค.
์ถ์ฒ ๋ฐ ์ฐธ๊ณ
์ธํ๋ฐ - ์คํ๋ง MVC 2ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํ์ฉ ๊ธฐ์
๋๊ธ