๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Spring

์Šคํ”„๋ง ๋ถ€ํŠธ ๋กœ๊ทธ์ธ (1) - ์ฟ ํ‚ค, ์„ธ์…˜์„ ์ด์šฉํ•ด ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

by Leica 2021. 8. 13.
๋ฐ˜์‘ํ˜•

 

์ธํ”„๋Ÿฐ - ์Šคํ”„๋ง 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ํŽธ - ๋ฐฑ์—”๋“œ ์›น ๊ฐœ๋ฐœ ํ™œ์šฉ ๊ธฐ์ˆ 

 

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€