본문 바로가기

Java/자바 성능 개선

14. 서버는 어떻게 세팅해야 할까?

💡 본 게시글은 이상민 저자의 '자바 성능 튜닝 이야기' 교재를 공부하고, 이에 대해 정리한 내용입니다.

들어가며

 제가 처음으로 개발 프로젝트를 진행하면서 처음에는 코드 작성에만 집중했습니다. 그러나, 실제 서비스를 운영하려면 프로그램이 실행되는 환경, 즉 '서버 설정'이 중요하다는 것을 깨달았습니다.

 

 이는 프로그램의 성능에 결정적인 영향을 미치기 때문입니다. 대개 기본값으로 최대한의 성능을 낼 수 있는 것은 없습니다. 웹 기반의 시스템도 정상적으로 작동하게 하려면 세팅이 중요합니다. 실제로 프로그램에 문제는 없었지만, 서버 설정 값 하나 때문에 성능이 저하되는 경험을 했습니다. 이를 통해, 성능 저하의 원인이 서버 설정에 있다면, 그 문제는 비교적 쉽게 해결할 수 있다는 것을 알게 되었습니다.


1) 서버에서 설정해야 하는 대상

 서버의 세팅은 개발만큼 중요한 작업입니다. 개발된 프로그램이 0.1초 걸린다고 해도, 잘못된 서버 세팅 때문에 실행 시간이 1초나 심지어 10초까지 걸릴 수 있습니다.

 

 이런 문제를 미리 파악하고 대응하기 위한 가장 좋은 방법은 성능 테스트를 통해 병목 지점을 파악하는 것입니다. 애플리케이션 위주로 병목을 찾는 것보다, 문제가 될만한 세팅 값들을 먼저 진단하는 것이 효율적입니다.

 

웹 기반의 시스템에서 성능에 영향을 줄 수 있는 주요 세팅 대상들을 나열해 보면 다음과 같습니다:

  • 웹 서버 세팅
  • WAS 서버 세팅
  • DB 서버 세팅
  • 장비 세팅

 

이제 각 서버별로 어떤 세팅 값들에 주의를 기울여야 하는지 자세히 살펴보겠습니다.


2) 아파치 웹 서버의 설정 

 웹 서버의 세팅은 매우 중요합니다. Web Application Server (WAS)는 웹 서버가 아니므로 웹 서버의 역할을 수행하면서는 안 됩니다. 웹 서버는 WAS 앞에 배치해야 합니다. 그렇지 않으면, 이미지, CSS, 자바 스크립트, HTML 등을 처리하는데 WAS 서버의 스레드를 낭비하게 됩니다. 따라서 상용 웹 서버나 아파치 웹 서버를 WAS 앞에 두고 운영해야 합니다.

 

 아파치 웹 서버는 Multi-Processing Module (MPM)을 사용합니다. 이는 여러 개의 프로세싱 모듈 기반의 서비스를 제공한다는 의미입니다. 아파치 웹 서버의 설정을 변경하는 가장 간편한 방법은 설치 폴더 하단의 conf 디렉터리에 있는 httpd.conf 파일을 수정하는 것입니다.

 

아파치 웹 서버에서 성능에 영향을 줄 수 있는 설정들은 다음과 같습니다:

  • ThreadsPerChild : 웹 서버가 사용하는 스레드의 개수를 지정합니다. 아파치 프로세스 하나당 250개의 스레드가 생성되며, 이 수치가 적으면 늘려서 서버가 더 많은 사용자 요청을 처리할 수 있게 해야 합니다.
  • MaxRequestsPerChild : 최대 요청 개수를 지정합니다. 0은 제한 없음을 의미하며, 이 값이 10이면 그 이상의 처리는 하지 않게 됩니다. 일반적으론 기본값인 0을 사용하는 것이 좋습니다.

 

 더 세부적인 스레드 설정을 하려면 httpd.conf 파일에서 주석 처리된 "include conf/extra/httpd-mpm.conf"를 주석 해제합니다. 이후 httpd-mpm.conf 파일을 통해 세부 스레드 설정 정보를 지정할 수 있습니다.

 

아래는 httpd-mpm.conf 파일의 예시입니다:

<IfModule mpm_worker_module>
StartServers 2
MaxClients 150
MinSpareThreads 25
MaxSpareThreads 75
ThreadsPerChild 25
MaxRequestsPerChild 0
</IfModule>

설정 값들의 의미는 다음과 같습니다:

  • StartServers : 서버를 시작할 때 프로세스의 개수를 지정합니다.
  • MaxClients : 최대 처리 가능한 클라이언트의 수를 지정합니다.
  • MinSpareThreads : 최소 여유 스레드 수를 지정합니다.
  • MaxSpareThreads : 최대 여유 스레드 수를 지정합니다.
  • ThreadsPerChild : 프로세스 당 스레드 수를 지정합니다.
  • MaxRequestsPerChild : 최대 요청 개수를 지정합니다.

