[3] k6: 성능 튜닝 가이드(DB, JVM, 그리고 Redis 캐시)

2025. 12. 13. 21:18·Test/k6

5. 튜닝

5.1. application.properties 튜닝

max-threads, maximum-pool-size 등을 늘려서 요청 병렬 처리 성능을 높일 수 있다.

 

[1] application.properties 수정

spring.application.name=step03_app_test

server.address=0.0.0.0

# Server Configuration
server.port=8080

# Redis Configuration
spring.data.redis.host=127.0.0.1
spring.data.redis.client-name=default
spring.data.redis.password=redis1234
spring.data.redis.port=6379
spring.data.redis.timeout=2000
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=-1

# Datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useSSL=false&serverTimezone=Asia/Seoul
spring.datasource.username=testuser
spring.datasource.password=testpass
#spring.datasource.hikari.maximum-pool-size=20
#spring.datasource.hikari.minimum-idle=10
#spring.datasource.hikari.connection-timeout=30000
#spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

# JPA / Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
logging.level.org.hibernate.SQL=DEBUG


###########################################
#             튜  닝   설  정              #
###########################################

## Tomcat max-threads
server.tomcat.max-threads=100
server.tomcat.accept-count=200
server.connection-timeout=20000 

spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.connection-timeout=30000

###########################################

 

 

[2] 배포 및 테스트 수행

1. .jar 파일 재빌드
2. Ubuntu로 SFTP 전송
3. 기존 프로세스 종료 후 재실행
ps -ef | grep java
sudo kill -9 <PID>
nohup java -jar step03_app_test-0.0.1-SNAPSHOT.jar &
k6 run --vus 100 --duration 30s get-user-ubuntu-script.js

 


5.2. JVM 튜닝

이번엔 JVM 자체의 힙 메모리 설정을 조정하고 GC 로그를 분석해 보자.

 

[1] JVM 설정

nohup java -Xms512m -Xmx1024m \
  -Xlog:gc*:file=gc.log:time,uptime,level,tags \
  -jar step03_app_test-0.0.1-SNAPSHOT.jar &

 

[1] Xms, Xmx: 힙 초기/최대 크기를 지정
[2] -Xlog:gc*: GC 로그를 gc.log 파일로 저장

 

 

[2] JVM 튜닝 후 다시 성능 테스트

k6 run --vus 100 --duration 30s get-user-ubuntu-script.js

 

 

✅ vCPU + RAM 별 힙 크기 추천 (Xmx 기준)

vCPU RAM 권장 Heap 크기 비고
1 1GB 512MB ~ 768MB 작은 힙, 작은 GC pause
1 2GB 1GB Java 11 이상에서 G1GC/ZGC 권장
2 2~4GB 1GB ~ 2GB GC 튜닝에 따라 조절 가능
4 4~8GB 2GB ~ 4GB GC pause 감소 목적
8 8~16GB 4GB ~ 8GB G1GC, ZGC, Shenandoah 등 권장
16+ 32GB+ 8GB ~ 16GB+ GC 튜닝 필수, 세부 옵션 조정 필요
💡 일반적으로 RAM의 25~50% 범위에서 설정한다. vCPU가 적을수록 너무 큰 힙은 피하는 것이 좋다 (GC 오버헤드 발생 가능성).

 

 

✅ JVM 힙 및 GC 설정 예시

1. 일반 환경

# 2 vCPU + 4GB RAM
java -Xms1g -Xmx1g -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar

2. 중간 규모 서버

# 4 vCPU + 8GB RAM
java -Xms2g -Xmx2g -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags -jar app.jar

 

 

✅ Kubernetes(HPA) 환경에서의 JVM 힙 설계

HPA(Horizontal Pod Autoscaler)가 적용된 환경이라면, JVM 힙 설계 방식도 다르다.

