본문으로 건너뛰기

2.1. 원격 데이터그리드 모드 설정

시작하기 전에

이 장에서는 애플리케이션이 세션을 WAS 메모리 대신 데이터그리드에 저장하도록 바꾸는 작업을 합니다. 1부에서 본 세 가지 구성 가운데 데이터그리드만 쓰는 구성을 기준으로 설명합니다.

애플리케이션 코드는 고치지 않습니다. HttpSession 을 쓰던 코드는 그대로 두고, 서블릿 필터를 하나 끼워 넣어 세션을 읽고 쓰는 자리만 바꿉니다.

준비물

무엇확인할 것
IMDG 서버세션을 저장할 In Memory Data Grid 서버가 떠 있고, HotRod 포트(기본 11222)로 접속됩니다. 서버 2대 이상에 각 인스턴스 2개를 권장합니다(1부 "데이터그리드는 몇 대로 구성하나" 참고). OPENMARU Installer 로 설치할 수 있습니다
애플리케이션Servlet 2.5 이상을 지원하는 WAS 에서 동작합니다 (WebLogic, JEUS, Tomcat, JBoss EAP 등)
세션에 담는 객체직렬화(Serializable)되어 있어야 합니다. 아래 "세션에 담는 객체는 직렬화되어야 합니다" 참고
기존 필터 목록애플리케이션에 이미 등록된 필터를 확인해 둡니다. 이 필터를 그 앞에 놓아야 합니다(3단계 참고)

전체 절차

단계무엇을 하나어디를 고치나
1라이브러리를 애플리케이션에 넣습니다pom.xml
2세션 저장소(IMDG 서버) 주소를 알려 줍니다hotrod.properties
3세션을 가로챌 필터를 등록합니다web.xml 또는 Java 설정
4세션이 저장소로 가는지 확인합니다애플리케이션 실행

1단계. 라이브러리 추가

애플리케이션이 세션 필터와 HotRod 클라이언트를 쓸 수 있도록 라이브러리를 넣습니다.

OPENMARU Cluster 는 공개 저장소에 올라가 있지 않습니다. 담당 엔지니어가 라이브러리 파일을 전달합니다. 전달받은 방식에 따라 아래 둘 중 하나로 넣습니다.

라이브러리 파일을 직접 넣는 경우

전달받은 .jar 파일을 애플리케이션의 WEB-INF/lib 디렉터리에 넣습니다.

myapp.war
└── WEB-INF/
├── lib/
│ ├── khan-session-core-5.1.0.jar ← 전달받은 파일
│ ├── khan-session-hotrod-5.1.0.jar ← 전달받은 파일
│ └── (함께 전달받은 나머지 파일들)
└── web.xml

전달받은 파일을 하나도 빠뜨리지 않고 모두 넣습니다. 제품 라이브러리 외에 그것이 동작하는 데 필요한 라이브러리가 함께 전달됩니다. 하나라도 빠지면 애플리케이션 기동 중 ClassNotFoundException 또는 NoClassDefFoundError 가 납니다.

같은 WAS 에서 여러 애플리케이션을 운영한다면 애플리케이션마다 WEB-INF/lib 에 넣거나, WAS 의 공통 라이브러리 디렉터리에 한 번만 두는 방법도 있습니다. 공통 디렉터리의 위치는 WAS 제품마다 다르므로 해당 제품 문서를 확인합니다.

Maven·Gradle 을 쓰는 경우

사내 저장소(Nexus, Artifactory 등)에 라이브러리가 등록되어 있다면 의존성 선언으로 대신할 수 있습니다. 등록 여부는 담당 엔지니어에게 확인합니다.

<dependency>
<groupId>com.opennaru.khan</groupId>
<artifactId>khan-session-hotrod</artifactId>
<version>5.1.0</version>
</dependency>

제대로 들어갔는지 확인

어느 방법으로 넣었든, 아래 클래스가 애플리케이션에서 보여야 합니다. 3단계에서 이 이름을 씁니다.

com.opennaru.khan.session.filter.InfinispanHotRodSessionFilter

2단계. 세션 저장소 주소 설정

필터가 어느 IMDG 서버에 세션을 저장할지 알려 줘야 합니다. hotrod.properties 파일을 만들고 서버의 IP 와 포트를 적습니다. 이 파일은 애플리케이션의 클래스패스에 둡니다.

infinispan.client.hotrod.server_list = 192.168.0.11:11222

