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

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