모눈종이에 사각사각

HttpSessionListener 사용하기 본문

활동기록

HttpSessionListener 사용하기

모눈종이씨 2025. 1. 19. 20:10

HttpSessionListener

같은 아이디로 중복 로그인을 했을 경우, 먼저 들어온 사용자의 세션을 차단하는 기능을 만들어야 하는 상황입니다. 어떻게 구현하면 좋을까 검색을 하다가 HttpSessionListener를 사용해서 구현하는 방법을 알게 되었고, 이를 공부하면서 알게 된 점을 글로 작성해보았습니다.

HttpSessionListener란?

HttpSessionListener는 Java Servlet API에서 제공하는 인터페이스 중 하나로, 웹 애플리케이션에서 HttpSession 객체의 생명주기 이벤트를 감지하고 처리할 수 있도록 해줍니다. 즉, 세션이 생성되거나 소멸될 때 이를 감지하여 특정 로직을 실행할 수 있는 기능을 제공합니다. HttpSessionListener는 javax.servlet 패키지에 포함되어 있으며, 다음 두 가지 메서드를 제공합니다

  1. default void sessionCreated(HttpSessionEvent se)
    • 새로운 세션이 생성될 때 호출됩니다.
    • 사용자 인증, 초기 설정, 세션 관리 로직 등을 실행할 수 있습니다.
  2. default void sessionDestroyed(HttpSessionEvent se)
    • 세션이 소멸될 때 호출됩니다.
    • 리소스 정리, 로그 기록, 세션 통계 계산 등을 처리하는 데 유용합니다.

동작 원리

  1. 세션 생성 감지
    • 사용자가 웹 애플리케이션에 접근할 때 새로운 세션이 필요하면 sessionCreated 메서드가 호출됩니다.
  2. 세션 소멸 감지
    • 세션이 만료되거나 명시적으로 제거될 때 sessionDestroyed 메서드가 호출됩니다.
    • 이 이벤트는 서버의 설정된 세션 타임아웃에 의해 자동으로 발생하거나, HttpSession.invalidate() 메서드를 호출하여 강제로 발생시킬 수 있습니다.

HttpSessionListener 사용 방법

  1. 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);
		}
}

  1. 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);
	}
}
  1. 사용자가 로그인을 할 경우 사용자의 Id를 세션에 담게 됩니다. 따라서 세션에서 USER_ID에 해당하는 값으로 어떤 사용자인지 파악할 수 있습니다.
  2. 중복 로그인 세션을 지울 때 먼저 sessions Map 안에 현재 진입한 사용자의 id와 같은 id를 사용하는 세션이 있는지 찾습니다.(sessions에 이미 자신의 세션도 들어가 있으므로, 그 경우는 제외합니다.)
  3. 그리고 같은게 있다면 기존에 있던 세션을 제거합니다.

세션을 확인하고 지우는 과정에서 여러 스레드가 접근할 수 있으므로 synchroinized 키워드를 사용해 하나의 스레드만 처리할 수 있도록 했습니다.

아직 개선할 점이 많은 코드이지만, 전반적인 흐름을 잡고 다음 단계로 나아가는 데 도움이 되었으면 합니다.

Comments