서버가 여러 대면 쉼표로 나열합니다. 클라이언트는 이 목록에 있는 서버들에 나눠 접속합니다.

infinispan.client.hotrod.server_list = 192.168.0.11:11222,192.168.0.12:11222

이 파일의 이름은 3단계의 configFile 설정으로 필터에 전달합니다.

이 파일에는 접속 풀 크기나 직렬화 방식 같은 항목도 넣을 수 있습니다. 대부분의 환경에서는 서버 목록만 적으면 되고 나머지는 기본값으로 충분합니다. 조정이 필요하면 담당 엔지니어에게 문의하십시오.

IMDG 서버 자체의 설치와 운영은 이 문서의 범위가 아닙니다. OPENMARU Installer 로 자동 설치할 수 있습니다.

3단계. 필터 등록

세션을 가로챌 필터를 애플리케이션에 등록합니다. 여기서 세션 저장소 위치, 쿠키 이름, 타임아웃 같은 동작 방식을 함께 정합니다. 설정 항목 하나하나의 뜻은 아래 "설정 항목" 표에 있습니다.

XML 설정을 쓰는 환경과 Java 설정을 쓰는 환경 가운데 해당하는 쪽만 보면 됩니다.

이 필터는 반드시 가장 먼저 실행되어야 합니다

애플리케이션에 등록된 필터 가운데 이 필터가 첫 번째여야 합니다.

이 필터는 들어온 요청을 감싼 새 요청 객체를 만들어 다음 필터로 넘깁니다. 그래야 뒤에 오는 필터와 애플리케이션이 request.getSession() 을 호출했을 때 WAS 메모리의 세션이 아니라 데이터그리드의 세션을 받습니다.

앞에 다른 필터가 있으면 그 필터는 감싸이지 않은 원본 요청을 봅니다. 그 필터가 세션을 건드리면 WAS 가 자기 세션을 먼저 만들어 버리고, 그 뒤부터는 세션이 두 곳에 따로 생깁니다. 겉으로는 오류 없이 동작하지만 WAS 를 재시작하면 로그인이 풀리고, 인스턴스가 바뀌면 세션이 이어지지 않습니다.

특히 다음 필터들이 세션을 건드리므로 반드시 뒤에 두어야 합니다.

  • 인증·인가 필터 (Spring Security 등)
  • 문자 인코딩 필터 가운데 세션에 값을 넣는 것
  • 로깅·감사 필터 가운데 세션 ID 나 로그인 사용자를 기록하는 것
  • 직접 만든 로그인 확인 필터

web.xml 을 쓰는 경우

<filter> 안에 <init-param> 으로 설정을 넣고, <filter-mapping> 으로 모든 요청(/*)에 적용합니다. 세션이 만들어지고 없어지는 것을 감지하려면 <listener> 도 함께 등록합니다.

서블릿 규격은 <filter-mapping> 이 적힌 순서대로 필터를 실행합니다. 그러므로 이 필터의 <filter-mapping>web.xml 안의 모든 <filter-mapping> 보다 위에 두어야 합니다. <filter> 선언 위치는 순서에 영향을 주지 않습니다 — 순서를 정하는 것은 <filter-mapping> 입니다.

반드시 넣어야 하는 항목은 configFile 하나입니다. 2단계에서 만든 저장소 접속 파일의 이름이며, 없으면 기동할 때 오류가 납니다.

나머지는 넣지 않으면 기본값이 쓰입니다. 다만 아래 셋은 값을 확인해 두십시오.

항목확인할 이유
sessionTimeout기본값이 10분 입니다. 기존 애플리케이션의 세션 타임아웃과 다르면 사용자가 예상보다 빨리 로그아웃됩니다
excludeRegExp기본값이 없습니다. 넣지 않으면 이미지·CSS 요청에도 세션이 만들어져 저장소 부하가 늘어납니다
allowDuplicateLogin기본값이 false중복 로그인 방지가 처음부터 켜져 있습니다. 허용해야 한다면 true 를 넣습니다

infinispanCacheinfinispanLoginCache 는 넣지 않으면 각각 KHAN_SESSION, KHAN_SESSION_LOGIN 이 쓰입니다. 아래 예제는 ${환경변수:기본값} 형태로 다른 이름을 지정하는 방법을 함께 보여 줍니다.

아래는 자주 쓰는 항목을 넣은 예입니다.

<?xml version="1.0" encoding="UTF-8"?>
{/*
~ Opennaru, Inc. http://www.opennaru.com/
~
~ Copyright (C) 2014 Opennaru, Inc. and/or its affiliates.
~ All rights reserved by Opennaru, Inc.
*/}
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="session1" version="2.5">

