JiwonDev

Spring Login#1 ์ฟ ํ‚ค์™€ ์„ธ์…˜

by JiwonDev

๋กœ๊ทธ์ธ (LoginForm.html)์—์„œ ID,PW๋ฅผ ๋ฐ›์•„์„œ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ์ „์†กํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด์ž.

์ฟ ํ‚ค๋ฅผ ์ „์†กํ•˜๋Š” ๊ณผ์ •์€ ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋‹ค. ํ•ต์‹ฌ์€ ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•ด์„œ '์–ด๋–ป๊ฒŒ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•  ๊ฒƒ์ธ๊ฐ€?' ์ด๋‹ค.

 

๊ณผ๊ฑฐ์˜ ์›น ๋ธŒ๋ผ์šฐ์ €๋Š” ์ฟ ํ‚ค์™€ ์„ธ์…˜์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ฐฐ์›Œ๋ณด์ž.

 

# ๋กœ๊ทธ์ธ ์š”๊ตฌ์‚ฌํ•ญ

  1. ํ™ˆ ํ™”๋ฉด - ๋กœ๊ทธ์ธ ์ „
    โœ” ํšŒ์› ๊ฐ€์ž…
    โœ” ๋กœ๊ทธ์ธ
  2. ํ™ˆ ํ™”๋ฉด - ๋กœ๊ทธ์ธ ํ›„
    โœ” ๋ณธ์ธ ์ด๋ฆ„(๋ˆ„๊ตฌ๋‹˜ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค.)
    โœ” ์ƒํ’ˆ ๊ด€๋ฆฌ
    โœ” ๋กœ๊ทธ ์•„์›ƒ
  3. ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ
    โœ” ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž๋งŒ ์ƒํ’ˆ์— ์ ‘๊ทผํ•˜๊ณ , ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ
    โœ” ๋กœ๊ทธ์ธ ํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ ๊ด€๋ฆฌ์— ์ ‘๊ทผํ•˜๋ฉด ๋กœ๊ทธ์ธ ํ™”๋ฉด์œผ๋กœ ์ด๋™
    โœ” ํšŒ์› ๊ฐ€์ž…, ์ƒํ’ˆ ๊ด€๋ฆฌ

 

 

@ ํŒจํ‚ค์ง€ ๊ตฌ์กฐ

๋„๋ฉ”์ธ์€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ์กด์žฌํ•ด์•ผ ํ•œ๋‹ค. ์›น๊ณผ ๊ด€๋ จ๋œ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉ, ์˜์กดํ•˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ค์ž.

  • ๐Ÿ“Œ Domain = (ํ™”๋ฉด, UI, ๊ธฐ์ˆ  ์ธํ”„๋ผ๊ฐ€ ์—†๋Š”) ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ์˜์—ญ
  • ๐Ÿ“Œ Web = ์›น๊ณผ ๊ด€๋ จ๋œ ์˜์—ญ

 

 

 

@ ๋„๋ฉ”์ธ ๋งŒ๋“ค๊ธฐ

  • ํ•œ๋ฒˆ ๋” ๋งํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ ๊ฐ์ฒด์—๋Š” ์›น๊ณผ ๊ด€๋ จ๋œ ์ฝ”๋“œ(ItemUpdateForm๋“ฑ)๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์•ˆ๋œ๋‹ค.

 

๐Ÿ“‘Member ๐Ÿ“‘MemberRepository โžก DB ์ฝ”๋“œ๋Š” ๊ฐ„๋‹จํ•œ static HashMap<>์œผ๋กœ ๋Œ€์ฒดํ•˜์˜€๋‹ค.

๋”๋ณด๊ธฐ
@Data
public class Member {
private Long id;
@NotEmpty
private String loginId; //๋กœ๊ทธ์ธ ID
@NotEmpty
private String name; //์‚ฌ์šฉ์ž ์ด๋ฆ„
@NotEmpty
private String password;
}
@Slf4j
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); //static ์‚ฌ์šฉ
private static long sequence = 0L;//static ์‚ฌ์šฉ
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 findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}

