Spring

Spring Boot 성능을 극대화하는 10가지 방법

Spring Boot Tuning 10 ways

안녕하세요, 개발자 여러분! Devloo 입니다 🙂 여러분도 한 번쯤 “내 애플리케이션이 더 빠르고 효율적일 수 없을까?”라는 고민을 해보신 적이 있으신가요? 저는 종종 하는 것 같습니다. 많은 프로젝트를 진행하면서 성능 최적화가 얼마나 중요한지, 그리고 그 과정에서 어떤 어려움이 있는지를 절실히 느끼고 있는 것 같습니다.

스프링 부트 성능을 극대화하는 10가지 방법
스프링 부트 성능을 극대화하는 10가지 방법

스프링 부트를 사용하다 보면, 성능 문제에 부딪혀 답답함을 느끼는 순간들이 종종 있죠. 우리는 애플리케이션이 빠르고 반응성이 뛰어나기를 원하지만, 현실은 그렇지 못한 경우가 많습니다. 그래서 이러한 문제를 해결하기 위해 이 글을 준비했습니다.

스프링 프레임워크는 자바 생태계에서 가장 인기 있고 성숙한 애플리케이션 개발 프레임워크 중 하나입니다. 스프링 부트는 사전 구성된 모듈, 자동 구성, 스타터 종속성 등을 제공하여 스프링 기반 애플리케이션을 쉽게 생성할 수 있도록 합니다. 이러한 단순함, 인기도, 성숙도 덕분에 많은 시스템이 스프링 부트를 사용하여 구현하고 있지만, 그 중 일부는 최적화되지 않았거나 성능이 떨어질 가능성이 있습니다.

이 글에서는 먼저 성능이란 무엇인지 전반적으로 논의한 후, 스프링 부트의 성능을 극대화하는 10가지 방법에 대해 알아보도록 하겠습니다.

성능이란 무엇인가?

전통적으로 성능은 애플리케이션이 작업을 얼마나 효과적으로 수행하는지(런타임 효율성)와 시스템 자원을 얼마나 효율적으로 사용하는지(자원 효율성)를 의미합니다.

현대 소프트웨어 개발에서는 성능이 여러 측면을 포함합니다:

  • 확장성 (Scalability): 더 높은 부하, 사용자 수 또는 데이터 양에서 성능과 기능을 유지하는 능력.
  • 신뢰성 (Reliability): 중단이나 오류 없이 일관되게 기능을 실행하는 능력.
  • 처리량 (Throughput): 특정 기간 동안 데이터를 원활하게 처리하는 능력.

보통 소프트웨어 애플리케이션의 성능은 원활하고 빠르며 효과적으로 실행되는지에 달려 있습니다. 이는 자원을 효율적으로 관리하고 사용자들의 기대를 충족시키는 것을 포함합니다.

스프링 부트 성능을 향상시키는 방법

이번 섹션에서는 스프링 부트 애플리케이션의 성능을 높이기 위한 10가지 최적화 방법을 설명합니다. 순서는 중요하지 않으며, 일부 방법은 최신 스프링 부트 버전에만 적용될 수 있습니다.

1. 가능한 한 최신 버전의 스프링 부트 사용

Spring Framework와 Spring Boot의 모든 버전에서는 새로운 기능과 기능을 도입하는 것 외에도 Spring 핵심 컨테이너와 모듈을 최적화하고 버그를 수정하는 등의 성능 개선이 있습니다. 따라서 가능한 한 최신 버전의 Spring Boot를 사용하는 것이 좋습니다.

2. JVM 버전 및 튜닝

Spring Boot와 마찬가지로, 모든 Java LTS 릴리스에는 많은 성능 개선이 포함되어 있습니다. 최신 버전의 Spring Boot에서 성능 개선을 활용하려면 최신 버전의 Java를 사용해야 할 때도 있습니다.

