Aspectran은 특정 웹 컨테이너나 서블릿 스펙에 얽매이지 않고, 독립 실행형 데몬, 대화형 CLI 셸, 마이크로서비스, 그리고 대규모 웹 애플리케이션 서버에 이르기까지 모든 실행 환경에서 일관된 상태 관리(State Management)를 지원하도록 자체 세션 관리 아키텍처를 제공합니다.
이 가이드에서는 Aspectran Session Manager의 내부 설계 원리와 컴포넌트 구조, 신규/일반 세션 분리 기반의 정교한 생명주기 제어, 파일 및 Redis 기반 분산 클러스터링 저장소 구성, 그리고 Aspectow Enterprise(Undertow)와 Aspectow Edge(Netty) 등 각 서버 환경에서의 구체적인 설정 방법과 API 활용법을 종합적으로 다룹니다.
1. 핵심 아키텍처와 설계 철학
웹 애플리케이션의 전유물로 여겨졌던 세션의 개념을 추상화하여 모든 애플리케이션 런타임에서 재사용할 수 있도록 설계되었습니다. 서블릿 컨테이너의 세션 메커니즘을 흉내 내는 것에 그치지 않고, 클라우드 네이티브 환경의 고가용성과 확장성을 충족하도록 역할별로 명확히 분리된 컴포넌트 생태계를 갖추고 있습니다.
1.1. 주요 컴포넌트 구성
SessionManager(기본 구현체:DefaultSessionManager)- 세션 관리의 전체 흐름을 관장하는 중앙 컨트롤러이자 진입점입니다.
- 세션의 생성, 조회, 갱신, 무효화(Invalidation) 등 전체 수명주기 이벤트를 총괄합니다.
- 내부적으로
SessionCache,SessionStore,HouseKeeper등 핵심 컴포넌트를 조율하여 최적의 I/O 효율을 보장합니다. 세션 조회 요청 시 1차 메모리 캐시를 확인하고, 캐시 미스 발생 시 영속 저장소에서 데이터를 로드합니다.
Session(구현체:ManagedSession)- 개별 사용자의 상태 정보를 보관하는 세션 데이터 객체입니다.
- 세션 고유 식별자(ID), 생성 시각, 최종 접근 시각, 최대 유휴 시간 등의 메타데이터와 사용자가 바인딩한 속성(Attribute) 맵을 관리합니다.
SessionCache(기본 구현체:DefaultSessionCache)- 활성 세션 객체를 JVM 힙 메모리에 상주시키는 고속 캐시 계층입니다.
- 물리적 저장소(파일, Redis 등)로 향하는 디스크 및 네트워크 I/O를 최소화하여 동시성 처리 성능을 극대화합니다.
SessionStore(저장소 추상화 인터페이스)- 세션 데이터를 물리적으로 영속화하고 인스턴스 간에 공유하는 스토리지 계층입니다.
- 저장소 플러그형(
Pluggable) 아키텍처로 구현되어 있어 코드 변경 없이 설정을 통해 파일 스토리지나 Redis 분산 스토리지 등으로 자유롭게 교체할 수 있습니다.
HouseKeeper- 백그라운드에서 주기적으로 가동되어 만료된 세션을 감지하고 정리하는 세션 청소부(Scavenger) 스레드입니다.
- 장시간 방치된 비활성 세션을 영구 삭제하여 시스템 메모리와 저장소의 누수를 원천 차단합니다.
SessionIdGenerator- 보안 난수(SecureRandom) 알고리즘을 활용하여 전역적으로 고유하며 추측 불가능한 세션 ID를 생성합니다. 클러스터 환경에서는 노드 식별 및 세션 고정(Sticky Session) 라우팅을 위한 라우트 식별자(
routeId) 접미사를 조합할 수 있습니다.
- 보안 난수(SecureRandom) 알고리즘을 활용하여 전역적으로 고유하며 추측 불가능한 세션 ID를 생성합니다. 클러스터 환경에서는 노드 식별 및 세션 고정(Sticky Session) 라우팅을 위한 라우트 식별자(
1.2. 세션 수명주기 상호작용 흐름
- 세션 생성 흐름:
SessionManager.createSession()호출SessionIdGenerator를 통해 안전하고 유일한 세션 ID 발급- 신규 세션 메타데이터를 포함하는
ManagedSession인스턴스 생성 SessionCache메모리 계층에 세션 등록- 단일 서버 모드(지연 저장) 또는 클러스터 모드(즉시 저장) 정책에 따라
SessionStore.save()수행
- 세션 조회 흐름:
- 클라이언트 요청에 포함된 세션 ID로
SessionManager.getSession(id)호출 - 1차적으로
SessionCache.get(id)를 통해 메모리 캐시 탐색 - 캐시 미스(Miss) 시
SessionStore.load(id)를 호출하여 영속 저장소에서 역직렬화 로드 - 로드된 세션을
SessionCache에 재적재한 후 호출자에게 반환
- 클라이언트 요청에 포함된 세션 ID로
- 세션 정리(Scavenging) 흐름:
- 백그라운드
SessionScheduler가 설정된 주기마다HouseKeeper스레드 트리거 HouseKeeper가 활성 목록 및 영속 저장소의 세션 메타데이터를 순회하며 만료 여부 확인- 유효 시간이 초과된 세션에 대해
session.invalidate()실행 - 등록된 세션 소멸 리스너에게 통지하고,
SessionCache및SessionStore에서 완전 제거
- 백그라운드
2. 저장소 플러그인과 클러스터링 전략
Aspectran Session Manager는 비즈니스 환경의 규모와 영속성 요구사항에 따라 세션 저장 방식을 매끄럽게 확장할 수 있도록 완벽히 분리된 스토리지 플러그인을 제공합니다. 특히 sessionStore를 전혀 지정하지 않는 순수 인메모리 모드부터, 파일 시스템 영속화, 대규모 Redis 분산 클러스터링에 이르기까지 3단계 저장 전략을 지원합니다.
2.1. 순수 인메모리 세션 (스토어리스 / SessionStore 생략)
- 동작 원리: 세션 관리자 Bean 정의 시
sessionStore프로퍼티를 명시하지 않고, 오직SessionManagerConfig만 주입하여 가동합니다. - 특징:
- 외부 디스크 파일 I/O나 Redis 네트워크 통신이 전혀 발생하지 않으며, 오직 JVM 힙 메모리(
ConcurrentHashMap) 내에서만 세션 데이터가 관리됩니다. - 디스크나 외부 서버 연결에 따른 오버헤드가 전무하므로, 현존하는 모든 구성 중 가장 빠르고 가벼운 극상의 성능을 제공합니다.
- 세션 영속화가 불필요한 콘솔 데모 환경(예:
aspectow-demo-console), 단기 테스트, 임시 세션 토큰 관리, 또는 상태 유지가 중요하지 않은 경량 서비스에 매우 이상적입니다. - 서버 프로세스가 재시작되면 기존 세션 데이터는 모두 초기화됩니다.
- 외부 디스크 파일 I/O나 Redis 네트워크 통신이 전혀 발생하지 않으며, 오직 JVM 힙 메모리(
<!-- SessionStore가 없는 순수 인메모리 초경량 세션 관리자 구성 예시 -->
<bean id="netty.context.root.sessionManager"
class="com.aspectran.netty.server.session.NettySessionManager"
scope="prototype">
<property name="sessionManagerConfig">
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: rn0
maxActiveSessions: 100
maxIdleSeconds: 300
</argument>
</bean>
</property>
</bean>
2.2. 로컬 파일 세션 스토어 (FileSessionStore)
- 동작 원리: Java 직렬화 메커니즘을 사용하여 세션 객체를 로컬 디스크의 지정된 디렉터리에 개별 파일로 안전하게 기록합니다.
- 장점: 외부 데이터베이스나 인메모리 캐시 서버 없이도 동작하므로 로컬 개발 환경, 독립형 데몬, 단일 노드 운영 환경에서 가볍고 빠르게 사용할 수 있습니다.
- 안정성: 애플리케이션 재시작 시 디스크에 남아있는 유효 세션 파일을 자동으로 복구하여 개발 편의성을 대폭 향상시킵니다.
- 주의점: 서버 인스턴스 간 파일시스템 공유가 어렵기 때문에 다중 노드 로드밸런싱 환경에서는 적합하지 않습니다.
- 파일 세션 스토어 전용 설정 파라미터 (
FileSessionStoreFactoryBean):storeDir: 세션 데이터 파일이 저장될 로컬 디렉터리 경로 (예:/work/_sessions/tow, 미지정 시java.io.tmpdir)deleteUnrestorableFiles: 손상되거나 역직렬화할 수 없는 유효하지 않은 세션 파일의 자동 삭제 여부 (기본값:true)
2.3. 고성능 Redis 세션 스토어 (LettuceSessionStore)
- 동작 원리: 논블로킹 비동기 Redis 클라이언트인 Lettuce를 기반으로 세션 데이터를 중앙 집중형 Redis 서버 또는 Redis 클러스터에 영속화합니다.
- 데이터 구조:
- 세션 데이터 본체: 각 세션은 Redis의 바이너리 세이프
String타입으로 저장되며,네임스페이스:세션ID포맷의 키를 가집니다. 값은 고속 직렬화된 세션 바이트 스트림입니다. - ZSET 기반 만료 인덱스 (
expiryIndexKey): 만료 세션 정리를 위해 전체 키스페이스를 스캔(KEYS또는SCAN)하는 $O(N)$ 오버헤드를 원천 배제하고자 Redis 정렬 집합(Sorted Set) 인덱스(예:aspectran:sessions:expiry)를 운용합니다.- 세션이 생성되거나 최종 접근 시각이 갱신되어 저장될 때 만료 타임스탬프(밀리초)를 점수(Score)로, 세션 ID를 멤버(Member)로 하여
ZADD로 인덱스를 원자적 동기화합니다. - 세션이 명시적으로 무효화되거나 삭제될 때는
ZREM을 통해 인덱스에서 즉시 제거됩니다. - 스캐빈저 및 만료 검사 시
ZRANGEBYSCORE를 사용하여 만료 시점에 도달한 세션 ID들만 $O(\log N + M)$의 극도로 낮은 비용으로 정밀 조회합니다.
- 세션이 생성되거나 최종 접근 시각이 갱신되어 저장될 때 만료 타임스탬프(밀리초)를 점수(Score)로, 세션 ID를 멤버(Member)로 하여
- 세션 데이터 본체: 각 세션은 Redis의 바이너리 세이프
- 초경량 코덱 (
SessionDataCodec) 매직 바이트 아키텍처:- 세션 데이터와 ZSET 인덱스 멤버 간의 직렬화 오버헤드를 최적화하기 위해 바이트 스트림 선두에 1바이트 매직 바이트(
Magic Byte)를 적용한 2단계 인코딩 전략을 사용합니다.MAGIC_FULL (0x01): 세션 ID, 생성 시각, 최종 접근 시각, 유효 시간, 세션 속성(attributes) 맵 등 전체 세션 상태를 포함하는 완전 직렬화 바이너리 페이로드입니다.MAGIC_ID_ONLY (0x02): ZSET 멤버, 분산 락 토큰 등 세션 식별자 문자열만 필요할 때 전체 세션 역직렬화 비용 없이 고속으로 ID만 인코딩/디코딩하는 경량 페이로드입니다.
- 세션 데이터와 ZSET 인덱스 멤버 간의 직렬화 오버헤드를 최적화하기 위해 바이트 스트림 선두에 1바이트 매직 바이트(
- Redis 분산 락 기반 경합 제어 (Distributed Locking):
- 다중 노드 클러스터 환경에서 만료 세션 정리 및 고아 세션 삭제 시 발생하는 노드 간 불필요한 중복 조회와 I/O 경합을 차단하기 위해 Redis의 원자적 분산 락(
SET key value NX EX lockTtl)을 활용합니다. - 스토어 만료 세션 스윕 락 (
scavenge-lock):expiryIndexKey + ":scavenge-lock"키를 선점한 단 1개의 노드만 Redis ZSET 만료 인덱스를 조회하여 스토어 전용 만료 세션을 회수합니다. - 고아 세션 정리 락 (
clean-lock):expiryIndexKey + ":clean-lock"키를 선점한 단 1개의 노드만 장시간 방치된 고아 세션 물리 삭제(doCleanOrphans)를 수행합니다. - 두 작업의 분산 락 키가 완벽히 분리되어 있어 상호 간섭 없이 독립적으로 안전하게 병렬 실행됩니다.
- 다중 노드 클러스터 환경에서 만료 세션 정리 및 고아 세션 삭제 시 발생하는 노드 간 불필요한 중복 조회와 I/O 경합을 차단하기 위해 Redis의 원자적 분산 락(
- 장점: 대규모 트래픽 분산 환경에서 여러 WAS 인스턴스가 완벽한 무상태(Stateless) 구조를 유지하면서 실시간으로 세션 상태를 공유할 수 있습니다. 노드가 예기치 않게 다운되더라도 클러스터 내 다른 노드가 즉시 세션을 이어받아 무중단 페일오버를 달성합니다.
- Lock-Free 스트라이프 커넥션 풀링:
- Lettuce의 논블로킹 멀티플렉싱 커넥션을 스트라이핑(
poolSize, 기본값: 8, 범위: 2~32)하여AtomicInteger기반 라운드로빈으로 고속 분배합니다. - 락 경합(Lock Contention)이 없는 Lock-Free 구조로 설계되어 Java 21+ 가상 스레드(Virtual Threads) 및 대규모 동시성 환경에서도 극대화된 I/O 처리량을 제공합니다.
- Lettuce의 논블로킹 멀티플렉싱 커넥션을 스트라이핑(
- Dynamic Proxy 및 자동 재연결(Auto-Reconnect) 보장:
- 공유 커넥션은 Dynamic Proxy로 래핑되어
try-with-resources블록에서close()를 호출하더라도 실제 TCP 소켓을 닫지 않고 no-op 처리됩니다. - 일시적인 네트워크 장애나 Redis 재시작 시 Lettuce 내부 Netty 채널의 자동 재연결(Auto-Reconnect) 메커니즘이 백그라운드에서 동작하여 인스턴스 재생성 없이 즉시 연결이 복구됩니다.
- 공유 커넥션은 Dynamic Proxy로 래핑되어
- 지원 토폴로지:
- 단일 노드(Standalone):
RedisConnectionPoolConfig를 통한 기본 단일 인스턴스 연결 - 클러스터(Cluster):
RedisClusterConnectionPoolConfig를 통한 다중 마스터/슬레이브 자동 라우팅 및 토폴로지 갱신 - 주-복제(Primary-Replica):
RedisPrimaryReplicaConnectionPoolConfig를 통한 읽기/쓰기 분리 및 페일오버 지원
- 단일 노드(Standalone):
- Redis 커넥션 풀 설정 파라미터 (
AbstractConnectionPoolConfig):uri/redisURI: 단일 Redis 접속 URI (예:"redis://localhost:6379/0")nodes/redisURIs: 클러스터 또는 주-복제 환경의 다중 노드 URI 목록 (예:"redis://node1:6379,node2:6379")poolSize: 스트라이프 풀 내 공유 멀티플렉싱 커넥션 개수 (기본값:8, 범위: 2~32)timeout: 커맨드 및 연결 타임아웃 (예:"5s","5000ms","1m", 기본값:5s)clientOptions: 소켓 옵션(Keep-Alive, TCP NoDelay), SSL 설정, 단절 시 명령 버퍼링 정책(DisconnectedBehavior), 클러스터 토폴로지 자동 갱신(ClusterTopologyRefreshOptions) 등 Lettuce 클라이언트 동작 정밀 제어clientResources: NettyEventLoopGroup스레드 풀 공유, 커스텀 DNS/주소 해석기(SocketAddressResolver, NAT 및 도커 포트 포워딩 환경 매핑용) 등 고급 리소스 설정
2.4. 세션 스토어 공통 설정 옵션 (AbstractSessionStore)
모든 영속화 세션 스토어(FileSessionStore, LettuceSessionStore 등) 및 해당 팩토리(AbstractSessionStoreFactory)는 다음과 같은 공통 생명주기 및 저장 최적화 파라미터를 제공합니다.
gracePeriodSecs:- 스캐빈저(Scavenger)가 만료된 세션을 일괄 정리할 때 클러스터 노드 간 시계 오차(Clock Skew)나 파일/네트워크 I/O 지연으로 인한 조기 만료 및 오삭제를 방지하기 위해 부여하는 유예 시간(초)입니다 (기본값:
60). - 최초 만료 검사 시에는
gracePeriodSecs * 3이전 만료 세션만 조회하고, 이후 정기 검사 시에는gracePeriodSecs이전 만료 세션을 조회하여 안전하게 정리합니다.
- 스캐빈저(Scavenger)가 만료된 세션을 일괄 정리할 때 클러스터 노드 간 시계 오차(Clock Skew)나 파일/네트워크 I/O 지연으로 인한 조기 만료 및 오삭제를 방지하기 위해 부여하는 유예 시간(초)입니다 (기본값:
savePeriodSecs:- 세션 속성(
attributes)의 변경(dirty) 없이 클라이언트의 단순 조회 요청(HTTP GET 등)으로 최종 접근 시간(lastAccessedTime)만 갱신될 때, 빈번한 영속 저장소 쓰기 네트워크/디스크 I/O를 최적화하기 위한 최소 저장 간격(초)입니다 (기본값:0). savePeriodSecs: 0(기본값): 세션 데이터가 수정(dirty)되지 않은 단순 조회 요청이라도 세션 만료 시간 최신화를 위해 매 요청 종료 시 즉시 영속 저장소에 저장합니다.savePeriodSecs > 0: 세션 데이터가 수정(dirty)되지 않은 경우, 마지막으로 영속 저장소에 기록된 시각으로부터savePeriodSecs이상 경과했을 때만 저장소에 접근하여 최종 접근 시각과 만료 시간을 갱신합니다. (단, 세션 속성이 추가/수정/삭제되어 dirty 상태가 된 경우에는savePeriodSecs설정과 무관하게 요청 종료 즉시 저장소에 반영됩니다.)
- 세션 속성(
nonPersistentAttributes및 비영속 속성 제어:- 세션 속성 중 영속 스토어(File, Redis 등)에 직렬화하여 저장하지 않고 로컬 힙 메모리(세션 캐시)에서만 유지할 속성을 지정하는 두 가지 방법을 지원합니다.
- 속성 이름 기반 지정 (
nonPersistentAttributes): 세션 스토어 설정 시 제외할 속성 키 이름들의 문자열 배열을 등록하여 영속화 대상에서 제외합니다 (예: 임시 인증 토큰 등). - 마커 인터페이스 기반 지정 (
NonPersistent): 세션에 저장되는 객체 클래스가NonPersistent인터페이스를 구현하면, 속성 이름과 무관하게 직렬화 시 자동으로 영속화 대상에서 제외됩니다. - 래퍼 클래스 활용 (
NonPersistentValue): 클래스 소스코드를 직접 수정할 수 없는 외부 라이브러리 객체(예: Netty WebSocket 세션, Undertow 내부 속성 등)는NonPersistentValue.wrap(value)로 감싸서 세션에 저장하면 비영속 속성으로 안전하게 관리되며, 필요 시NonPersistentValue.unwrap(value)로 원본 객체를 추출할 수 있습니다.
2.5. 단일 서버 모드 vs 분산 클러스터 모드 동작 비교
SessionManagerConfig의 clusterEnabled 설정값은 세션 데이터의 신뢰 원천과 동기화 빈도를 제어하는 핵심 분기점입니다.
| 구분 | 단일 서버 모드 (clusterEnabled: false) | 분산 클러스터 모드 (clusterEnabled: true) |
|---|---|---|
| 데이터 신뢰 기준 (Source of Truth) | SessionCache (로컬 메모리)를 1차 신뢰 | SessionStore (중앙 Redis 등)를 절대적 신뢰 원천으로 취급 |
| 세션 로드 전략 | 메모리 캐시에 존재하면 외부 스토어에 일체 접근하지 않음 (최대 성능) | 캐시에 존재하더라도 타 노드에 의한 변경 가능성을 검증하여 최신 상태 동기화 |
| 세션 저장 시점 | 마지막 요청 처리 완료 시점 또는 캐시 축출 시점에 배치 저장 | 세션 생성 즉시, 매 요청 종료 시, 메모리 축출 시 즉각 영속화 |
| 설계 목표 | I/O 최소화를 통한 극한의 단일 처리량 확보 | 다중 인스턴스 간 완벽한 데이터 정합성 보장 |
3. 세션 생명주기 제어 및 타임아웃 최적화
웹 서비스 운영 중 발생하는 대표적인 성능 저하 요인 중 하나는 검색 엔진 크롤러, 봇, 헬스 체크 프로브 등이 남기고 가는 대량의 “유령 세션”입니다. Aspectran은 이를 방지하기 위해 신규 세션과 일반 세션을 이원화하여 관리하는 지능형 타임아웃 알고리즘을 내장하고 있습니다.
3.1. 신규 세션과 일반 세션의 분리 관리
- 신규 세션 (New Session): 클라이언트가 처음 방문하여 세션이 생성되었으나 아직 두 번째 요청을 발생시키지 않은 상태의 세션입니다.
- 일반 세션 (Normal Session): 동일 세션 쿠키를 지참하고 두 번째 이상의 유효한 요청을 정상적으로 수행한 상태입니다.
- 최적화 원리: 웹 크롤러나 일회성 API 호출은 단 한 번의 요청 후 다시 방문하지 않습니다. Aspectran은 이러한 세션에 대해
maxIdleSecondsForNew를 매우 짧게 적용하여 메모리와 Redis 저장소를 잠식하기 전에 빠르게 회수합니다.
3.2. SessionManagerConfig 핵심 설정 파라미터
XML Bean 정의 또는 APON 설정 블록에서 사용되는 SessionManagerConfig의 파라미터 명세는 다음과 같습니다.
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: node1
maxActiveSessions: 50000
maxIdleSeconds: 1800
evictionIdleSeconds: 600
maxIdleSecondsForNew: 60
evictionIdleSecondsForNew: 30
scavengingIntervalSeconds: 60
clusterEnabled: true
saveOnCreate: true
removeUnloadableSessions: true
</argument>
</bean>
routeId:- 클러스터 및 로드밸런서(L4/L7) 연동 환경에서 Sticky Session 라우팅을 위해 세션 ID 뒤에 부착되는 고유한 라우트 식별자 접미사(예:
session123.node1)입니다. - 미지정 시 접미사 없이 순수 세션 ID가 발급됩니다. 분산 환경에서는 시스템 프로퍼티(
%{system:aspectow.node.route}) 등을 통해 동적으로 주입하여 운영할 수 있습니다.
- 클러스터 및 로드밸런서(L4/L7) 연동 환경에서 Sticky Session 라우팅을 위해 세션 ID 뒤에 부착되는 고유한 라우트 식별자 접미사(예:
maxActiveSessions:- 메모리 캐시에 동시에 유지할 수 있는 최대 세션 개수입니다.
- 허용 한도를 초과하면 유휴 시간이 긴 세션부터 메모리 캐시에서 선제적으로 축출(Evict)하여 OutOfMemory 오류를 방지합니다.
maxIdleSeconds:- 일반 세션의 최대 유휴 시간(초 단위)입니다. (기본 예시: 1800초 = 30분)
- 마지막 요청 이후 이 시간이 지나면 세션은 영구 만료 처리됩니다.
maxIdleSecondsForNew:- 신규 세션에만 적용되는 최대 유휴 시간(초 단위)입니다. (권장: 30초~60초)
- 크롤러가 대량으로 세션을 발급받더라도 1분 이내에 만료 대상으로 분류되어 즉각 정리됩니다.
evictionIdleSeconds:- 세션이 활성 상태이더라도 메모리 절약을 위해 로컬 힙 캐시에서만 축출(Evict)하기까지의 유휴 시간(초)입니다.
- 캐시에서 제거되어도
SessionStore에 데이터가 남아있으므로, 사용자가 다시 요청을 보내면 투명하게 복원됩니다.
evictionIdleSecondsForNew:- 신규 세션에 대한 로컬 힙 캐시 축출 유휴 시간입니다.
scavengingIntervalSeconds:- 백그라운드 청소부(
HouseKeeper)가 실행되는 주기(초)입니다. - 너무 자주 실행하면 CPU 오버헤드가 발생하고, 너무 길면 만료 세션이 오래 남아있으므로 일반적으로 60초~120초가 권장됩니다.
- 백그라운드 청소부(
clusterEnabled:true설정 시 분산 클러스터링 모드로 가동되며 중앙 세션 스토어와의 일관성 동기화가 활성화됩니다.
saveOnCreate:- 신규 세션 생성 즉시 영속 저장소에 쓸 것인지 여부입니다. (단일 서버 모드 최적화 옵션이며,
clusterEnabled: true인 경우 항상 생성 즉시 저장됩니다.)
- 신규 세션 생성 즉시 영속 저장소에 쓸 것인지 여부입니다. (단일 서버 모드 최적화 옵션이며,
removeUnloadableSessions:- 애플리케이션 클래스 구조 변경 등으로 인해 저장소의 세션 바이너리를 역직렬화할 수 없을 때, 에러를 반복 발생시키지 않고 해당 세션 데이터를 저장소에서 즉시 삭제할 것인지 여부입니다.
3.3. 대표 환경별 최적화 권장 설정 (Best Practice Profiles)
처음 Aspectran Session Manager를 도입할 때 각 타임아웃과 캐시 수치를 어떻게 조율해야 할지 고민하는 설계자를 위해, 실제 운영에서 검증된 대표적인 4가지 환경별 최적화 프로파일을 제시합니다.
환경별 설정값 비교 매트릭스
| 설정 파라미터 | 1) 관리자 관제 콘솔 | 2) 대규모 대고객 서비스 | 3) 초경량 엣지 API | 4) 로컬 개발 및 테스트 |
|---|---|---|---|---|
routeId | cn0 | rn0 | edge0 | dev0 |
maxActiveSessions | 999 | 50000 | 5000 | 100 |
maxIdleSeconds | 600 (10분) | 1800 (30분) | 300 (5분) | 3600 (1시간) |
evictionIdleSeconds | 300 (5분) | 600 (10분) | 120 (2분) | 1800 (30분) |
maxIdleSecondsForNew | 120 (2분) | 60 (1분) | 30 (30초) | 300 (5분) |
evictionIdleSecondsForNew | 60 (1분) | 30 (30초) | 15 (15초) | 180 (3분) |
scavengingIntervalSeconds | 180 (3분) | 60 (1분) | 60 (1분) | 300 (5분) |
clusterEnabled | false | true | false | false |
| 권장 세션 저장소 | 순수 인메모리 또는 File | Redis Cluster (Lettuce) | 순수 인메모리 | File (FileSessionStore) |
프로파일 1: 관리자 관제 콘솔 컨텍스트 (cn0 / Console Context)
소수의 인가된 운영진만 접근하는 관리자 전용 평면(/console) 환경입니다.
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: cn0
maxActiveSessions: 999
maxIdleSeconds: 600
evictionIdleSeconds: 300
maxIdleSecondsForNew: 120
evictionIdleSecondsForNew: 60
scavengingIntervalSeconds: 180
clusterEnabled: false
</argument>
</bean>
- 설계 의도:
- 보안 강화: 관리자 화면을 열어두고 자리를 비웠을 때의 보안 사고를 방지하기 위해 일반 유휴 시간을 10분(
maxIdleSeconds: 600)으로 비교적 타이트하게 제한합니다. - 메모리 절약: 5분(
evictionIdleSeconds: 300)간 활동이 없으면 힙 메모리에서 우선 축출합니다. - 로그인 편의성: 일반 웹 고객과 달리 관리자는 OTP 입력이나 복잡한 인증 절차를 거치므로 신규 세션 유예 시간을 2분(
maxIdleSecondsForNew: 120)으로 충분히 부여합니다. - 스케줄러 오버헤드 최소화: 접속자 수가 제한적이므로 청소 주기(
scavengingIntervalSeconds)를 3분(180초)으로 늦춰 CPU 간섭을 줄입니다.
- 보안 강화: 관리자 화면을 열어두고 자리를 비웠을 때의 보안 사고를 방지하기 위해 일반 유휴 시간을 10분(
프로파일 2: 대규모 대고객 웹 서비스 (High-Traffic Public Web / Enterprise)
수많은 동시 접속자와 불특정 다수의 검색 봇/크롤러가 유입되는 대규모 대외 서비스 환경입니다.
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: rn0
maxActiveSessions: 50000
maxIdleSeconds: 1800
evictionIdleSeconds: 600
maxIdleSecondsForNew: 60
evictionIdleSecondsForNew: 30
scavengingIntervalSeconds: 60
clusterEnabled: true
saveOnCreate: true
</argument>
</bean>
- 설계 의도:
- 크롤러 방어 최적화: 봇이나 크롤러는 첫 요청 후 재방문하지 않으므로, 신규 세션 타임아웃을 1분(
maxIdleSecondsForNew: 60), 캐시 축출을 30초로 설정하여 유령 세션이 힙 메모리와 Redis를 잠식하는 것을 원천 차단합니다. - 고가용성 클러스터링:
clusterEnabled: true와 중앙 Redis 스토어를 결합하여 다중 WAS 노드 간 실시간 세션 동기화 및 무중단 페일오버를 달성합니다. - 신속한 청소: 대량의 만료 세션이 빠르게 쌓이므로 청소 주기를 1분(60초)으로 기민하게 유지합니다.
- 크롤러 방어 최적화: 봇이나 크롤러는 첫 요청 후 재방문하지 않으므로, 신규 세션 타임아웃을 1분(
프로파일 3: 초경량 엣지 마이크로서비스 (Aspectow Edge / In-Memory)
서블릿 컨테이너 없이 Netty 채널 파이프라인에서 빠른 API 응답과 임시 상태만을 다루는 초경량 서비스 환경입니다.
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: edge0
maxActiveSessions: 5000
maxIdleSeconds: 300
evictionIdleSeconds: 120
maxIdleSecondsForNew: 30
evictionIdleSecondsForNew: 15
scavengingIntervalSeconds: 60
clusterEnabled: false
</argument>
</bean>
- 설계 의도:
- 순수 인메모리 극대화:
sessionStore를 완전히 생략하여 디스크/네트워크 I/O를 제로화하고 최고 처리량을 보장합니다. - 신속한 메모리 회수: 5분(
maxIdleSeconds: 300)의 짧은 유효 시간과 30초 신규 세션 정리로 힙 메모리 점유율을 항상 낮게 유지합니다.
- 순수 인메모리 극대화:
프로파일 4: 로컬 개발 및 테스트 환경 (Development & Testing)
개발자가 로컬 머신에서 기능을 개발하고 디버깅하는 환경입니다.
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: dev0
maxActiveSessions: 100
maxIdleSeconds: 3600
evictionIdleSeconds: 1800
maxIdleSecondsForNew: 300
evictionIdleSecondsForNew: 180
scavengingIntervalSeconds: 300
clusterEnabled: false
</argument>
</bean>
- 설계 의도:
- 개발 생산성: 디버깅 중이나 코드 수정 중에 세션이 자주 끊기면 반복 로그인을 해야 하므로 유휴 시간을 1시간(
maxIdleSeconds: 3600)으로 넉넉하게 설정합니다. - 재기동 복구:
FileSessionStoreFactoryBean을 함께 사용하여 서버를 재시작해도 로그인 상태가 그대로 유지되도록 합니다.
- 개발 생산성: 디버깅 중이나 코드 수정 중에 세션이 자주 끊기면 반복 로그인을 해야 하므로 유휴 시간을 1시간(
4. 분산 클러스터 환경에서의 고신뢰 스캐빈징 & 고아 세션 정리 아키텍처
다중 인스턴스가 로드밸런서(L4/L7) 뒤에서 클러스터로 묶여 동작할 때, 개별 노드의 로컬 캐시 수명 주기와 중앙 Redis 영속 스토어의 세션 수명 주기가 어긋나면서 발생할 수 있는 여러 난제(조기 만료 오삭제, 다중 노드 I/O 경합, 이벤트 중복 발행, 방치된 고아 세션 메모리 누수 등)를 완벽하게 해결하도록 정교한 4단계 고신뢰 스캐빈징 아키텍처를 제공합니다.
4.1. 로컬 캐시 비활성 축출 vs 스토어 세션 상태의 분리 (Silent Eviction)
- 문제 상황: 클러스터 환경에서 사용자의 이전 요청이 노드 A에서 처리된 후 로컬 캐시에 적재되었으나, 이후 후속 요청들이 로드밸런서에 의해 노드 B로 라우팅되어 계속 활발히 사용 중일 수 있습니다. 이 상태에서 노드 A의 세션 비활성 타이머가 도래했을 때 무조건
session.invalidate()를 호출하면 Redis의 원본 세션까지 조기 삭제되어 다른 노드(노드 B)의 정상 사용자가 로그아웃되는 심각한 장애가 발생합니다. - 해결 원리 (
checkActiveInStore):- 노드 로컬에서 세션 비활성 타이머가 만료(
sessionInactivityTimerExpired)되면, 즉시 무효화하지 않고sessionStore.checkActiveInStore(session)를 호출하여 Redis 중앙 저장소의 실제 최종 접근 시각과 만료 여부를 사전 조회합니다. - 타 노드에서 활성 중인 경우: Redis 상의 세션이 여전히 유효하고 활성 상태라면, 해당 세션은 노드 A의 힙 메모리에서만 조용히 축출(
SessionCache.evict(), Silent Eviction)합니다. 이 과정에서 Redis의 원본 세션 데이터와 세션 소멸 리스너 이벤트(sessionDestroyed)는 일체 건드리지 않습니다. - 저장소에서도 만료된 경우: Redis 상에서도 비활성 유효 시간을 초과한 경우에만 비로소 정상적인 무효화(
session.invalidate()) 및 소멸 통지 절차를 밟습니다.
- 노드 로컬에서 세션 비활성 타이머가 만료(
flowchart TD
A["로컬 비활성 타이머 만료<br/>(sessionInactivityTimerExpired)"] --> B{"스토어 활성 상태 검증<br/>(checkActiveInStore)"}
B -- "타 노드에서 갱신되어 활성 중" --> C["로컬 캐시에서만 조용히 축출<br/>(SessionCache.evict / Silent Eviction)<br/>* 스토어 데이터 보존 & 리스너 미호출"]
B -- "스토어에서도 만료/비활성" --> D["정상 무효화 파이프라인 가동<br/>(session.invalidate)"]
4.2. 이중화된 스캐빈징 파이프라인 (Local Candidates + Store Sweeper)
백그라운드 청소부(HouseKeeper)가 정기적으로 가동될 때, 로컬 캐시에 상주하는 세션과 어떤 노드의 캐시에도 존재하지 않는 스토어 전용(Store-Only) 만료 세션을 이원화하여 처리합니다.
sequenceDiagram
autonumber
participant HK as HouseKeeper
participant SC as SessionCache
participant SS as LettuceSessionStore (Redis)
participant SL as SessionListener
HK->>SC: 1. checkExpiration(candidates) [로컬 캐시 만료 후보 검사]
SC-->>HK: 로컬 검증 만료 세션 목록 반환
HK->>SS: 2. getExpired(candidates) [스토어 전용 만료 세션 수집]
activate SS
SS->>SS: SET expiryIndexKey:scavenge-lock NX EX (분산 락 선점 시도)
alt 락 획득 성공 (단일 마스터 노드)
SS->>SS: ZRANGEBYSCORE expiryIndexKey 0 (now - gracePeriod)
SS-->>HK: 로컬 후보 + 스토어 전용 만료 세션 합집합 반환
else 락 획득 실패 (타 노드 선점)
SS-->>HK: 로컬 후보 세션만 반환 (스토어 스윕 스킵)
end
deactivate SS
loop 수집된 만료 세션 무효화
HK->>SS: 3. invalidate() -> c.del(sessionKey) (원자적 삭제)
alt 삭제 성공 (DEL == 1)
HK->>SL: 4. onSessionDestroyed() (정확히 1회 이벤트 통지)
else 이미 타 노드가 삭제함 (DEL == 0)
HK-->>HK: 리스너 통지 스킵
end
end
- 1단계: 로컬 캐시 후보군 즉시 판별 (
sessionCache.checkExpiration(candidates)):- 각 노드는 자신의 로컬 메모리 캐시에 등록된 세션 목록 중 현재 시각(
now) 기준으로 만료된 세션 ID 목록을 즉시 수집합니다. - 로컬 검증 과정에서는 외부 Redis 네트워크 락 획득 여부와 상관없이 각 노드가 독립적으로 자신의 유휴 캐시를 신속하게 정리합니다.
- 각 노드는 자신의 로컬 메모리 캐시에 등록된 세션 목록 중 현재 시각(
- 2단계: 스토어 전용 만료 세션 스윕 (
sessionStore.getExpired()) &scavenge-lock분산 락:- 사용자의 모든 요청이 완료된 후 오랜 시간이 흘러 모든 노드의 메모리 캐시에서 이미 축출되었으나 Redis에만 남아있는 만료 세션을 수거합니다.
- 클러스터 내 수십~수백 대의 노드가 동시에 Redis ZSET 만료 인덱스를 중복 조회하고 동일 세션들을 반복 로드하는 극심한 네트워크 I/O 경합을 방지하기 위해 분산 락(
expiryIndexKey + ":scavenge-lock")을 선점한 단 1개의 노드만 ZSET 스윕을 수행합니다. - 락을 획득하지 못한 노드는 불필요한 Redis ZSET 조회를 안전하게 건너뛰고 자신의 로컬 후보군 정리만 신속히 완료합니다.
4.3. 원자적 스토어 삭제 및 정확히 1회 소멸 이벤트 보장 (Exactly-Once Listener Notification)
- 만료 세션에 대해
session.invalidate()가 실행되면 세션 스토어의delete()를 호출하여 Redis에서 해당 세션 키를 삭제합니다. - Redis의 단일 커맨드 원자성(
DEL sessionKey)에 의해 실제로 키를 삭제하는 데 성공한 단 1개의 노드만deletedInStore = true를 반환받습니다. - 세션 관리자는 오직
deletedInStore == true인 노드에서만SessionListener.sessionDestroyed()이벤트를 발행합니다. - 이를 통해 클러스터 내 다중 노드가 동일 세션의 만료를 동시에 감지하더라도, 사용자 로그아웃 감사 로그 기록이나 세션 소멸 후속 로직이 중복 실행되지 않고 클러스터 전체에서 정확히 1회만 실행됩니다.
4.4. 장기 방치 고아 세션 물리적 청소 (doCleanOrphans & clean-lock)
- 고아 세션 발생 원인: 서버 프로세스의 갑작스러운 비정상 강제 종료(OOM-Killer, 하드웨어 장애, 전원 차단 등)나 네트워크 단절로 인해 정상적인 스캐빈징 수명 주기에서 누락된 세션이 ZSET 만료 인덱스에 잔류할 수 있습니다.
- 안전한 물리 청소 (
doCleanOrphans):- 정상 유휴 만료 시간보다 훨씬 긴 임계치(기본값:
now - 100분)를 초과하여 방치된 고아 세션들을 주기적으로 감지합니다. - 별도의 분산 락인
expiryIndexKey + ":clean-lock"을 선점한 단일 노드가 리스너 이벤트 호출 없이 Redisc.del()및zremrangebyscore()를 통해 일괄 물리 삭제합니다.
- 정상 유휴 만료 시간보다 훨씬 긴 임계치(기본값:
- 락 분리를 통한 동시성 최적화:
- 스토어 전용 세션 스윕 락(
scavenge-lock)과 고아 세션 청소 락(clean-lock)이 분리되어 있으므로, 통상적인 1~2분 단위 세션 스캐빈징과 장기 고아 세션 청소 작업이 서로를 블로킹하지 않고 독립적으로 안전하게 병렬 가동됩니다.
- 스토어 전용 세션 스윕 락(
5. 비영속 세션 속성 제어: NonPersistent & NonPersistentValue
분산 환경에서 세션을 Redis나 파일 저장소에 영속화(직렬화)할 때, 네트워크 소켓, 데이터베이스 커넥션, 대용량 버퍼와 같이 직렬화가 불가능하거나 외부 저장이 부적절한 객체가 포함되어 있으면 직렬화 예외가 발생하거나 네트워크 대역폭이 낭비됩니다.
Aspectran은 이러한 임시 데이터가 세션 캐시(로컬 힙 메모리)에는 유지되면서도 영속 스토어에는 저장되지 않도록 하는 타입 안전한 비영속(Non-persistent) 제어 메커니즘을 제공합니다.
5.1. 마커 인터페이스 (NonPersistent)
세션 속성으로 저장할 커스텀 도메인 객체가 NonPersistent 인터페이스를 구현하도록 선언하면, SessionData 직렬화 시 자동으로 저장 대상에서 제외됩니다.
package com.aspectran.example;
import com.aspectran.core.component.session.NonPersistent;
/**
* 힙 메모리 세션에는 유지되지만 Redis나 파일 저장소로는 전송/저장되지 않는 임시 데이터
*/
public class TemporarySecurityContext implements NonPersistent {
private String temporaryToken;
private Object activeConnection;
public String getTemporaryToken() {
return temporaryToken;
}
public void setTemporaryToken(String temporaryToken) {
this.temporaryToken = temporaryToken;
}
public Object getActiveConnection() {
return activeConnection;
}
public void setActiveConnection(Object activeConnection) {
this.activeConnection = activeConnection;
}
}
5.2. 래퍼 클래스 유틸리티 (NonPersistentValue)
서드파티 라이브러리 객체나 프레임워크 내부 리소스(예: Netty Channel, Undertow 내부 속성 등)처럼 소스코드를 직접 수정하여 NonPersistent를 구현할 수 없는 경우, NonPersistentValue 래퍼를 사용하여 손쉽게 비영속 속성으로 등록할 수 있습니다.
import com.aspectran.core.component.session.NonPersistentValue;
import com.aspectran.core.component.session.Session;
// 1. 세션에 비영속 객체 저장
Object rawResource = getNativeConnection();
session.setAttribute("runtimeResource", NonPersistentValue.wrap(rawResource));
// 2. 세션에서 객체 조회 및 언래핑
Object retrieved = session.getAttribute("runtimeResource");
Connection conn = NonPersistentValue.unwrap(retrieved);
5.3. 비영속 속성 적용 대상 및 동작 원리
- 주요 적용 대상:
- 직렬화 불가능한 런타임 리소스 (
Socket,Connection,Thread,Channel) - 민감한 일회성 보안 인증 토큰 (외부 스토리지 노출 방지)
- 용량이 매우 큰 임시 렌더링 캐시 데이터 (Redis 직렬화/네트워크 전송 비용 절감)
- 직렬화 불가능한 런타임 리소스 (
- 동작 원리:
SessionData.serialize()수행 시 각 속성 값에 대해instanceof NonPersistent검사를 수행합니다.NonPersistent를 구현한 객체(또는NonPersistentValue로 래핑된 객체)는 직렬화 스트림에 기록되지 않고 스킵됩니다.- 로컬 메모리의 세션 캐시에는 원본 인스턴스가 그대로 보존되므로, 동일 요청이나 동일 노드 내에서는 제약 없이 빠르게 접근할 수 있습니다.
6. 실행 환경별 세션 구성 실전 가이드
Aspectran Session Manager의 가장 강력한 장점은 동일한 세션 라이프사이클 엔진을 기반으로, 실행 대상 인프라에 맞춰 최적화된 바인딩 구성을 손쉽게 적용할 수 있다는 점입니다.
6.1. Standalone / Shell / Daemon 환경 구성
웹 컨테이너가 없는 CLI 쉘 콘솔이나 백그라운드 데몬 프로세스에서도 사용자 로그인 상태나 작업 컨텍스트를 유지하기 위해 세션을 활용합니다. aspectran-config.apon에 직접 선언합니다.
/app/config/aspectran-config.apon:
shell: {
session: {
routeId: shell
maxActiveSessions: 1
maxIdleSeconds: 1800
scavengingIntervalSeconds: 600
fileStore: {
storeDir: /work/_sessions/shell
}
enabled: true
}
}
6.2. Aspectow Enterprise 환경 구성 (Undertow 서블릿 바인딩)
Aspectow Enterprise에서는 TowSessionManager를 통해 서블릿 스펙(io.undertow.server.session.SessionManager)과 Aspectran 코어 세션 엔진을 브릿징합니다.
/app/config/server/undertow/tow-context-root.xml:
<!-- 서블릿 웹 컨텍스트 정의 -->
<bean id="tow.context.root" class="com.aspectran.undertow.server.servlet.TowServletContext">
<property name="contextPath">/</property>
<!-- 서블릿 세션 관리자 바인딩 -->
<property name="sessionManager">#{tow.context.root.sessionManager}</property>
<!-- 서블릿 표준 세션 쿠키 정책 정의 -->
<property name="servletSessionConfig">
<bean class="com.aspectran.undertow.server.servlet.TowServletSessionConfig">
<property name="cookieName">JSESSIONID</property>
<property name="cookiePath">/</property>
<property name="cookieDomain">.aspectran.com</property>
<property name="httpOnly" valueType="boolean">true</property>
<property name="secure" valueType="boolean">false</property>
<property name="sessionTrackingModes">
<value>COOKIE</value>
</property>
</bean>
</property>
</bean>
<!-- Undertow 세션 관리자 및 저장소 프로파일 분기 -->
<bean id="tow.context.root.sessionManager"
class="com.aspectran.undertow.server.session.TowSessionManager"
scope="prototype">
<property name="sessionManagerConfig">
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: %{system:aspectow.node.route}
maxActiveSessions: 10000
maxIdleSeconds: 1800
evictionIdleSeconds: 900
maxIdleSecondsForNew: 60
evictionIdleSecondsForNew: 30
scavengingIntervalSeconds: 90
clusterEnabled: false
</argument>
</bean>
</property>
<!-- 로컬 개발 환경: 파일 기반 세션 저장소 -->
<properties profile="!prod">
<item name="sessionStore">
<bean class="com.aspectran.core.component.session.FileSessionStoreFactoryBean">
<property name="storeDir">%{system:aspectran.workPath:/work}/_sessions/%{tow.context.root.name}</property>
<property name="gracePeriodSecs" valueType="int">30</property>
</bean>
</item>
</properties>
<!-- 운영 환경: 고가용성 Redis 분산 세션 클러스터링 -->
<properties profile="prod">
<item name="sessionStore">
<bean class="com.aspectran.core.component.session.redis.lettuce.DefaultLettuceSessionStoreFactoryBean">
<property name="poolConfig">
<bean class="com.aspectran.core.component.session.redis.lettuce.RedisConnectionPoolConfig">
<property name="uri">%{system:redis.uri}/10</property>
</bean>
</property>
</bean>
</item>
</properties>
</bean>
6.3. Aspectow Edge 환경 구성 (Netty 논-서블릿 바인딩)
Aspectow Edge에서는 서블릿 컨테이너 오버헤드를 배제하고 Netty 채널 파이프라인에서 직접 가동되는 NettySessionManager와 NettySessionConfig를 사용합니다.
/app/config/server/netty/netty-context-root.xml:
<!-- Netty 컨텍스트 정의 -->
<bean id="netty.context.root" class="com.aspectran.netty.server.NettyContext">
<property name="contextPath">/</property>
<!-- Netty 경량 세션 관리자 바인딩 -->
<property name="sessionManager">#{netty.context.root.sessionManager}</property>
</bean>
<!-- Netty 세션 관리자 설정 -->
<bean id="netty.context.root.sessionManager"
class="com.aspectran.netty.server.session.NettySessionManager"
scope="prototype">
<!-- Netty HTTP 세션 쿠키 정책 -->
<property name="sessionConfig">
<bean class="com.aspectran.netty.server.session.NettySessionConfig">
<property name="cookieName">JSESSIONID</property>
<property name="cookiePath">/</property>
<property name="cookieDomain">.aspectran.com</property>
<property name="httpOnly" valueType="boolean">true</property>
<property name="secure" valueType="boolean">false</property>
<property name="sameSite">Lax</property>
<property name="maxAge" valueType="int">-1</property>
</bean>
</property>
<!-- 세션 생명주기 및 클러스터 정책 -->
<property name="sessionManagerConfig">
<bean class="com.aspectran.core.context.config.SessionManagerConfig">
<argument>
routeId: %{system:aspectow.node.route}
maxActiveSessions: 50000
maxIdleSeconds: 1800
evictionIdleSeconds: 600
maxIdleSecondsForNew: 60
evictionIdleSecondsForNew: 30
scavengingIntervalSeconds: 60
clusterEnabled: false
</argument>
</bean>
</property>
<!-- 로컬 개발 환경: 파일 세션 스토어 -->
<properties profile="!prod">
<item name="sessionStore">
<bean class="com.aspectran.core.component.session.FileSessionStoreFactoryBean">
<property name="storeDir">%{system:aspectran.workPath:/work}/_sessions/%{netty.context.root.name}</property>
<property name="gracePeriodSecs" valueType="int">30</property>
</bean>
</item>
</properties>
<!-- 운영 환경: 고가용성 Redis 분산 클러스터링 -->
<properties profile="prod">
<item name="sessionStore">
<bean class="com.aspectran.core.component.session.redis.lettuce.DefaultLettuceSessionStoreFactoryBean">
<property name="poolConfig">
<bean class="com.aspectran.core.component.session.redis.lettuce.RedisConnectionPoolConfig">
<property name="uri">%{system:redis.uri}/10</property>
</bean>
</property>
</bean>
</item>
</properties>
</bean>
NettySessionConfig 쿠키 설정 파라미터
cookieName: 세션 식별 쿠키 이름 (기본값:JSESSIONID)cookiePath: 세션 쿠키 유효 경로 (기본값:/)cookieDomain: 서브도메인 간 공유를 위한 도메인 범위 (예:.example.com)httpOnly: JavaScript의document.cookie접근을 차단하여 XSS 공격 방어 (기본값:true)secure: HTTPS 암호화 채널에서만 전송되도록 제어 (운영 환경 권장:true)sameSite: CSRF 공격 방어를 위한 SameSite 정책 (Strict,Lax,None지원, 기본값:Lax)maxAge: 쿠키 생존 시간(초). 기본값-1은 브라우저 종료 시 삭제되는 세션 쿠키 동작을 의미
7. 세션 생명주기 이벤트 리스너
세션의 생성, 소멸, 속성 변경 시 감사 로그 기록이나 접속자 수 통계를 집계하기 위해 세션 리스너를 등록할 수 있습니다.
7.1. 세션 리스너 구현
package com.aspectran.example.listener;
import com.aspectran.core.component.session.Session;
import com.aspectran.core.component.session.SessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserSessionTrackingListener implements SessionListener {
private static final Logger logger = LoggerFactory.getLogger(UserSessionTrackingListener.class);
@Override
public void sessionCreated(Session session) {
logger.info("New session created: id={}", session.getId());
}
@Override
public void sessionDestroyed(Session session) {
logger.info("Session destroyed: id={}, lastAccessed={}", session.getId(), session.getLastAccessedTime());
}
}
7.2. 세션 리스너 등록 방법
Aspectran에서는 애플리케이션의 아키텍처와 요구사항에 맞춰 세션 리스너를 등록할 수 있도록 두 가지 표준 등록 방식을 제공합니다.
방식 1: DefaultSessionListenerRegistration 빈을 이용한 간편 등록
SessionListenerRegistration 인터페이스의 표준 구현체인 DefaultSessionListenerRegistration을 Bean으로 선언하여 사용하는 선언적 등록 방식입니다.
- 동작 원리:
- 내장 WAS 서버 빈 ID(예:
tow.server,netty.server) 및 기본 대상 컨텍스트 이름/경로(root,/등)를 미리 지정하여 초기화할 수 있습니다. - 서버 빈 ID를 생략하면 BeanRegistry 내에 등록된 유일한
SessionManagerProvider빈(Undertow의TowServer또는 Netty의NettyServer)을 자동 감지(Auto-detect)하여 바인딩합니다. nameOrPath가null,"","/","root"인 경우 루트 컨텍스트 또는 단일 배포 컨텍스트의SessionManager를 자동으로 해석하여 유연하게 연결합니다.
- 내장 WAS 서버 빈 ID(예:
- XML Bean 정의 (
support.xml):
<!-- 1. 특정 서버 ID와 기본 컨텍스트(root)를 지정하여 선언 -->
<bean id="sessionListenerRegistration"
class="com.aspectran.core.component.session.DefaultSessionListenerRegistration"
lazyInit="true">
<argument>netty.server</argument>
<argument>root</argument>
</bean>
<!-- 2. 서버 ID 없이 SessionManagerProvider를 자동 감지하도록 선언 -->
<bean id="sessionListenerRegistration"
class="com.aspectran.core.component.session.DefaultSessionListenerRegistration"
lazyInit="true"/>
- 자바 코드에서의 리스너 등록 및 해제:
package com.aspectran.example.support;
import com.aspectran.core.component.bean.annotation.Autowired;
import com.aspectran.core.component.bean.annotation.Component;
import com.aspectran.core.component.bean.ablility.InitializableBean;
import com.aspectran.core.component.bean.ablility.DisposableBean;
import com.aspectran.core.component.session.DefaultSessionListenerRegistration;
import com.aspectran.core.component.session.SessionListener;
import com.aspectran.example.listener.UserSessionTrackingListener;
@Component
public class SessionListenerManager implements InitializableBean, DisposableBean {
private final DefaultSessionListenerRegistration registration;
private final SessionListener trackingListener = new UserSessionTrackingListener();
@Autowired
public SessionListenerManager(DefaultSessionListenerRegistration registration) {
this.registration = registration;
}
@Override
public void initialize() {
// 기본 지정된 컨텍스트(root)에 리스너 등록
registration.register(trackingListener);
// 특정 컨텍스트(예: "admin")를 지정하여 등록할 수도 있음 (컨텍스트 이름 및 경로 모두 지원)
// registration.register(trackingListener, "admin");
}
@Override
public void destroy() {
// 리스너 등록 해제
registration.remove(trackingListener);
}
}
방식 2: SessionManagerProvider 또는 SessionManager 빈을 통한 직접 등록
서버 빈 자체(SessionManagerProvider) 또는 개별 컨텍스트의 SessionManager 인스턴스를 직접 주입받아 리스너를 동적으로 등록하는 프로그래밍 방식입니다.
- 동작 원리:
- Undertow의
TowServer와 Netty의NettyServer는 모두SessionManagerProvider인터페이스를 구현하고 있습니다. - 서버 빈으로부터
server.getSessionManager()또는server.getSessionManager(contextNameOrPath)를 호출하여 대상SessionManager를 획득한 후addSessionListener(listener)를 직접 호출합니다. - 세션 관리자 인스턴스를 이미 확보한 상태라면 별도의
SessionListenerRegistration빈을 거치지 않고 가장 직관적이고 빠르게 리스너를 부착할 수 있습니다.
- Undertow의
- 자바 컴포넌트 직접 등록 예시:
package com.aspectran.example.support;
import com.aspectran.core.component.bean.annotation.Autowired;
import com.aspectran.core.component.bean.annotation.Component;
import com.aspectran.core.component.bean.ablility.InitializableBean;
import com.aspectran.core.component.bean.ablility.DisposableBean;
import com.aspectran.core.component.session.SessionListener;
import com.aspectran.core.component.session.SessionManager;
import com.aspectran.core.component.session.SessionManagerProvider;
import com.aspectran.example.listener.UserSessionTrackingListener;
@Component
public class DirectSessionListenerRegistrar implements InitializableBean, DisposableBean {
private final SessionManagerProvider sessionManagerProvider;
private final SessionListener trackingListener = new UserSessionTrackingListener();
@Autowired
public DirectSessionListenerRegistrar(SessionManagerProvider sessionManagerProvider) {
this.sessionManagerProvider = sessionManagerProvider;
}
@Override
public void initialize() {
// 루트 컨텍스트의 SessionManager 획득 후 리스너 직접 등록
SessionManager sessionManager = sessionManagerProvider.getSessionManager();
if (sessionManager != null) {
sessionManager.addSessionListener(trackingListener);
}
// 특정 컨텍스트(예: "console")의 SessionManager에 등록하는 경우:
// SessionManager consoleSessionManager = sessionManagerProvider.getSessionManager("console");
// if (consoleSessionManager != null) {
// consoleSessionManager.addSessionListener(trackingListener);
// }
}
@Override
public void destroy() {
SessionManager sessionManager = sessionManagerProvider.getSessionManager();
if (sessionManager != null) {
sessionManager.removeSessionListener(trackingListener);
}
}
}
8. 다중 컨텍스트 환경에서의 세션 격리 전략
Aspectow 서버 아키텍처의 강력한 보안 특징 중 하나는 다중 컨텍스트 세션 완전 격리입니다.
- 격리 원리: 단일 서버 인스턴스 내에서 일반 사용자가 접속하는 메인 서비스 컨텍스트(
/)와 운영진이 접속하는 관리자 관제 컨텍스트(/console)는 서로 다른 독립된SessionManager인스턴스를 소유합니다. - 보안성: 관리자 콘솔에서 로그인하여 생성된 세션 ID와 인증 정보는 메인 서비스 컨텍스트와 메모리 캐시 및 스토리지 키스페이스 수준에서 완전히 격리됩니다. 메인 웹 애플리케이션의 세션 탈취 취약점이 관리자 권한 탈취로 전이되지 않습니다.
- 도메인 공유 패턴: 필요에 따라 동일 호스트 내에서 특정 SSO(Single Sign-On)가 요구될 경우,
cookieDomain과 Redis 스토어 네임스페이스를 일치시켜 도메인 단위로 안전하게 세션을 공유할 수 있습니다.
9. 코드 레벨의 일관된 세션 조작 (Session API)
비즈니스 로직(Translet 액션, 컨트롤러, 서비스 Bean) 내부에서는 서블릿 API(HttpServletRequest, HttpSession)나 Netty 네이티브 객체에 종속되지 않고, Aspectran이 제공하는 통합 SessionAdapter 인터페이스를 통해 세션을 다룹니다.
package com.aspectran.example.action;
import com.aspectran.core.activity.Translet;
import com.aspectran.core.adapter.SessionAdapter;
import com.aspectran.core.component.bean.annotation.Action;
import com.aspectran.core.component.bean.annotation.Component;
@Component
public class LoginAction {
@Action("login")
public String login(Translet translet) {
String username = translet.getParameter("username");
String password = translet.getParameter("password");
if (authenticate(username, password)) {
// 환경 독립적 SessionAdapter 획득
SessionAdapter sessionAdapter = translet.getSessionAdapter();
// 세션 속성 저장 (자동 캐싱 및 영속 스토어 동기화)
sessionAdapter.setAttribute("currentUser", username);
sessionAdapter.setAttribute("loginTime", System.currentTimeMillis());
return "SUCCESS";
}
return "FAIL";
}
@Action("logout")
public void logout(Translet translet) {
SessionAdapter sessionAdapter = translet.getSessionAdapter();
if (sessionAdapter != null) {
// 세션 즉시 무효화 및 스토리지 삭제
sessionAdapter.invalidate();
}
}
private boolean authenticate(String u, String p) {
return "admin".equals(u) && "secret".equals(p);
}
}
- 환경 이식성: 위 Java 코드는 Undertow 서블릿 환경이든, Netty 비동기 환경이든, 또는 자동화 테스트 쉘 환경이든 단 1줄의 코드 수정 없이 100% 동일하게 실행됩니다.
- 스레드 안전성:
SessionAdapter내부의 속성 조작은DefaultSessionManager에 의해 동시성 보호를 받으므로 멀티스레드 환경에서도 데이터 오염 없이 안전하게 작동합니다.
10. 결론
Aspectran Session Manager는 단순한 키-값 저장소를 넘어선 차세대 엔터프라이즈 상태 관리 솔루션입니다.
- 인프라 독립성: 서블릿, Netty, CLI, 데몬 전 영역에 걸친 단일한 개발 및 운영 패러다임을 확립합니다.
- 지능형 리소스 보호: 신규/일반 세션 분리 알고리즘을 통해 봇과 크롤러로부터 메모리와 스토리지를 완벽하게 방어합니다.
- 유연한 확장성: 설정 변경만으로 로컬 개발용 파일 스토리지에서 대규모 Redis 분산 세션 클러스터링으로 무중단 전환됩니다.
- 고신뢰 분산 스캐빈징: Sorted Set 만료 인덱스, 초경량 매직 바이트 코덱, 분산 락 기반 경합 제어, Silent Eviction, Exactly-Once 소멸 이벤트 보장을 통해 대규모 클러스터 환경에서도 무결점 동기화를 유지합니다.
- 보안과 성능의 양립:
NonPersistent및NonPersistentValue를 통한 선택적 영속화, 정교한 쿠키 보안 플래그(HttpOnly,SameSite,Secure), 다중 컨텍스트 완전 격리를 통해 엔터프라이즈 환경이 요구하는 엄격한 보안 요건을 충족합니다.