๐Ÿ“‘Item ๐Ÿ“‘ItemRepository โžก DB ์ฝ”๋“œ๋Š” ๊ฐ„๋‹จํ•œ static HashMap<>์œผ๋กœ ๋Œ€์ฒดํ•˜์˜€๋‹ค.

๋”๋ณด๊ธฐ
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
}
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); //static
private static long sequence = 0L; //static
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id) {
return store.get(id);
}
public List<Item> findAll() {
return new ArrayList<>(store.values());
}
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}

๐Ÿ“‘LoginService โžก ๋กœ๊ทธ์ธ ์•„์ด๋””๋ฅผ ์ฐพ์•„ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋น„๊ตํ•œ๋‹ค. ์•„์ด๋””๊ฐ€ ์—†๋‹ค๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

@Service
@RequiredArgsConstructor
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);
}
}

 

 

@ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€, Form ๊ฐ์ฒด, ์ปจํŠธ๋กค๋Ÿฌ

๐ŸŽจloginForm.html โžก ํƒ€์ž„๋ฆฌํ”„ ์‚ฌ์šฉ. ์Šคํƒ€์ผ ๊ด€๋ จ ์ฝ”๋“œ๋Š” ์ƒ๋žต

๋”๋ณด๊ธฐ
bootstrap.min.css
0.15MB
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<!-- ๋ถ€ํŠธ์ŠคํŠธ๋žฉ์„ ๋‹ค์šด๋ฐ›์•„ ์‚ฌ์šฉ -->
<link href="../css/bootstrap.min.css"
rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<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" method="post" th:action th:object="${loginForm}">
<!-- ๊ธ€๋กœ๋ฒŒ ์—๋Ÿฌ (๊ฐ์ฒด์—์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ)๋Š” ์กฐ๊ฑด์œผ๋กœ ํ™•์ธํ•œ๋‹ค. -->
<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 class="form-control" id="loginId" th:errorclass="field-error" th:field="*{loginId}"
type="text"> <!-- th:errors๋Š” FieldError๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ๋งŒ ๋ Œ๋”๋ง๋œ๋‹ค. -->
<div class="field-error" th:errors="*{loginId}"/>
</div>
<div>
<label for="password">๋น„๋ฐ€๋ฒˆํ˜ธ</label>
<input class="form-control" id="password" th:errorclass="field-error" th:field="*{password}"
type="password"> <!-- th:errors๋Š” FieldError๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ๋งŒ ๋ Œ๋”๋ง๋œ๋‹ค. -->
<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>

๐Ÿ“‘LoginForm, ๐Ÿ“‘LoginController โžก LoginForm์„ ์ด์šฉํ•œ๋‹ค. ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๋ฉด ์ฟ ํ‚ค๋ฅผ ๋ณด๋‚ด์ค€๋‹ค.

๋”๋ณด๊ธฐ
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
HttpServletResponse response) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
return "login/loginForm";
}
//๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
//์ฟ ํ‚ค์— ์‹œ๊ฐ„ ์ •๋ณด๋ฅผ ์ฃผ์ง€ ์•Š์œผ๋ฉด ์„ธ์…˜ ์ฟ ํ‚ค๊ฐ€ ๋จ(๋ธŒ๋ผ์šฐ์ € ์ข…๋ฃŒ์‹œ ๋ชจ๋‘ ์ข…๋ฃŒ)
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
return "redirect:/";
}
}

 


# ์ฟ ํ‚ค๋ฅผ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ์„ธ์…˜ ์œ ์ง€ํ•˜๊ธฐ

