T_era
Spring Boot 대량 데이터 처리 성능 튜닝 본문
개요
10,000건의 이벤트 데이터를 처리하는 과정에서 발생한 성능 문제를 해결한 방법. 초기에는 컨트롤러에서 3-4초, 서비스에서 900ms 정도 소요되던 작업을 최종적으로 80% 정도의 성능을 개선했다.
초기 성능 문제 상황
성능 측정 결과 (개선 전)
1단계 - Event 저장 완료: 22ms
2단계 - ProductApiClient 호출 완료: 236ms (조회된 상품 수: 10000)
3단계 - EventItem 객체 생성 완료: 4ms
4단계 - EventItem insert 완료: 1052ms
5단계 - Event에 EventItem 리스트 설정 완료: 0ms
6단계 - WSEventProduct 객체 생성 완료: 1ms
7단계 - 이벤트 발행 완료: 799ms
=== createEvent 총 실행시간: 2115ms ===
8단계 - 컨트롤러 실행 완료: 4126ms
문제점:
- 컨트롤러 전체 실행 시간이 4126ms로 심각한 지연
- EventItem 벌크 insert가 1052ms로 큰 병목
- 이벤트 리스너가 동기적으로 처리되어 지연 발생
성능 튜닝 과정
1. Notification 벌크 인서트 적용
개선 전: 개별 insert
// 기존 방식 - 개별 insert
notificationRepository.save(notification);
개선 후: 벌크 insert
@Service
public class NotificationService {
private final List<Notification> buffer = new ArrayList<>();
public void addNotification(Notification notification) {
synchronized (lock) {
buffer.add(notification);
if(buffer.size() >= 1000) {
// 1000개씩 벌크 insert
insertBatch();
}
}
}
@Async
public void insertBatch() {
List<Notification> notifications;
synchronized (lock) {
notifications = new ArrayList<>(buffer);
buffer.clear();
}
notificationRepository.insertNotifications(notifications);
}
}
개선 효과: 개별 insert → 벌크 insert로 변경하여 DB 호출 횟수 대폭 감소
2. EventItem 벌크 인서트 적용
개선 전: JPA saveAll 사용
// 기존 방식
eventItemRepository.saveAll(eventItems);
개선 후: JdbcTemplate batchUpdate 사용
@Repository
public class EventItemInsertRepository {
public void insertEventItem(List<EventItem> eventItems, Long eventId) {
String sql = "INSERT INTO event_items (event_id, product_id, product_name, original_price, discount_price, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, eventItems, 1000, (ps, eventItem) -> {
ps.setLong(1, eventId);
ps.setLong(2, eventItem.getProductId());
ps.setString(3, eventItem.getProductName());
ps.setBigDecimal(4, eventItem.getOriginalPrice());
ps.setBigDecimal(5, eventItem.getDiscountPrice());
ps.setObject(6, LocalDateTime.now());
ps.setObject(7, LocalDateTime.now());
});
}
}
개선 효과:
- JPA saveAll → JdbcTemplate batchUpdate로 변경
- 배치 크기 1000으로 설정하여 메모리 효율성 향상
3. 이벤트 발행 비동기 처리 + 트랜잭션 일관성 보장
문제 상황
@Transactional
public EventResponse createEvent(EventCrateRequest request) {
// ... 데이터 저장 ...
// 이벤트 발행 (동기적)
wsEventProducts.forEach(wsEvent -> {
eventPublisher.publishEvent(wsEvent);
});
return new EventResponse(event);
}
문제점: 이벤트 발행이 동기적으로 처리되어 지연 발생
해결 방안: @TransactionalEventListener 적용
@Component
public class NotificationListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void addProductDiscountEvent(WSEventProduct event) {
try {
log.info("addProductDiscountEvent 시작 - 단일 이벤트: {}", event.product_id());
ListenProductEvent listenProductEvent = new ListenProductEvent(event);
notificationService.notifyProductEventMessage(listenProductEvent);
log.info("addProductDiscountEvent 종료");
} catch (Exception e) {
log.error("addProductDiscountEvent 처리 실패 message : {}", e.getMessage());
}
}
}
개선 효과:
- 트랜잭션 커밋 후 이벤트 리스너 실행으로 데이터 일관성 보장
- 비동기 처리로 컨트롤러 응답 시간 단축
- 트랜잭션 롤백 시 이벤트 리스너 미실행으로 안정성 확보
4. JPA 연관관계 매핑 제거
개선 전: 일대다 연관관계
@Entity
public class Event {
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
private List<EventItem> products;
}
@Entity
public class EventItem {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id")
private Event event;
}
문제점:
- 조인 테이블(events_products)에 대량 insert 쿼리 발생
- 트랜잭션 종료 시 지연 발생
- 메모리 사용량 증가
개선 후: 연관관계 제거
@Entity
public class Event {
@Transient // 조회용으로만 사용
private List<EventItem> products;
}
@Entity
public class EventItem {
private Long eventId; // 단순 외래키만 저장
}
개선 효과:
- 조인 테이블 insert 쿼리 제거
- 트랜잭션 종료 시 오버헤드 대폭 감소
- 메모리 사용량 최적화
최종 성능 개선 결과
성능 측정 결과 (개선 후)
1단계 - Event 저장 완료: 29ms
2단계 - ProductApiClient 호출 완료: 260ms (조회된 상품 수: 10000)
3단계 - EventItem 객체 생성 완료: 3ms
4단계 - EventItem 벌크 insert 완료: 520ms
5단계 - Event에 EventItem 리스트 설정 완료: 0ms
6단계 - WSEventProduct 객체 생성 완료: 1ms
7단계 - 이벤트 발행 완료: 10ms
=== createEvent 총 실행시간: 823ms ===
8단계 - 컨트롤러 실행: 844ms
성능 개선 요약
| 구분 | 개선 전 | 개선 후 | 개선율 | 주요 개선 사항 |
|---|---|---|---|---|
| 서비스 레이어 | 2115ms | 823ms | 61% | 벌크 인서트 + 비동기 이벤트 |
| 컨트롤러 레이어 | 4126ms | 844ms | 80% | 전체 시스템 최적화 |
| EventItem Insert | 1052ms | 520ms | 51% | JdbcTemplate batchUpdate |
| 이벤트 발행 | 799ms | 10ms | 99% | @TransactionalEventListener |
핵심 개선 포인트
1. 벌크 인서트 최적화
- 개별 insert → 벌크 insert: DB 호출 횟수 대폭 감소
- JdbcTemplate batchUpdate: JPA 오버헤드 제거
- 배치 크기 최적화: 메모리와 성능의 균형점 설정
2. 트랜잭션 일관성 보장
- @TransactionalEventListener: 트랜잭션 완료 후 이벤트 처리
- 비동기 처리: 컨트롤러 응답 시간 단축
- 데이터 일관성: 트랜잭션 롤백 시 이벤트 미실행
3. JPA 연관관계 최적화
- 연관관계 제거: 조인 테이블 insert 쿼리 제거
- 단순 외래키 사용: 성능과 복잡성의 균형
- @Transient 활용: 조회용 데이터만 메모리에 로드
결론
대량 데이터 처리 시 성능 튜닝의 핵심은 DB 호출 최소화, 트랜잭션 일관성 보장, JPA 오버헤드 제거. 특히 10,000건 이상의 데이터를 처리할 때는 JPA의 편의성보다는 성능 최적화가 우선되어야 한다.
이번 튜닝을 통해 컨트롤러 응답 시간을 80% 단축하고, 시스템의 안정성과 확장성을 동시에 확보할 수 있었다.
'Programing > Spring' 카테고리의 다른 글
| 설문 프로젝트 모니터링 및 부하 테스트 + 성능 개선 (2) | 2025.08.07 |
|---|---|
| Chapter 1: WebSocket과 STOMP 프로토콜 (2) | 2025.07.11 |
| Spring WebSocket 알림 기능 구현 및 테스트 경험 정리 (1) | 2025.07.09 |
| RestTemplate 직접 생성 vs 빌더 생성 차이와 동작 원리 (0) | 2025.07.08 |
| 실시간 데이터 전송 방식 정리 (1) | 2025.07.01 |