HTTP 세션은 웹 애플리케이션에서 사용자의 상태 정보를 유지하기 위한 메커니즘이다. HTTP 프로토콜은 기본적으로 상태를 유지하지 않는 Stateless 프로토콜이므로, 웹 서버는 각 요청을 독립적으로 처리한다.
그러나 모든 웹 애플리케이션에서는 사용자의 로그인 상태, 장바구니 내역 등과 같은 정보를 유지해야 한다. 이를 실현하기 위해 세션을 사용하여 상태 정보를 보관하고 관리 할 수 있다.
오로지 HTTP 요청/응답 프로세스만을 이용하여 회원 정보를 유지할 수 없다.
이유는 다음과 같다.
HTTP 요청을 보낼 때 사용자의 정보를 포함할 수 없다.
왜냐하면 HTTP 요청 안에 보안 때문에 회원정보가 포함될 수 없기 때문이다.
만약 회원 ID, 비밀번호 등이 HTTP에 포함된다면 개인정보 유출의 위험성이 매우 클 것이다.
HTTP의 Stateless 속성에 따라 서버가 상태를 유지하지 않는다.
HTTP 프로토콜은 각 요청 간에 상태 정보를 유지하지 않기 때문에, 서버는 각 요청을 독립적으로 처리한다.
설령 회원정보가 암호화하여 서버에게 전달된다고 하더라도, 서버는 오직 HTTP를 이용해서는 회원 정보를 유지할 수 없다.
예를 들어 아래와 같은 HTTP 요청을 서버에 보냈다고 가정하자.
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}
서버는 이 HTTP 요청을 읽고 로그인 처리를 한다.
그러나 HTTP는 Stateless 프로토콜이므로, 서버는 응답을 보내자마자 이 로그인 정보를 잊어버리게 된다. 이로 인해 다음과 같은 문제가 발생할 수 있다.
이 것은 결국 모든 페이지에서 인증/인가 작업이 수행되는 것이다.
이 문제를 해결하기 위해 서버는 세션이나 토큰 기반 인증을 사용하여 사용자의 상태를 관리한다. 세션 ID나 JWT(JSON Web Token)와 같은 메커니즘을 사용하면, 클라이언트가 이후 요청에서 해당 인증 정보를 포함하여 서버에 전달함으로써 서버가 사용자의 상태를 지속적으로 인식할 수 있다.

