프로젝트마다 직면하는 N+1 문제에 대해서 이번에는 Map으로 해결해보았다.
✅ 해결 결과, N+1개의 쿼리에서 2개의 쿼리로 줄였다.
그 과정을 포스팅하고자 한다.
내가 직면한 N+1 문제는 아래 링크를 통해 확인할 수 있다.
👉🏻 N+1 문제 직면 - [내일배움캠프 Spring 4기 - 최종 프로젝트] 92일차 TIL : 페이징 구현
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class EventResponse {
private Long id;
private String title;
private String content;
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime openAt;
private Long couponId;
private List<Long> eventProducts;
public static EventResponse from(Event event, List<Long> eventProducts) {
return new EventResponse(
event.getId(),
event.getTitle(),
event.getContent(),
event.getOpenAt(),
event.getCoupon().getId(),
eventProducts
);
}
}
public EventResponse getEvent(Long eventId) {
Event event = findEvent(eventId);
List<Long> eventProducts = eventQuery.getEventProducts(event.getId());
return EventResponse.from(event, eventProducts);
}
public Page<EventResponse> getAllEvents(Pageable pageable) {
return eventRepository.findAll(pageable)
.map(event -> {
List<Long> eventProducts = eventQuery.getEventProducts(event.getId());
return EventResponse.from(event, eventProducts);
});
}
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "event")
@SQLDelete(sql = "UPDATE event SET deleted_at=CURRENT_TIMESTAMP where id=?")
@SQLRestriction("deleted_at IS NULL")
public class Event extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String content;
@Column
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime openAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@OneToOne
@JoinColumn(name = "coupon_id")
private Coupon coupon;
public Event(EventRequest request, User user) {
this.title = request.getTitle();
this.content = request.getContent();
this.openAt = request.getOpenAt();
this.user = user;
}
public void update(EventUpdateRequest request) {
this.title = request.getTitle();
this.content = request.getContent();
this.openAt = request.getOpenAt();
}
public void addCoupon(Coupon coupon) {
this.coupon = coupon;
}
}
@Getter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class EventProduct {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id")
private Event event;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
public EventProduct(Event event, Product product) {
this.event = event;
this.product = product;
}
}
@Getter
public class EventProductResponse {
private Long productId;
public EventProductResponse(EventProduct eventProduct) {
this.productId = eventProduct.getProduct().getId();
}
}
public List<EventProduct> getEventProducts(List<Long> eventIds) {
return jpaQueryFactory.select(
QEventProduct.eventProduct)
.from(QEventProduct.eventProduct)
.where(QEventProduct.eventProduct.event.id.in(eventId))
.fetch();
}
@Transactional(readOnly = true)
public Page<EventResponse> getAllEvents(Pageable pageable) {
Page<Event> events = eventRepository.findAll(pageable);
List<Long> eventIds = events.stream().map(Event::getId).toList();
List<EventProduct> eventProducts = eventQuery.getEventProducts(eventIds);
Map<Long, List<EventProductResponse>> eventProductMap = eventProducts.stream()
.collect(Collectors.groupingBy(eventProduct -> eventProduct.getEvent().getId(),
Collectors.mapping(EventProductResponse::new,
Collectors.toList())));
List<EventResponse> eventResponses = events.stream().map(event -> EventResponse.from(event,
eventProductMap.getOrDefault(event.getId(), List.of()))).toList();
return new PageImpl<>(eventResponses, pageable, events.getTotalElements());
}
EventProduct
객체를 Event
ID에 매핑하는 람다 표현식과 EventProductResponse
객체로 변환한 후 리스트로 수집하는 컬렉터를 사용하여 그룹핑한다.eventProductMap
은 Event
ID를 키로 사용하고, 해당 Event
ID에 해당하는 EventProductResponse
객체들의 리스트를 값으로 가지는 map이 된다.