개요
SessionManager는 서버에서 직접 세션을 관리하는 커스텀 세션 매니저이다. HTTP의 무상태(Stateless) 특성을 극복하기 위해 쿠키와 서버 저장소를 활용하여 사용자의 로그인 상태를 유지한다.
전체 구조
1
2
3
4
5
6
7
8
| @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) { ... }
public Object getSession(HttpServletRequest request) { ... }
}
|
핵심 구성 요소
- SESSION_COOKIE_NAME: 쿠키의 이름 (
"mySessionId") - sessionStore: 세션 데이터를 저장하는 Map
- Key: sessionId (UUID)
- Value: 저장할 객체 (예: Member 객체)
세션 생성 과정 (createSession)
단계별 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
| 1. 사용자 로그인 성공
↓
2. SessionManager.createSession(memberObject, response) 호출
↓
3. UUID 생성: sessionId = "abc-123-def-456"
↓
4. sessionStore에 저장:
sessionStore.put("abc-123-def-456", memberObject)
↓
5. 쿠키 생성 및 응답:
Cookie(name="mySessionId", value="abc-123-def-456")
↓
6. 클라이언트 브라우저에 쿠키 저장
|
시각적 표현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| 서버 메모리 (SessionManager)
┌─────────────────────────────────────┐
│ sessionStore (ConcurrentHashMap) │
│ ┌───────────────┬──────────────┐ │
│ │ Key (String) │ Value (Object)│ │
│ ├───────────────┼──────────────┤ │
│ │abc-123-def... │ Member{ │ │
│ │ │ id=1, │ │
│ │ │ name="김철수"│ │
│ │ │ } │ │
│ └───────────────┴──────────────┘ │
└─────────────────────────────────────┘
↓ 쿠키 전송
클라이언트 브라우저
┌─────────────────────────────────────┐
│ Cookie Storage │
│ ┌─────────────────────────────────┐ │
│ │ mySessionId=abc-123-def-456 │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘
|
코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public void createSession(Object value, HttpServletResponse response) {
// 1. 세션 ID 생성 (추정 불가능한 랜덤 값)
String sessionId = UUID.randomUUID().toString();
// 예: "550e8400-e29b-41d4-a716-446655440000"
// 2. 서버 메모리에 세션 데이터 저장
sessionStore.put(sessionId, value);
// Key: 세션ID, Value: 로그인한 사용자 정보 (Member 객체)
// 3. 세션 ID를 쿠키로 만들어서 클라이언트에 전달
Cookie mySessionCookie = new Cookie(SESSION_COOKIE_NAME, sessionId);
response.addCookie(mySessionCookie);
// 브라우저는 이 쿠키를 저장하고 다음 요청부터 자동으로 포함시킨다
}
|
세션 조회 과정 (getSession)
단계별 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 1. 클라이언트가 HTTP 요청
(쿠키 자동 포함: mySessionId=abc-123-def-456)
↓
2. SessionManager.getSession(request) 호출
↓
3. request.getCookies()로 모든 쿠키 배열 가져오기
↓
4. 쿠키 배열을 순회하며 name이 "mySessionId"인 쿠키 찾기
↓
5. 찾은 쿠키의 value 추출: "abc-123-def-456"
↓
6. sessionStore.get("abc-123-def-456")로 Member 객체 조회
↓
7. Member 객체 반환
|
상세 시각적 표현
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
| HTTP Request
┌────────────────────────────────────────────┐
│ GET /items HTTP/1.1 │
│ Host: localhost:8080 │
│ Cookie: mySessionId=abc-123-def-456; │
│ otherCookie=xyz │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ request.getCookies() 실행 │
│ │
│ Cookie[] cookies = [ │
│ Cookie(name="mySessionId", │
│ value="abc-123-def-456"), ← 이것 │
│ Cookie(name="otherCookie", │
│ value="xyz"), │
│ ... │
│ ] │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ for 문으로 쿠키 배열 순회 │
│ │
│ if (cookie.getName().equals("mySessionId"))│
│ → 찾았다! │
│ │
│ String sessionId = cookie.getValue(); │
│ → "abc-123-def-456" │
└────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────┐
│ sessionStore.get("abc-123-def-456") │
│ │
│ Map에서 Key로 검색: │
│ ┌───────────────┬──────────────┐ │
│ │ Key │ Value │ │
│ ├───────────────┼──────────────┤ │
│ │abc-123-def... │ Member{ │ ← 반환 │
│ │ │ id=1, │ │
│ │ │ name="김철수" │ │
│ │ │ } │ │
│ └───────────────┴──────────────┘ │
└────────────────────────────────────────────┘
|
코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public Object getSession(HttpServletRequest request) {
// 1. 요청에 포함된 모든 쿠키 가져오기
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return null; // 쿠키가 하나도 없으면 null 반환
}
// 2. 쿠키 배열을 순회하며 세션 쿠키 찾기
for (Cookie cookie : cookies) {
if (cookie.getName().equals(SESSION_COOKIE_NAME)) {
// 3. 쿠키의 이름이 "mySessionId"인 것을 찾음
// 4. 쿠키의 값(세션ID)으로 sessionStore에서 조회
return sessionStore.get(cookie.getValue());
// sessionStore.get("abc-123-def-456")
// → Member 객체 반환
}
}
return null; // 세션 쿠키를 찾지 못하면 null 반환
}
|
핵심 이해 포인트
1. 쿠키와 세션 데이터의 역할 분리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 쿠키 (클라이언트 저장)
┌─────────────────────────────────────────────┐
│ - 이름: mySessionId │
│ - 값: abc-123-def-456 (단순 식별자) │
│ - 민감한 정보 없음 (UUID만 저장) │
└─────────────────────────────────────────────┘
세션 데이터 (서버 메모리 저장)
┌─────────────────────────────────────────────┐
│ - Key: abc-123-def-456 │
│ - Value: Member { id=1, loginId="user1", │
│ name="김철수", ... } │
│ - 민감한 정보는 서버에만 보관 │
└─────────────────────────────────────────────┘
|
2. sessionStore.get()의 동작 원리
1
2
3
4
5
6
7
8
| // Map의 기본 동작
Map<String, Object> map = new HashMap<>();
// 저장 (createSession 시)
map.put("abc-123", memberObject);
// 조회 (getSession 시)
Object result = map.get("abc-123"); // memberObject 반환
|
getSession에서의 적용:
1
2
3
4
| // cookie.getValue() = "abc-123-def-456" (쿠키에서 꺼낸 sessionId)
sessionStore.get(cookie.getValue())
// = sessionStore.get("abc-123-def-456")
// = Member 객체
|
3. 이 설계의 이유
보안성 — 클라이언트에는 의미 없는 UUID만 저장된다. 실제 사용자 정보는 서버 메모리에만 존재하므로, 쿠키를 탈취당하더라도 sessionId만 노출될 뿐이다.
무상태성 극복 — HTTP는 기본적으로 이전 요청을 기억하지 못한다. 쿠키를 통해 매 요청마다 “나는 이 세션의 사용자”라고 알려주고, 서버는 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
| [클라이언트] [서버 - SessionManager] [sessionStore]
로그인 요청
│──────────────→ createSession(member)
│ │
│ UUID 생성
│ "abc-123-def"
│ │
│ │───────────→ put("abc-123", member)
│ │ │
│ │ 저장 완료
│ │←───────────────────┘
│ │
│ 쿠키 응답 (abc-123)
│←──────────────│
│
쿠키 저장
│
│
다음 요청 (쿠키 포함)
│──────────────→ getSession(request)
│ │
│ 쿠키 배열 순회
│ "mySessionId" 찾기
│ │
│ │───────────→ get("abc-123")
│ │ │
│ │ member 조회
│ │←───────────────────┘
│ Member 객체 반환
│←──────────────│
│
|
비유로 이해하기
은행 금고 시스템으로 비유하면 이해가 쉽다.
sessionStore = 은행 금고실sessionId (UUID) = 금고 열쇠 번호 (예: #12345)Member 객체 = 금고 안의 귀중품 (사용자 정보)
로그인(createSession): 은행원이 귀중품(Member)을 금고에 넣고, 금고 번호(#12345)를 고객에게 알려준다. 고객은 그 번호를 수첩(쿠키)에 적어둔다.
다음 방문(getSession): 고객이 “제 금고 #12345 열어주세요”라고 말하면, 은행원이 수첩(쿠키)에서 번호를 확인하고 금고실(sessionStore)에서 해당 금고를 열어 귀중품(Member)을 꺼내준다.
주의사항 및 한계
1. 메모리 기반 저장소
1
| private Map<String, Object> sessionStore = new ConcurrentHashMap<>();
|
서버를 재시작하면 모든 세션 데이터가 소실된다. 실제 운영 환경에서는 Redis, 데이터베이스 등의 외부 저장소를 사용한다.
2. 세션 만료 미구현
현재 구현에는 세션 타임아웃 기능이 없다. 실무에서는 일정 시간이 지나면 세션을 자동으로 삭제해야 한다.
3. 보안 강화 필요
- HTTPS 사용 (쿠키 탈취 방지)
- Secure, HttpOnly 플래그 설정
- CSRF 토큰 사용
참고: Spring HttpSession과의 비교
직접 구현한 SessionManager
1
2
3
4
5
| // 세션 생성
sessionManager.createSession(member, response);
// 세션 조회
Member member = (Member) sessionManager.getSession(request);
|
Spring의 HttpSession
1
2
3
4
5
6
| // 세션 생성
HttpSession session = request.getSession();
session.setAttribute("member", member);
// 세션 조회
Member member = (Member) session.getAttribute("member");
|
차이점:
SessionManager는 교육용으로, 세션의 동작 원리를 이해하기 위한 구현이다.HttpSession은 실무용으로, 세션 만료, 클러스터링 등 고급 기능을 제공한다.
정리
- 세션 ID는 단순 식별자다. 쿠키에는 UUID만 저장된다.
- 실제 데이터는 서버에 보관된다. sessionStore Map에 Key-Value 형태로 저장된다.
- 쿠키는 자동 전송된다. 브라우저가 매 요청마다 쿠키를 자동으로 포함시킨다.
- Map의 get/put이 핵심이다. sessionId를 Key로 사용하여 데이터를 저장하고 조회한다.
이 방식을 이해하면 Spring의 HttpSession, JWT, OAuth 같은 인증 메커니즘도 훨씬 쉽게 이해할 수 있다.