톰캣, 스프링, 마이바티스 성능 개선 이야기

궁금하면 500원·2025년 9월 22일
0

미생의 개발 이야기

목록 보기
58/58

대규모 트래픽 앞에서 살아남기

Tomcat, Spring, MyBatis 기반 서비스 성능 개선하기

안녕하세요.
오늘은 제가 실제 운영 환경에서 겪었던 대규모 트래픽 부하 문제 해결 경험에 대해 이야기해 보려고 합니다.
특히, Tomcat, Spring 3.x, MyBatis, Oracle로 구성된 레거시 시스템에서 발생한 'This website is under heavy load (queue full)' 에러를 어떻게 해결했는지 자세히 다뤄보겠습니다.
이 경험은 단순한 이론적 지식을 넘어, 실제 서비스의 안정성을 확보하는 과정이었기에 더욱 의미가 깊었습니다.


🚨 문제 발생: "This website is under heavy load"

서비스 운영 중 특정 시간대에 사용자가 몰리는 현상이 반복적으로 발생했습니다. 이로 인해 "This website is under heavy load (queue full)" 이라는 에러 메시지가 화면에 노출되기 시작했습니다.

  • Tomcat 스레드 풀 포화: 동시 접속자가 급증하자, Tomcat의 스레드 풀이 한계에 도달했습니다. 모든 스레드가 작업을 처리하느라 바빠졌고, 새로운 요청은 대기 큐에 쌓이기 시작했습니다.
  • DB 커넥션 부족: DB 연결 역시 포화 상태였습니다. DBCP(또는 자체 커넥션 풀)의 커넥션이 모두 소진되면서, DB 세션을 기다리는 요청들이 누적되었습니다.
  • 서비스 응답 불가: Tomcat의 스레드와 DB 커넥션이 모두 고갈되자, JSP 페이지를 렌더링할 수 없게 되었고, 결국 서비스는 응답 불능 상태에 빠졌습니다.

🔍 원인 분석: 문제의 근원을 찾아라

장애 상황을 단순히 해결하는 것을 넘어, 근본적인 원인을 파악하기 위해 시스템 전반을 면밀히 분석했습니다.

1. Tomcat 스레드 한계

초기 Tomcat 설정은 기본값에 가까운 **maxThreads(200 미만)**로 운영되고 있었습니다. 이는 예상치 못한 동시 접속 폭증에 대비하기에는 턱없이 부족했습니다. 갑작스러운 트래픽 증가에 대기 큐가 쌓이는 것은 당연한 결과였습니다.

2. DB Connection Pool 부족

MyBatis는 Oracle 데이터베이스와 연결할 때 **커넥션 풀(DBCP)**을 사용합니다. 이 커넥션 풀의 크기가 너무 작게 설정되어 있어, 많은 요청이 동시에 DB에 접근할 때마다 DB 세션 대기가 발생했습니다. 이는 전체적인 응답 지연의 주요 원인이 되었습니다.

3. 비효율적인 쿼리

통계 화면이나 리스트를 보여주는 페이지의 쿼리가 비효율적이었습니다. SELECT * 와 같은 전체 칼럼 조회나, 불필요한 **JOIN**이 과도하게 사용되어 쿼리 실행 시간이 길어졌습니다. 이는 DB 세션 점유 시간을 늘려 다른 요청의 처리를 지연시키는 결과를 낳았습니다.

4. 캐싱 미흡

로그인 후 접속하는 메인 페이지나 공지사항 등 자주 접근하는 데이터도 매번 DB에서 조회하고 있었습니다. 캐싱이 전혀 이루어지지 않아, 동일한 데이터를 반복적으로 조회하는 불필요한 DB 부하가 계속 발생했습니다.


🛠 해결 방법: 시스템을 강하게 만드는 4가지 전략

원인 분석을 통해 얻은 인사이트를 바탕으로, 네 가지 핵심 해결책을 실행에 옮겼습니다.

1. Tomcat 튜닝

서버의 하드웨어 스펙을 고려하여 server.xml의 설정을 조정했습니다.

  • maxThreads: 동시 처리 가능한 스레드 수를 기존 200 미만에서 300으로 상향 조정했습니다.
  • acceptCount: 요청 대기 큐의 크기를 100으로 늘려, 트래픽 폭주 상황에서 즉시 거부되는 요청을 줄였습니다.
  • Keep-Alive 타임아웃 단축: HTTP Keep-Alive 타임아웃을 60초에서 15초로 단축하여, 불필요하게 연결을 유지하는 것을 막고 스레드를 빠르게 반환하도록 했습니다.
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           maxThreads="300"
           acceptCount="100"
           keepAliveTimeout="15000" />

2. DB 커넥션 풀 확장

MyBatis의 DataSource 설정 파일을 수정하여 커넥션 풀의 크기를 확장했습니다.

  • maxActive: 커넥션 풀의 최대 커넥션 수를 20개에서 100개로 늘렸습니다.
  • 세션 모니터링: Oracle의 **Active Session History(ASH)**와 같은 툴을 활용해 실제 피크 타임의 세션 수를 모니터링하여, 적정 커넥션 수를 동적으로 조정했습니다.
  • 커넥션 누수 방지: try-with-resource가 없던 시절이었기에, finally 블록에서 **Connection, Statement, ResultSet**을 확실하게 close()하도록 기존 코드를 전면적으로 보강했습니다.

3. 쿼리 최적화

데이터베이스 관점에서 성능을 개선하기 위해 쿼리 하나하나를 분석했습니다.

  • Oracle 실행 계획 분석: **Explain Plan**을 통해 쿼리 실행 방식을 확인하고, Full Table Scan이 발생하는 지점을 찾아냈습니다.
  • 인덱스 추가: WHERE 절 조건이나 조인(Join) 키에 B-Tree 인덱스를 추가하여 데이터 조회 속도를 획기적으로 개선했습니다.
  • 페이징 처리 개선: 기존의 비효율적인 ROWNUM 방식 대신, SQL 표준인 ROW_NUMBER() OVER() 함수를 사용하여 페이징 쿼리를 재작성했습니다. 이로써 대용량 데이터의 페이징 처리 속도를 향상시킬 수 있었습니다.

4. 캐싱 도입

자주 변경되지 않는 데이터에 캐싱 전략을 도입하여 불필요한 DB 접근을 줄였습니다.

  • Ehcache 적용: 공지사항, 시스템 코드값 등 공통 데이터를 Ehcache를 이용해 메모리에 캐싱했습니다. 이로 인해 DB 부하를 30% 이상 감소시키는 효과를 얻을 수 있었습니다.
  • JVM 메모리 캐싱: Redis와 같은 외부 캐시 솔루션이 없었기 때문에, 세션별 공통 데이터는 ConcurrentHashMap과 같은 JVM 메모리 기반 캐싱을 활용하여 빠른 접근을 가능하게 했습니다.

📈 결과: 안정적인 서비스와 성능 향상

이러한 전방위적인 개선 작업을 통해 서비스는 눈에 띄게 안정화되었습니다.

  • 장애 해소: 피크 타임에도 더 이상 'queue full' 에러가 발생하지 않고, 모든 요청을 안정적으로 처리할 수 있게 되었습니다.
  • 응답 시간 단축: 평균 응답 시간이 40% 단축되어, 사용자 경험이 크게 향상되었습니다.
  • DB 부하 감소: DB 커넥션 대기 현상이 사라졌고, DB 서버의 부하가 30% 감소하여 시스템의 전반적인 건강도가 개선되었습니다.

이번 경험을 통해 단순한 개발 스킬을 넘어, 시스템의 전체적인 흐름을 이해하고 병목 지점을 찾아내 해결하는 문제 해결 능력의 중요성을 다시 한번 깨달았습니다.
대규모 트래픽 앞에서 시스템이 무너지지 않도록 방어하는 것은 개발자에게 매우 중요한 역량이라고 생각합니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글