← 목록으로
📅 2026.05.26
WAS 이중화 환경에서의 세션 불일치 문제와 해결 방안
TroubleshootingWASTomcatSessionClusteringJWTRedisLoadBalanceSpringBoot

서비스의 가용성과 트래픽 분산을 위해 WAS(Tomcat 기반)를 이중화 또는 다중화하여 구성하는 경우가 많습니다.
이때 L4/L7 로드밸런서의 분산 규칙에 따라 유저의 요청이 서로 다른 WAS 프로세스로 번갈아가며 유입될 수 있으며,
WAS 간에 인증/인가 세션 데이터가 공유되지 않으면 크리티컬한 문제로 이어집니다.


문제 정의: WAS 이중화와 세션 불일치 (Session Inconsistency)

정상적인 단일 서버 환경과 달리, 이중화된 WAS 간에 세션 데이터가 공유되지 않으면 다음과 같은 문제가 발생합니다.

  • 중복 로그인 및 재인증 요구: 유저가 페이지를 이동하거나 API를 호출할 때마다 매번 로그인을 다시 해야 하는 최악의 UX를 제공합니다.
  • 인증 로직 무한 루프: 보안 필터나 인터셉터단에서 세션 확인 실패로 인한 리다이렉트가 반복되면서 비즈니스 로직이 무한 루프에 빠져 서버 자원을 고갈시킬 수 있습니다.

이러한 문제를 해결하기 위한 3가지 수준(인프라, WAS, 애플리케이션)의 해결 방안을 살펴보겠습니다.


1. 인프라 레벨 해결: 스티키 세션 (Sticky Session) 활성화

가장 손쉽게 접근할 수 있는 방법으로, 로드밸런서(L4/L7) 설정으로 최초 접속한 유저의 요청이 항상 동일한 WAS로만 전달되도록 고정하는 방식입니다.

Apache 환경 적용법
mod_jk 모듈을 사용하여 각 Tomcat의 jvmRoute(nodeId)를 지정하고, 아파치 워커 설정과 매핑하여 세션의 소속을 유지합니다.

Nginx 환경 적용법
upstream 블록 내에 ip_hash 지시어 또는 sticky cookie를 사용하여 클라이언트를 특정 WAS에 고정합니다.

1. Apache + Tomcat (mod_jk) 스티키 세션 설정 예시

Apache와 Tomcat을 mod_jk 모듈로 연동할 때는 Tomcat의 jvmRoute 값과 Apache의 workers.properties에 정의된 워커(Worker) 이름을 일치시키는 것이 핵심입니다.
이를 통해 Apache가 쿠키(JSESSIONID) 뒤에 붙은 서버 ID를 확인하고 해당 서버로 요청을 고정합니다.

Tomcat 설정 (server.xml)
각각의 Tomcat 서버 인스턴스에 고유한 jvmRoute 식별자를 부여합니다.

<!-- Tomcat 인스턴스 1 (node1) -->
<Engine name="Catalina" defaultHost="localhost" jvmRoute="node1">
 
<!-- Tomcat 인스턴스 2 (node2) -->
<Engine name="Catalina" defaultHost="localhost" jvmRoute="node2">

Apache 설정 (workers.properties)
로드밸런서 타입의 워커를 정의하고, sticky_session 옵션을 활성화합니다.

# 워커 리스트 정의 (로드밸런서와 상태 확인용 워커)
worker.list=balancer,jkstatus
 
# Tomcat 인스턴스 1 설정 (jvmRoute와 이름이 일치해야 함)
worker.node1.port=8009
worker.node1.host=192.168.0.10
worker.node1.type=ajp13
worker.node1.lbfactor=1
 
# Tomcat 인스턴스 2 설정 (jvmRoute와 이름이 일치해야 함)
worker.node2.port=8009
worker.node2.host=192.168.0.11
worker.node2.type=ajp13
worker.node2.lbfactor=1
 
# 로드밸런서 설정
worker.balancer.type=lb
worker.balancer.balance_workers=node1,node2
 
# 스티키 세션 활성화 (기본값은 1 또는 True)
worker.balancer.sticky_session=1
# 세션 쿠키가 없는 첫 요청이 실패했을 때 다른 서버로 리다이렉트 여부
worker.balancer.sticky_session_force=0
2. Nginx 스티키 세션 설정 예시

