Spring Boot + JBoss + Apache 이중화 아키텍처 설계

· 약 11분 읽기 · 조회 2 #Spring Boot#JBoss#Apache#이중화#아키텍처

공공·금융 SI에서는 Spring Boot 애플리케이션을 내장 Tomcat이 아니라 JBoss EAP 같은 WAS에 WAR로 올리고, 앞에 Apache를 두는 구성이 여전히 많다. 서버를 두 벌 놓는 것만으로는 이중화가 되지 않는다. 버전 조합, 세션 처리, 헬스 체크, 배포 순서가 맞아야 서버 한 대가 죽어도 사용자가 모른다. 이 글은 L4 → Apache 2대 → JBoss 2대 → DB 구성을 기준으로 설계 결정과 설정 예시, 그리고 이중화를 검증하는 시나리오를 정리한 것이다.

엔지니어가 서버 랙 한 대를 열어 점검하는 동안에도 돌 관문을 지난 봉투들이 두 랙으로 나뉘어 계속 흘러가는 데이터센터

전체 구성

계층 구성 역할
L4 스위치 VIP 1개, Apache 2대로 분산 단일 진입점, Apache 헬스 체크
Web Apache HTTP Server 2대 TLS 종료, 정적 파일, JBoss로 분산(sticky)
WAS JBoss EAP 2대(HA 프로파일) Spring Boot WAR 실행, 세션 복제
DB Active–Standby 데이터 저장, 장애 시 전환

사용자 요청이 L4 VIP를 거쳐 Apache 2대로, 다시 sticky 분산으로 JBoss 2대로 가고, JBoss끼리는 세션을 복제하며 DB는 Active에서 Standby로 복제되는 이중화 구성

모든 계층이 둘씩 있어야 하고, 각 계층은 바로 아래 계층의 상태를 스스로 확인해야 한다. L4는 Apache를, Apache는 JBoss를 확인한다. 이 확인이 빠지면 죽은 서버로 요청을 계속 보낸다.

버전 조합부터 맞춘다

가장 먼저 정할 것은 Spring Boot와 JBoss EAP의 버전 조합이다. 둘이 쓰는 API 패키지가 다르면 배포 자체가 되지 않는다.

조합 서블릿 API Java 비고
Spring Boot 2.7 + JBoss EAP 7.4 javax.* (Java EE 8) EAP 지원 목록 확인 기존 사업에 많은 조합
Spring Boot 3.x + JBoss EAP 8 jakarta.* (Jakarta EE 10) 17 이상 신규 사업이면 이 조합

Spring Boot 3는 jakarta.*만 지원하므로 EAP 7.x에는 올릴 수 없다. 제안요청서에 WAS 버전이 지정돼 있으면 그 버전이 Spring Boot 버전을 정한다. 이 결정은 ADR로 남긴다.

Spring Boot를 JBoss에 올리기

WAR로 패키징하고, 내장 Tomcat은 실행 파일에서 빼고, 서블릿 컨테이너가 앱을 시작하게 한다.

@SpringBootApplication
public class Application extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}
<!-- pom.xml -->
<packaging>war</packaging>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

JBoss가 제공하는 모듈과 앱의 라이브러리가 부딪치지 않게 WEB-INF/jboss-deployment-structure.xml을 둔다. 로그를 앱의 Logback으로 남기려면 JBoss 로깅 서브시스템을 이 배포에서 뺀다.

<jboss-deployment-structure>
    <deployment>
        <exclude-subsystems>
            <subsystem name="logging"/>
        </exclude-subsystems>
    </deployment>
</jboss-deployment-structure>

DB 연결은 두 가지 중 하나를 고른다. JBoss 데이터소스를 쓰면 접속 정보와 커넥션 검증을 운영팀이 WAS 설정으로 관리할 수 있다. 이때 앱은 spring.datasource.jndi-name=java:/jdbc/AppDS처럼 JNDI 이름만 안다. 앱 안의 HikariCP를 쓰면 설정이 코드와 같이 배포되지만 운영팀이 WAS 콘솔로 볼 수 없다. 공공 사업에서는 운영 이관을 고려해 JBoss 데이터소스를 쓰는 경우가 많다.