이 설정에 따르면, 프로세스 수(StartServers)가 2개이고, 프로세스 당 스레드 수(ThreadsPerChild)가 25이므로 기본적으로 50개의 요청을 처리할 수 있습니다. 또한, 최대 여유 스레드(MaxSpareThreads)가 75개이므로, 최대 사용 가능한 클라이언트 수(MaxClients)는 150입니다. 이는 150명이 최대 요청 수라는 것을 의미합니다. 150명 이상의 요청은 서버 리소스에 여유가 있어도 처리하지 않습니다.

 

 이러한 설정이 있음에도 불구하고 사용자가 늘어나거나 WAS가 멈추면 문제가 발생할 수 있습니다.

이럴 때는 아래와 같은 조치를 취해야 합니다:

  1. 서버를 늘립니다.
  2. 서비스를 튜닝합니다.
  3. GC 튜닝을 합니다.
  4. 각종 옵션 값을 튜닝합니다.

이 모든 것을 고려하면, 서버가 더 많은 요청을 처리해야 할 경우 httpd.conf 파일이나 httpd-mpm.conf 파일을 수정하여 서버가 최대한의 자원을 사용하도록 변경해야 합니다.


3) 아파치 웹 서버의 설정 

 웹 서버를 설정할 때 중요한 설정 값 중 하나는 KeepAlive입니다. 아파치 웹 서버의 경우, httpd.conf 파일에 아래와 같이 추가하면 됩니다.

 
KeepAlive on

 

 웹 서버와 웹 브라우저가 연결될 때 KeepAlive 기능이 활성화되지 않으면, 매번 HTTP 연결을 맺었다 끊는 작업을 반복합니다. 간단한 초기 화면을 가진 구글 같은 사이트는 KeepAlive 설정 없이도 빠르게 로딩될 수 있습니다. 그러나 네이버나 다음과 같이 초기 화면에서 많은 이미지와 CSS, 자바 스크립트 등의 파일을 로딩하는 사이트에서는 KeepAlive 옵션 없이는 초기 화면 로딩에 몇 분이 소요될 수 있습니다. 즉, 모든 요소에 대해 매번 서버에 접속해야 하는 상황이 발생합니다.

 

하지만 KeepAlive 기능이 활성화되면 몇 개의 연결을 유지하고 재사용하므로, 연결을 위한 대기 시간이 짧아지고 사용자가 느끼는 응답 속도도 크게 향상됩니다.

 

많은 사용자가 접근하는 사이트에서는 이미지와 CSS 같은 정적 파일들을 일반적인 웹 사이트에서 처리하는 대신, CDN(Content Delivery Network) 서비스를 사용합니다. 이는 별도의 URL에서 해당 컨텐츠를 다운로드하도록 설정하고, 동적 컨텐츠는 WAS에서 처리하도록 설정함으로써 웹-WAS 서버의 부담을 줄일 수 있습니다. (단, 비용이 상대적으로 높다는 단점이 있습니다.)

 

KeepAlive 설정과 함께 해야 하는 또 다른 설정은 KeepAliveTimeout입니다. 이 설정은 KeepAlive가 끊기는 시간을 초 단위로 설정하는 것입니다.

KeepAliveTimeout 15

 

 이러한 설정은 마지막 연결이 끝난 후 다음 연결이 이루어질 때까지 기다리는 시간을 지정합니다. 만약 사용자가 너무 많아 접속이 잘 안 될 경우, 이 설정을 5초로 줄이는 것이 서버 리소스를 효율적으로 사용하는 방법이 될 수 있습니다.

 

 그러나 KeepAlive 옵션을 무조건 켜야 하는 것은 아닙니다. 서비스의 상황에 따라 KeepAlive 옵션을 끄는 것이 더 좋은 성능을 제공하는 경우도 있습니다. 따라서, 상황에 맞게 KeepAlive 설정을 사용해야 합니다.


