니즈
JPA 개발 관련해서 주의할 것은 나도 모르게 나가는 쿼리를 인지하냐 못하냐 인 것 같다.
물론 개발중에 로그에 찍힌 쿼리를 보면 얼추 짐작은 할 수 있다.
다만 경우에 따라선, 로그양이 상당히 방대할 수 있다.
ex> ??? 파라미터 빵꾸뚤린 sql, 파라미터 채워진 sql, 주석달린 sql 기타 etc etc 뭔가 굉장히 많이 찍힘.
접근
갓영한 님께 문의해봤다.
질문 > 프로그러머틱하게 쿼리를 세는 방법이 있을까요?
답변
안녕하세요. 이기영님^^
여러가지 방법이 떠오르네요. ㅎㅎ
단순하게 DB로 몇 번 쿼리를 쏘는지는 딱 제공하는 기능은 없습니다.
대신에 프로그래머틱하게 해결할 수 있는 방법은 있습니다.
제가 아이디어를 드리자면, DataSource를 한번 래핑해서 count를 셀 수 있도록 구현하면 됩니다.
(아마 필요하면 ThreadLocal 이라는 것도 알아보셔야 할거에요. 쉬운 작업은 아니겠지만 여러가지 좋은 인사이트를 얻으실 수 있을꺼에요)
정답이라기 보다는 방향성을 찾아드리는 답변이 되었네요 ㅎㅎ
대략적인 방향은 잡았다. → 쓰레드로컬은 써야겠구나.
구글의 힘을 빌렸다.
검색 키워드
Counting Queries per Request with Hibernate and Spring
꽤 괜찮은 내용들이 몇개 대표적으로 2개 있었다. 링크 참고 (1개 외국, 1개 머루님)
구현
어드민,클라이언트,노아 모두 붙였다. 코드는 조금씩 상이하나, 메커니즘은 똑같다.
1] [시작,끝을 기록하는 인터셉터를 구현]
- HandlerInterceptorAdapter 를 상속받아서 구현한다. → 시작과 끝을 체크하기 위함.
- preHandle, afterCompletion 메소드를 오버라이드해서 여기서 로직을 구현한다.
2] 개수 세는것은 StatementInspector 인터페이스를 받아서 구현
- ThreadLocal 를 활용한다. 로직은 복붙했다.
3] 조립한다.
- 쿼리개수 세는 로직을 HandlerInterceptorAdapter 구현한 클래스에 넣어준다.
4] 속성에 StatementInspector 인터페이스를 구현한 클래스 파일 위치를 넣어준다.
spring.jpa.properties.hibernate.session_factory.statement_inspector=app.config.interceptor.HibernateInterceptor
(이 부분은 속성으로든, 개발적으로든 다 풀어내는게 가능하다.)
5] WebMvcConfigurer 인터페이스를 받아서 구현한 클래스에서 addInterceptors 오버라이드 메소드에서 registry 에 추가로 내가 구현한 인터셉터를 등록해준다.
6] 개발/운영기에선 동작하면 안되니 profile 을 읽어서 로컬인 경우에만 핸들러가 동작하도록 했다.
소스
@Slf4j
@Component
@RequiredArgsConstructor
public class RequestCountInterceptor extends HandlerInterceptorAdapter {
private final HibernateInterceptor hibernateInterceptor;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
hibernateInterceptor.start();
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Counter counter = hibernateInterceptor.getCount();
long duration = System.currentTimeMillis() - counter.getTime();
Long count = counter.getCount().get();
log.info("time : {}, count : {} , url : {}", duration, count, request.getRequestURI());
if (count >= 10) {
log.error("한 request 에 쿼리가 10번 이상 날라갔습니다. 날라간 횟수 : {} ", count);
}
hibernateInterceptor.clear();
}
}
@Component
@Slf4j
public class HibernateInterceptor implements StatementInspector {
private static ThreadLocal<Counter> queryCount = new ThreadLocal<>();
void start() {
queryCount.set(new Counter(new AtomicLong(0), System.currentTimeMillis()));
}
Counter getCount() {
return queryCount.get();
}
void clear() {
queryCount.remove();
}
@Override
public String inspect(String sql) {
log.info("sql = " + sql);
Counter counter = queryCount.get();
if (counter != null) {
AtomicLong count = counter.getCount();
count.addAndGet(1);
}
return sql;
}
@Data
class Counter {
private final AtomicLong count;
private final Long time;
}
}
@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
...
@Value("${spring.profile.value}")
private String profileValue;
private final AuthInterceptor authInterceptor;
private final RequestCountInterceptor requestCountInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/**", "/api/health/check");
if (profileValue.equals("local")) {
registry.addInterceptor(requestCountInterceptor);
}
WebMvcConfigurer.super.addInterceptors(registry);
}
spring.jpa.properties.hibernate.session_factory.statement_inspector=app.interceptor.HibernateInterceptor
결론
api 호출 걸린 시간과, 쿼리가 나간 개수를 로컬에서 편히 볼 수 있다!
이제 새로 api 를 개발하거나, 튜닝을 할 때 보다 명확하게 인지하고 개발을 할 수 있을거 같다! 예~~
a.i.RequestCountInterceptor: time : 1180, count : 6 , url : /api/product/2326/reviews
참고링크
http://wonwoo.ml/index.php/post/1179