Development/Code

Spring Boot의 성능을 향상시키는 10가지 방법

Danny Seo 2024. 6. 28. 00:07

Spring Framework는 자바 생태계에서 가장 인기 있고 성숙한 애플리케이션 개발 프레임워크 중 하나입니다. Spring Boot는 사전 구성된 모듈, 자동 설정, 스타터 종속성을 제공하여 Spring 기반 애플리케이션 개발을 간소화합니다. 이러한 간편함과 인기, 성숙도로 인해 많은 시스템이 Spring Boot로 구현되었으며, 이 중 일부는 최적화되지 않았을 가능성이 큽니다.

 

이 글에서는 먼저 성능이란 무엇인지 논의한 후, Spring Boot를 빠르고 자원 효율적으로 만드는 10가지 성능 최적화 방법을 다루겠습니다.

Spring Boot의 성능을 향상시키는 10가지 방법

아래는 목차입니다.

  • 성능이란 무엇을 의미하나요?
  • Spring Boot 성능을 향상시키는 방법?
    1. 최신 버전의 Spring Boot 사용
    2. JVM 버전 및 튜닝
    3. JDK 21에서 웹 MVC 스택에 가상 스레드 사용
    4. Spring AOT 및 Spring GraalVM 네이티브 이미지
    5. JVM 체크포인트 복원 기능 (Project CRaC)
    6. 클래스 데이터 공유 (CDS)
    7. Spring MVC 및 데이터베이스 중심 앱의 스레드 구성
      • 🌐 컨트롤러 계층 스레드 구성:
      • 🗄️ 데이터베이스 접근 계층 스레드 구성:
    8. 캐싱 전략 사용
    9. 복원력 패턴 및 모범 사례 채택
    10. 모니터링 및 프로파일링
    • Digma가 Spring Boot 앱 성능을 향상시키는 방법
  • 마무리 생각

성능이란 무엇을 의미하나요?

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

 

현대 소프트웨어 개발에서 성능은 다음과 같은 다양한 측면을 포함합니다:

  • 확장성: 더 높은 작업 부하, 사용자 수 또는 데이터 양에서도 성능과 기능을 유지할 수 있는 능력.
  • 신뢰성: 중단이나 오류 없이 일관되게 의도된 기능을 수행할 수 있는 능력.
  • 처리량: 특정 기간 동안 데이터를 부드럽게 처리할 수 있는 능력.

일반적으로 소프트웨어 애플리케이션의 성능은 원활하고, 빠르고, 효율적으로 실행되는지 여부를 말합니다. 또한 자원을 효율적으로 관리하고 사용자의 기대를 충족시키는 것도 포함됩니다.


Spring Boot 성능을 향상시키는 방법?

다음 섹션에서는 Spring Boot 애플리케이션의 성능을 향상시키기 위한 10가지 방법을 다루겠습니다. 이는 특정 순서로 나열된 것이 아니며, 일부는 최신 버전의 Spring Boot에서만 사용할 수 있습니다.

1. 최신 버전의 Spring Boot 사용

Spring Framework와 Spring Boot의 각 버전에서는 새로운 기능과 기능성뿐만 아니라 Spring 코어 컨테이너와 모듈 최적화, 버그 수정 등을 통해 성능 개선도 이루어집니다. 따라서 가능한 한 최신 버전의 Spring Boot를 사용하는 것이 좋습니다.

2. JVM 버전 및 튜닝

Spring Boot와 마찬가지로 각 Java LTS 릴리스는 많은 성능 개선을 포함합니다. 최신 버전의 Spring Boot 성능 향상을 위해 최신 버전의 Java를 사용해야 할 때도 있습니다.

 

최신 버전의 JVM은 Spring Boot 애플리케이션 성능을 향상시킬 수 있지만, JVM 튜닝을 통해 성능을 더욱 개선할 수 있습니다.

JVM 튜닝은 이 글의 범위를 벗어나지만 주의해야 할 몇 가지 영역을 언급할 수 있습니다:

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

애플리케이션과 환경에 따라 최적의 JVM 및 GC 설정은 다릅니다. 다양한 구성을 테스트하고 성능을 모니터링하여 최적의 설정을 식별하는 것이 중요합니다.

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

버전 3.2부터 Spring Boot는 다양한 방식으로 가상 스레드(Project Loom)를 지원하기 시작했습니다.

이 기능에 대한 자세한 정보는 아래 글에서 확인하실 수 있습니다 :
https://0soo.tistory.com/261

 

Java 가상 스레드(Virtual Thread) : SpringBoot에서 사용하기 -3

SpringBoot With Virtual Thread springboot 3.2 + 자바 21 버전부터 virtual thread를 사용할 수 있습니다. property 설정으로 AutoCofngiruation을 통해 활성화 할 수 있습니다. # virtual thread enabled/disabled spring.threads.virtual.

0soo.tistory.com

 

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

가상 스레드의 가장 큰 장점은 가벼운 스레드 모델을 도입하여 Spring Boot 애플리케이션의 확장성과 성능을 향상시킨다는 점입니다. 또한, 성능을 저하시키지 않으면서도 이전 버전과의 호환성을 유지하고 비동기 프로그래밍의 복잡성을 줄여줍니다.

Spring Boot에서 이 구성을 사용하여 쉽게 가상 스레드를 활성화할 수 있습니다:

spring.threads.virtual.enabled=true

이 구성을 통해 Spring Boot Web MVC는 컨트롤러 메서드와 같은 웹 요청을 가상 스레드에서 처리합니다.

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

GraalVM 네이티브 이미지는 JVM에 비해 메모리 사용량을 줄이고 시작 시간을 크게 단축하는 새로운 접근 방식을 제공합니다.