최신 버전의 JVM은 Spring Boot 애플리케이션의 성능을 향상시킬 수 있지만, JVM 튜닝을 통해 Spring Boot 애플리케이션의 성능을 더욱 향상시킬 수 있습니다. JVM 튜닝은 이 글의 범위를 벗어나지만, 주목해야 할 몇 가지 영역을 언급하곘습니다:

  • -Xms와 -Xmx를 사용한 초기 및 최대 힙 크기 설정
  • 기타 JVM 메모리 설정
  • 적절한 GC 구현 선택

최적의 JVM 및 GC 설정은 애플리케이션과 환경에 따라 다릅니다. 다양한 설정을 테스트하고 성능을 모니터링하는 것이 최적의 설정을 찾는데 필수적입니다.

3. JDK 21에서 Web MVC 스택에 가상 스레드 사용

버전 3.2부터 Spring Boot는 다양한 방식으로 가상 스레드(Project Loom)를 지원하기 시작했습니다. 이 기능에 대한 더 자세한 정보는 해당 링크에서 확인하실 수 있습니다.

이 기능을 사용하려면 JDK 21 이상이 필요합니다.

가상 스레드의 가장 큰 장점은 경량 스레딩 모델을 도입하여 Spring Boot 애플리케이션의 확장성과 성능을 높일 수 있다는 점입니다. 이와 더불어, 성능을 희생하지 않으면서도 비동기 프로그래밍의 복잡성을 줄이고 기존 버전과의 호환성을 유지할 수 있습니다.

Spring Boot에서 다음과 같은 설정으로 가상 스레드를 쉽게 활성화할 수 있습니다:

spring.threads.virtual.enabled=true

이 설정이 적용되면 Spring Boot Web MVC는 컨트롤러의 메서드와 같은 웹 요청을 가상 스레드에서 처리하게 됩니다.

4. Spring AOT 및 GraalVM 네이티브 이미지

GraalVM 네이티브 이미지는 메모리 사용량을 줄이고 JVM보다 훨씬 빠르게 시작할 수 있는 새로운 Java 애플리케이션 실행 방식을 제공합니다. (Spring AOT와 GraalVM 설명글)

이 기능은 컨테이너 기반 배포와 Function-as-a-Service 플랫폼에 최적화되어 있습니다. GraalVM 네이티브 이미지는 정적 코드 분석을 통해 실행 파일을 생성하기 위해 사전 처리가 필요합니다. 그 결과, JVM 없이도 실행 가능한 플랫폼 전용 독립 실행 파일이 만들어집니다.

그러나 GraalVM은 리플렉션, 리소스, 직렬화, 동적 프록시와 같은 Java 코드의 동적 요소를 직접적으로 인식하지 못합니다. 따라서 이러한 요소들에 대한 정보를 GraalVM에 제공해야 합니다. 반면, Spring Boot 애플리케이션은 런타임에 구성되는 동적 특성을 가지고 있습니다.

Spring AOT(Ahead-of-Time) 처리는 Spring 프레임워크에서 제공하는 기능으로, Spring 애플리케이션을 GraalVM과 더 호환되도록 변환해줍니다.

Spring AOT는 빌드 타임 동안 사전 처리를 수행하며, GraalVM의 사전 컴파일을 돕는 Java 코드, 바이트코드, GraalVM JSON 힌트 파일과 같은 추가 자산을 생성합니다.

Spring 프레임워크의 사전 최적화
Spring 프레임워크의 사전 최적화

이를 위해 Spring Boot 프로젝트에 GraalVM 빌드 도구 플러그인을 추가해야 합니다:

<plugin>
  <groupId>org.graalvm.buildtools</groupId>
  <artifactId>native-maven-plugin</artifactId>
</plugin>

그런 다음, 네이티브 프로필을 활성화한 상태로 spring-boot:build-image 목표를 실행하여 GraalVM 네이티브 이미지를 빌드할 수 있습니다:

mvn -Pnative spring-boot:build-image

이 명령은 Buildpacks를 사용하여 Docker 이미지를 생성합니다.

