도입: 웹은 당신을 기억하지 못한다
우리가 매일 사용하는 웹 브라우저는 HTTP 프로토콜을 기반으로 통신한다. HTTP의 가장 치명적인 특징은 무상태(Stateless)라는 점이다. 서버는 클라이언트가 요청을 보낼 때만 응답할 뿐, 응답이 끝나면 클라이언트를 매몰차게 잊어버린다.
만약 무상태 그대로 웹을 운영한다면, 사용자는 페이지를 이동할 때마다 매번 아이디와 비밀번호를 입력해야만 한다. 이 기억 상실증을 치료하기 위해 웹 개발자들은 '상태 유지 기술'을 만들어냈는데, 그 핵심이 바로 Cookie, Session, 그리고 Scope 메커니즘이다.
Insight: 무상태(Stateless)는 서버의 부하를 줄여주는 훌륭한 특성이지만, 비즈니스를 영위하기 위해서는 반드시 상태를 강제로 기억하게 만드는 추가적인 기술이 필요하다.
기억의 외주화: 쿠키(Cookie)
가장 원초적인 해결책은 "서버가 기억하기 힘드니, 사용자(브라우저)에게 기억을 떠넘기는 것"이다. 서버가 사용자에게 '쿠키'라는 작은 텍스트 조각을 쥐여주면, 사용자는 다음번 요청 때마다 그 쿠키를 서버에 보여주며 자신을 증명한다.
// 쿠키 생성 및 발급
Cookie myCookie = new Cookie("user_theme", "dark_mode");
myCookie.setMaxAge(60 * 60 * 24); // 24시간 동안 유지 (초 단위)
response.addCookie(myCookie); // 응답에 담아서 브라우저로 전송
하지만 쿠키에는 치명적인 약점이 있다. 데이터가 사용자 브라우저에 저장되다 보니 변조가 쉽고 보안에 극도로 취약하다는 점이다. 만약 is_admin=false라는 쿠키를 사용자가 true로 조작한다면 끔찍한 보안 사고가 발생할 수 있다.
Insight: 쿠키는 "오늘 하루 이 창을 보지 않기"나 "장바구니" 같이 유실되거나 조작되어도 치명적이지 않은 데이터를 저장할 때만 사용해야 한다.
서버의 V.I.P 장부: 세션(Session)
쿠키의 보안 문제를 해결하기 위해 등장한 것이 바로 세션(Session)이다. 세션은 민감한 정보를 브라우저에 주지 않고 서버의 안전한 메모리 공간(장부)에 기록한다. 그리고 브라우저에게는 JSESSIONID라는 무의미한 난수표(입장권)만 쿠키 형태로 발급한다.
// Bad Code: 로그인 시 보안을 위협하는 세션 저장 방식
HttpSession session = request.getSession();
// 위험: 비밀번호를 세션(메서리)에 그대로 올리는 행위
session.setAttribute("loginUser", userId);
session.setAttribute("userPw", password); // 절대로 해서는 안 되는 코드
여기서 한 가지 방어적 프로그래밍의 철학이 등장한다. 로그인 인증을 위해 DB와 대조를 마쳤다면, 세션에는 오직 `userId`나 회원의 이름 같은 최소한의 식별 정보만 담아야 한다. 패스워드를 세션에 담아둔다면 만에 하나 세션 데이터가 탈취되었을 때 계정 자체가 완전히 넘어가게 된다.
// Good Code: 로그아웃 시 깔끔한 세션 파기
HttpSession session = request.getSession();
// session.removeAttribute("loginUser"); // 개별 삭제는 번거롭고 실수가 발생할 수 있음
session.invalidate(); // 장부에서 해당 유저의 공간을 완전히 폭파시킴 (권장)
response.sendRedirect("index.jsp"); // 무의 상태로 로비로 돌려보냄
로그아웃을 구현할 때도 개별 속성을 지우기보다는 session.invalidate()를 호출하여 해당 사용자의 세션 자체를 무효화(폭파)시켜버리는 것이 훨씬 깔끔하고 안전한 설계다.
| 구분 | Cookie (쿠키) | Session (세션) |
| 저장 위치 | 클라이언트 (브라우저) | 서버 (메모리) |
| 보안성 | 매우 취약함 | 매우 우수함 |
| 서버 부하 | 없음 | 접속자가 많을수록 증가 |
Insight: 세션은 안전하지만 서버의 메모리를 갉아먹는다. Spring Security 같은 최신 프레임워크가 인증 처리를 할 때도, 결국 내부적으로는 이 세션을 얼마나 안전하고 효율적으로 관리하느냐가 핵심이다.
데이터의 생명주기: 4대 Scope
웹 프로그래밍에서 데이터를 어딘가에 담아두려면, "이 데이터를 언제까지 살려둘 것인가"를 결정해야 한다. 이를 **스코프(Scope)**라고 하며 4가지 생명주기가 존재한다.
| Scope | 생명 주기 (Life Cycle) | 사용 목적 |
| Page | 해당 JSP 파일 내에서만 유효 | 단일 페이지 내의 임시 변수 처리 |
| Request | 응답(Response)이 완료될 때까지 유효 | Forward를 통해 다른 페이지로 데이터를 넘길 때 (MVC 핵심) |
| Session | 브라우저가 닫히거나, 만료시간이 될 때까지 유효 | 로그인 인증, 사용자별 장바구니 등 |
| Application | 서버(Tomcat)가 꺼질 때까지 영구 유효 | 전체 방문자 수, 글로벌 설정값 등 |
Session이나 Application에 데이터를 마구잡이로 넣으면 서버 메모리가 터져버린다. 따라서 개발자는 데이터의 생명력은 가급적 가장 짧은 스코프(Page, Request)로 제한하는 습관을 들여야 한다.
Insight: 좋은 코드는 변수의 생명주기가 짧은 코드다. 꼭 필요한 정보가 아니라면 Session 스코프 사용을 최대한 자제해야 한다.
Forward vs Redirect: 포워딩이 필수적인 이유
가장 중요한 핵심은 Request Scope의 동작 원리다. 사용자가 버튼을 눌러 요청(Request)을 보내면 그 객체는 응답(Response)이 끝나는 순간 소멸한다. 하지만 우리가 DB에서 조회한 데이터를 다른 JSP 화면에 뿌려주고 싶다면, 이 데이터를 살려둔 채로 화면으로 넘어가야 한다.
// 1. Redirect 방식 (데이터 소멸)
request.setAttribute("userInfo", userDto); // 택배 상자에 담았지만...
response.sendRedirect("result.jsp");
// 브라우저에게 "result.jsp로 새로 다시 접속해!" 라고 명령.
// 결과: 기존 request 객체 파괴, userInfo는 날아감.
// 2. Forward 방식 (데이터 유지)
request.setAttribute("userInfo", userDto); // 택배 상자에 담고...
RequestDispatcher dispatcher = request.getRequestDispatcher("result.jsp");
dispatcher.forward(request, response);
// 서버 내부에서 조용히 result.jsp로 처리를 위임.
// 결과: URL은 바뀌지 않으며, 기존 request 택배 상자가 고스란히 전달됨!
사용자가 게시글을 조회하려고 할 때, 서블릿이 DB에서 글을 찾아 Request 스코프에 담아두더라도 리다이렉트(Redirect)를 해버리면 애써 찾아온 데이터가 몽땅 날아가 버린다.
반면 포워드(Forward)는 서버 내부에서 은밀하게 작업 지시서를 넘기는 것과 같다. 사용자(브라우저) 입장에서는 URL이 변하지도 않고 페이지가 바뀌었는지조차 모르지만, 서버 내부에서는 서블릿이 DB 조회를 끝내고 그 데이터를 고스란히 JSP에게 넘겨주어 화면을 렌더링하게 만든다.
조회하려면 적어도 무엇인가 데이터를 들고 있어야 하는데, 그것을 유지하며 보내줄 수 있는 유일한 열쇠가 바로 포워딩(Forwarding)인 것이다.
Insight: MVC 패턴에서 Controller(서블릿)와 View(JSP) 사이의 통신은 전적으로 Forward에 의존한다. Forward 없이는 데이터를 안전하고 가볍게 넘겨줄 방법이 없기 때문이다.
Q. 세션 타임아웃은 보통 ms 단위인가요?
자바에서 날짜/시간을 계산할 때(예: `System.currentTimeMillis()`)는 정밀성을 위해 밀리초(ms) 단위를 씁니다. 하지만 서블릿 API의 세션 만료 시간(session.setMaxInactiveInterval())은 초(Second) 단위를 사용합니다. 인간의 체감 시간(예: 로그인 유지 30분)을 설정하기엔 초 단위가 직관적이기 때문입니다.
Q. 리다이렉트할 때 세션이 끊길 수도 있나요?
리다이렉트는 브라우저가 새로운 요청을 보내는 것이므로 시간이 지체될 수 있습니다. 만약 리다이렉트 하는 그 찰나의 순간에 세션 만료 시간이 다 되어버린다면 다음 페이지에서 인증이 풀릴 수 있습니다. 따라서 아주 정교한 시스템에서는 중요한 리다이렉트 전에 세션 age를 갱신(touch)해주는 테크닉을 쓰기도 합니다.
Q. Spring Security는 아이디/비밀번호 대조를 어떻게 하나요?
Spring Security 역시 내부적으로는 DB에서 유저 정보를 조회하여 입력받은 비밀번호를 해시(Hash) 대조하는 과정을 거칩니다. 단, 어떤 것이 ID이고 어떤 것이 PW인지를 프레임워크가 알 수 있도록 UserDetailsService라는 규격화된 인터페이스를 제공하여, 개발자가 직접 매핑해주도록 설계되어 있습니다.
마치며
웹은 본질적으로 무상태(Stateless)이지만, 우리는 쿠키와 세션을 통해 사용자를 기억하게 만들고, 4대 Scope와 Forwarding 메커니즘을 통해 컴포넌트 간에 효율적으로 데이터를 주고받는다.
특히 Forwarding을 통한 Request Scope의 활용은, 화면(View)과 로직(Controller)을 완벽하게 분리하는 MVC 패턴의 가장 결정적인 토대가 된다.
무상태성(Stateless)을 극복하기 위한 쿠키와 세션의 차이, 4대 Scope의 생명주기, 그리고 Forward 방식이 MVC 패턴에서 필수적인 이유를 분석.
Environment: Windows 11, JDK 21, Apache Tomcat 10.1, VS Code