<display-name>Test</display-name>
<description>Test App</description>
{/* <distributable/> */}
<filter>
<filter-name>KhanSessionFilter</filter-name>
{/* Hotrod Mode */}
<init-param>
<param-name>configFile</param-name>
<param-value>${OPENMARU_CONFIG_FILE:hotrod.properties}</param-value>
</init-param>
<init-param>
<param-name>infinispanCache</param-name>
<param-value>${OPENMARU_INFINISPAN_CACHE:OPENMARU_SESSION}</param-value>
</init-param>
<init-param>
<param-name>infinispanLoginCache</param-name>
<param-value>${OPENMARU_INFINISPAN_LOGIN_CACHE:OPENMARU_SESSION_LOGIN}</param-value>
</init-param>
<init-param>
<param-name>sessionId</param-name>
<param-value>__KSMSID__</param-value>
</init-param>
<init-param>
<param-name>domain</param-name>
<param-value></param-value>
</init-param>
<init-param>
<param-name>path</param-name>
<param-value>/test1</param-value> {/* 서로 다른 WebApp간 세션 공유하려면 '/' 으로 설정 */}
</init-param>
<init-param>
<param-name>secure</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>httpOnly</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>sessionTimeout</param-name>
<param-value>30</param-value>
</init-param>
<init-param>
<param-name>excludeRegExp</param-name>
<param-value>/.+\.(html|jpg|jpeg|png|gif|js|css|swf)</param-value>
</init-param>
<init-param>
<param-name>allowDuplicateLogin</param-name> {/* 중복 로그인을 허용하려면 true로 설정 */}
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>duplicateLoginPolicy</param-name> {/* 중복 로그인 정책: none, legacy, custom */}
<param-value>legacy</param-value>
</init-param>
<init-param>
<param-name>invalidateDuplicateLogin</param-name> {/* false 로 설정 하면 내부적으로 invalidate를 호출하지 않음(중복 여부 API 로 체크 후 직접 invalidate API 호출) */}
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>logoutUrl</param-name> {/* 중복 로그인시 logout URL 설정 */}
<param-value>/logout.jsp</param-value>
</init-param>
<init-param>
<param-name>enableImmediateSave</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>KhanSessionFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>ERROR</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<listener>
<listener-class>com.opennaru.khan.session.listener.SessionListener</listener-class>
</listener>

SpringBoot 처럼 Java 설정을 쓰는 경우

web.xml 을 쓰지 않는 환경에서는 같은 설정을 Java 코드로 넣습니다. FilterRegistrationBean 으로 필터를 등록하고 addInitParameter 로 설정 항목을 하나씩 전달합니다.

setOrder(Integer.MIN_VALUE) 를 반드시 넣습니다. 위에서 설명한 "가장 먼저 실행" 을 Java 설정에서 보장하는 방법입니다. 값이 작을수록 먼저 실행되므로 가장 작은 값을 줍니다.

Spring Security 를 함께 쓰는 경우 특히 주의합니다. Spring Security 의 필터 체인은 기본 순서가 -100 이라 지정하지 않으면 이 필터보다 먼저 돕니다.