네이티브 실행 파일을 생성하려면 Docker 이미지 대신 다음 명령을 실행할 수 있습니다. (이전에 GraalVM 배포판이 설치되어 있어야 합니다):

mvn -Pnative native:compile
5. JVM 체크포인트 복원 기능(Project CRaC)

이 기능은 Spring Boot 3.2 버전부터 추가되었으며, JVM에서 실행 중인 Java 애플리케이션의 상태를 “체크포인트”로 저장하고 나중에 해당 상태를 복원할 수 있도록 지원합니다. Spring Boot는 이 기능을 ApplicationContext 생명주기와 통합하여 자동 체크포인트를 지원하며, -Dspring.context.checkpoint=onRefresh 매개변수를 추가하는 것만으로 간단히 사용할 수 있습니다.

이 기능은 다음과 같은 방식으로 Spring Boot 애플리케이션의 성능을 향상시킬 수 있습니다:

  1. 빠른 시작 시간: 초기화 과정에서 발생하는 시간을 절약하기 위해, 예열된 JVM 상태를 저장하고 재시작 시 시간 소모적인 초기화를 건너뛰어 Spring Boot 애플리케이션의 성능을 최적화할 수 있습니다.
  2. 안정성 향상: 신뢰할 수 있는 체크포인트에서 JVM 상태를 복원하여 초기화 관련 문제를 회피함으로써 Spring Boot 애플리케이션의 안정성을 높일 수 있습니다.
  3. 리소스 사용량 감소: 체크포인트에서 기존 JVM 리소스를 재사용함으로써 Spring Boot 애플리케이션의 전체 리소스 소비를 줄여 리소스 사용을 최적화할 수 있습니다.

이 기능을 사용하려면, CRaC 기능이 활성화된 JDK 버전을 설치하고 Spring Boot 3.2 이상을 사용해야 합니다.

6. 클래스 데이터 공유(CDS)

Class Data Sharing (CDS)는 Java Virtual Machine (JVM)의 기능으로, Java 애플리케이션의 시작 시간을 줄이고 메모리 사용량을 감소시킬 수 있습니다. Spring Boot 3.3 버전부터 이 기능이 통합되었습니다.

CDS 기능은 크게 두 가지 주요 단계로 구성됩니다:

  1. CDS 아카이브 생성: 애플리케이션이 종료될 때 애플리케이션 클래스가 포함된 .jsa 형식의 아카이브 파일을 생성합니다.
  2. CDS 아카이브 사용: .jsa 파일을 메모리에 로드합니다.

CDS는 공유 아카이브에 접근하는 것이 JVM 시작 시 클래스를 로드하는 것보다 빠르기 때문에 Spring Boot 애플리케이션의 시작 시간을 단축시킵니다. 또한, CDS는 메모리 효율적인 솔루션입니다. 그 이유는 아래와 같습니다:

  • 동일한 호스트에서 여러 JVM 프로세스가 읽기 전용으로 공유 아카이브의 일부를 매핑하여 사용할 수 있습니다.
  • 공유 아카이브에는 Java Hotspot VM이 사용하는 형태의 클래스 데이터가 포함되어 있습니다.
7. 스프링 MVC 및 데이터베이스 중심 애플리케이션의 스레드 구성

Spring Boot 애플리케이션은 다수의 요청을 병렬로 처리할 수 있으며, 높은 처리량을 달성하기 위해서는 충분한 스레드가 필요합니다. 병목 현상을 일으킬 수 있는 중요한 두 가지 계층은 컨트롤러 계층과 데이터베이스 접근 계층입니다.

🌐 컨트롤러 계층 스레드 구성

가상 스레드 기능을 Spring MVC 애플리케이션에서 사용할 수 없는 경우, 컨트롤러 계층의 스레드 풀을 적절히 구성하는 것이 매우 중요합니다.