Apache와 JBoss 연결: sticky session

로그인 세션이 있는 업무 시스템은 같은 사용자의 요청을 같은 JBoss로 보내야 한다. JBoss(Undertow)는 노드 이름(instance-id)을 세션 ID 끝에 붙이고, Apache는 그 값을 보고 같은 노드로 보낸다.

JBoss는 노드마다 기동 옵션으로 이름을 준다.

# node1 서버
./standalone.sh -c standalone-ha.xml -Djboss.node.name=node1 -b 10.0.1.11

Apache는 mod_proxy_balancer로 분산하고, route 값을 노드 이름과 맞춘다. mod_proxy_hcheck를 쓰면 Apache가 JBoss 상태를 주기적으로 확인해 죽은 노드를 뺀다.

<Proxy "balancer://app">
    BalancerMember "http://10.0.1.11:8080" route=node1 hcmethod=GET hcuri=/app/actuator/health/liveness hcinterval=10
    BalancerMember "http://10.0.1.12:8080" route=node2 hcmethod=GET hcuri=/app/actuator/health/liveness hcinterval=10
    ProxySet stickysession=JSESSIONID|jsessionid lbmethod=byrequests
</Proxy>
ProxyPass        "/app" "balancer://app/app"
ProxyPassReverse "/app" "balancer://app/app"

AJP(mod_jk)로 연결하는 구성도 많다. AJP를 쓰면 리스너를 내부망 주소에만 열고 공유 비밀값을 설정한다. 외부에 노출된 AJP 포트는 Tomcat의 Ghostcat(CVE-2020-1938)처럼 실제 공격 통로가 된 사례가 있다.

세션: sticky만으로는 부족하다

sticky session은 평소에는 잘 동작하지만, 노드 하나가 죽으면 그 노드에 붙어 있던 사용자의 세션이 사라진다. 다시 로그인해도 되는 시스템인지부터 발주처와 합의하고, 안 되면 세션을 살릴 방법을 고른다.

방식 노드 장애 시 장점 단점
sticky만 그 노드 사용자는 재로그인 단순, 추가 구성 없음 장애가 사용자에게 보인다
JBoss 세션 복제 다른 노드의 복제본으로 이어짐 앱 코드 변경이 적다 HA 프로파일, 클러스터 통신 설정, 세션 객체 직렬화 필요
외부 세션 저장소(Spring Session + Redis 또는 JDBC) 저장소에서 이어짐 WAS와 무관, 노드 추가가 쉽다 저장소 자체도 이중화해야 한다

JBoss 세션 복제를 쓰려면 세 가지가 필요하다. standalone-ha.xml처럼 클러스터링이 들어간 프로파일로 기동하고, WEB-INF/web.xml에 <distributable/>을 넣고, 세션에 담는 객체를 모두 Serializable로 만든다. 망 정책상 멀티캐스트가 막혀 있으면 클러스터 통신(JGroups)을 TCP 방식으로 바꾸고 노드 주소를 고정으로 적는다.

사용자 요청이 node1로 가던 중 node1이 죽자 Apache가 헬스 체크로 node1을 빼고 같은 세션을 node2로 보내며, node2가 복제본에서 세션을 찾아 로그인을 유지한 채 응답하는 순서

헬스 체크는 계층마다, 의미에 맞게

헬스 체크를 잘못 걸면 이중화가 오히려 전체 장애를 만든다. 흔한 예가 DB 상태까지 포함한 /actuator/health를 Apache 헬스 체크에 쓰는 경우다. DB가 잠깐 흔들리면 두 JBoss가 동시에 실패로 판정되어 Apache가 둘 다 빼 버린다.

  • L4 → Apache: Apache가 정적 파일 하나를 응답하는지 본다.
  • Apache → JBoss: 앱 프로세스가 살아 있는지만 보는 liveness를 쓴다(management.endpoint.health.probes.enabled=true로 /actuator/health/liveness, /readiness를 켠다).
  • DB 연결 상태: readiness와 모니터링으로 따로 보고 알림을 보낸다. 분산 대상에서 빼는 기준으로 쓰지 않는다.