4) DB Connection Pool 및 스레드 개수 설정

  웹 애플리케이션 서버(WAS)에서 설정해야 할 값들은 많으며, 그 중에서도 가장 성능에 큰 영향을 미치는 것이 바로 DB 연결 풀과 스레드의 개수입니다. 이 두 가지 요소는 서버의 메모리와 직접적으로 연관이 있습니다.

 

 더 많은 스레드와 DB 연결 풀을 사용하면 더 많은 메모리를 점유하게 됩니다. 그러나 메모리 사용량을 줄이기 위해 이들을 적게 지정하면, 서버는 많은 요청을 처리하는 데 어려움을 겪게 될 수 있습니다.

 

 대부분의 WAS에서는 DB 연결 풀의 개수를 최소치, 증가치, 최대치 등으로 세부적으로 지정할 수 있습니다. 최소치는 서버가 시작될 때 생성하는 연결의 개수를 의미합니다. 개발자용 PC에서는 이 값이 크게 필요하지 않으므로, 이 값을 최소한으로 설정하는 것이 좋습니다. 만약 최소치가 너무 크다면, 서버의 시작 시간이 길어지게 되므로, 개발자가 디버깅을 위해 서버를 여러 번 재시작하는 경우에는 부담이 될 수 있습니다.

 

 반면, 서버가 운영 중일 때는 최소치와 최대치를 동일하게 설정하는 것이 좋습니다. 사용자의 수가 갑자기 증가하면 DB 연결 풀의 개수도 증가해야 하고, 이 때 발생하는 대기 시간을 줄일 수 있기 때문입니다. 만약 DB 서버의 리소스가 부족하다면, 최소치를 낮추는 것도 한 방법이 될 수 있습니다. 대부분의 WAS에서 이 두 가지 설정의 기본값은 약 10~20개입니다.

 

 그러므로 이 기본값을 그대로 사용하면, 서버는 요청 처리에 어려움을 겪을 수 있습니다. 일반적으로 DB 연결 풀은 40~50개, 스레드 개수는 이보다 10개 정도 더 설정하는 것이 좋습니다. 이렇게 설정하는 이유는, 스레드 개수가 DB 연결 풀의 개수보다 적으면, 필요한 연결 개수보다 적은 연결만 사용하게 되기 때문입니다.

 

 즉, 스레드는 입구, DB 연결 풀은 출구라고 생각하면 됩니다. 그렇다면 왜 스레드의 개수가 DB 연결 개수보다 많아야 할까요?  그 이유는 모든 애플리케이션 또는 화면이 DB에 연결하는 것이 아니라는 점, 그리고 관리자 콘솔을 통해 서버에 접속할 수 있기 때문입니다. 이런 이유로 일반적으로 스레드 개수는 여유분을 갖도록 설정합니다.

 

 그럼 가장 적합한 DB 연결 풀과 스레드 개수는 몇 개일까요? 웹 서버의 설정 값과 마찬가지로, 이 값은 서버와 애플리케이션의 상황에 따라 달라지며, 완벽한 값은 없습니다. 서비스와 서버의 상황에 따라 적절한 값을 설정해야 하며, 가장 좋은 방법은 성능 테스트를 통해 가장 적절한 값을 찾는 것입니다.

 

 DB 연결 풀의 개수를 기준으로 적절한 값을 찾는 방법에 대해 알아보겠습니다. DB 연결 풀의 개수를 40개로 설정했다고 가정해봅시다. 이 40개를 모두 사용하면서 DB의 CPU 사용량이 100%에 도달했다면, DB의 CPU를 과도하게 사용하는 쿼리를 찾아 튜닝해야 합니다. 즉, 인덱스가 없거나 테이블 전체를 스캔하는 쿼리가 있는지 확인해봐야 합니다.

 

 만약 이렇게 해도 문제가 해결되지 않는다면, DB 연결 풀의 개수를 늘려도 소용이 없습니다. 이 경우, DB와의 모든 연결을 사용하게 되고, 응답 시간은 매우 느려질 뿐입니다. 이번에는 DB의 CPU 사용량이 50%도 되지 않는 상황에서 WAS의 CPU 사용량이 100%에 도달하고 있는 상황을 가정해봅시다. 이 때 사용하는 DB 연결 풀의 개수는 20개 정도밖에 되지 않습니다. 이런 경우에는 WAS의 애플리케이션을 튜닝해야 합니다. 하지만 이미 튜닝이 완료된 상태라면, 이 서버의 DB 연결 풀의 개수는 약간의 여유를 두고 25~30개 정도로 설정하는 것이 좋습니다.

 

 DB 연결 풀의 개수만큼 중요한 값이 있습니다. 그것은 바로 대기 시간(wait time)과 관련된 값입니다. MyBatis와 같은 DB와 자바 프로그램을 매핑(mapping)해 주는 프레임워크에는 이와 관련된 설정 값들이 있습니다.

 

 대기 시간이 중요한 이유는 DB 연결 풀의 개수를 초과했을 때 애플리케이션이 다른 연결을 기다리는 시간이 바로 대기 시간이기 때문입니다. MyBatis에서는 poolTimeToWait라는 값으로 이 대기 시간을 설정하며, 기본 값은 20초입니다. 즉, 이 값을 변경하지 않으면 DB 연결을 기다리는 사용자들은 최소 20초 동안 대기해야 합니다.

 

 대기 시간을 너무 짧게 설정하면 어떻게 될까요? 예를 들어 100ms 정도로 줄여보겠습니다. 1GB의 메모리를 할당한 WAS에서는 Full GC 시간을 300ms 이하로 줄이는 것이 매우 어렵습니다. 만약 DB에 연결하려고 대기하는 순간에 Full GC가 발생하면, 그 순간에 대기하고 있는 모든 스레드가 DB와의 연결에 실패하고 Timeout을 반환할 수 있습니다. 따라서 DB와 연동하는 프레임워크를 설정할 때는 연결 풀의 개수, 대기 시간과 관련된 값, 그리고 Timeout과 관련된 설정들을 시스템의 상황에 맞게 조정해야 합니다. 그렇게 하면 사용자들이 예기치 않은 오류 페이지를 만나는 것을 방지할 수 있습니다.