Spring MVC 애플리케이션은 Tomcat, Jetty, Undertow와 같은 서블릿 컨테이너에서 실행되기 때문에, 서블릿 컨테이너에 맞는 특정 구성 키를 알아야 스레드 풀을 구성할 수 있습니다. 예를 들어, Tomcat의 경우 스레드 구성에 중요한 두 가지 키가 있습니다:

  • server.tomcat.threads.max: 요청 처리를 위한 최대 워커 스레드 수
  • server.tomcat.threads.min-spare: 살아있는 최소 워커 스레드 수로, 시작 시 생성되는 스레드 수와 동일합니다.
server:
  tomcat:
    connection-timeout: 2s
    keep-alive-timeout: 10s
    threads:
      max: 500
      min-spare: 100

사용 중인 서블릿 컨테이너에 따라 성능을 향상시키기 위해 조정할 수 있는 추가 설정이 있을 수 있습니다. 예를 들어, Tomcat의 경우, 성능 향상을 위해 조정할 수 있는 두 가지 타임아웃 관련 설정(server.tomcat.connection-timeoutserver.tomcat.keep-alive-timeout)이 있습니다.

🗄️ 데이터베이스 접근 계층 스레드 구성

JDBC는 데이터베이스와의 통신을 위한 연결 지향 표준이므로, 연결 풀을 사용하는 것이 매우 중요합니다. 기본적으로 Spring Boot는 HikariCP를 연결 풀로 사용합니다. 서블릿 컨테이너와 마찬가지로 각 연결 풀 라이브러리는 자체 구성 키를 가지고 있으며, HikariCP의 경우 가장 중요한 구성 요소는 다음과 같습니다:

  • spring.datasource.hikari.maximum-pool-size: 풀에서 사용할 수 있는 최대 데이터베이스 연결 수
  • spring.datasource.hikari.minimum-idle: 풀에서 유지할 최소 유휴 데이터베이스 연결 수

예를 들어, 다음과 같이 설정할 수 있습니다:

spring:
  datasource:
    username: deli
    password: p@ssword
    url: jdbc:postgresql://localhost:5432/sample_db
    hikari:
      maximum-pool-size: 400
      minimum-idle: 100
      connection-timeout: 2000 

이 설정은 데이터베이스 연결 풀의 성능을 최적화하여, 데이터베이스 접근 계층에서 병목 현상을 줄이고 높은 처리량을 유지하는 데 도움이 됩니다.

8. 캐싱 전략 사용

우리 Spring Boot 애플리케이션에 적절한 캐싱 전략을 선택하면 성능을 크게 향상시킬 수 있습니다. 요즘에는 많은 마이크로서비스들이 데이터를 서로 분산해서 사용합니다. 적절한 캐싱 전략을 활용하면 성능을 개선하고 여러 번의 왕복을 줄일 수 있습니다.

애플리케이션의 규모, 데이터 접근 패턴, 성능 요구 사항에 따라 캐싱에 대한 두 가지 중요한 측면을 결정해야 합니다:

  1. 무엇을 캐시할 것인가? 애플리케이션에서 어떤 정보를 캐시에 보관할 것인지 결정합니다.
  2. 어떻게 캐시할 것인가? 어떤 메커니즘이나 접근 방식을 사용할 것인지 결정합니다. 로컬 캐시를 사용할지, 분산 캐시를 사용할지, 아니면 둘 다 사용할지? 데이터를 캐시에 얼마나 오래 보관할지?

다행히도 Spring Boot는 캐싱을 도와주는 유용한 라이브러리 세트를 제공합니다. Spring Boot의 캐싱 전략에 대해서는 해당 링크를 참조하세요 🙂

9. 복원력 패턴 (Resiliency Patterns) 및 모범 사례 채택

저는 마이크로서비스 아키텍처를 기반으로 한 애플리케이션의 증가로 인해 이 주제가 특히 요즘 매우 중요하다고 생각합니다.