무중단 배포 순서

이중화의 가장 큰 이득은 서비스를 멈추지 않고 배포하는 것이다. 노드를 하나씩 돌린다.

  1. Apache balancer-manager에서 node1을 드레인(Drain) 상태로 바꿔 새 세션이 가지 않게 한다.
  2. 기존 세션이 빠지기를 기다린다(세션 복제를 쓰면 바로 다음 단계로 가도 된다).
  3. node1에 새 WAR를 배포하고 기동한다.
  4. liveness와 주요 화면 하나를 직접 확인한다.
  5. node1을 다시 활성화하고, node2에 같은 순서를 반복한다.

DB 스키마 변경이 있으면 구버전과 신버전 앱이 잠시 함께 도는 동안 둘 다 동작하도록 변경을 나눈다(컬럼 추가 먼저, 삭제는 다음 배포).

이중화 검증 시나리오

"이중화 구성 완료"는 검수 기준이 아니다. 실제로 하나씩 내려 보고 숫자를 남긴다.

시나리오 기대 결과 잴 것
JBoss node1 프로세스 종료 node2로 전환, 로그인 유지(복제 시) 전환 시간, 실패한 요청 수
Apache 1대 종료 L4가 다른 Apache로 전환 전환 시간, 실패 요청 수
node1 복구 후 재투입 헬스 체크 통과 뒤 자동으로 분산 재개 재투입까지 걸린 시간
DB Active → Standby 전환 커넥션 재연결 후 정상 처리 오류 지속 시간, 재연결 여부
노드 하나씩 순차 배포 서비스 중단 없음 배포 중 실패 요청 수

부하를 건 상태에서 시험해야 의미가 있다. 아무도 안 쓰는 시스템에서는 전환 중 실패가 0으로 나온다.

자주 하는 실수

  • 버전 조합을 늦게 확인한다. Spring Boot 3를 EAP 7.x에 올리려다 배포 단계에서 막힌다.
  • sticky만 걸고 이중화라고 부른다. 노드 장애 때 그 노드 사용자는 모두 로그아웃된다.
  • 헬스 체크에 DB 상태를 넣는다. DB가 흔들리면 WAS 전체가 분산 대상에서 빠진다.
  • 세션 객체 직렬화를 확인하지 않는다. 복제를 켰는데도 특정 화면에서만 세션이 끊긴다.
  • 장애 시험을 무부하로 한다. 실제 전환 중 실패를 볼 수 없다.

점검표

  • Spring Boot와 JBoss EAP 버전 조합을 정하고 ADR로 남겼다
  • WAR 패키징, 내장 Tomcat provided, 서블릿 초기화 클래스가 있다
  • jboss-deployment-structure.xml로 충돌 모듈을 정리했다
  • 노드 이름과 Apache route 값이 일치한다
  • 노드 장애 시 세션 처리 방식을 발주처와 합의했다
  • 세션 복제를 쓰면 HA 프로파일, <distributable/>, 직렬화를 확인했다
  • Apache → JBoss 헬스 체크는 liveness만 본다
  • AJP를 쓰면 내부망에만 열고 비밀값을 설정했다
  • 노드 단위 무중단 배포 절차를 문서로 만들고 리허설했다
  • 부하 상태에서 장애 전환 시나리오를 재고 결과를 남겼다

이중화는 서버를 두 대 사는 일이 아니라, 한 대가 사라졌을 때 무슨 일이 일어날지 계층마다 정해 두는 일이다. 버전, 세션, 헬스 체크, 배포 순서를 정하고 실제로 하나씩 내려 봐야 끝난다.

댓글 0개

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

댓글 쓰기

← 목록으로 돌아가기