활동기록
HttpSessionListener 사용하기
모눈종이씨
2025. 1. 19. 20:10
HttpSessionListener
같은 아이디로 중복 로그인을 했을 경우, 먼저 들어온 사용자의 세션을 차단하는 기능을 만들어야 하는 상황입니다. 어떻게 구현하면 좋을까 검색을 하다가 HttpSessionListener를 사용해서 구현하는 방법을 알게 되었고, 이를 공부하면서 알게 된 점을 글로 작성해보았습니다.
HttpSessionListener란?
HttpSessionListener는 Java Servlet API에서 제공하는 인터페이스 중 하나로, 웹 애플리케이션에서 HttpSession 객체의 생명주기 이벤트를 감지하고 처리할 수 있도록 해줍니다. 즉, 세션이 생성되거나 소멸될 때 이를 감지하여 특정 로직을 실행할 수 있는 기능을 제공합니다. HttpSessionListener는 javax.servlet 패키지에 포함되어 있으며, 다음 두 가지 메서드를 제공합니다
- default void sessionCreated(HttpSessionEvent se)
- 새로운 세션이 생성될 때 호출됩니다.
- 사용자 인증, 초기 설정, 세션 관리 로직 등을 실행할 수 있습니다.
- default void sessionDestroyed(HttpSessionEvent se)
- 세션이 소멸될 때 호출됩니다.
- 리소스 정리, 로그 기록, 세션 통계 계산 등을 처리하는 데 유용합니다.
동작 원리
- 세션 생성 감지
- 사용자가 웹 애플리케이션에 접근할 때 새로운 세션이 필요하면 sessionCreated 메서드가 호출됩니다.
- 세션 소멸 감지
- 세션이 만료되거나 명시적으로 제거될 때 sessionDestroyed 메서드가 호출됩니다.
- 이 이벤트는 서버의 설정된 세션 타임아웃에 의해 자동으로 발생하거나, HttpSession.invalidate() 메서드를 호출하여 강제로 발생시킬 수 있습니다.
HttpSessionListener 사용 방법
- HttpSessionListener를 구현한 SessionUtil 클래스 생성
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class SessionUtil implements HttpSessionListener {
private final Logger logger = LoggerFactory.getLogger(SessionUtil.class);
@Override
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
sessions.put(session.getId(), session);
logger.debug("sessionCreated :: {} ", session.getId());
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
String sessionId = event.getSession().getId();
sessions.remove(sessionId);
logger.debug("sessionDestroyed :: {} ", sessionId);
}
}
- web.xml에 등록
<web-app>
<listener>
<listener-class>com.example.SessionUtil</listener-class>
</listener>
</web-app>
멀티스레드 환경을 고려한 SessionUtil
중복 로그인 구현 시, 다수의 사용자가 동시에 로그인을 시도하거나 로그아웃할 때 세션 정보를 관리하는 과정에서 동시성 문제가 발생할 수 있습니다. 이를 방지하기 위해 ConcurrentHashMap을 사용해 스레드 안전성을 보장하고, 필요한 경우 synchronized 키워드로 추가 동기화를 적용했습니다.
public class SessionUtil implements HttpSessionListener {
private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(SessionUtil.class);
@Override
public void sessionCreated(HttpSessionEvent event) {
HttpSession session = event.getSession();
sessions.put(session.getId(), session);
logger.debug("sessionCreated :: sessionId={}, userId={}", session.getId(), session.getAttribute("USER_ID"));
}
@Override
public void sessionDestroyed(HttpSessionEvent event) {
String sessionId = event.getSession().getId();
sessions.remove(sessionId);
logger.debug("sessionDestroyed :: sessionId={} ", sessionId);
}
public synchronized static void removeSessionForDoubleLogin(String userId, String nowSessionId) {
for (String key : sessions.keySet()) {
HttpSession session = sessions.get(key);
if (
userId.equals((String) session.getAttribute("USER_ID"))
&& !nowSessionId.equals(session.getId())
) {
sessions.remove(session.getId());
break;
}
}
}
public static boolean isValidSession(String sessionId) {
return sessions.containsKey(sessionId);
}
}
- 사용자가 로그인을 할 경우 사용자의 Id를 세션에 담게 됩니다. 따라서 세션에서 USER_ID에 해당하는 값으로 어떤 사용자인지 파악할 수 있습니다.
- 중복 로그인 세션을 지울 때 먼저 sessions Map 안에 현재 진입한 사용자의 id와 같은 id를 사용하는 세션이 있는지 찾습니다.(sessions에 이미 자신의 세션도 들어가 있으므로, 그 경우는 제외합니다.)
- 그리고 같은게 있다면 기존에 있던 세션을 제거합니다.
세션을 확인하고 지우는 과정에서 여러 스레드가 접근할 수 있으므로 synchroinized 키워드를 사용해 하나의 스레드만 처리할 수 있도록 했습니다.
아직 개선할 점이 많은 코드이지만, 전반적인 흐름을 잡고 다음 단계로 나아가는 데 도움이 되었으면 합니다.