Nginx에서는 오픈소스 기본 모듈을 사용하는 방식(ip_hash)과 상용 버전(Nginx Plus) 또는 서드파티 모듈을 사용하는 방식(sticky cookie)으로 나뉩니다.

방법 A: ip_hash 사용 (Nginx 오픈소스 기본 제공)
클라이언트의 IP 주소를 해싱하여 특정 WAS로 매핑하는 방식입니다. 별도의 모듈 설치 없이 가장 쉽게 구현할 수 있습니다.

http {
    upstream backend_servers {
        # 클라이언트 IP 기반으로 세션을 고정하는 지시어
        ip_hash;
 
        server 192.168.0.10:8080 max_fails=3 fail_timeout=10s;
        server 192.168.0.11:8080 max_fails=3 fail_timeout=10s;
    }
 
    server {
        listen 80;
        server_name example.com;
 
        location / {
            proxy_pass http://backend_servers;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

주의점: 동일한 사설 IP나 프록시(NAT)를 거쳐 들어오는 대규모 사내 유저의 경우, ip_hash 적용 시 특정 WAS 한 곳으로 트래픽이 몰리는 병목 현상이 발생할 수 있습니다.

방법 B: sticky cookie 사용 (Nginx Plus 또는 서드파티 모듈 필요)
Nginx가 자체적으로 쿠키를 생성하여 클라이언트 브라우저에 심고, 이 쿠키를 기반으로 추적하는 방식입니다. Apache의 mod_jk 메커니즘과 유사하며 부하 분산이 가장 정교합니다.

http {
    upstream backend_servers {
        # 'route_cookie'라는 이름의 쿠키를 생성하여 1시간 동안 유지
        sticky cookie route_cookie expires=1h domain=.example.com path=/;
 
        server 192.168.0.10:8080;
        server 192.168.0.11:8080;
    }
 
    server {
        listen 80;
        server_name example.com;
 
        location / {
            proxy_pass http://backend_servers;
        }
    }
}
장단점 요약
구분내용
장점백엔드 코드나 WAS 설정을 복잡하게 수정할 필요가 없어 소규모 서비스에서 효율적입니다.
단점특정 WAS(예: A 서버)에 장애가 발생하면, 해당 서버에 종속된 유저들은 B 서버로 이동하면서 세션 데이터가 유실되므로 인증 로직을 처음부터 다시 태워야 합니다. 또한 부하 분산의 균형이 깨질 수 있습니다.

2. WAS 레벨 해결: 톰캣 세션 클러스터링 (Tomcat Membership)

WAS 자체의 기능을 활용하여 서버 간 세션 데이터를 실시간으로 복제·동기화하는 방식입니다.

동작 방식
Tomcat의 <Cluster> 태그 설정을 활성화하고 DeltaManager 또는 BackupManager를 사용합니다.
멀티캐스트(Multicast) 통신이나 정적 멤버십(Static Membership) 설정을 통해 A 서버에서 생성된 세션 데이터를 B 서버로 실시간 복제(Replication)합니다.

Tomcat Cluster 설정 예시
<!-- server.xml -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster">
    <Manager className="org.apache.catalina.ha.session.DeltaManager"
             expireSessionsOnShutdown="false"
             notifyListenersOnReplication="true"/>
 
    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership className="org.apache.catalina.tribes.membership.McastService"
                    address="228.0.0.4"
                    port="45564"
                    frequency="500"
                    dropTime="3000"/>
        <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                  address="auto"
                  port="4000"
                  autoBind="100"
                  selectorTimeout="5000"
                  maxThreads="6"/>
        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
        </Sender>
    </Channel>
 
    <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
    <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>
    <Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
              tempDir="/tmp/war-temp/"
              deployDir="/tmp/war-deploy/"
              watchDir="/tmp/war-listen/"
              watchEnabled="false"/>
    <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>

세션 클러스터링 적용 시, 세션에 저장되는 모든 VO/DTO 클래스는 Serializable을 구현하고 serialVersionUID를 명시적으로 고정해야 합니다.

// 세션에 저장되는 모든 객체에 반드시 적용
public class UserSessionInfo implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userId;
    private String role;
    // ...
}
장단점 요약
구분내용
장점인프라나 애플리케이션 레이어를 크게 건드리지 않고 WAS 설정만으로 세션 영속성을 보장할 수 있습니다. 한 서버가 다운되어도 다른 서버에 세션이 복제되어 있으므로 유저 단절 현상이 없습니다.
단점서버 대수가 늘어날수록 세션 복제를 위한 네트워크 트래픽(All-to-All 통신)과 메모리 오버헤드가 급격히 증가합니다. 대규모 트래픽 환경에서는 성능 저하의 원인이 됩니다.

3. 애플리케이션(Java/Spring Boot) 레벨 해결

WAS 자체에 세션을 두지 않고 외부 저장소를 활용하거나, 세션 상태를 아예 유지하지 않는(Stateless) 구조로 변경하여 근본적인 문제를 해결하는 방식입니다.
최근 엔터프라이즈 아키텍처에서 가장 선호되는 접근법입니다.

방법 A: 공유 세션 저장소 (Spring Session + Redis / RDBMS)

기존 WAS 메모리에 저장하던 세션 정보를 별도의 데이터베이스로 이관합니다.
Spring Boot 환경에서는 Spring Session 라이브러리를 통해 매우 간단하게 구현할 수 있습니다.

구현 프로세스

  1. 세션 공유용 테이블 생성 (RDBMS 사용 시) 또는 인메모리 NoSQL 세팅 (Redis 사용 시)
  2. Spring Session 의존성 주입 후 세션 스토어 지정
  3. 공통 인증/인가 영역(Filter/Interceptor)에서 세션 고유 Key/Value를 조회하여 유효성 검증 수행
Spring Session + Redis 설정 예시
<!-- pom.xml - Redis 기반 Spring Session 의존성 -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# application.yml
spring:
  session:
    store-type: redis
    timeout: 30m
  data:
    redis:
      host: redis-host
      port: 6379
@EnableRedisHttpSession
@Configuration
public class SessionConfig {
    // Spring Session이 자동으로 세션 직렬화/역직렬화 처리
}

메모리 기반의 Redis를 활용하면 고성능 Key-Value 조회가 가능하여, 정기적인 디스크 I/O 부담 없이 대용량 세션을 안정적으로 처리할 수 있습니다.


방법 B: JWT (JSON Web Token) 도입을 통한 무상태(Stateless) 인증

서버에 세션 자체를 저장하지 않고, 인증 정보를 암호화된 토큰 형태로 클라이언트가 직접 보유하게 만드는 방식입니다.

JWT를 선택하는 핵심 이유

유저 정보를 안전하게 보호하기 위해 시간 기반(Time-based)의 암호화 키 또는 만료 시간(Expiration)을 엄격하게 세팅할 수 있기 때문입니다.
서버 측 저장소에 의존하지 않으므로 WAS가 몇 대로 증설되더라도 아무런 제약 없이 인증 상태를 유지할 수 있습니다.

보안 보완 전략: 토큰 이원화

JWT의 탈취 위험을 최소화하기 위해 토큰의 생명주기를 이원화합니다.

토큰 이원화 전략
토큰 종류유효시간역할
Access Token짧음 (예: 30분)실질적인 API 인가에 사용
Refresh Token비교적 김 (예: 2주)Access Token 만료 시 재발급용, Redis 등 DB에 저장
JWT 필터 설정 예시
// Spring Boot - JWT 필터 예시
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);
 
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
 
        filterChain.doFilter(request, response);
    }
 
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

결론 및 인사이트

WAS 이중화 환경에서 세션 공유 실패 이슈를 해결하는 가장 이상적인 방향은,
애플리케이션 단에서 컨트롤이 가능하고 서버 확장성이 뛰어난 JWT 또는 외부 세션 스토어(Redis)를 도입하는 것입니다.
인프라와 애플리케이션이 완벽하게 분리되어 향후 아키텍처 변경에 유연하게 대응할 수 있기 때문입니다.

다만, 시스템의 규모·동시 접속 유저 수·개발 공수 및 유지보수 비용을 종합적으로 고려해야 합니다.
엔드유저의 규모가 작고 빠른 서비스 안정화가 필요하다면 스티키 세션과 같은 심플하고 비용 효율적인 인프라 측면의 해결책도 충분히 훌륭한 선택지가 될 수 있습니다.

해결 방법별 비교
해결 방법레벨확장성구현 난이도장애 내성
스티키 세션인프라낮음쉬움낮음 (서버 다운 시 세션 유실)
Tomcat 클러스터링WAS중간중간중간 (서버 간 복제 오버헤드)
Spring Session + Redis애플리케이션높음중간높음
JWT (Stateless)애플리케이션매우 높음높음매우 높음