프로젝트를 진행하고 있는 도중
스케쥴러를 이용해서 데이터를 30초마다 갱신하는 작업을 하고 있는데
N+1 문제가 발생을 하였다.
package inhatc.yulo.server.detection.entity;
import inhatc.yulo.server.building.entity.Building;
import inhatc.yulo.server.camera.entity.Camera;
import inhatc.yulo.server.common.BaseTimeEntity;
import inhatc.yulo.server.model.entity.Model;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Detection extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "detection_seq")
private Long id; // 감지내역 시퀀스
private int detectionData; // 검출내역 (임시)
private String deleteYn; // 삭제 여부(for 메인화면 감지내역)
private String detectionName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "building_seq")
private Building building; // 호관 join
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "model_seq")
private Model model; // 모델 join
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "camera_seq")
private Camera camera; // 카메라 join
}

감지entity는 호관, 모델, 카메라 entity와 join을 한 상태이다.
처음에 원인은 repository에 너무 많은 쿼리를 선언해서 서버를 실행할 때 스케쥴러가 이를 다 불러오는걸로 착각했는데.........원인은 이게 아니였다.
기존 코드에서 발생한 N+1 문제의 원인을 분석해본다.
DetectionUpdateScheduler.java
List<Detection> detections = detectionRepository.findAll();
detections.forEach(detection -> {
detection.setDetectionData(detection.getDetectionData() + 1);
detectionRepository.save(detection);
});
findAll(): DetectionRepository에서 모든 Detection 엔티티를 조회한다. 이 조회는 한 번의 쿼리로 수행된다.
forEach 루프에서 save() 반복 호출: 각 Detection 엔티티에 대해 detectionRepository.save(detection)가 호출되며, 이 때 매번 별도의 UPDATE 쿼리가 실행된다.
이런 반복적인 쿼리 호출이 바로 N+1 문제이다.
-> 1개의 조회 + N개의 수정 쿼리가 발생하기 때문에 성능에 큰 영향을 준다.
DetectionRepositoryImpl.java
@Override
@Transactional
public void incrementDetectionDataForAll() {
entityManager.createQuery("UPDATE Detection d SET d.detectionData = d.detectionData + 1")
.executeUpdate();
}
DetectionUpdateScheduler.java
@Scheduled(fixedRate = 30000)
public void updateDetections() {
detectionListService.updateAllDetections();
System.out.println("Detection table updated successfully");
}
| 기존 코드 | 리팩토링된 코드 |
|---|---|
| findAll()로 전체 조회 후 루프에서 save()를 반복 실행 | JPQL로 하나의 쿼리로 모든 데이터를 업데이트 |
| N개의 엔티티에 대해 N개의 UPDATE로 쿼리 발생 | 1개의 쿼리로 모든 데이터 업데이트 |
| N+1 문제 발생 | N+1 문제 해결 |
DetectionCustomRepositoryImpl.java
@Override
@Transactional
public void incrementDetectionDataForAll() {
entityManager.createQuery("UPDATE Detection d SET d.detectionData = d.detectionData + 1")
.executeUpdate();
}
DetectionListService.java
public void updateAllDetections() {
detectionRepository.incrementDetectionDataForAll();
}
DetectionUpdateScheduler.java
@Scheduled(fixedRate = 30000)
public void updateDetections() {
detectionListService.updateAllDetections();
System.out.println("Detection table updated successfully");
}
DetectionList.html
setInterval(function () {
location.reload();
}, 30000);
기존 코드에서 각 엔티티를 수정할 때마다 별도의 UPDATE 쿼리를 실행해 N+1 문제가 발생했다.
여러가지 해결방법을 시도했지만..(entityGraph, fatch join, Batch size 등등.. ) 해결하지 못했다.
리팩토링된 코드에서는 JPQL 대량 업데이트를 사용하여 하나의 쿼리로 모든 데이터를 수정했기 때문에 문제가 발생하지 않는다.