컨테이너 기반 배포 및 함수형 서비스(Function-as-a-Service) 플랫폼에 이상적인 GraalVM 네이티브 이미지는 정적 코드 분석을 통해 실행 파일을 생성하기 위해 사전 처리(ahead-of-time processing)가 필요합니다. 그 결과는 플랫폼에 종속적이며 실행 시 JVM이 필요 없는 독립 실행형 파일입니다.

 

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

Spring AOT(사전 처리) 기능은 Spring Framework에서 제공하는 기능으로, Spring 애플리케이션을 GraalVM과 더 호환되도록 변환합니다.

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

 

이를 위해 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 이미지를 생성합니다.

네이티브 실행 파일을 생성하려면 이 명령을 실행할 수 있습니다(머신에 GraalVM 배포판이 설치되어 있는지 확인하세요):

mvn -Pnative native:compile

5. JVM 체크포인트 복원 기능 (Project CRaC)

이 기능은 Spring Boot 3.2 버전부터 추가되었으며, Java 애플리케이션이 실행 중인 상태를 "체크포인트"로 저장한 다음 나중에 그 상태를 복원할 수 있게 합니다. Spring Boot는 ApplicationContext 라이프사이클과 통합하여 자동 체크포인트를 제공하며,

-Dspring.context.checkpoint=onRefresh 매개변수를 추가하여 간단히 사용할 수 있습니다.

 

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

  • 빠른 시작 시간: 워밍업된 JVM 상태를 저장하고 재시작 시 초기화 과정을 생략하여 성능을 향상시킵니다.
  • 향상된 안정성: 신뢰할 수 있는 체크포인트에서 JVM 상태를 복원하여 초기화 관련 문제를 피하고 더 안정적인 애플리케이션 실행을 보장합니다.
  • 자원 사용 감소: 기존 JVM 자원을 재사용하여 전체 자원 소비를 줄입니다.

이 기능을 사용하려면 CRaC 지원 JDK 버전을 설치하고 Spring Boot 3.2 이상을 사용해야 합니다.

이 기능에 대한 자세한 내용은 아래 글을 참고하세요 :
https://mangkyu.tistory.com/320

 

[Java] JVM의 체크포인트 생성과 복구를 위한 CRaC(Coordinated Restore at Checkpoint) 프로젝트

1. JVM의 체크포인트 생성과 복구를 위한 CRaC 프로젝트 [ CRaC 프로젝트란? ] CRaC(Coordinated Restore at Checkpoint) 프로젝트는 실행 중인 Java 인스턴스에 체크포인트(이미지 혹은 스냅샷의 생성)를 생성하

mangkyu.tistory.com

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

클래스 데이터 공유(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. Spring 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-timeout 및 server.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는 캐싱을 도와줄 수 있는 유용한 라이브러리 세트를 제공합니다.

캐싱 전략에 관한 유용한 글이 있습니다. 영문으로 되어있지만 한번 보시면 좋을 것 같습니다.

https://digma.ai/how-to-detect-cache-misses-using-observability/

 

How to Detect Cache Misses Using Observability - Digma

In this article, we'll examine cache misses using observability, learn about the caching concept and how to implement it in Spring Boot.

digma.ai

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

특히 요즘 마이크로서비스 아키텍처 기반 애플리케이션이 증가함에 따라 이 주제는 매우 중요하다고 생각합니다.

 

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

 

Spring Boot는 Resilience4j와 같은 인기 있는 라이브러리와의 통합을 통해 복원력 패턴을 지원합니다. Spring Boot는 필요한 빈을 자동 구성하고 속성 또는 어노테이션을 통해 복원력 패턴을 구성하는 편리한 방법을 제공합니다.

 

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"
    );
  }
}

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

모니터링과 프로파일링을 결합하여 Spring Boot 애플리케이션의 성능을 깊이 이해하고, 데이터 기반 결정을 통해 성능을 향상시킬 수 있습니다.

 

Spring Boot는 애플리케이션의 상태를 모니터링하고, 메트릭을 수집하며, 성능 병목 현상을 식별하는 데 도움을 주는 Spring Boot Actuator를 기본으로 제공합니다. 한편, Spring Boot 3.x부터는 Micrometer와의 좋은 통합이 있어 더 나은 메트릭과 분산 추적을 통해 애플리케이션 모니터링을 개선할 수 있습니다.

Digma가 Spring Boot 앱 성능을 향상시키는 방법

흥미롭게도 Digma는 애플리케이션을 배포할 필요 없이 Spring Boot 애플리케이션을 프로파일링하고 모니터링할 수 있는 도구입니다.

https://digma.ai/?source=post_page-----c392f32497e8--------------------------------

 

Digma Continuous Feedback

The Digma IDE plugin finds bottlenecks, scaling challenges, and query problems lurking in your codebase.

digma.ai

Digma는 개발 중 IDE 내부에서 병목 현상, 느린 쿼리, 캐시 미스 등의 문제를 찾아낼 수 있도록 도와줍니다. 이러한 기능은 Spring Boot 애플리케이션 성능을 향상시키기 위한 강력한 도구입니다.

마무리 생각

이 글에서는 Spring Boot 애플리케이션 성능을 향상시키기 위한 10가지 방법을 살펴보았습니다. 일부는 런타임 성능을 개선하고, 일부는 애플리케이션이 소비하는 자원을 최적화합니다. 하지만 일부 접근 방식은 특정 Java 및 Spring Boot 버전을 사용해야 합니다. 물론 코딩 수준에서 적용할 수 있는 모범 사례도 많이 있습니다. 다른 모범 사례에 대해 댓글로 알려주시면 감사하겠습니다.