HTTPSession에 대한 글이기 때문에 세션 인증방식은 간단하게 짚고 넘어만 가겠다.
세션 저장소를 개발자가 직접 만들어도 되기는 하지만,
보통은 프레임워크에서 미리 만들어서 기본으로 제공한다.
자바 계통(서블릿, 스프링) 개발자들은 서블릿에서 제공하는 HTTP Session을 이용하면 된다.
세션은 Key - Value로 이루어진다.
주로 userid, username, email, role, loginTime,preferences(환경설정 정보)등을 넣으며,
실제 웹사이트에선 3~5개 정도를 넣어서 회원을 구분한다고 한다.
너무 많으면 오버헤드가 발생하므로 적당히 넣는게 좋을 것이다.
🧸 세션 관리 문제
HTTP Session이 기본적으로 저장되는 공간은 톰캣이다.
애플리케이션을 다시 실행하면 사용자가 로그아웃된다는 점이 문제가 될 수 있다.
왜냐하면 세션이 내장된 톰캣 서버의 메모리에 저장되기 때문이다.
🧸 실제로 사용하는 세션 저장소
현업에서는 다음 세 가지 세션 저장 방식을 주로 사용한다.
1.톰캣 세션 사용
기본 방식: 별다른 설정 없이 기본적으로 사용되는 방식이다.
문제점: 여러 대의 WAS를 사용할 때, 각 톰캣 서버 간에 세션을 공유하려면 추가 설정이 필요하다.
2.데이터베이스 (예: MySQL) 사용
장점: 여러 WAS 간에 세션을 쉽게 공유할 수 있다.
단점: 로그인 요청마다 데이터베이스를 조회해야 해서 성능 문제가 발생할 수 있다.
그래서 주로 로그인 요청이 적은 백오피스나 사내 시스템에서 사용한다.
3.메모리 DB (예: Redis, Memcached) 사용
장점: B2C 서비스에서 많이 사용하는 방식이다. 메모리 기반이라 빠르고, 여러 서버 간에 세션을 쉽게 공유할 수 있다.
단점: 메모리 사용이 고비용이다. 메모리 기반 데이터베이스는 RAM을 사용하기 때문에, 저장할 데이터가 많아질수록 비용이 증가한다. 특히 대규모 애플리케이션에서는 상당한 양의 RAM이 필요할 수 있다.
🧸 어차피 세 방법 모두 서버 컴퓨터의 RAM을 사용하는데, 굳이 나누는 이유가 뭘까?
서비스의 특성에 따라 더 알맞은 방법을 사용하는 것이 중요하기 때문이다.
예를 들어, 사용자가 아주 많은 인터파크같은 웹사이트를 운영한다고 가정할 때,
세션 저장소를 RDB에 두는 것보다는 메모리 DB에 두는 것이 더 효율적일 것이다.
아무튼 다시 HTTP Session에 되돌아와서, 어떤 메소드들을 제공하는지 알아보자.
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpSession;
public class CustomHttpSession implements HttpSession {
private final String id;
private final long creationTime;
private long lastAccessedTime;
private int maxInactiveInterval;
private final ServletContext servletContext;
private final Map<String, Object> attributes = new HashMap<>();
private boolean invalidated = false;
private boolean isNew = true;
public CustomHttpSession(String id, ServletContext servletContext) {
this.id = id;
this.creationTime = System.currentTimeMillis();
this.lastAccessedTime = this.creationTime;
this.servletContext = servletContext;
}
/**
* 이 세션이 생성된 시간을 밀리초 단위로 반환한다.
* 1970년 1월 1일 0시 0분 0초(GMT) 이후로 경과된 시간이다.
* @return 세션이 생성된 시간(밀리초 단위)
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public long getCreationTime() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return creationTime;
}
/**
* 이 세션에 할당된 고유 식별자를 문자열로 반환한다.
* @return 세션의 고유 식별자
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public String getId() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return id;
}
/**
* 마지막으로 이 세션에 접근한 시간을 밀리초 단위로 반환한다.
* 1970년 1월 1일 0시 0분 0초(GMT) 이후로 경과된 시간이다.
* @return 마지막 접근 시간(밀리초 단위)
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public long getLastAccessedTime() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return lastAccessedTime;
}
/**
* 이 세션이 속한 ServletContext를 반환한다.
* @return 세션의 ServletContext 객체
*/
@Override
public ServletContext getServletContext() {
return servletContext;
}
/**
* 세션의 최대 비활성화 간격 시간을 초 단위로 설정한다.
* @param interval 최대 비활성화 간격(초 단위)
*/
@Override
public void setMaxInactiveInterval(int interval) {
this.maxInactiveInterval = interval;
}
/**
* 세션의 최대 비활성화 간격 시간을 초 단위로 반환한다.
* @return 최대 비활성화 간격(초 단위)
*/
@Override
public int getMaxInactiveInterval() {
return maxInactiveInterval;
}
/**
* 지정된 이름으로 세션에 바인딩된 객체를 반환한다.
* @param name 객체의 이름
* @return 바인딩된 객체
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public Object getAttribute(String name) {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return attributes.get(name);
}
/**
* 세션에 바인딩된 모든 객체의 이름을 Enumeration 형태로 반환한다.
* @return 바인딩된 모든 객체의 이름
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public Enumeration<String> getAttributeNames() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return new Enumeration<>() {
private final String[] keys = attributes.keySet().toArray(new String[0]);
private int index = 0;
@Override
public boolean hasMoreElements() {
return index < keys.length;
}
@Override
public String nextElement() {
return keys[index++];
}
};
}
/**
* 세션에 객체를 바인딩한다. 동일한 이름으로 객체가 이미 바인딩되어 있으면 교체한다.
* @param name 바인딩할 객체의 이름
* @param value 바인딩할 객체
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public void setAttribute(String name, Object value) {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
attributes.put(name, value);
}
/**
* 지정된 이름으로 바인딩된 객체를 세션에서 제거한다.
* @param name 제거할 객체의 이름
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public void removeAttribute(String name) {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
attributes.remove(name);
}
/**
* 이 세션을 무효화하고, 세션에 바인딩된 모든 객체를 언바인딩한다.
* @exception IllegalStateException 세션이 이미 무효화된 경우
*/
@Override
public void invalidate() {
if (invalidated) {
throw new IllegalStateException("Session is already invalidated");
}
invalidated = true;
attributes.clear();
}
/**
* 클라이언트가 아직 세션에 참여하지 않았거나, 클라이언트가 세션에 참여하지 않기로 선택한 경우 true를 반환한다.
* @return 세션이 새로 생성된 경우 true
* @exception IllegalStateException 세션이 무효화된 경우
*/
@Override
public boolean isNew() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
return isNew;
}
// 세션에 접근하여 새로운 상태를 리셋하는 커스텀 메소드
public void access() {
if (invalidated) {
throw new IllegalStateException("Session is invalidated");
}
isNew = false;
lastAccessedTime = System.currentTimeMillis();
}
}
HTTP Session과 함께 사용되는 클래스와 인터페이스들은 대표적으로 아래의 것들이 있다.
HttpSession: 세션 관리의 기본 인터페이스.
StandardSession: HttpSession 인터페이스를 구현하는 클래스.
ManagerBase: 세션 관리를 위한 추상 클래스.
Manager: ManagerBase를 확장한 구체적인 세션 관리 클래스.
StandardManager: Manager를 확장한 구체적인 세션 관리 클래스.
PersistentManager: 세션의 영속성을 관리하는 클래스.
HttpSessionListener: 세션 생성 및 소멸 이벤트를 처리하는 인터페이스.
HttpSessionAttributeListener: 세션 속성 변경 이벤트를 처리하는 인터페이스.
HttpSessionBindingListener: 객체가 세션에 바인딩되거나 바인딩이 해제될 때 호출되는 인터페이스.
SessionIdGenerator: 세션 ID를 생성하는 클래스.
구조는 아래와 같다. (화살표 아래가 구현체임)
HttpSession
↑
StandardSession
ManagerBase
↑
Manager
↑
StandardManager
↑
PersistentManager
HttpSessionListener
HttpSessionAttributeListener
HttpSessionBindingListener
SessionIdGenerator(톰캣의 일부)
개발을 하다보면 HttpSession 클래스를 이용한 다양한 메소드들을 사용한다.
그 중 가장 자주 쓰이는 메소드들을 뽑아서 어떻게 실행되는지 자세히 알아보자.
🧸 setAttribute(String name, Object value)
설명: 세션에 속성을 설정한다.
관련 요소 및 메소드:
StandardSession 클래스: setAttribute(String name, Object value) 메소드
HttpSessionAttributeListener 인터페이스: 속성이 설정될 때 이벤트를 처리하는 메소드 (attributeAdded 메소드)
// HttpSession 인터페이스
public interface HttpSession {
void setAttribute(String name, Object value);
// ... 기타 메소드
}
// StandardSession 클래스
public class StandardSession implements HttpSession {
// 세션 속성 저장소
private Map<String, Object> attributes = new HashMap<>();
@Override
public void setAttribute(String name, Object value) {
Object oldValue = attributes.put(name, value);
if (oldValue == null) {
// 속성 추가 이벤트 트리거
// HttpSessionAttributeListener가 있다면 attributeAdded 호출
} else {
// 속성 변경 이벤트 트리거
}
}
// ... 기타 메소드 구현
}
// HttpSessionAttributeListener 인터페이스
public interface HttpSessionAttributeListener {
void attributeAdded(HttpSessionBindingEvent event);
// ... 기타 메소드
}
🧸 invalidate()
설명: 세션을 무효화시킨다 (로그아웃)
관련 요소 및 메소드:
StandardSession 클래스: invalidate() 메소드
HttpSessionListener 인터페이스: 세션이 무효화될 때 이벤트를 처리하는 메소드 (sessionDestroyed 메소드)
// HttpSession 인터페이스
public interface HttpSession {
void invalidate();
// ... 기타 메소드
}
// StandardSession 클래스
public class StandardSession implements HttpSession {
@Override
public void invalidate() {
// 세션 무효화 작업
// HttpSessionListener가 있다면 sessionDestroyed 호출
}
// ... 기타 메소드 구현
}
// HttpSessionListener 인터페이스
public interface HttpSessionListener {
void sessionDestroyed(HttpSessionEvent event);
// ... 기타 메소드
}
🧸 getId()
설명: 세션의 ID를 반환한다.
관련 요소 및 메소드:
StandardSession 클래스: getId() 메소드
동작 과정:
// HttpSession 인터페이스
public interface HttpSession {
String getId();
// ... 기타 메소드
}
// StandardSession 클래스
public class StandardSession implements HttpSession {
private String id;
@Override
public String getId() {
return id;
}
// ... 기타 메소드 구현
}
만약 세션에
{{"loginNickname", "john123"}}과 같은 유저의 정보가 들어있고, 이 사람의 JSESSIONID가 12345A라면, 이는 서버의 어느 곳에 저장되어 있을까?
정답은 톰캣이다.
JSESSIONID는 톰캣이 내부적으로 내려주는 것이다.
톰캣은 JSESSIONID를 key로 사용하여 세션들을 관리한다.
JSESSIONID를 가지고 세션을 보관하는 저장소에서 특정 세션을 꺼낸다.
동일한 JSESSIONID라면 서버는 동일한 사용자의 요청으로 인식한다.
로그인한 사용자라면 jsessionid를 통해 꺼내온 세션에 loginMember가 있을 것이고 아니라면 loginMember가 없을 것이다.
https://jwlim94.tistory.com/16
https://oh-sh-2134.tistory.com/111