본문 바로가기

카테고리 없음

JPA 쿼리 나가는 개수 세기

 

니즈

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://knes1.github.io/blog/2015/2015-07-08-counting-queries-per-request-with-hibernate-and-spring.html

http://wonwoo.ml/index.php/post/1179