@Configuration
public class OpenmaruFilterConfiguration implements WebMvcConfigurer {

@Bean
public FilterRegistrationBean getFilterRegistrationBean() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(new InfinispanHotRodSessionFilter());
registrationBean.setOrder(Integer.MIN_VALUE);

registrationBean.addInitParameter(Constants.INFINISPAN_CONFIGFILE_KEY, "hotrod.properties");
registrationBean.addInitParameter(Constants.INFINISPAN_CACHE_KEY, "KHAN_SESSION");
registrationBean.addInitParameter(Constants.INFINISPAN_LOGIN_CACHE_KEY, "KHAN_SESSION_LOGIN");
registrationBean.addInitParameter(Constants.SESSION_ID, "__KSMSID__");
registrationBean.addInitParameter(Constants.DOMAIN, "");
registrationBean.addInitParameter(Constants.PATH, "/");
registrationBean.addInitParameter(Constants.SECURE, "false");
registrationBean.addInitParameter(Constants.HTTP_ONLY, "false");
registrationBean.addInitParameter(Constants.SESSION_TIMEOUT, "30"); // minute
registrationBean.addInitParameter(Constants.SESSION_SAVE_DELAY, "5");
registrationBean.addInitParameter(Constants.EXCLUDE_REG_EXP, "/.+\\.(html|jpg|jpeg|png|gif|js|css|swf)");
registrationBean.addInitParameter(Constants.ALLOW_DUPLICATE_LOGIN, "true");
registrationBean.addInitParameter(Constants.DUPLICATE_LOGIN_POLICY, "legacy"); // none, legacy, custom
registrationBean.addInitParameter(Constants.DUPLICATE_LOGIN_EXCLUSTION_TYPE, "");
registrationBean.addInitParameter(Constants.INVALIDATE_DUPLICATE_LOGIN, "true");
registrationBean.addInitParameter(Constants.LOGOUT_URL, "");
registrationBean.addInitParameter(Constants.ENABLE_IMMEDIATED_SAVE, "true");
registrationBean.addInitParameter(Constants.ENABLE_STATISTICS, "true");
registrationBean.addInitParameter(Constants.ENABLE_MEMORY_STATISTICS, "false");
registrationBean.addInitParameter(Constants.LICENSE_KEY,
"#### LICENSE KEY ###\n" +
"> REQUEST sales@openmaru.io"
);

registrationBean.setUrlPatterns(Arrays.asList("/*"));
registrationBean.setDispatcherTypes(DispatcherType.ERROR, DispatcherType.INCLUDE, DispatcherType.FORWARD, DispatcherType.REQUEST);
return registrationBean;
}

@Bean
public HttpSessionListener httpSessionListener() {
SessionListener sessionListener = new SessionListener();
System.out.println("SessionListener started.");
return sessionListener;
}
}

4단계. 적용 확인

세 단계를 마쳤으면 애플리케이션을 기동해 세션이 실제로 저장소로 가는지 확인합니다.

1. 브라우저에서 쿠키를 봅니다. 로그인한 뒤 개발자 도구의 쿠키 목록에 JSESSIONID__KSMSID__ 가 함께 보여야 합니다.

JSESSIONID 는 WAS 가 원래 발급하는 쿠키라 그대로 남습니다. 필터는 그것을 지우지 않고 __KSMSID__ 쿠키를 하나 더 발급합니다. 데이터그리드에서 세션을 찾을 때 쓰는 것은 __KSMSID__ 입니다.

__KSMSID__추가로 보이는지가 확인 기준입니다. JSESSIONID 만 있고 __KSMSID__ 가 없으면 필터가 동작하지 않은 것입니다. 쿠키 이름은 sessionId 설정으로 바꿀 수 있습니다.

2. WAS 를 재시작해 봅니다. 로그인한 상태에서 WAS 인스턴스를 재시작한 뒤 화면을 새로 고쳤을 때 로그인이 유지되면 세션이 WAS 밖에 저장된 것입니다. 재시작 후 로그인 화면으로 돌아간다면 필터가 적용되지 않았거나 저장소에 연결되지 않은 것입니다.

3. 인스턴스를 두 대 이상 띄워 봅니다. 한 대에서 로그인한 뒤 다른 인스턴스로 요청이 가도 로그인 상태가 유지되어야 합니다.

잘 되지 않으면 아래를 확인합니다.

증상확인할 것
__KSMSID__ 쿠키가 보이지 않고 JSESSIONID 만 있음필터가 등록되지 않았거나 요청에 걸리지 않았습니다. <filter-mapping>url-pattern/* 인지, Java 설정이면 필터 순서가 가장 앞인지 봅니다
기동 중 저장소 연결 오류2단계의 hotrod.properties 경로와 server_list 의 IP·포트를 확인합니다. 그 파일이 클래스패스에 있어야 합니다
세션에 값을 넣을 때 오류담은 객체가 Serializable 을 구현했는지 봅니다(아래 참고)
재시작하면 로그인이 풀림필터는 동작하는데 저장소에 저장되지 않는 경우입니다. 저장소 연결과 캐시 이름(infinispanCache)을 확인합니다
__KSMSID__ 는 발급되는데 재시작하면 로그인이 풀림다른 필터가 먼저 실행되고 있을 가능성이 큽니다. web.xml 이면 이 필터의 <filter-mapping> 이 맨 위인지, Java 설정이면 setOrder(Integer.MIN_VALUE) 가 있는지 확인합니다