์œ„์˜ ์ฝ”๋“œ์—์„œ Login์„ ์„ฑ๊ณตํ•˜๋ฉด ์„œ๋ฒ„๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—๊ฒŒ ์„ธ์…˜์ฟ ํ‚ค(memberId)๋ฅผ ์ „์†กํ•ด์คฌ๋‹ค.
  • ์ด์ œ ๋ธŒ๋ผ์šฐ์ €๋Š” ์ข…๋ฃŒํ•˜๊ธฐ ์ „๊นŒ์ง€ Response์— ์„ธ์…˜์ฟ ํ‚ค๋ฅผ ๊ณ„์†ํ•ด์„œ ์ „์†กํ•ด์ค€๋‹ค.

๐ŸŽจloginhome.html โžก ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ–ˆ์„ ๋•Œ ๋ณด์—ฌ์ค„ ํ™ˆ ํ™”๋ฉด ๋ทฐ์ด๋‹ค.

๋”๋ณด๊ธฐ
bootstrap.min.css
0.15MB
<!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">
<button class="w-100 btn btn-secondary btn-lg" type="button"
th:onclick="|location.href='@{/items}'|">
์ƒํ’ˆ ๊ด€๋ฆฌ
</button>
</div>
<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>

 

์ด์ œ ๋ชจ๋“  ๊ฑด ์ค€๋น„๋˜์—ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ „์†กํ•˜๋Š” ์ฟ ํ‚ค๊ฐ’์„ ์ด์šฉํ•ด์„œ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž.

 


@ ๋กœ๊ทธ์ธ ์ƒํƒœ์œ ์ง€1 - ๋‹จ์ˆœํ•˜๊ณ  ๋ฉ์ฒญํ•œ ๋ฐฉ๋ฒ•

  • @CookieValue๋ฅผ ์ด์šฉํ•ด์„œ ์ „์†ก๋œ ์ฟ ํ‚ค๊ฐ’(memberId)์ด ์žˆ์œผ๋ฉด ๋กœ๊ทธ์ธ ์ƒํƒœ๋กœ ํŒ๋‹จํ•œ๋‹ค.
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
@GetMapping("/")
public String homeLogin(
@CookieValue(name = "memberId", required = false) Long memberId,
Model model) {
if (memberId == null) { return "home";}
//๋กœ๊ทธ์ธ
Member loginMember = memberRepository.findById(memberId);
if (loginMember == null) { return "home";}
model.addAttribute("member", loginMember);
return "loginHome"; // ์ฟ ํ‚ค๊ฐ€ ์žˆ๋‹ค๋ฉด Login๋œ ํ™ˆํ™”๋ฉด ์ „์†ก
}
}

 

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ๊ฐœ๋ฐœํ–ˆ๋‹ค๊ฐ€๋Š” ํฐ์ผ๋‚œ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์•„๋ฌด๋ฆฌ ๋ง‰์•„๋ด์•ผ, ์ฟ ํ‚ค๋Š” ์ง์ ‘ ์ƒ์„ฑํ•ด์„œ ์กฐ์ž‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ๊ณ„์ •์„ ํ•ดํ‚นํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ฟ ํ‚ค์— ๋ณด๊ด€๋œ ๊ฐœ์ธ ๋ฐ์ดํ„ฐ๋Š” ๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผํ•ด์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.
     โžก ์ฆ‰ ์•„์ด๋””, ์ด๋ฆ„, ์นด๋“œ์ •๋ณด์™€ ๊ฐ™์€ ์ƒํƒœ ๋ฐ์ดํ„ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ €์žฅํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค.
  • ์ฟ ํ‚ค๋ฅผ ์•”ํ˜ธํ™” ํ–ˆ๋‹ค๊ณ  ํ•œ๋“ค, ์ฟ ํ‚ค ์ž์ฒด๋ฅผ ๋ชฐ๋ž˜ ํ›”์ณ์„œ ํ‰์ƒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๋ฌธ์ œ์ ์ด ์žˆ๋‹ค.
     โžก ๋กœ๊ทธ์ธ ์ฟ ํ‚ค์˜ ๋งŒ๋ฃŒ ๊ธฐํ•œ์ด ํ•„์š”ํ•˜๋‹ค.
๋†€๋ž๊ฒŒ๋„ ๋ธŒ๋ผ์šฐ์ € ์ž์ฒด์—์„œ ๊ฐœ๋ฐœ์ž๋„๊ตฌ๋กœ ์ฟ ํ‚ค๊ฐ’์˜ ๋ณ€๊ฒฝ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

 


@ ๋กœ๊ทธ์ธ ์ƒํƒœ์œ ์ง€2 - ์„œ๋ฒ„ ์„ธ์…˜์ €์žฅ์†Œ

์‚ฌ์‹ค ์ฒซ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์€ ๋„ˆ๋ฌด ๋ฉ์ฒญํ•œ ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค. ์œ„์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ์ ๋“ค์„ ํ•ด๊ฒฐํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

  • ์„œ๋ฒ„๋‹จ์—์„œ ์„ธ์…˜์ €์žฅ์†Œ๋ฅผ ๋งŒ๋“ค๊ณ  ์‚ฌ์šฉ์ž ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค. ์ฟ ํ‚ค์—๋Š” ์„ธ์…˜์ธ์ฆ๊ฐ’(UUID)์„ ๋Œ€์‹  ์ „์†กํ•œ๋‹ค.
     โžก Cookie: mySessionId=zz0101xx-bab9-4b92-9b32-dadb280f4b61
  • ์„ธ์…˜ ์ €์žฅ์†Œ์˜ ํ‚ค๋Š” ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ ์งง๊ฒŒ(์˜ˆ: 30๋ถ„) ์œ ์ง€ํ•œ๋‹ค. ํ›”์ณ์„œ ๋‚˜์ค‘์— ์žฌ์‚ฌ์šฉํ•˜๊ธฐ ํž˜๋“ค๊ฒŒ ๋งŒ๋“ ๋‹ค.
     โžก ํ•ดํ‚น์ด ์˜์‹ฌ๋˜๋Š” ๊ฒฝ์šฐ ์„œ๋ฒ„์—์„œ ๊ฐ•์ œ๋กœ ์„ธ์…˜์„ ์‚ญ์ œํ•˜๋Š” ๋กœ์ง์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
์ด์ œ ์ƒํƒœ๊ฐ’์„ ์„œ๋ฒ„์—์„œ ๊ด€๋ฆฌํ•˜๋ฏ€๋กœ, ์ฟ ํ‚ค์— ๋ฐ์ดํ„ฐ๋ฅผ ๋…ธ์ถœ์‹œํ‚ค์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

 

์„ธ์…˜ ์ƒ์„ฑ, ์กฐํšŒ, ๋งŒ๋ฃŒ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๐Ÿ“‘SessionManager ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. 

@Component
public class SessionManager {
public static final String SESSION_COOKIE_NAME = "mySessionId";
private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
//## ์„ธ์…˜ ์ƒ์„ฑ ##//
public void createSession(Object value, HttpServletResponse response) {
//์„ธ์…˜ id(UUID)๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๊ฐ’์„ ์„ธ์…˜์— ์ €์žฅ
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//์ฟ ํ‚ค ์ƒ์„ฑ
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
//## ์„ธ์…˜ ์กฐํšŒ ##//
public Object getSession(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie == null) {
return null;
}
return sessionStore.get(sessionCookie.getValue());
}
//## ์„ธ์…˜ ๋งŒ๋ฃŒ ##//
public void expire(HttpServletRequest request) {
Cookie sessionCookie = findCookie(request, SESSION_COOKIE_NAME);
if (sessionCookie != null) {
sessionStore.remove(sessionCookie.getValue());
}
}
// ์„œ๋ธ”๋ฆฟ์—์„œ ์„ธ์…˜ ๊ฐ์ฒด(Session)๋ฅผ ์ œ๊ณตํ•ด์ฃผ๊ธด ํ•˜์ง€๋งŒ, ์ดํ•ด๋ฅผ ๋•๊ธฐ์œ„ํ•ด ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค.
public Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) { // ์ฟ ํ‚ค๊ฐ’์€ array ๋˜๋Š” null ๋กœ ๋ฐ˜ํ™˜๋œ๋‹ค.
return null;
}
return Arrays.stream(request.getCookies()) // ํ•ด๋‹น ์ฟ ํ‚ค์ด๋ฆ„์„ ์ฐพ์•„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny() // ์ฒ˜์Œ์œผ๋กœ ๋ฐœ๊ฒฌ๋œ ๊ฐ’ ์•„๋ฌด๊ฑฐ๋‚˜
.orElse(null); // ์—†๋‹ค๋ฉด null ๋ฐ˜ํ™˜
}
}

