Physical Address
South Korea
Physical Address
South Korea


얼마 전 새로운 프로젝트에 투입되면서 어플리케이션 아키텍터와 인프라 요구사항을 조율하는 미팅이 있었습니다. 늘 하던 대로 “RHEL 서버에 톰캣 버전은 뭘로 맞추고 server.xml은 어떻게 세팅할까요?”라고 물었더니, 어플리케이션 아키텍터 분이 덤덤하게 대답하시더군요.
“아, 이번엔 서버에 톰캣 따로 안 깔아주셔도 됩니다. 그냥 자바만 있으면 돼요. 임베디드(내장형)로 쓸 거거든요.”
순간 머릿속이 복잡해졌습니다. 리눅스 서버 계정을 만들고, 공식 홈페이지에서 톰캣 압축 파일을 풀고, 기동 스크립트(startup.sh)를 서비스에 등록하는 게 수년 동안 해온 내 밥줄이자 당연한 루틴이었기 때문입니다. “서버에 톰캣이 없는데 웹 서비스가 돌 수 있다고?” 하는 의문이 생겨, 그날 밤 밤새도록 독립형(Standalone)과 임베디드형(Embedded)의 차이를 파고들었습니다.
특히 마침 저희 표준 OS로 도입된 RHEL 9.6 환경에서 두 방식이 실무적으로 어떻게 다른지, 인프라 관리자 관점에서 알아야 할 핵심을 정리해 보았습니다.
알고 보니 이건 단순한 기술 취향 차이가 아니라, 인프라를 바라보는 철학 자체가 바뀐 문제였습니다.
예전 전통적인 방식(Standalone)은 서버라는 도화지에 톰캣이라는 ‘서버 소프트웨어’를 먼저 예쁘게 세팅해 두고, 그 위에 개발자가 짠 WAR 파일을 얹는 형태였습니다.
이 방식의 치명적인 문제는 강한 결합(Tight Coupling)이었습니다. 서버 한 대에 여러 앱을 올렸다가 톰캣 버전을 올리거나 설정 하나만 잘못 바꾸면, 다른 멀쩡한 앱들까지 줄줄이 죽어나갔죠. 덕분에 주말이나 새벽에 나와 식은땀을 흘리며 복잡한 server.xml을 뒤적이곤 했습니다.
반면 요즘 대세라는 임베디드 톰캣 방식은 전혀 달랐습니다. 쉽게 말해, 개발자가 빌드한 결과물인 JAR 파일 내부에 톰캣 엔진 라이브러리가 통째로 쏙 들어가 있습니다.
‘서버 위에 앱을 올리는 것’이 아니라, ‘앱이 실행되면서 스스로 서버를 켜는 것’이죠. 클라우드나 마이크로서비스(MSA) 환경처럼 서버가 수시로 생성되고 사라지는 요즘 시대에는, 어떤 환경에서든 java -jar 명령어 딱 한 줄로 똑같은 톰캣 엔진이 구동되는 이 방식이 표준이 될 수밖에 없었던 겁니다.
■ 전통적 방식 (인프라 중심)
[OS 준비] → [Java 설치] → [Tomcat 엔진 설치/튜닝] → [WAR 배포] → [복잡한 환경설정]
■ 현대적 방식 (애플리케이션 중심)
[OS 준비] → [Java 설치] → [JAR 파일 실행 (내장 톰캣 자동 구동)] → [즉시 오픈!]
결과적으로 임베디드 방식을 쓰면 관리자가 서버마다 찾아다니며 server.xml 설정값을 일일이 제어하는 생노가다가 완전히 사라집니다. 대신 프로세스가 살아있는지, 리소스를 얼마나 먹는지만 체크하면 되니 인프라 관리자 입장에서도 업무 생산성이 확 올라갑니다.