Circuit Breaker, Timeouts, Fallback, Retries와 같은 복원력 패턴을 따르면 Spring Boot 애플리케이션이 장애를 더 잘 처리하고, 자원을 효율적으로 할당하며, 일관된 성능을 제공하여 궁극적으로 전체 애플리케이션 성능을 개선할 수 있습니다.

Spring Boot는 Resilience4j와 같은 인기 있는 라이브러리와의 통합을 통해 여러 내장 기능으로 복원력 패턴을 지원합니다. Spring Boot는 필요한 빈을 자동으로 구성하고 속성이나 어노테이션을 통해 복원력 패턴을 쉽게 설정할 수 있는 방법을 제공하여 Resilience4j의 통합을 간소화합니다.

Spring Cloud Circuit Breaker는 서킷 브레이커에 대한 추상화 계층을 제공하며, Resilience4j는 이를 배후에서 사용할 수 있도록 설정할 수 있습니다.

@Service
public static class DemoControllerService {
    private RestTemplate rest;
    private CircuitBreakerFactory cbFactory;

    public DemoControllerService(RestTemplate rest, CircuitBreakerFactory cbFactory) {
        this.rest = rest;
        this.cbFactory = cbFactory;
    }

    public String slow() {
        return cbFactory.create("slow").run(
            () -> rest.getForObject("/slow", String.class), 
            throwable -> "fallback"
        );
    }
}

위의 코드 예제는 Resilience4j와 Spring Cloud Circuit Breaker를 사용하여 DemoControllerService 클래스에서 복원력 패턴을 구현하는 방법을 보여줍니다. 이 클래스는 RestTemplate을 사용하여 /slow 엔드포인트에 요청을 보내고, 요청이 실패하면 “fallback” 문자열을 반환합니다.

10. 모니터링 및 프로파일링

모니터링과 프로파일링을 결합하면 Spring Boot 애플리케이션의 성능을 깊이 이해하고, 데이터를 기반으로 성능을 향상시키기 위한 결정을 내릴 수 있습니다.

Spring Boot는 기본적으로 이 목적을 위한 많은 라이브러리와 도구를 제공합니다. Spring Boot Actuator는 애플리케이션의 상태를 모니터링하고, 메트릭을 수집하며, 성능 병목 현상을 식별할 수 있습니다. 한편, Spring Boot 3.x부터는 Spring 자동 설정을 사용하여 Micrometer와의 통합이 매우 잘 되어 있어, 더 나은 메트릭과 분산 추적을 제공하여 궁극적으로 애플리케이션 모니터링을 향상시킵니다.

Digma가 Spring Boot 애플리케이션 성능 향상에 도움이 되는 방법

흥미롭게도 Digma는 애플리케이션을 배포할 필요 없이 Spring Boot 애플리케이션을 프로파일링하고 모니터링할 수 있는 도구입니다. Digma는 개발 중 IDE 내에서 병목 현상, 느린 쿼리, 캐시 미스 등을 찾아주는 데 도움을 줄 수 있습니다. 이러한 기능 덕분에 Digma는 Spring Boot 애플리케이션 성능을 개선하기 위한 숨겨진 무기가 될 수 있습니다.

결론

이 글에서는 Spring Boot 애플리케이션의 성능을 향상시키는 10가지 방법을 살펴보았습니다. 일부는 런타임 성능을 개선하고, 일부는 애플리케이션의 자원 소비를 최적화합니다. 다만, 몇몇 접근법은 특정 Java 및 Spring Boot 버전을 사용해야 합니다. 물론, 코딩 단계에서 적용할 수 있는 모범 사례를 포함하여 Spring 프로그램을 개선할 수 있는 다른 방법들도 있습니다. 다른 사례가 있다면 댓글로 공유해 주시면 감사하겠습니다.

이번 시간에는 Spring Boot 성능을 극대화하는 10가지 방법에 대해 알아보았습니다.
혹시 궁금하신 점이 있으시면 댓글 달아주세요 !! 감사합니다 :))

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *