오픈소스 인사이트
home
오픈소스 기술 동향
home
💡

WildFly 세션 소멸 추적: Byteman으로 invalidate 범인 검거하기

WildFly 운영 중 "분명 세션을 생성했는데 갑자기 사라졌다"는 사용자의 불만을 들어 보셨나요? 로그에는 아무 흔적도 없는데 세션이 무효화(Invalidate)되어 로그아웃되는 현상, 어떻게 잡아야 할까요?

Q. "IllegalStateException: Session is invalid" > 특정 로직 수행 중 세션이 끊기거나, 유령 세션 현상이 발생한다면?

소스 수정 없이 Byteman으로 세션의 시작과 끝을 완벽히 추적해 보겠습니다
java.lang.IllegalStateException: UT000010: Session is invalid Lh7WglR47CXXuIS3AFZYVemQfeSIG4kLszKH7EIo at io.undertow.server.session.InMemorySessionManager$SessionImpl.getAttribute(InMemorySessionManager.java:548) at io.undertow.servlet.spec.HttpSessionImpl.getAttribute(HttpSessionImpl.java:122) at org.apache.jsp.victim_005fwork_jsp._jspService(victim_005fwork_jsp.java:103) at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70) at javax.servlet.http.HttpServlet.service(HttpServlet.java:590) .....
Bash
복사

세션이 예기치 않게 종료되는 주요 원인 3가지

1.
명시적인 invalidate() 호출: 특정 로직(로그아웃, 보안 체크)에서 세션을 파괴함
2.
세션 타임아웃(Timeout): 설정된 유효 시간이 지나 컨테이너에 의해 삭제됨
3.
세션 데드락 및 동기화 오류: 다중 스레드 환경에서 세션 객체 참조 충돌

Byteman을 통한 추적 원리 및 설정

Byteman 이란?

Byteman은 자바 애플리케이션이나 JDK 런타임의 동작을 실시간으로 추적(Trace), 모니터링, 테스트할 수 있도록 도와주는 강력한 도구입니다. 가장 큰 특징은 소스 코드 수정이나 재컴파일, 서버 재배포 없이 실행 중인 애플리케이션의 메서드에 원하는 자바 코드를 주입할 수 있다는 점입니다.

1. 핵심 동작 원리: ECA 규칙

Byteman은 **ECA(Event-Condition-Action)**라는 단순하고 명확한 규칙 시스템을 기반으로 작동합니다.
Event (어디서): 코드를 주입할 특정 지점(메서드 진입, 종료, 특정 라인 등)을 지정합니다.
Condition (언제): 주입된 코드가 실행될 조건(자바 불리언 표현식)을 설정합니다.
Action (무엇을): 조건이 충족되었을 때 실행할 자바 로직(로그 출력, 값 변경, 예외 발생 등)을 정의합니다.

2. 주요 특징

비침습적 주입: 원본 소스나 패키지를 건드리지 않습니다. JVM 기동 시점은 물론, 이미 실행 중인 상태에서도 동적으로 규칙을 적용하거나 제거할 수 있습니다.
광범위한 적용: 사용자 정의 클래스뿐만 아니라, 소스가 없는 외부 라이브러리나 **JDK 내부 클래스(String, Thread 등)**까지도 침투가 가능합니다.
강력한 접근 권한: private 메서드나 변수에도 접근하여 데이터를 확인하거나 메서드를 호출할 수 있습니다.
타입 안전성: 주입되는 코드가 대상 메서드의 시그니처와 호환되는지 자동으로 검증하여 런타임 안정성을 보장합니다.

3. 언제 사용하나요?

실시간 디버깅: 운영 환경에서 재배포 없이 특정 변수값이나 제어 흐름을 로그로 확인하고 싶을 때
장애 유도 테스트(Fault Injection): 특정 메서드에서 강제로 예외를 던지거나(Throw), 가짜 값(Return)을 반환하게 하여 시스템의 예외 처리 능력을 검증할 때
멀티스레드 레이스 컨디션 재현: 스레드 간의 타이밍을 맞추기 위해 특정 지점에 의도적인 지연(Delay)이나 동기화 로직을 주입할 때
TIP: 엔지니어에게 Byteman은 "운영 중인 서비스의 심장을 멈추지 않고도 내부를 들여다보고, 필요하다면 즉석에서 처방(코드 주입)을 내릴 수 있는 런타임 수술 도구"와 같습니다.

Byteman 에이전트 설정 방법 (Standalone 모드)

JAVA_OPTS 설정

standalone.conf 또는 실행 스크립트에 아래 옵션을 추가합니다. {WILDFLY_HOME}/instance 경로에 에 필요한 파일을 준비했다고 가정합니다.
#Byteman 에이전트 및 스크립트 경로 설정 JAVA_OPTS="$JAVA_OPTS -javaagent:/{WILDFLY_HOME}/instance/byteman-3.0.22.jar=script:/{WILDFLY_HOME}/instance/session.btm,sys:/{WILDFLY_HOME}/instance/byteman-3.0.22.jar -Dorg.jboss.byteman.transform.all=true"
Bash
복사

추적 규칙 작성 (session.btm)

세션의 생성(init)과 파괴(invalidate) 시점에 로그를 남기도록 정의합니다.
RULE init INTERFACE javax.servlet.http.HttpSession METHOD <init> AT EXIT IF TRUE DO traceStack("--------------------------->HttpSession.init " + $0.getId() + "\n"); ENDRULE RULE invalidate INTERFACE javax.servlet.http.HttpSession METHOD invalidate AT ENTRY IF TRUE DO traceStack("--------------------------->HttpSession.invalidate " + $0.getId() + "\n"); ENDRULE
Bash
복사

세션 장애 재현 시나리오

단일 서버에서 두 개의 JSP를 이용해 "누가 내 세션을 죽였나"를 재현합니다. 일반적으로 세션이 만료되면 WildFly(Undertow)는 매우 방어적으로 동작합니다.
보안 및 안정성 로직: 세션이 만료된 후 request.getSession(false)를 호출하면 컨테이너는 에러를 던지는 대신 조용히 null을 반환합니다.
재현의 어려움: 즉, 이미 '정리된' 세션에 접근하면 애플리케이션은 단순히 세션이 없는 것으로 판단할 뿐, 우리가 원하는 IllegalStateException이라는 명확한 '문제 상황'을 로그에 남기지 않습니다.
따라서, "세션 객체를 참조하고 있는 찰나에 옆에서 강제로 파괴"하는 극적인 타이밍을 구성해야만 JVM 수준에서 발생하는 세션 충돌 오류를 목격할 수 있습니다.

테스트 코드 1: victim_work.jsp (피해자)

// 1. 세션 감시 시작 HttpSession session = request.getSession(true); for (int i=1; i<=10; i++) { Thread.sleep(1000); // 1초 간격 모니터링 try { // [핵심] 세션 상태 체크 (무효화 시 IllegalStateException 발생) session.getAttribute("check"); log.info(i + "s: 세션 정상..."); } catch (IllegalStateException e) { // 🎯 암살 감지: 서버 로그에 에러 스택 및 Byteman 로그 출력 log.error("!!! 세션 파괴 감지 !!! ID: " + session.getId(), e); break; } }
Plain Text
복사

테스트 코드 2: hidden_killer.jsp (범인)

<% HttpSession sess = request.getSession(false); if (sess != null) { // 이 호출이 일어나는 순간 Byteman 로그에 스택트레이스가 찍힙니다. sess.invalidate(); out.println("<h2>세션 암살 성공</h2>"); } %>
Plain Text
복사

시나리오 및 결과 확인

1. 준비: WildFly에 Byteman 설정을 적용하고 서버를 재시작합니다.
WildFly의 JAVA_OPTS 설정이 제대로 되었는지 확인합니다.
#서버 기동 로그 org.jboss.byteman.agent.loaded = true org.jboss.byteman.agent.version = 3.0.22 org.jboss.byteman.transform.all = true #ps -ef 상에서 javaagent 구문이 있는지 확인 -javaagent:/{WILDFLY_HOME}/instance/byteman-3.0.22.jar=script:/{WILDFLY_HOME}/instance/session.btm,sys:/{WILDFLY_HOME}/instance/byteman-3.0.22.jar -Dorg.jboss.byteman.transform.all=true
Bash
복사
2. 작업 시작: 브라우저 탭 1에서 victim_work.jsp를 실행합니다.
session.btm 에 설정된 규칙에 의하여 다음과 같은 세션 로그가 출력 됩니다.
--------------------------->HttpSession.init Lh7WglR47CXXuIS3AFZYVemQfeSIG4kLszKH7EIo io.undertow.servlet.spec.HttpSessionImpl.<init>(HttpSessionImpl.java:59) io.undertow.servlet.spec.HttpSessionImpl.forSession(HttpSessionImpl.java:69) io.undertow.servlet.core.SecurityActions.forSession(SecurityActions.java:92) io.undertow.servlet.core.SessionListenerBridge.sessionCreated(SessionListenerBridge.java:63) io.undertow.server.session.SessionListeners.sessionCreated(SessionListeners.java:52) io.undertow.server.session.InMemorySessionManager.createSession(InMemorySessionManager.java:191) io.undertow.servlet.spec.ServletContextImpl.getSession(ServletContextImpl.java:977) io.undertow.servlet.spec.HttpServletRequestImpl.getSession(HttpServletRequestImpl.java:425) io.undertow.servlet.spec.HttpServletRequestImpl.getSession(HttpServletRequestImpl.java:430) org.apache.jasper.runtime.PageContextImpl.initialize(PageContextImpl.java:137) org.apache.jasper.runtime.JspFactoryImpl.internalGetPageContext(JspFactoryImpl.java:109) org.apache.jasper.runtime.JspFactoryImpl.getPageContext(JspFactoryImpl.java:60) org.apache.jsp.victim_005fwork_jsp._jspService(victim_005fwork_jsp.java:83)
Bash
복사
3. 세션 강제 종료 실행 및 원인 추적: 즉시 새로운 탭을 열어 hidden_killer.jsp를 호출해보겠습니다. (반드시 같은 브라우저여야 세션 ID가 공유됩니다.)
Victim 로그: 루프 도중 갑자기 IllegalStateException 에러가 뜹니다.
java.lang.IllegalStateException: UT000010: Session is invalid Lh7WglR47CXXuIS3AFZYVemQfeSIG4kLszKH7EIo at io.undertow.server.session.InMemorySessionManager$SessionImpl.getAttribute(InMemorySessionManager.java:548) at io.undertow.servlet.spec.HttpSessionImpl.getAttribute(HttpSessionImpl.java:122) at org.apache.jsp.victim_005fwork_jsp._jspService(victim_005fwork_jsp.java:103) at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70) at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
Bash
복사
Killer 로그: Byteman이 남긴 HttpSession.invalidate 호출 스택이 찍힙니다. 이를 통해 어떤 로직(여기서는 hidden_killer.jsp)이 세션을 죽였는지 역추적할 수 있습니다.
--------------------------->HttpSession.invalidate 6Ynag1G4AGfD1... java.lang.Throwable: STACKTRACE at io.undertow.servlet.spec.HttpSessionImpl.invalidate(HttpSessionImpl.java:214) at org.apache.jsp.hidden_killer_jsp._jspService(hidden_killer_jsp.java:110) // 👈 범인 지점 확인! at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70) ...
Bash
복사

주의 사항

traceStack()은 현재 스레드의 호출 스택 전체를 문자열로 변환하므로 비용이 큽니다. 빈번하게 호출되는 메서드에 적용할 경우 시스템 성능 저하를 유발할 수 있으니 운영 환경에서는 주의가 필요합니다.
초당 수천 건의 세션이 생성되는 서비스에서 모든 init을 기록하면 디스크 I/O 부하와 함께 로그 용량이 순식간에 불어납니다. 가능하다면 필요한 조건(IF)을 설정하여 타겟팅 로깅을 수행하는 것이 좋습니다.
장애 진단이 완료된 후에는 JAVA_OPTS에서 에이전트 설정을 제거하고 서버를 재시작해야 합니다. 불필요한 바이트코드 변환 로직이 남아있는 것은 잠재적인 위험 요소입니다.
WildFly의 Java Security Manager가 활성화된 경우 Byteman의 클래스 변환이 차단될 수 있습니다. 설정 시 해당 권한을 사전에 검토가 필요할 수 있습니다.

마치며

Byteman을 통해 소스 코드 수정이나 재배포 없이, 운영 중인 서버에 즉시 투입하여 세션 관련 장애 원인(Stack Trace)을 특정할 수 있는 강력한 툴입니다. 이를 통해 세션을 비정상 종료시키는 로직을 찾거나 이슈를 해결하기 위한 강력한 단서를 얻을 수 있습니다.
세션 유실 문제는 개발자의 로직 실수, 타임아웃 설정 오류, 혹은 인프라의 동기화 문제 등 원인이 매우 다양합니다. 이때 Byteman은 "어디서, 누가, 왜" 죽였는지를 명확히 짚어주는 가장 확실한 목격자가 됩니다.
한정상 프로
에스코어에서 미들웨어 엔지니어로 근무하며, 삼성 그룹사를 비롯한 국내 주요 대기업과 공공기관의 미들웨어 설계 및 기술지원을 담당하고 있어요