HPA 여부 힙 설계 전략
❌ 없음 가능한 큰 힙 설정 → GC 최소화
✅ 있음 작은 힙 + 빠른 GC → Pod 수 증가로 부하 분산
HPA + JVM 설계 시 권장
- Pod 1개 RAM 1~2GB → Heap 512MB ~ 1GB
- Pod 1개 RAM 4GB → Heap 1~2GB
- Pod 1개 RAM 8GB → Heap 2~3GB

5.3. MySQL Slow Query Log 튜닝

실제 서비스에서 데이터베이스 성능 문제를 추적할 때 가장 먼저 보는 로그 중 하나가 슬로우 쿼리 로그이다. 이 로그를 통해 응답 시간이 오래 걸리는 쿼리, 인덱스를 사용하지 않는 쿼리 등을 찾아낼 수 있다. 

 

MySQL은 지정된 시간 이상 소요된 쿼리를 로그 파일에 기록하는 기능을 제공한다. 이걸 슬로우 쿼리 로그(Slow Query Log)라고 부른다.

항목  설명
대상 쿼리 long_query_time 이상 걸린 쿼리 (기본 10초)
기록 내용 실행 SQL, 실행 시간, 락 시간, 검사한 row 수 등
로그 위치 보통 /var/lib/mysql/slow.log (RDS는 CloudWatch)
사용 조건 slow_query_log = 1 + long_query_time 설정 필요

 

⚙️ 주요 설정 값 정리

설정 항목 예시 값 설명
slow_query_log 1 슬로우 쿼리 로그 활성화
slow_query_log_file /var/lib/mysql/slow.log 로그 저장 경로
long_query_time 0.5 0.5초 이상 쿼리만 기록 (실무에선 보통 0.1~0.5)
log_queries_not_using_indexes 1 인덱스를 사용하지 않은 쿼리도 기록

 

🛠️ 설정 방법

① my.cnf에 직접 설정 (로컬/VM 환경)

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 0.5
log_queries_not_using_indexes = 1

 

② Docker 환경에서 설정

# docker-compose.yaml 예시
services:
  mysql:
    image: mysql:8.0
    container_name: mysql-server
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: testdb
    ports:
      - "3306:3306"
    volumes:
      - ./my.cnf:/etc/mysql/conf.d/my.cnf
      - mysql-data:/var/lib/mysql
    command:
      --slow_query_log=1
      --slow_query_log_file=/var/lib/mysql/slow.log
      --long_query_time=0.5
      --log_queries_not_using_indexes=1
volumes:
  mysql-data:

 

🔍 슬로우 쿼리 로그 확인

Docker/MySQL이 실행 중이라면 컨테이너 내부로 들어가 로그를 확인할 수 있다.

docker exec -it mysql-server bash
cat /var/lib/mysql/slow.log
항목 설명
Query_time SQL 실행 시간
Lock_time 테이블/행 락 대기 시간
Rows_examined 실제 조회한 row 수
Rows_sent 클라이언트로 보낸 row 수

 

☁️ AWS RDS에서 슬로우 쿼리 로그 설정

AWS RDS는 my.cnf를 직접 수정할 수 없기 때문에 Parameter Group을 통해 설정한다.

 

✅ 설정할 파라미터

파라미터  값  설명
slow_query_log 1 슬로우 쿼리 로그 켜기
long_query_time 0.5 0.5초 이상 쿼리만 기록
log_queries_not_using_indexes 1 인덱스 없는 쿼리도 기록
log_output FILE 로그 파일로 저장 (권장)
 

✅ 적용 순서

  1. RDS 콘솔 → Parameter Groups → 새 그룹 생성 또는 기존 그룹 수정
  2. 위 파라미터들 설정
  3. 해당 파라미터 그룹을 RDS 인스턴스에 연결
  4. 설정 반영을 위해 RDS 재시작

📌 CloudWatch Logs로 연동하면 로그를 외부에서도 쉽게 확인할 수 있다.

 


 

6. Redis 캐시 적용 (Read 성능 극대화 전략)

 

위의 테스트와 같이 애플리케이션에서 반복적으로 DB를 조회하는 API가 있다면, 캐시(Cache)를 활용해 응답 속도를 대폭 향상시킬 수 있다.

 

6.1. 환경 구성

[1] RedisConfig 수정

Spring에서 Redis 연결을 설정하고, 캐시 매니저(CacheManager)를 등록

더보기
더보기
@Configuration
@EnableCaching
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Value("${spring.data.redis.password}")
    String password;

    // Redis 커넥션 객체 생성
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        config.setPassword(password);
        return new LettuceConnectionFactory(config);
    }


    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

    @Bean
    public CacheManager cacheManager(LettuceConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(
                        RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                );

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

 

[2] UserService 수정

@Cacheable: 조회 시 먼저 캐시에 있는지 확인하고, 없으면 DB 조회 후 캐시에 저장
@CacheEvict: 데이터 생성/수정 시 해당 캐시 무효화 (항상 최신 데이터 보장)
더보기
더보기
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    @CacheEvict(value = "userCache", allEntries = true)
    public void initUsers() {
        if (userRepository.count() == 0) {
            IntStream.rangeClosed(1, 1000).forEach(i -> {
                userRepository.save(User.builder()
                        .name("User" + i)
                        .email("user" + i + "@test.com")
                        .age(20 + (i % 30))
                        .build());
            });
        }
    }

    @Transactional(readOnly = true)
    @Cacheable(value = "userCache", key = "#id")
    public User findById(Long id) {
        simulateDbDelay(); // DB hit 테스트 체감용 (선택)
        return userRepository.findById(id).orElse(null);
    }

    @Transactional
    @CacheEvict(value = "userCache", allEntries = true)
    public List<User> createBulk(List<User> users) {
        return userRepository.saveAll(users);
    }

    private void simulateDbDelay() {
        try {
            Thread.sleep(50);  // DB hit 차이를 체감하기 위한 인위적 지연
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

 

[3] pom.xml

Redis 사용을 위한 필수 라이브러리: spring-boot-starter-data-redis, spring-boot-starter-cache 등

더보기
더보기
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>step03_app_test</groupId>
    <artifactId>step03_app_test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>step03_app_test</name>
    <description>step03_app_test</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.34</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

 

6.2. 캐시 동작 개념 정리

어노테이션 동작 방식
@Cacheable(value = "userCache", key = "#id") id로 조회 시, 캐시에 있으면 그 값을 리턴하고 DB는 호출하지 않음
@CacheEvict(value = "userCache", allEntries = true) 데이터 생성 또는 초기화 시, 모든 캐시 삭제 (신선한 상태 유지)

 

6.3. 테스트

6.3.1. 1차 실행 (캐시가 없는 상태 - Miss)

[1] 테스트 실행

k6 run get-user-ubuntu-script.js

 

[2] 결과 확인

첫 요청이므로 Redis에 캐시된 데이터가 없고, 모든 요청이 DB까지 조회하게 되어 응답이 느리다.

더보기
더보기
ubuntu@ubuntu:~/k6$ k6 run --vus 10 --duration 30s get-user-ubuntu-script.js 

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: get-user-ubuntu-script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
              * default: 10 looping VUs for 30s (gracefulStop: 30s)

INFO[0000] 테스트 시작                                        source=console
INFO[0031] 테스트 종료                                        source=console


  █ TOTAL RESULTS 

    checks_total.......................: 484    15.618815/s
    checks_succeeded...................: 93.59% 453 out of 484
    checks_failed......................: 6.40%  31 out of 484

    ✓ status is 200
    ✗ response time < 500ms
      ↳  87% — ✓ 211 / ✗ 31

    HTTP
    http_req_duration.......................................................: avg=227.34ms min=10.29ms med=61.83ms max=2.05s p(90)=602.85ms p(95)=1.18s
      { expected_response:true }............................................: avg=227.34ms min=10.29ms med=61.83ms max=2.05s p(90)=602.85ms p(95)=1.18s
    http_req_failed.........................................................: 0.00% 0 out of 242
    http_reqs...............................................................: 242   7.809408/s

    EXECUTION
    iteration_duration......................................................: avg=1.26s    min=1.01s   med=1.07s   max=3.07s p(90)=1.66s    p(95)=2.18s
    iterations..............................................................: 242   7.809408/s
    vus.....................................................................: 4     min=4        max=10
    vus_max.................................................................: 10    min=10       max=10

    NETWORK
    data_received...........................................................: 44 kB 1.4 kB/s
    data_sent...............................................................: 20 kB 633 B/s




running (0m31.0s), 00/10 VUs, 242 complete and 0 interrupted iterations

 

 

6.3.2. 2차 실행 (캐시가 있는 상태 - Hit)

[1] 테스트 실행

k6 run get-user-ubuntu-script.js

 

[2] 결과 확인

이미 Redis에 user1 정보가 캐시된 상태이므로, 모든 요청이 캐시에서 즉시 응답되어 성능이 대폭 개선된다.

더보기
더보기
ubuntu@ubuntu:~/k6$ k6 run --vus 10 --duration 30s get-user-ubuntu-script.js 

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: get-user-ubuntu-script.js
        output: -

     scenarios: (100.00%) 1 scenario, 10 max VUs, 1m0s max duration (incl. graceful stop):
              * default: 10 looping VUs for 30s (gracefulStop: 30s)

INFO[0000] 테스트 시작                                        source=console
INFO[0031] 테스트 종료                                        source=console


  █ TOTAL RESULTS 

    checks_total.......................: 568     18.32562/s
    checks_succeeded...................: 100.00% 568 out of 568
    checks_failed......................: 0.00%   0 out of 568

    ✓ status is 200
    ✓ response time < 500ms

    HTTP
    http_req_duration.......................................................: avg=66.16ms min=10.81ms med=47.63ms max=302.52ms p(90)=153.86ms p(95)=192.22ms
      { expected_response:true }............................................: avg=66.16ms min=10.81ms med=47.63ms max=302.52ms p(90)=153.86ms p(95)=192.22ms
    http_req_failed.........................................................: 0.00% 0 out of 284
    http_reqs...............................................................: 284   9.16281/s

    EXECUTION
    iteration_duration......................................................: avg=1.08s   min=1.01s   med=1.05s   max=1.5s     p(90)=1.17s    p(95)=1.22s   
    iterations..............................................................: 284   9.16281/s
    vus.....................................................................: 10    min=10       max=10
    vus_max.................................................................: 10    min=10       max=10

    NETWORK
    data_received...........................................................: 52 kB 1.7 kB/s
    data_sent...............................................................: 23 kB 742 B/s




running (0m31.0s), 00/10 VUs, 284 complete and 0 interrupted iterations
default ✓ [======================================] 10 VUs  30s

 

✅ 결과 요약

구분 캐시 미적용 (Miss) 캐시 적용 (Hit)
평균 응답 시간 약 227ms 약 66ms
성공률 93.5% 100%
느린 응답 수 31건 0건

 

 


 

 

'Test > k6' 카테고리의 다른 글

[4] k6: 테스트 시각화(k6, InfluxDB, Grafana 대시보드 구축)  (0) 2025.12.13
[2] k6: 실전 API 부하 테스트(Spring Boot 환경 구축과 트러블 슈팅)  (0) 2025.12.13
[1] k6: 성능 테스트 기초(테스트 이론과 k6 입문)  (0) 2025.07.04
'Test/k6' 카테고리의 다른 글
  • [4] k6: 테스트 시각화(k6, InfluxDB, Grafana 대시보드 구축)
  • [2] k6: 실전 API 부하 테스트(Spring Boot 환경 구축과 트러블 슈팅)
  • [1] k6: 성능 테스트 기초(테스트 이론과 k6 입문)
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[3] k6: 성능 튜닝 가이드(DB, JVM, 그리고 Redis 캐시)
상단으로

티스토리툴바