5) DB Connection Pool 및 스레드 개수 설정

  WAS 인스턴스의 최적 개수를 결정하는 절대적인 기준은 없습니다. 그러나 이는 인스턴스를 무제한으로 늘릴 수 있다는 의미가 아닙니다.

 

 서버의 WAS 인스턴스 개수가 증가할수록 CPU가 처리해야 하는 작업량도 증가합니다. 예를 들어, 4~8개의 CPU 코어를 갖고 있는 장비에서 20개 이상의 인스턴스를 운영한다면, 성능 저하가 발생할 확률이 큽니다. 이는 여러 인스턴스가 CPU를 공유하면서 경합을 일으키기 때문입니다. 대량의 메모리를 장착하더라도, 서버의 처리량은 증가하지 않습니다. 일반적으로 1~2개의 CPU에 하나의 인스턴스를 할당하는 것이 좋다고 알려져 있지만, 최적의 인스턴스 개수는 성능 테스트를 통해 결정하는 것이 가장 바람직합니다.

 

※ 예시

 CPU 코어가 36개인 장비가 있다고 가정해봅시다. 일반적인 권장사항에 따르면 이 장비는 18~26개의 인스턴스를 운영해야 할까요? 인스턴스 1개일 때 500 TPS(초당 트랜잭션 수), 인스턴스 2개일 때 700 TPS, 인스턴스 3개일 때 720TPS, 인스턴스 4개일 때 730 TPS가 나온다고 가정하면, 인스턴스를 2~3개 운영하는 것이 적절할 수 있습니다. 인스턴스를 더 증가시킨다 해도 TPS는 크게 증가하지 않고, 유지보수성은 떨어지므로 합리적이지 않습니다. 4GB의 여유 메모리가 있는 WAS 장비에서 하나의 인스턴스에 4GB의 메모리를 할당하는 것은 권장되지 않습니다. 이는 Full GC가 발생했을 때 많은 시간이 소요될 수 있기 때문입니다. 대략 512MB ~ 2GB 사이에서 메모리를 할당하는 것이 적절합니다. 위 예시 상황에서는, 1GB 메모리를 할당하여 2개의 인스턴스를 운영하는 것이 바람직할 것입니다.

 

  주의사항

 단독 인스턴스 구성은 예기치 못한 상황 발생 시 서비스가 중단될 수 있으므로 피해야 합니다. 장비가 한 대라도, 두 대 이상의 인스턴스가 클러스터링을 통해 사용자 세션 정보(일반적으로 로그인 정보 포함)를 공유하도록 하는 것이 좋습니다. 또한, 스레드나 DB Connection Pool의 개수를 100개 이상 설정하려는 경우에는, 인스턴스를 두 개로 분리하는 것이 일반적입니다.


6) 세션 타임아웃 시간 설정 

  세션의 타임아웃 설정은 많은 사람들이 쉽게 간과하는 부분입니다. 이 설정은 WAS에 종속되지 않으며, WEB-INF 폴더 아래의 web.xml 파일에서 설정할 수 있습니다. 이는 서블릿 스펙에 따라 정의된 표준 설정 값입니다.

<session-timeout>30</session-timeout>

 

 위의 설정 값은 세션의 타임아웃 시간을 분 단위로 설정합니다. 설정된 분만큼 요청이 없으면, 시스템은 세션을 메모리에서 제거합니다. 이 설정을 하지 않았거나, WAS에서 별도로 설정한 값이 없고, 세션 객체의 invalidate() 메서드가 수행되지 않았다면, 세션은 메모리에서 제거되지 않습니다. 따라서 이 점을 유의해야 합니다.