두 방식의 차이점을 인프라 실무자 시선에서 보기 쉽게 표로 비교해 봤습니다.
| 구분 | 임베디드 톰캣 (Embedded) | 독립형 톰캣 (Standalone) |
| 핵심 용도 | 마이크로서비스(MSA), Spring Boot, 클라우드 | 기존 레거시 시스템, 통합 웹 서버 운영 |
| 배포 형태 | 실행 가능한 단일 파일 (Fat JAR) | 웹 아카이브 파일 (WAR) |
| 설치 및 관리 | 자바(JRE)만 깔려 있으면 끝 | 톰캣 엔진 별도 설치, 환경변수 필수 |
| 기동 명령어 | java -jar myapp.jar | systemctl start tomcat 또는 startup.sh |
| 엔진 업그레이드 | 개발자가 라이브러리 버전 바꾸고 재빌드 | 관리자가 서버 내 톰캣 재설치 |
| 관리자의 역할 | 프로세스 및 OS 리소스 모니터링 중심 | 미들웨어 설정 및 엔진 내부 튜닝 중심 |
RHEL 9.6은 최신 커널과 강력한 보안 기능을 제공합니다. 이 환경에서 내장형 톰캣 앱을 안정적으로 돌리기 위해 제가 직접 검증한 단계별 실무 가이드를 공유합니다.
최신 Spring Boot 3.0 이상 기반의 앱이라면 최소 Java 17 이상이 필수입니다. RHEL 9.6 표준 리포지토리를 이용해 깔끔하게 설치해 줍니다.
Bash
# RHEL 9.6 패키지 업데이트 및 Java 17 설치
sudo dnf update -y
sudo dnf install java-17-openjdk-devel -y
# 설치 확인
java -version
임베디드 방식에서는 java -jar 명령어가 곧 웹 서버를 띄우는 행위입니다. 하지만 실무에서 그냥 띄우면 큰일 나겠죠? 메모리 옵션과 포트 지정을 제대로 해줘야 합니다.
java -jar myapp.jarjava -jar myapp.jar --server.port=8081> **팁:** RHEL의 메모리 효율을 극대화하려면 서버 스펙에 맞춰 최소(`-Xms`)와 최대(`-Xmx`) 메모리를 동일하게 맞추는 것이 가비지 컬렉션(GC) 부하를 줄이는 데 좋습니다.개발 장비가 아니라 운영 서버라면, 리눅스가 예기치 않게 재부팅되거나 프로세스가 죽었을 때 자동으로 살아나도록 systemd 등록이 필수입니다. 특히 보안을 위해 절대 root 계정으로 띄우면 안 됩니다.
1단계: 톰캣 실행용 전용 유저 생성 (로그인 권한 차단)
Bash
sudo useradd -m -s /sbin/nologin tomcat-user
2단계: 서비스 설정 파일 생성
Bash
sudo vi /etc/systemd/system/myapp.service
파일 안에 아래 내용을 붙여넣습니다. 경로와 메모리 옵션은 본인 환경에 맞게 수정하세요.
Ini, TOML
[Unit]
Description=Embedded Tomcat Spring Boot Application
After=network.target
[Service]
User=tomcat-user
Group=tomcat-user
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -Xms1024m -Xmx1024m -jar /opt/myapp/myapp.jar
# 프로세스가 죽으면 5초 후에 무조건 다시 살린다!
Restart=always
RestartSec=5
# 로그가 흩어지지 않게 깔끔하게 파일로 저장
StandardOutput=append:/var/log/myapp/stdout.log
StandardError=append:/var/log/myapp/stderr.log
[Install]
WantedBy=multi-user.target
3단계: 서비스 활성화 및 가동
Bash
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
“톰캣 내부 설정을 개발자가 다 하면, 인프라 담당자는 놀아도 되나요?” 하실 수 있지만, OS 레벨에서 챙겨야 할 보안과 트러블슈팅은 여전히 우리 몫입니다.
firewalld)RHEL 9.6은 기본적으로 방화벽이 빡빡합니다. 내장형 톰캣이 사용하는 포트(예: 8080)를 열어주지 않으면 외부에서 접속이 안 됩니다.
Bash
# 8080 포트 영구 허용
sudo firewall-cmd --permanent --add-port=8080/tcp
# 보안 강화를 위해 특정 사설 IP 대역만 허용할 때
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="8080" accept'
sudo firewall-cmd --reload
SELinux 대응하기이것 때문에 삽질하시는 분들 정말 많습니다. RHEL은 보안을 위해 SELinux가 켜져 있는데, 이 때문에 내장 톰캣이 특정 포트를 바인딩하지 못하거나 파일 쓰기 권한이 막히는 증상이 발생하곤 합니다.
Bash
# 앱이 안 뜰 때 SELinux 차단 로그 확인하는 명령어
sudo ausearch -m avc -ts recent
# 8081 같은 비표준 포트를 써야 한다면 SELinux에 허용 정책 추가
sudo semanage port -a -t http_port_t -p tcp 8081
이제 우리는 톰캣 매니저 창을 보는 게 아니라 OS 명령어로 서버 건강 상태를 체크해야 합니다.
jstat -gc [PID] 1000 명령어로 가비지 컬렉션(GC) 추이를 1초 단위로 모니터링합니다.jstack [PID] 명령어로 쓰레드 덤프를 떠서 어떤 메서드가 병목을 유발하는지 개발자에게 던져주면 사랑받을 수 있습니다.Q. 정말 인프라 관리자가 톰캣 내부 설정을 전혀 몰라도 되나요?
A. 네, 놀랍게도 그렇습니다. 최대 쓰레드 수, 커넥션 풀(DB Connection Pool) 같은 핵심 튜닝 값들은 이제 개발자가 애플리케이션 내부 설정 파일(application.yml)에 넣어서 빌드해 줍니다. 우리는 인프라의 CPU, 메모리, 네트워크 세션이 버텨주는지만 보면 됩니다. 역할 분담이 아주 명확해졌죠.
Q. 한 서버에 JAR 파일을 여러 개 띄우면 톰캣 엔진도 여러 개 도는 건데, 메모리 낭비 아닌가요?
A. 과거 메모리가 귀하던 시절엔 그렇게 생각했습니다. 하지만 지금은 시스템의 ‘격리성’이 훨씬 중요합니다. 독립형 톰캣 시절엔 앱 하나가 메모리를 다 먹어서 톰캣 자체가 죽으면 같은 서버에 있던 다른 서비스까지 다 죽었습니다. 지금은 각 앱이 독립된 프로세스로 돌기 때문에 하나가 죽어도 나머지는 끄떡없습니다. 심지어 앱마다 자바 버전을 다르게 가져갈 수도 있습니다.
Q. 로그 관리는 어떻게 하나요? catalina.out 파일은 어디 있나요?
A. 임베디드형에는 catalina.out이 없습니다. 대신 애플리케이션 내부 로그 라이브러리(Logback 등)가 지정한 경로에 직접 파일을 남기거나, 위에서 제가 소개한 systemd 서비스 파일 설정을 통해 표준 출력을 지정된 로그 파일로 모니터링할 수 있습니다. journalctl -u myapp 명령어로 리눅스 시스템 로그와 통합 관리하는 것도 가능합니다.
처음에는 “내가 편하게 관리하던 독립형 톰캣이 좋은데…”라며 변화를 거부하고 싶었던 것도 사실입니다. 하지만 직접 RHEL 9.6 환경에 임베디드 방식을 올리고 운영해 보니 생각이 완전히 바뀌었습니다.
기존 독립형 방식은 서버마다 미세하게 설정이 달라져 생기는 ‘기술 부채’가 어마어마했습니다. 배포할 때마다 “개발 장비에선 됐는데 왜 운영 장비에선 안 되지?” 하며 밤을 새우는 원인의 80%는 톰캣 엔진 설정이나 라이브러리 충돌 문제였습니다. 임베디드 톰캣은 개발자가 테스트를 마친 엔진 그대로 서버에서 구동되기 때문에 이런 배포 스트레스가 제로에 가깝습니다. 문제가 생겨도 그냥 이전 버전 JAR 파일로 교체해 버리면 끝나는 즉각적인 롤백이 가능하죠.
혹시 저처럼 인프라나 서버 관리를 담당하시면서 내장형 톰캣 도입을 고민하거나 주저하고 계신 분이 있다면, 과감하게 넘어가 보시길 추천합니다. RHEL 9.6의 견고한 안정성 위에 임베디드 톰캣의 유연함을 더하는 것, 이것이 바로 현재와 미래를 관통하는 가장 트렌디하고 강력한 엔터프라이즈 서버 운영 방식이니까요.
[GitLab CI/CD를 위한 SSH Key 생성 및 배포 환경 구성]
https://docs.spring.io/spring-boot/docs/3.2.5/reference/htmlsingle