요구사항
- 홈 화면 - 로그인 전
- 회원 가입
- 로그인
- 홈 화면 - 로그인 후
- 본인 이름(~님 환영합니다.)
- 상품 관리
- 로그 아웃
- 보안 요구사항
- 로그인 사용자만 상품에 접근하고, 관리 가능
- 로그인을 하지 않은 사용자가 상품 관리에 접근하면 로그인 화면으로 이동
회원 관리, 상품 관리
Package 구조
- domain
- item
- ex) Item, ItemRepository,
- member
- login
- item
- web
- item
- ex) ItemController, ItemSaveForm, ItemUpdateForm
- member
- login
- item
도메인 = 화면, UI, 기술 인프라 등등의 영역은 제외한 시스템이 구현해야 하는 핵심 비즈니스 업무 영역
web을 다른 기술로 바꾸어도 도메인은 그대로 유지할 수 있어야 함.
이런 구조가 가능하려면 web은 domain을 알고있지만 domain은 web을 모르도록 설계해야한다. (= web은 domain을 의존하지만, domain은 web을 의존 X)
ItemSaveForm 같은 경우, domain에 들어갈 것 같지만 web에 들어간다.
회원 가입
- Member
1
2
3
4
5
6
7
8
9
10
11
12
@Data
public class Member {
private Long id;
@NotEmpty
private String loginId; //로그인ID
@NotEmpty
private String name; //사용자 이름
@NotEmpty
private String password;
}
- MemberRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j
@Repository
public class MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
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 findAll().stream()
.filter(m -> m.getLoginId().equals(loginId))
.findFirst();
}
public List<Member> findAll() {
ArrayList<Member> members = new ArrayList<>(store.values());
return members;
}
public void clearStore() {
store.clear();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("/add")
public String addForm(@ModelAttribute Member member) {
return "members/addMemberForm";
}
@PostMapping("/add")
public String save(@Valid @ModelAttribute Member member, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "members/addMemberForm";
}
memberRepository.save(member);
return "redirect:/";
}
}
보면서 Bean Validation 복습.
로그인
- LoginService (domain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);
}
}
LoginService를 이용하는 부분은 LoginController 이다. 따라서 domain에 위치한다.
- LoginForm (web)
1
2
3
4
5
6
7
8
@Data
public class LoginForm {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
- LoginController (web)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@GetMapping("/login")
public String loginForm(@ModelAttribute LoginForm form) {
return "login/loginForm";
}
@PostMapping("/login")
public String login(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리 TODO
return "redirect:/";
}
}
참고
로그인을 다루는 HTML 부분이다.
이 때 password를 입력하는 부분이 input type=”password”로 되어있는데, 로그인에 실패해서 login/loginForm으로 이동을 하면 아이디는 입력했던 값이 유지될 때 password는 입력이 유지되지 않도록 해준다.
로그인 처리
로그인을 한 후 어떤 회원이 로그인이 되어있는 지 나타내주기로 했다. 이를 구현하기 위해서는 로그인의 상태를 유지해야 한다.
세션을 이용할 수도 있고, 쿠키를 이용할 수도 있다.
쿠키 이용
서버에서 로그인에 성공하면 HTTP 응답에 쿠키를 담아 브라우저에 전달하도록 한다.
- 영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지되는 쿠키
- 세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료 시 까지 유지되는 쿠키
따라서 로그인 처리는 세션 쿠키를 이용해 할 수 있다.
구현 (쿠키)
1
2
3
4
//LoginController login() HttpServletResponse 추가
Cookie idCookie = new Cookie("memberId", String.valueOf(loginMember.getId()));
response.addCookie(idCookie);
memberId (로그인 아이디 X)로 쿠키를 생성해준다.
- HomeController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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";
}
- @CookieValue : 쿠키를 가져올 수 있다. HttpServletRequest 로도 가져올 수 있지만 스프링이 제공하는 @CookieValue를 사용했다.
- 쿠키는 String인데 Long으로 해도 되는가? - 스프링에서 타입 자동 변환 지원해준다.
- 쿠키값이 없는 사용자도 홈화면에 들어올 수 있기 때문에 required = false 해줘야 함.
쿠키가 없으면 일반 home화면을, 쿠키가 너무 옛날에 만들어졌거나 해서 DB에 memberId가 ID인 회원이 없을 수 있으니 일반 home화면으로 예외처리.
그리고 해당 쿠키값의 ID 조건이 다 충족되는 회원이 있다면 model에 추가해주고, loginHome을 보여준다. (loginHome에서 회원 이름을 가져오기 위한 model 추가.)
- logout
1
2
3
4
5
6
7
8
9
10
11
12
13
LoginController에 추가.
@PostMapping("/logout")
public String logout(HttpServletResponse response) {
expireCookie(response, "memberId");
return "redirect:/";
}
private static void expireCookie(HttpServletResponse response, String cookieName) {
Cookie cookie = new Cookie(cookieName, null);
cookie.setMaxAge(0);
response.addCookie(cookie);
}
새로운 쿠키를 만들고 MaxAge를 정해줘 쿠키를 만료시켜 구현한다. 이 외 다른 방법도 있다.
쿠키 보안문제
쿠키를 사용해 로그인을 유지하는 방법을 알아보았는데 이는 심각한 보안문제가 있다.
- 쿠키 값은 임의로 변경 가능하다.
- 클라이언트가 쿠키를 강제로 변경하면 다른 사용자가 된다.
- 실제 웹브라우저 개발자모드 -> Application -> Cookie 변경
- 쿠키에 보관된 정보는 훔쳐갈 수 있다.
- 만약 쿠키에 개인정보나, 신용카드 정보가 있다면?
- 쿠키는 웹 브라우저에도 보관되고, 네트워크 요청 시 매번 서버로 전달된다.
- 쿠키의 정보가 로컬 PC를 통해 털릴수도, 네트워크 전송 구간에서 털릴 수도 있다.
- 해커가 쿠키를 한 번 훔쳐가면 평생 사용할 수 있다.
- 해커가 쿠키를 가져가면 그 쿠키를 통해 악의적인 요청을 계속 시도할 수 있다.
대안
- 쿠키에 중요한 값을 노출하지 않고, 사용자 별로 예측 불가능한 임의의 토큰(랜덤) 으로 대체한다. 서버에서는 토큰과 사용자 id를 매핑해 인식하고 서버에서 토큰을 관리한다.
- 토큰은 해커가 임의의 값을 넣어도 찾을 수 없도록 예상 불가능 해야 한다.
- 해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 (ex - 30분) 유지한다.
- 해킹이 의심되는 경우 서버에서 해당 토큰을 강제로 제거하면 된다.
세션 동작 방식 이용
서버에 중요한 정보를 보관하고 그 연결을 유지하는 방법을 세션이라고 한다.
사용자가 loginId, password 정보를 전달하면 서버에서 해당 사용자가 맞는지 확인한다.
사용자가 맞으면 세선 ID를 생성하는데 이는 추정 불가능해야한다.
UUID는 추정이 불가능하다 (Cookie: mySessionId=zz0101xx-bab9-4b92…)
생성된 세션 ID와 세션에 보관할 값(‘memberA’)을 서버의 세션 저장소에 보관한다.
클라이언트와 서버는 결국엔 쿠키로 연결되는 것은 맞다.
서버는 클라이언트에 mySessionId 라는 이름으로 세션 ID만 쿠키에 담아 전달한다. 그리고 클라이언트는 쿠키 저장소에 mySessionId 쿠키를 보관한다.
핵심
- 회원과 관련된 정보는 전혀 클라이언트에 전달하지 않는다.
- 추정 불가능한 세션 ID만 쿠키를 통해 클라이언트에 전달된다.
- 이 세션 ID를 매핑해 세션 저장소에서 꺼내는 것이다.
서버에서는 클라이언트가 전달한 mySessionId 쿠키 정보로 세션 저장소를 조회해 로그인 시 보관한 세션 정보를 사용한다.
정리
- 쿠키 값을 변조 가능 -> 예상 불가능한 복잡한 세션 ID를 사용해 세션 저장소에 매핑할 수 없도록 해서 해결.
- 쿠키에 보관하는 정보는 클라이언트 해킹 시 털릴 가능성이 있다. -> 세션 ID가 털려도 중요한 정보가 없기 때문에 괜찮다.
- 쿠키 탈취 후 사용 -> 서버에서 세션의 만료시간을 짧게 설정하여 해커가 토큰을 가져가도 일정 시간 후 사용할 수 없도록 하여 해결. 혹은 해당 세션을 강제 제거하는 방식으로 해결가능.
구현 (세션 직접 만들어 적용)
- 세션 생성
- sessionId 생성
- 세션 저장소에 sessionId와 보관할 값을 저장
- sessionId로 응답 쿠키를 생성해 클라이언트에 전달.
- 세션 조회
- 클라이언트가 요청한 sessionId 쿠키 값으로 세션 저장소에 보관한 값을 조회
- 세션 만료
- 클라이언트가 요청한 sessionId 쿠키 값으로 세션 저장소에 보관한 sessionId와 값 제거
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@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 생성, 값을 세션에 저장.
String sessionId = UUID.randomUUID().toString();
sessionStore.put(sessionId, value);
//쿠키 생성
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
}
public Cookie findCookie(HttpServletRequest request, String cookieName) {
if (request.getCookies() == null) {
return null;
}
return Arrays.stream(request.getCookies())
.filter(cookie -> cookie.getName().equals(cookieName))
.findAny()
.orElse(null);
}
/**
* 세션 조회
*/
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());
}
}
}
UUID로 sessionID를 생성해 Map에 저장한다 (sessionId가 key, Member객체가 value). 그리고 그 세션id를 바탕으로 쿠키를 생성한다.
쿠키를 찾는 것은 쿠키name에 맞는 쿠키가 있으면 그 쿠키를 반환하는데 그 쿠키에는 쿠키name과 세션ID가 들어있다.
세션을 가져오는 부분은 쿠키를 찾고 쿠키가 없으면 null을 반환, 쿠키가 있다면 그 쿠키의 value인 sessionId를 가지고 sessionStore 맵에서 가져온다.
세션을 만료하기 위해서는 쿠키를 찾고 그 쿠키에 있는 sessionId를 기반으로 맵에서 지워주면 된다.
(response는 응답에 쿠키를 생성해 담아주는 역할, request는 요청에서 쿠키를 가져오기 위한 역할)
- 세션 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
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);
Assertions.assertThat(result).isEqualTo(member);
//세션 만료
sessionManager.expire(request);
Object expired = sessionManager.getSession(request);
Assertions.assertThat(expired).isNull();
}
- HttpSevletResponse, Request 같은 부분은 인터페이스라 구현체를 넣어주어야 하는데 테스트에서 넣기 어렵다.
- 앞에 Mock을 붙이면 Test에서 사용하기 쉽도록 지원해주는 것을 지원해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LoginController에 추가.
@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:/";
}
@PostMapping("/logout")
public String logoutV2(HttpServletRequest request) {
sessionManager.expire(request);
return "redirect:/";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//homeController
@GetMapping("/")
public String homeLoginV2(HttpServletRequest request, Model model) {
//세션 관리자에 저장된 회원 정보 조회
Member member = (Member)sessionManager.getSession(request);
if (member == null) {
return "home";
}
model.addAttribute("member", member);
return "loginHome";
}
세션과 쿠키의 개념을 명확하게 이해하기 위해 직접 구현한 것이고, 세션이라는 것 자체도 특별한 것이 아닌 쿠키를 사용하지만 서버에서 데이터를 유지하는 방법이라는 차이가 있는 것이었다.
프로젝트마다 이렇게 세션 개념을 직접 개발하면 불편할 것이고, 서블릿이 이를 위해 세션 개념을 지원한다.
로그인 처리 - 서블릿 HTTP 세션 사용
서블릿이 지원하는 세션은 위와 동작 방식은 거의 비슷하고, 세션을 일정시간 사용하지 않으면 해당 세션을 삭제하는 기능도 제공한다.
HttpSession
서블릿을 통해 HttpSession을 생성하면 아래와 같은 쿠키를 생성한다.
쿠키 이름은 JSESSIONID이고 값은 추정 불가능한 랜덤 값이다.
Cookie: JSESSIONID=5878E23B513F50164DFDD
구현 1
1
2
3
public class SessionConst {
public static final String LOGIN_MEMBER = "loginMember";
}
(단순히 이름을 관리하기 위한 부분이라 interface로 만드는 것이 더 좋다.)
- LoginController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@PostMapping("/login")
public String loginV3(@Valid @ModelAttribute LoginForm form, BindingResult bindingResult, HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(form.getLoginId(), form.getPassword());
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
//로그인 성공 처리
//세션이 있으면 있는 세션 반환, 없으면 신규 세션을 생성
HttpSession session = request.getSession();
//세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:/";
}
- 세션을 생성하려면 request.getSession(true)를 사용하면 된다.
- default = true이다. (생략 가능)
- true이면 세션이 있을 때 기존 세션을 반환하고 세션이 없으면 새로운 세션을 생성해 반환한다.
- false이면 세션이 있을 때 기존 세션을 반환하고 세션이 없을 때 새로운 세션을 생성하지 않고 null을 반환한다.
- session.setAttribute()
- model.addAttribute() 처럼 하나의 세션에 여러 값을 저장할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
//LoginController logout 처리
@PostMapping("/logout")
public String logoutV3(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return "redirect:/";
}
세션을 지우는 것이 목적이므로 false를 사용해 세션을 가져올 때 없으면 새로 생성하지 못하도록 한다.
- homeController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
HttpSession session = request.getSession(false);
if (session == null) {
return "home";
}
Member loginMember = (Member)session.getAttribute(SessionConst.LOGIN_MEMBER);
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
로그인 하지 않은 멤버들도 접근하는 곳이기 때문에 session을 가져올 때 세션 생성에 의한 메모리 낭비를 방지하기 위해 false를 쓴다.
구현 2
스프링이 제공하는 `@SessionAttribute’ 를 이용해보자.
- homeController
1
2
3
4
5
6
7
8
9
10
@GetMapping("/")
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
if (loginMember == null) {
return "home";
}
model.addAttribute("member", loginMember);
return "loginHome";
}
세션을 가져와서 세션이 있는지 없는지에 따라 처리하고, 있으면 세션을 통해Member 객체를 가져와 다루던 부분을 애노테이션 하나로 해결한다.
물론 로그인 할 때는 똑같이 request로 세션을 생성하고(가져오고), 그 세션에 setAttribute로 value로 Member를 넣어주어야 한다.
TrackingModes
로그인을 처음 시도하면 URL에 jsesessionID를 포함하고 있다.
웹 브라우저가 쿠키를 지원하지 않을 때, 쿠키 대신 URL을 통해 세션을 유지하기 위함이다. 이 방법을 사용하려면 URL에 계속 이 정보를 포함해야 한다.
타임리프 같은 템플릿은 엔진을 ㅌ오해 링크를 걸면 jsesessionID를 URL에 자동으로 포함해준다. 서버 입장에서 웹 브라우저가 쿠키를 지원하는지 하지 않는지 최초에는 판단하지 못하므로, 쿠키 값도 전달하고, URL에 jsesessionID도 함께 전달한다.
URL 전달 방식을 끄고 항상 쿠키를 통해서만 세션을 유지하고 싶다면 아래 옵션을 추가해주면 된다.
- application.properties
1
server.servlet.session.tracking-modes=cookie
세션 정보 접근
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j
@RestController
public class SessionInfoController {
@GetMapping("/session-info")
public String sessionInfo(HttpServletRequest request) {
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());
log.info("getMaxInactiveInterval={}", session.getMaxInactiveInterval());
log.info("creationTime={}", new Date(session.getCreationTime()));
log.info("lastAccessedTime={}", new Date(session.getLastAccessedTime()));
log.info("isNew={}", session.isNew());
return "세션 출력";
}
}
- sessionId : 세션 ID, JSESESSIONID
- maxInactiveInterval : 세션의 유효 시간 ex) 1800초
- creationTime : 세션 생성 일시
- lastAccessedTime : 세션과 연결된 사용자가 최근에 서버에 접근한 시간, 클라이언트에서 서버로 sessionId를 요청한 경우 갱신된다.
- isNew : 새로 생성된 세션인지, 아니면 이미 과거에 만들어지고 클라이언트에서 서버로 요청해 조회된 세션인지 여부.
세션 타임아웃 설정
세션은 사용자가 로그아웃을 직접 호출해 session.invalidate() 가 호출되는 경우에 삭제된다. 그런데 대부분 사용자는 로그아웃을 선택하지 않고 그냥 웹 브라우저를 종료한다.
문제는 HTTP가 비 연결성이므로 서버 입장에서는 사용자가 웹 브라우저를 종료한 것인지 아닌지를 인식할 수 없다.
따라서 서버에서 세션 데이터를 언제 삭제해야 하는지 판단하기가 어렵다.
남아있는 세션을 보관하면 아래와 같은 문제가 발생할 수 있다.
- 세션과 관련된 쿠키를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청이 가능하다.
- 세션은 기본적으로 메모리에 생성된다. 메모리의 크기는 무한하지 않기 때문에 꼭 필요한 경우에만 생성해서 사용해야 한다.
그래서 세션의 종료시점을 어떻게 정해?
가장 단순하게는 세션 생성 시점으로부터 30분 정도 이후로 종료하면 될 것 같다.
하지만 이러한 방법은 30분 이상 사이트에서 활동할 경우 30분마다 계속 로그인을 해서 세션을 생성해야 한다.
따라서 세션 생성 시점이 아닌 사용자가 서버에 최근 요청한 시간을 기준으로 30분 정도를 유지해주면 된다. HttpSession은 이러한 방식을 사용한다.
세션 타임아웃 설정
- 스프링부트로 글로벌 설정
1
2
3
//application.properties
server.servlet.session.timeout=60
(60초, 기본은 1800, 글로벌 설정은 분 단위로 설정해야 한다.)
- 특정 세션 단위로 시간 설정
1
session.setMaxInactiveInterval(1800);
세션의 타임아웃 시간은 해당 세션과 관련된 JSESESSIONID를 전달하는 HTTP 요청이 있으면 현재 시간으로 다시 초기화 된다. 이렇게 초기화 되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용할 수 있다.
LastAccessedTime 이후로 timeout 시간이 지나면, WAS가 내부에서 해당 세션을 제거한다.
실무
실무에서 주의할 점은 세션에는 최소한의 데이터만 보관해야 한다는 점이다. 보관할 데이터 용량 * 사용자 수로 세션의 메모리 사용량이 급격하게 늘어나 장애로 이어질 수 있다.
위의 예시들에서는 id, loginId, password, name이 들어있는 member 객체를 그대로 사용했는데, 로그인 용 member객체를 따로 만들고 거기에 memberId 라던지 간단하게 사용할 수 있는 정보만 담아 그걸 세션에 저장하는 방식으로 하는 것이 좋다.
추가로 세션의 시간을 너무 길게 가져가면 메모리 사용이 계속 누적될 수 있으므로 시간도 적절하게 선택하는 것이 좋다.