우당탕탕
[Spring] 대용량 트래픽을 위한 스프링 튜닝 팁 feat. 실무에서 바로 쓰는 전략 본문
대용량 트래픽을 위한 스프링 튜닝 팁
급하게 개발해야할 때 보통은 "일단 돌아가게 만든 후 최적화 한다" 라는 생각으로 개발을 진행하게 됩니다. 보통 이러한 접근은 대용량 트래픽이 발생할 때 치명적으로 돌아올 수 있습니다. 이번 글에서는 실제 서비스 장애를 얻으며 경험한 바탕으로, 트래픽 폭증 시 시스템을 안정적으로 유지하는 핵심 전략을 예시 코드와 함께 작성 해 보겠습니다.
1. 캐싱 전략: Redis 및 Caffeine Cache 사용을 통한 DB 부하 분산
"DB 호출 1회 감소" = 초당 수천 건의 트래픽 여유를 확보합니다.
캐시는 자주 조회되는 데이터를 미리 저장해두는 공간입니다.
예를 들어, 인기 상품 정보를 매번 DB에서 꺼내오면 느려지니까,
한 번만 DB에서 꺼내서 캐시에 저장해두고, 다음부터는 캐시에서 바로 꺼내면 훨씬 빨라집니다.
- Caffeine Cache (로컬 캐시)란? : https://mozzi-devlog.tistory.com/61
사용예시
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
로컬 캐시 (Caffeine Cache) vs 글로벌 캐시 (Redis) 조합사용
자주 변경되지 않는 데이터(ex: 상품 정보) -> Redis 클러스터를 활용
초단기 TTL 데이터 (ex: 인기 검색어) -> Caffeine 로컬 캐시 활용
캐싱 전략을 사용할 때는 @CacheEvict와 같은 캐시 무효화 전략도 같이 사용해야합니다
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepository.save(product);
}
2. 비동기 처리: WebFlux로 Thread 효율 극대화
"Blocking I/O" = Thread 자원 고갈의 주범입니다.
비동기란, 어떤 작업이 오래 걸릴 때 기다리지 않고 다른 일을 먼저 하는 것입니다.
예를 들어, 외부 API를 호출할 때 결과를 기다리지 않고,
다른 요청을 먼저 처리할 수 있으면 서버가 훨씬 더 많은 요청을 처리할 수 있습니다.
사용예시
public Mono<String> fetchDataAsync() {
return WebClient.create("https://api.example.com")
.get()
.retrieve()
.bodyToMono(String.class);
}
@Async vs WebFlux vs Kafka
I/O 집약적 작업 (ex: 외부 API 호출 등)에서는 WebClient를 활용
CPU 집약적 작업에서는 별도 쓰레드 풀에 @Async 적용
Tip: 블로킹 코드가 리액티브 체인에 포함되지 않도록 주의!!
3. DB 튜닝 : Batch 처리와 인덱스 전략
"단일 쿼리 10ms 개선" = 초당 100건 처리 시 1초 절약
DB가 느리면 서버 전체가 느려지게 됩니다.
인덱스를 추가하면 원하는 데이터를 더 빨리 찾을 수 있습니다.
사용예시
/* 인덱스 추가 예시 */
CREATE INDEX idx_orders_user_id ON orders(user_id);
한 번에 여러 데이터를 저장할 때는 Batch Insert로 묶어서 처리하면 더 빠르게 처리가 가능합니다.
@Transactional
public void bulkInsert(List<Order> orders) {
for (int i = 0; i < orders.size(); i++) {
entityManager.persist(orders.get(i));
if (i % 100 == 0) entityManager.flush();
}
}
4. 서버 여러대로 나누기 ( 수평 확장 )
한 대의 서버만 쓰만 한계가 존재합니다.
여러 대의 서버를 두고, 트래픽이 많아지만 자동으로 서버를 늘릴 수 있습니다.
- 쿠버네티스(Kubernetes)로 서버를 관리하면 쉽게 확장이 가능합니다.
- 세션( 로그인 정보 )는 Redis 같은 외부 저장소에 저장해야 서버 여러 대를 사용해도 서로 공유가 가능합니다.
5. JVM( 자바 가상머신 ) 튜닝
스프링은 자바로 돌아가니까, 자바 가상머신(JVM)도 잘 설정해야 합니다.
- GC(가비지 컬렉션)이 너무 오래 걸리면 서버가 버벅일 수 있습니다.
- 적절한 옵션을 주면 GC가 빨리 끝나서 서버가 더 빨라질 수 있습니다.
GC란? -> https://mozzi-devlog.tistory.com/14 , https://mozzi-devlog.tistory.com/15
사용예시
java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xms2g -Xmx2g -jar app.jar
6. 모니터링하기
서버가 느려지거나 이상해지면 바로 알 수 있어야합니다.
- Spring Boot Actuator를 사용하면 서버 상태를 쉽게 확인할 수 있습니다.
- Prometheus, Grafana 같은 도구로 그래프를 그려볼 수도 있습니다.
7. 코드 튜닝
코드가 효율적이지 않으면 서비스가 느려질 수 있습니다.
코드가 효율적이라는게 판단하기는 쉽지않지만 문자열 더하기로 예시를 들어보겠습니다.
반복문에서 문자열을 더할 때는 StringBuilder를 써야 더 빠릅니다. ( 불필요한 객체 생성을 줄이면 메모리도 아낄 수 있습니다 )
예시코드
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
8. 장애 대비하기 (Circuit Breaker)
외부 서비스가 느려지면 우리 서버도 같이 느려질 수 있습니다.
Circuit Breaker는 외부 서비스가 문제가 생기면 잠깐 요청을 멈추고, 기본 응답을 돌려주는 안전장치 입니다.
예시코드
@CircuitBreaker(name = "externalApi", fallbackMethod = "fallback")
public String callExternalApi() {
// 외부 API 호출
}
public String fallback(Throwable t) {
return "기본 응답";
}
마무리
지금까지 대용량 트래픽을 견디는 스프링 서버를 만드는 여러가지 방법을 알아보았습니다.
처음에는 생소하고 어렵다고 느낄 수 있지만, 한 가지씩 직접 적용하다보면 서비스가 점점 튼튼해지는 것을 느낄 수 있을 것 같습니다.
중요한건 완벽하게 한 번에 모든 걸 적용하는 것이 아니라, 문제가 생길 때마다 원인을 찾고, 하나씩 개선해 나가는 과정이라고 생각합니다.
'Tech > Spring' 카테고리의 다른 글
[Spring] JPA 영속성 컨텍스트 (Persistence Context) 란? (0) | 2025.04.28 |
---|---|
[Spring] JPA Auditing을 활용한 생성일, 수정일 자동 관리하기 (0) | 2025.04.27 |
[Spring] JPA N+1 문제 해결법 ( 원인부터 해결까지 ) (0) | 2025.04.25 |
스프링 외부 API 호출 방법 비교: RestTemplate, WebClient, FeignClient, RestClient (0) | 2025.04.14 |
JPA vs MyBatis: 어떤 ORM을 선택해야 할까? (0) | 2025.04.10 |