๐Ÿ“‘SessionManager ๊ฐ€ ์ž˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ์œ„ํ•ด MockResponse ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธ ํ•ด๋ณด์ž.

๋”๋ณด๊ธฐ
class SessionManagerTest {
SessionManager sessionManager = new SessionManager();
@Test
@DisplayName("SessionManager ํ…Œ์ŠคํŠธ")
void sessionTest() {
//์„ธ์…˜ ์ƒ์„ฑ
MockHttpServletResponse response = new MockHttpServletResponse();
Member member = new Member();
sessionManager.createSession(member, response);
//์š”์ฒญ์— ์‘๋‹ต ์ฟ ํ‚ค ์ €์žฅ
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(response.getCookies());
//์„ธ์…˜ ์กฐํšŒ
Object result = sessionManager.getSession(request);
assertThat(result).isEqualTo(member);
//์„ธ์…˜ ๋งŒ๋ฃŒ
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
assertThat(expired).isNull();
}
}
์ž˜ ์ž‘๋™ํ•œ๋‹ค.

 

์„ธ์…˜ ๋งค๋‹ˆ์ € ๊ฐ์ฒด๋ฅผ ์™„์„ฑํ–ˆ๋‹ค๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ๐Ÿ“‘LoginController ๋ฅผ ์„ธ์…˜์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฐ”๊ฟ”์ฃผ์ž.

@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final SessionManager sessionManager;
@GetMapping("/login")
public String loginForm(@ModelAttribute("loginForm") LoginForm form) {
return "login/loginForm";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
@PostMapping("/login")
public String loginV2(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult,
HttpServletResponse response) {
if (bindingResult.hasErrors()) { return "login/loginForm";}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
return "login/loginForm";
}
//๋กœ๊ทธ์ธ ์„ฑ๊ณต ์ฒ˜๋ฆฌ
//์„ธ์…˜ ๊ด€๋ฆฌ์ž๋ฅผ ํ†ตํ•ด ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๊ณ , ํšŒ์› ๋ฐ์ดํ„ฐ ๋ณด๊ด€
sessionManager.createSession(loginMember, response);
return "redirect:/";
}
}

 

์ด์ œ ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด, ๋ธŒ๋ผ์šฐ์ €์—์„œ ์„ธ์…˜ID (UUID)๋ฅผ ์ฟ ํ‚ค๋กœ ๋ณด๋‚ด์ฃผ๊ฒŒ ๋œ๋‹ค.

@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
private final SessionManager sessionManager;
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
//์„ธ์…˜ ๊ด€๋ฆฌ์ž์— ์ €์žฅ๋œ ํšŒ์› ์ •๋ณด ์กฐํšŒ
Member member = (Member) sessionManager.getSession(request);
// [์„ธ์…˜ID ์ฟ ํ‚ค]์— ํ•ด๋‹น๋˜๋Š” ์„ธ์…˜ ๋ฐ์ดํ„ฐ์—†์Œ
if (member == null) { return "home";}
// ์„ธ์…˜์žˆ์Œ. ๋กœ๊ทธ์ธ์ƒํƒœ ์œ ์ง€
model.addAttribute("member", member);
return "loginHome";
}
}

 


@ ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€3 - ์„œ๋ธ”๋ฆฟ HTTP ์„ธ์…˜

๋งค๋ฒˆ SessionManager๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ์—๋Š” ๋ฒˆ๊ฑฐ๋กญ๋‹ค. ๊ทธ๋ž˜์„œ ์„œ๋ธ”๋ฆฟ ์ŠคํŽ™์—์„œ๋Š” HttpSession ๊ฐ์ฒด๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค. 

  • ์Šคํ”„๋ง์€ ๋งˆ๋ฒ•์˜ ๋„๊ตฌ๊ฐ€ ์•„๋‹ˆ๋‹ค. ๊ฒฐ๊ตญ ์Šคํ”„๋ง ๋‚ด๋ถ€์—์„œ๋„ ์ž๋ฐ” ์„œ๋ธ”๋ฆฟ์„ ์ด์šฉํ•˜์—ฌ ์›น ์„œ๋น„์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.
  • HttpSession์€ HttpServletRequest ์—์„œ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
  • HttpSession์€ JSESSIONID ๋ผ๋Š” ์„ธ์…˜ID ์ฟ ํ‚ค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    โžก Cookie: JSESSIONID=5B78E23B513F50164D6FDD8C97B0AD05
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
@PostMapping("/login")
public String login(... HttpServletRequest request){
// ... ์ƒ๋žต ...
//์„ธ์…˜์ด ์žˆ์œผ๋ฉด ์žˆ๋Š” ์„ธ์…˜ ๋ฐ˜ํ™˜, ์—†์œผ๋ฉด ์‹ ๊ทœ ์„ธ์…˜์„ ์ƒ์„ฑ
// request.getSession(false)๋กœ ํ•˜๋ฉด ์„ธ์…˜์ด ์—†๋‹ค๋ฉด null์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
HttpSession session = request.getSession();
//์„ธ์…˜์— ๋กœ๊ทธ์ธ ํšŒ์› ์ •๋ณด ๋ณด๊ด€
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
}
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); // ์„ธ์…˜ ์ œ๊ฑฐ
}
return "redirect:/";
}

HttpSession ๊ฐ์ฒด๋ฅผ ์ด์šฉํ•˜๋ฉด ๋งˆ์น˜ ์ฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์„ธ์…˜์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 


@๋กœ๊ทธ์ธ ์ƒํƒœ์œ ์ง€4 - ์Šคํ”„๋ง ์„œ๋ธ”๋ฆฟ HTTP ์„ธ์…˜

์Šคํ”„๋ง์—์„œ๋Š” ์œ„์™€ ๊ฐ™์€ HttpSession ๊ฐ์ฒด๋ฅผ @SessionAttribute ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ „์†กํ•˜๋Š” JSESSIONID๋ฅผ @SessionAttribute(~) ํ•˜๋‚˜๋กœ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
@Slf4j
@Controller
@RequiredArgsConstructor
public class HomeController {
private final MemberRepository memberRepository;
private final SessionManager sessionManager;
@GetMapping("/")
public String homeLoginV3Spring(
@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
Model model) {
//์„ธ์…˜์— ํšŒ์› ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด home
if (loginMember == null) { return "home";}
//์„ธ์…˜์ด ์œ ์ง€๋˜๋ฉด ๋กœ๊ทธ์ธ์œผ๋กœ ์ด๋™
model.addAttribute("member", loginMember);
return "loginHome";
}
}

 

 

@ ์ฐธ๊ณ  - HTTP TrackingModes

  • ๊ณผ๊ฑฐ์— ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฟ ํ‚ค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ, URL์— ์ฟ ํ‚ค๋ฅผ ์ ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ๋‹ค.
์ฃผ์†Œ URL์— ์ฟ ํ‚ค๊ฐ’์„ ํฌํ•จํ–ˆ๋˜ ๊ณผ๊ฑฐ๊ฐ€ ์žˆ์—ˆ๋‹ค. (๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฟ ํ‚ค๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ)

๊ทธ๋ž˜์„œ ์„œ๋ฒ„์—์„œ๋Š” TrackingModes๋ผ๊ณ  ํ•ด์„œ ์ฟ ํ‚ค์™€ URL ๋‘˜ ๋‹ค ๋ณด๋‚ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์—ˆ๋‹ค. ์š”์ฆ˜์€ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ด๋ฏ€๋กœ ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ์Šคํ”„๋ง application.properties์—์„œ ์ฟ ํ‚ค๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

# application.properties
server.servlet.session.tracking-modes=cookie

 


# HttpSession ์ •๋ณด์™€ ํƒ€์ž„์•„์›ƒ (๋งŒ๋ฃŒ) ์„ค์ •

์˜ˆ์ œ์—์„œ๋Š” ์„ธ์…˜์„ ๋‹จ์ˆœ ๋กœ๊ทธ์ธ๋งŒ ๊ฐ€๋Šฅํ•˜๋„๋ก ๋งŒ๋“ค์—ˆ๋Š”๋ฐ, HttpSession ๊ฐ์ฒด์—๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์ค€๋‹ค.

HttpSession session = request.getSession(false);
if (session == null) { return "์„ธ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค."; }
//์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ
session.getAttributeNames().asIterator()
.forEachRemaining(
name -> log.info("session name={}, value={}", name, session.getAttribute(name)));
log.info("sessionId={}", session.getId()); // JSESSION ID ๊ฐ’
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());// ์œ ํšจ์‹œ๊ฐ„(sec)
log.info("creationTime={}", new Date(session.getCreationTime()));// ์„ธ์…˜ ์ƒ์„ฑ์ผ์‹œ
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));//๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ์‹œ๊ฐ„
log.info("isNew={}", session.isNew());//์ƒˆ๋กœ ๋งŒ๋“  ์„ธ์…˜์ธ์ง€, ํด๋ผ์—๊ฒŒ ๋ฐ›์€ ์„ธ์…˜์ธ์ง€ ์—ฌ๋ถ€

 

์ด๋ ‡๊ฒŒ HttpSession์ด ์ œ๊ณตํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ์ด์šฉํ•ด์„œ ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

 โžก ๋งŒ๋ฃŒ์‹œ๊ฐ„์„ ์„ค์ •ํ•˜๋Š” ๊ฑด ๋ณด์•ˆ์ ์ธ ์ด์œ ๋„ ์žˆ์ง€๋งŒ, ์„œ๋ฒ„์˜ ๋ฉ”๋ชจ๋ฆฌ ์šฉ๋Ÿ‰์„ ๊ณ„์† ์ฐจ์ง€ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ•„์ˆ˜์ ์ด๋‹ค.

if(HTTP ์„ธ์…˜์ฟ ํ‚ค๊ฐ€ ์ „์†ก๋˜์—ˆ๋‹ค๋ฉด){
session.setMaxInactiveInterval(1800); // 1800์ดˆ๋กœ ๋งŒ๋ฃŒ๊ธฐํ•œ ๊ฐฑ์‹ 
}

 

์ด๋ ‡๊ฒŒ HttpSession์— ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์„ ์ง€์ •ํ•˜๋ฉด WAS๊ฐ€ ๋‚ด๋ถ€์—์„œ ํ•ด๋‹น ์„ธ์…˜์„ ์ œ๊ฑฐํ•œ๋‹ค.

 

์ฐธ๊ณ ๋กœ ๊ธฐ๋ณธ ์„ธ์…˜ ๋งŒ๋ฃŒ๊ธฐ๊ฐ„์„ ์Šคํ”„๋ง application.properties ์˜ต์…˜์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

# application.properties
server.servlet.session.timeout=60 # 60์ดˆ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ง€์ •

 

 

 

๋ธ”๋กœ๊ทธ์˜ ํ”„๋กœํ•„ ์‚ฌ์ง„

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