Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

여행 생성 및 여행 상세 조회 쿼리 개선 #651

Merged
merged 16 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
package hanglog.city.domain.repository;

import hanglog.city.domain.City;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface CityRepository extends JpaRepository<City, Long> {

@Query("""
SELECT c
FROM City c
WHERE c.id in :ids
""")
List<City> findCitiesByIds(@Param("ids") final List<Long> ids);

@Query("""
SELECT c
FROM City c, TripCity tc
WHERE c.id = tc.city.id
AND tc.trip.id = :tripId
""")
List<City> findCitiesByTripId(@Param("tripId") final Long tripId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hanglog.expense.dto.response;


import hanglog.city.domain.City;
import hanglog.city.dto.response.CityResponse;
import hanglog.currency.domain.Currency;
import hanglog.expense.domain.Amount;
Expand Down Expand Up @@ -60,4 +61,37 @@ public static TripExpenseResponse of(
dayLogExpenseResponses
);
}

public static TripExpenseResponse of2(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 사용이 안되고 있는것 같습니다.

final Trip trip,
final Amount totalAmount,
final List<City> cities,
final List<CategoryExpense> categoryExpenses,
final Currency currency,
final List<DayLogExpense> dayLogExpenses
) {
final List<CityResponse> cityExpenseResponses = cities.stream()
.map(CityResponse::of)
.toList();

final List<CategoryExpenseResponse> categoryExpenseResponses = categoryExpenses.stream()
.map(CategoryExpenseResponse::of)
.toList();

final List<DayLogExpenseResponse> dayLogExpenseResponses = dayLogExpenses.stream()
.map(DayLogExpenseResponse::of)
.toList();

return new TripExpenseResponse(
trip.getId(),
trip.getTitle(),
trip.getStartDate(),
trip.getEndDate(),
cityExpenseResponses,
totalAmount.getValue(),
categoryExpenseResponses,
ExchangeRateResponse.of(currency),
dayLogExpenseResponses
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package hanglog.global.detector;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿼리 카운터 이 pr에서 넣기로 한건가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i want...
이제 각자 쿼리 개선할텐데, 그때마다 N+1 detector 코드 복사해오고, push할 때 다시 없애는 일이 너무 번거로울 것 같아서요! 희망사항이긴합니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;

@RequiredArgsConstructor
public class ConnectionProxyHandler implements MethodInterceptor {

private static final String JDBC_PREPARE_STATEMENT_METHOD_NAME = "prepareStatement";

private final Object connection;
private final LoggingForm loggingForm;

@Nullable
@Override
public Object invoke(@Nonnull final MethodInvocation invocation) throws Throwable {
final Object result = invocation.proceed();

if (hasConnection(result) && hasPreparedStatementInvoked(invocation)) {
final ProxyFactory proxyFactory = new ProxyFactory(result);
proxyFactory.addAdvice(new PreparedStatementProxyHandler(loggingForm));
return proxyFactory.getProxy();
}

return result;
}

private boolean hasPreparedStatementInvoked(final MethodInvocation invocation) {
return invocation.getMethod().getName().equals(JDBC_PREPARE_STATEMENT_METHOD_NAME);
}

private boolean hasConnection(final Object result) {
return result != null;
}

public Object getProxy() {
final ProxyFactory proxyFactory = new ProxyFactory(connection);
proxyFactory.addAdvice(this);
return proxyFactory.getProxy();
}
}
30 changes: 30 additions & 0 deletions backend/src/main/java/hanglog/global/detector/LoggingForm.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package hanglog.global.detector;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class LoggingForm {

private String apiUrl;
private String apiMethod;
private Long queryCounts = 0L;
private Long queryTime = 0L;

public void queryCountUp() {
queryCounts++;
}

public void addQueryTime(final Long queryTime) {
this.queryTime += queryTime;
}

public void setApiUrl(final String apiUrl) {
this.apiUrl = apiUrl;
}

public void setApiMethod(final String apiMethod) {
this.apiMethod = apiMethod;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package hanglog.global.detector;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@RequiredArgsConstructor
public class PreparedStatementProxyHandler implements MethodInterceptor {

private static final List<String> JDBC_QUERY_METHOD =
List.of("executeQuery", "execute", "executeUpdate");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

줄바꿈..뭔가 묘하게 불편한데..List.of를 붙이고 안에 내용을 한줄에 하나씩으로 띄우는거 어떨까요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


private final LoggingForm loggingForm;

@Nullable
@Override
public Object invoke(@Nonnull final MethodInvocation invocation) throws Throwable {

final Method method = invocation.getMethod();

if (JDBC_QUERY_METHOD.contains(method.getName())) {
final long startTime = System.currentTimeMillis();
final Object result = invocation.proceed();
final long endTime = System.currentTimeMillis();

loggingForm.addQueryTime(endTime - startTime);
loggingForm.queryCountUp();

return result;
}

return invocation.proceed();
}
}
59 changes: 59 additions & 0 deletions backend/src/main/java/hanglog/global/detector/QueryCounterAop.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package hanglog.global.detector;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Aspect
@Slf4j
@Component
public class QueryCounterAop {
private final ThreadLocal<LoggingForm> currentLoggingForm;

public QueryCounterAop() {
this.currentLoggingForm = new ThreadLocal<>();
}

@Around("execution( * javax.sql.DataSource.getConnection())")
public Object captureConnection(final ProceedingJoinPoint joinPoint) throws Throwable {
final Object connection = joinPoint.proceed();

return new ConnectionProxyHandler(connection, getCurrentLoggingForm()).getProxy();
}

private LoggingForm getCurrentLoggingForm() {
if (currentLoggingForm.get() == null) {
currentLoggingForm.set(new LoggingForm());
}

return currentLoggingForm.get();
}

@After("within(@org.springframework.web.bind.annotation.RestController *)")
public void loggingAfterApiFinish() {
final LoggingForm loggingForm = getCurrentLoggingForm();

ServletRequestAttributes attributes =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

얜 final 아니에용?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

if (isInRequestScope(attributes)) {
HttpServletRequest request = attributes.getRequest();

loggingForm.setApiMethod(request.getMethod());
loggingForm.setApiUrl(request.getRequestURI());
}

log.info("{}", getCurrentLoggingForm());
currentLoggingForm.remove();
}

private boolean isInRequestScope(final ServletRequestAttributes attributes) {
return attributes != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ public class RequestLoggingFilter extends AbstractRequestLoggingFilter {

@Override
protected void beforeRequest(@NonNull final HttpServletRequest request, @NonNull final String message) {
logger.info(message);
if (!message.contains("prometheus")) {
logger.info(message);
}
Comment on lines +13 to +15
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 ㅋㅋㅋ 👍 👍 👍 👍 👍

}

@Override
protected void afterRequest(@NonNull final HttpServletRequest request, @NonNull final String message) {
logger.info(message);
// logger.info(message);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 처리하지말고 그냥 지우는건 어떤가요?

}
}
14 changes: 12 additions & 2 deletions backend/src/main/java/hanglog/trip/domain/DayLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import jakarta.persistence.OrderBy;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLDelete;
Expand Down Expand Up @@ -47,7 +49,7 @@ public class DayLog extends BaseEntity {

@OneToMany(mappedBy = "dayLog", cascade = REMOVE, orphanRemoval = true)
@OrderBy(value = "ordinal ASC")
private List<Item> items = new ArrayList<>();
private Set<Item> items = new HashSet<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private Set<Item> items = new HashSet<>();
private Set<Item> items = new LinkedHashSet<>();

아이템 순서도 보장하게 LinkedHashSet을 사용하는건 어떤가요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OrderBy가 붙여져있으면 내부적으로 LinkedHashSet으로 처리해줘서 그냥 두었는데 명시적으로 LinkedHashSet으로 할당하는 것도 좋겠군여

Copy link
Member Author

@jjongwa jjongwa Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

순서는 @OrderBy로 보장하고 있어서
굳이 HashSet보다 성능이 떨어지는(물론 둘 다 O(1)이긴 하지만) LinkedHashSet로 바꿀 필요까지는 없다는 생각입니다.!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부적으로 LinkedHashSet으로 처리한다면, 어떤 부분에서 성능이 다른가용 ?? 갖고 있을 때 메모리를 더 잡아먹는다 ??
저는 명시적으로 + 혹시 모를 버그 예방을 위해 LinkedHashSet에 한 표 하겠습니다 ㅎㅎ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OrderBy를 쓰면 HashSet으로 생성을 해도 순서가 보장이 되는지 몰랐네요.

저도 홍고와 라온 말처럼 명시적으로 혹은 혹시 모를 버그를 방지하기 위한 LinkedHashSet으로 할당하는 것에 한표 하겠습니다.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성능 차이는 일반 List와 LinkedList의 차이와 같습니다.!
어떤 자료구조를 사용하냐인데 이건 답글로 일일이 달 필요까지는 없다고 생각합니다.(저희 ArrayList LinkedList 미니 미션도 했으니까요..!)

그리고 명시적으로 라는 부분도 잘 모르곘는게,
Trip의 DayLogs라면 이해가 됩니다. 저희가 추가 삭제 할 때마다 순서에 맞게 기타 DayLog의 실제 위치를 변경했으니까요.!

그런데 DayLog의 Items는 다르다고 생각합니다.
저희가 아이템 위치 바꿀 때 기존에 존재하는 아이템들 사이에 끼워 넣나요?
그냥 order 필드만 바꾸고 있습니다..! 따라서 시간복잡도는 그냥 O(1)이죠
애초에 저희가 순서 변경을 할 때 컬랙션 내에서 item의 위치를 바꾸는게 아닌데
왜 LinkedHashSet으로 바꾸는게 명시적인 효과가 있는건지 잘 모르겠다는 의견입니다..!

그으으으런데 홍고의 말대로 @OrderBy가 붙여져있을 때
HashSet으로 선언해놔도 알아서 LinkedHashSet 형태로 저장되어 있다면
LinkedHashSet으로 바꿔도 좋을 것 같습니다. (근데 진짜 그렇게 바뀌나요? 그러면 코드 단에서 밑줄로 잡아줘야 하는거 아닌가)


public DayLog(
final Long id,
Expand All @@ -60,7 +62,7 @@ public DayLog(
this.title = title;
this.ordinal = ordinal;
this.trip = trip;
this.items = items;
this.items = new HashSet<>(items);
}

public DayLog(
Expand All @@ -83,4 +85,12 @@ public LocalDate calculateDate() {
final LocalDate startDate = trip.getStartDate();
return startDate.plusDays((long) ordinal - DEFAULT_DAY);
}

public void addItem(final Item item) {
items.add(item);
}

public List<Item> getItems() {
return new ArrayList<>(items);
}
}
18 changes: 16 additions & 2 deletions backend/src/main/java/hanglog/trip/domain/Trip.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
import jakarta.persistence.OrderBy;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.SQLDelete;
Expand Down Expand Up @@ -77,7 +79,7 @@ public class Trip extends BaseEntity {

@OneToMany(mappedBy = "trip", cascade = {PERSIST, REMOVE, MERGE}, orphanRemoval = true)
@OrderBy(value = "ordinal ASC")
private List<DayLog> dayLogs = new ArrayList<>();
private Set<DayLog> dayLogs = new HashSet<>();

@OneToOne(mappedBy = "trip", cascade = {PERSIST, REMOVE, MERGE}, orphanRemoval = true)
private SharedTrip sharedTrip;
Expand All @@ -104,7 +106,7 @@ public Trip(
this.endDate = endDate;
this.description = description;
this.sharedTrip = sharedTrip;
this.dayLogs = dayLogs;
this.dayLogs = new HashSet<>(dayLogs);
this.sharedStatus = sharedStatus;
this.publishedStatus = publishedStatus;
}
Expand Down Expand Up @@ -169,4 +171,16 @@ public void changePublishedStatus(final Boolean isPublished) {
public Boolean isWriter(final Long memberId) {
return this.member.getId().equals(memberId);
}

public void addDayLog(final DayLog dayLog) {
dayLogs.add(dayLog);
}

public void removeDayLog(final DayLog dayLog) {
dayLogs.remove(dayLog);
}

public List<DayLog> getDayLogs() {
return new ArrayList<>(dayLogs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package hanglog.trip.domain.repository;

import hanglog.trip.domain.DayLog;
import java.util.List;

public interface CustomDayLogRepository {

void saveAll(final List<DayLog> dayLogs);
}
Loading