1. ๊ฐ์
JPA๋ฅผ ์ฌ์ฉํ ๋, ์ง์ฐ ๋ก๋ฉ๊ณผ ๊ฐ์ ๊ธฐ๋ฅ์ ๋ฌด๋ถ๋ณํ๊ฒ ์ฌ์ฉํ๋ค๋ณด๋ฉด ์๋ ํ์ง ์์ ์๋ง์ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค. ํํ ์ด๋ฐ ๋ฌธ์ ๋ฅผ N+1 ๋ฌธ์ ๋ผ๊ณ ํ๋ค.
์ด๋ ๋ฏ ORM์ ์ฌ์ฉํ๋ค๋ณด๋ฉด ์์์น ๋ชปํ ๊ฐ์์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๊ธฐ๋ ํ๋๋ฐ, ์ด๋ฅผ ํ๋์ ํ์ ํ๊ธฐ๋ ์ฝ์ง ์๋ค. ๋ก๊ทธ์ ์ฟผ๋ฆฌ๊ฐ ์ถ๋ ฅ๋๊ธด ํ์ง๋ง ํ๋ํ๋ ์ธ๊ณ ์์๋ ์์ฐ์ฑ์ด ๋๋ฌด ๋จ์ด์ง๋ค.
๋ฐฉ๋ฒ์ด ์์๊น ์ฐพ์๋ณด๋ค ์ฟผ๋ฆฌ ์นด์ดํฐ์ ๋ํด ์๊ฒ๋๋ค.
๋ํ์ ์ธ, ๋ ์ฝ๊ฒ ๊ตฌํํ ์ ์๋ ๋ฐฉ๋ฒ์ Hibernate์์ ์ฟผ๋ฆฌ๋ฌธ์ ๊ฐ๋ก์ฑ ์ ์๋ StatementInspector๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก Spring AOP๋ฅผ ํ์ฉํ๋ ๊ฒ์ด๋ค.
๋ ๊ฐ์ง ๋ฐฉ๋ฒ์ ํ๋ํ๋ ์์๋ณด๋ ค ํ๋ค. ๊ฒฐ๋ก ๋ถํฐ ์๊ธฐํ์๋ฉด ๋๋ Spring AOP๋ฅผ ํ์ฉํ ์ฟผ๋ฆฌ ์นด์ดํ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ค.
2. StatementInspector
์ฟผ๋ฆฌ ์นด์ดํฐ๋ฅผ ๋ง๋ค๊ธฐ ์ํด์ ์๋ 2๊ฐ์ง ์กฐ๊ฑด์ ๋ง์กฑํด์ผ ํ๋ค.
1. ์ฟผ๋ฆฌ๊ฐ ์คํ๋๊ธฐ ๋ฐ๋ก ์ง์ queryCount ๊ฐ์ ์ฆ๊ฐ์ํค๋ ๋ฉ์๋๋ฅผ ์คํ
2. queryCount๋ฅผ ๊ด๋ฆฌํ๋ ๊ฐ์ฒด์ ์๋ช ์ฃผ๊ธฐ๋ HTTP request์ ์ผ์น
2๋ฒ์ ์กฐ๊ฑด์ @RequestScope๋ฅผ ํ์ฉํ์ฌ Http request ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ฐ์ง๋ ๋น์ ๋ง๋ค์ด ์ฃผ๋ฉด ๊ฐ๋จํ ํด๊ฒฐํ ์ ์๋ค. 1๋ฒ์ ๊ฒฝ์ฐ StatementInspector๊ฐ ์ด ์ญํ ์ ํด์ค๋ค.
StatementInspector๋ ํ์ด๋ฒ๋ค์ดํธ๊ฐ ์ ๊ณตํ๋ ์ธํฐํ์ด์ค์ด๋ค. inspect๋ผ๋ ๋ฉ์๋๋ฅผ ๊ฐ์ง๊ณ ์๋๋ฐ, HibernateProperty๋ก StatementInspector ๊ตฌํ์ฒด๋ฅผ ๋ฑ๋กํ๋ฉด ์ฟผ๋ฆฌ ์คํ ์์ ์ ์คํ๋ ์ฟผ๋ฆฌ๋ฌธ์ ์ธ์๋ก ๋ฐ๋ inspect ๋ฉ์๋๊ฐ ์คํ๋๋ค.
๋ฐ๋ผ์ StatementInspector ๊ตฌํ์ฒด๋ฅผ ์ปค์คํฐ๋ง์ด์งํ์ฌ ์ฟผ๋ฆฌ ๊ฐ์๋ฅผ ์ธ๋๋ฐ ํ์ฉํ๋๋ก ํ ๊ฒ์ด๋ค.
๋จผ์ HTTP request์ ์ฃผ๊ธฐ๋ฅผ ๊ฐ์ด ํ๋ ๋น์ ๋ฑ๋ก์์ผ์ค์ผ ํ๋ค. ๋จ์ํ ๊ฐ์๋ง ์ ์ฅํด์ฃผ๋ ๊ฐ์ฒด์ด๋ค.
@Component
@RequestScope
@Getter
public class ApiQueryCounter {
private int count;
public void increaseCount() {
count++;
}
}
์ด์ ์์์ ๋ง๋ ApiQueryCounter์ ๊ฐ์ ์ฆ๊ฐ์์ผ์ฃผ๋ ๋ก์ง์ ๊ฐ์ง StatementInspector ๊ตฌํ์ฒด๋ฅผ ์์ฑํด์ค๋ค.
@Component
public class ApiQueryInspector implements StatementInspector {
private final ApiQueryCounter apiQueryCounter;
public ApiQueryInspector(final ApiQueryCounter apiQueryCounter) {
this.apiQueryCounter = apiQueryCounter;
}
@Override
public String inspect(final String sql) {
if (isInRequestScope()) {
apiQueryCounter.increaseCount();
}
return sql;
}
private boolean isInRequestScope() {
return Objects.nonNull(RequestContextHolder.getRequestAttributes());
}
}
๋น์ผ๋ก ๋ง๋ค์ด ์ค ๋ค, ApiQueryCounter์ ์์กด์ฑ์ ์ฃผ์ ๋ฐ์์ค๋ค.
inspect ๋ฉ์๋๊ฐ ์คํ๋๋ฉด ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ค๋ ์๋ฏธ์ด๋ฏ๋ก apiQueryCounter.increaseCount๋ฅผ ํธ์ถํด์ฃผ๊ณ ๋งค๊ฐ๋ณ์๋ก ๋ฐ์ ์ฟผ๋ฆฌ๋ฅผ ๊ทธ๋๋ก ๋ฐํํ์ฌ ์คํ์์ผ์ฃผ๋ฉด ๋๋ค.
์ด์ HibernateConfig๋ฅผ ์์ฑํด์ค๋ค.
์ฐ๋ฆฌ๊ฐ ๋ง๋ ApiQueryInspector๋ ์ปค์คํ ๋น์ธ ApiQueryCounter๋ฅผ ์ฃผ์ ๋ฐ๊ณ ์๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋น๋ค์ด ๋ชจ๋ ์ด๊ธฐํ๋๊ณ ๋์ HibernateProperty๋ก ๋ฑ๋กํ๋๋ก ํด์ผํ๋ค.
@Configuration ์์์ @Bean์ผ๋ก ๋น ๋ฑ๋ก์ ํด์ฃผ๊ฒ ๋๋ฉด ์ปดํฌ๋ํธ ์ค์บ๋ณด๋ค ๋์ค์ ๋น ๋ฑ๋ก์ด ๋๋ค๋ ์ ์ ์ด์ฉํ์ฌ ApiQueryInspector๋ฅผ HibernateProperties์ ๋ฑ๋กํ๋๋ก ํ์๋ค.
@Configuration
@RequiredArgsConstructor
public class HibernateConfig {
private final ApiQueryInspector apiQueryInspector;
@Bean
public HibernatePropertiesCustomizer hibernatePropertyConfig() {
return hibernateProperties ->
hibernateProperties.put(AvailableSettings.STATEMENT_INSPECTOR, apiQueryInspector);
}
}
์ด๋ ๊ฒ ์ค์ ์ ๋๋ง์น๋ฉด ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๋๋ง๋ค inspect ๋ฉ์๋๊ฐ ํธ์ถ๋๊ฒ ๋๋ค.
์ด์ ๋ง์ง๋ง์ผ๋ก ์ธํฐ์ ํฐ์์ ์ผ ์ฟผ๋ฆฌ ๊ฐ์๋ฅผ ๋ก๊ทธ๋ก ๋จ๊ธธ ๊ฒ์ด๋ค. HandlerInterceptor๋ฅผ implementsํด LoggingInterceptor๋ฅผ ๋ง๋ค์ด์ฃผ์๋ค.
@Slf4j
@Component
@RequiredArgsConstructor
public class LoggingInterceptor implements HandlerInterceptor {
private static final String QUERY_COUNT_LOG_FORMAT = "STATUS_CODE: {}, METHOD: {}, URL: {}, QUERY_COUNT: {}";
private static final String QUERY_COUNT_WARNING_LOG_FORMAT = "์ฟผ๋ฆฌ๊ฐ {}๋ฒ ์ด์ ์คํ๋์์ต๋๋ค.";
private static final int QUERY_COUNT_WARNING_STANDARD = 10;
private final ApiQueryCounter apiQueryCounter;
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
final Object handler, final Exception ex) {
final int queryCount = apiQueryCounter.getCount();
log.info(QUERY_COUNT_LOG_FORMAT, response.getStatus(), request.getMethod(), request.getRequestURI(), queryCount);
// 10๊ฐ ์ด์์ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ ์ warn
if (queryCount >= QUERY_COUNT_WARNING_STANDARD) {
log.warn(QUERY_COUNT_WARNING_LOG_FORMAT, QUERY_COUNT_WARNING_STANDARD);
}
}
}
์ด์ WebConfig์ ๋ฑ๋กํด์ฃผ๋ฉด ๋ชจ๋ ์ค์ ์ ๋์ด๋๋ค.
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
public WebConfig(LoggingInterceptor loggingInterceptor) {
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor);
}
}
์๋์ ๊ฐ์ด ์ด๋ค URL์ Query๊ฐ ๋ช ๋ฒ ๋๊ฐ๋ ์ง ํ์ธํ ์ ์๋ค.
3. Spring AOP
StatementInspector์ ๊ฐ์ฅ ํฐ ๋จ์ ์ Hibernate์ ์ข ์์ ์ด๋ผ๋ ๊ฒ์ด๋ค.
๊ฐ๋ฐํ๋ค๋ณด๋ฉด ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ฆฌ๋ ๊ฒฝ์ฐ๋ ์์ ์ ์๊ธฐ ๋๋ฌธ์ ์๋ฐ์ ํ์ค ๋ฐ์ดํฐ ์ ๊ทผ ๊ธฐ์ ์ธ JDBC ๋จ์์ ์นด์ดํธ๋ฅผ ์ธ๋ ๊ฒ ๋ ๋์ ๋ฐฉ๋ฒ์ธ ๊ฒ ๊ฐ๋ค.
๋จผ์ Jdbc์ ์ฟผ๋ฆฌ๊ฐ ๋ ์๊ฐ๋ ๊ณผ์ ์ ์์์ผํ๋ค.
Datasource์์ connection์ ์ป๊ณ , ์ด๋ฅผ ์ด์ฉํ์ฌ ์ฟผ๋ฆฌ ์คํํ๋ค.
์ด ๊ณผ์ ์์ prepareStatement๊ฐ ์คํ๋๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
Connection์ AOP๋ฅผ ์ ์ฉํด prepareStatement๊ฐ ์คํ๋ ๋ ์ก์์ ์นด์ดํธ๋ฅผ ํ๋ฉด ๋๋ค.
ํ์ง๋ง ํ๊ฐ์ง ๋ฌธ์ ์ ์ด ์๋ค.
AOP๋ ์คํ๋ง ๋น์๋ง ์ ์ฉํ ์ ์๊ธฐ ๋๋ฌธ์ Connection์ ์ง์ ์ ์ผ๋ก ์ ์ฉํ์ง ๋ชปํ๋ค. ๋ฐ๋ผ์ ์คํ๋ง ๋น์ผ๋ก ๋ฑ๋ก๋ DataSource์ AOP๋ฅผ ์ ์ฉํ๊ณ ์ฟผ๋ฆฌ ์นด์ดํธ ๊ธฐ๋ฅ์ ๊ฐ์ง Connection์ ํ๋ก์ ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ผํ๋ค.
ํ๋ก์๋ ํ๊ฒ์ ๊ธฐ๋ฅ์ ํ์ฅํ๊ฑฐ๋ ํ๊ฒ์ ๋ํ ์ ๊ทผ์ ์ ์ดํ๊ธฐ ์ํ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉํ๋ ํด๋์ค๋ฅผ ๋งํ๋ค. ๊ทผ๋ฐ Connection์ ํ๋ก์๋ก ๊ตฌํ์ ํ๋ ค๋ฉด Connection์ ๋ชจ๋ ๋ฉ์๋๋ฅผ ๊ตฌํํด ์ค์ผ ๋๋๋ฐ ์ด๊ฑธ ์ ๋ถ ๊ตฌํํ๋ ๊ฑด..
๋ค์ด๋๋ฏน ํ๋ก์๋ฅผ ์ด์ฉํ์ฌ ๊ฐ๋จํ๊ฒ ํ๋ก์๋ฅผ ์์ฑํด ์ฃผ์.
๋ค์ด๋๋ฏน ํ๋ก์๋?
๋์ ์ธ ์์ ์ ํ๋ก์๋ฅผ ์๋์ผ๋ก ๋ง๋ค์ด ์ ์ฉํ๋ ๊ธฐ์ ์ ๋งํจ
QueryCounter ์์ฑ
@Getter
@Component
@RequestScope
public class QueryCounter {
private int count;
public void increaseCount() {
count++;
}
}
์นด์ดํธ ์ญํ ์ ํ๋ ์ด ๊ฐ์ฒด์ ์๋ช ์ฃผ๊ธฐ๋ ํน์ ์ค๋ ๋์ ์์ฒญ ๋์๋ง ์ฌ์ฉ๋๋ค. ThreadLocal์ ์จ์ ์ ์ฅ, ์ญ์ ํด ์ค ์๋ ์๊ฒ ์ง๋ง Spring์์ RequstScope๋ฅผ ์ง์ํด ์ฃผ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ํ์ฉํด ์ค๋ค.
ConnectionProxyHandler ์์ฑ
Connection ๋ค์ด๋๋ฏน ํ๋ก์ ๊ตฌํ์ ์ํด handler๋ฅผ ๋จผ์ ๋ง๋ค์ด์ผ ๋๋ค. ์ํ๋ ๋์(์นด์ดํธ)์ ์ค์ ํ ์ ์๋๋ก InvocationHandler๋ฅผ ๊ตฌํํด ์ค๋ค.
public class ConnectionProxyHandler implements InvocationHandler {
private static final String QUERY_PREPARE_STATEMENT = "prepareStatement";
private final Object connection;
private final QueryCounter queryCounter;
public ConnectionProxyHandler(Object connection, QueryCounter queryCounter) {
this.connection = connection;
this.queryCounter = queryCounter;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
countQuery(method);
return method.invoke(connection, args);
}
private void countQuery(Method method) {
if (isPrepareStatement(method) && isRequest()) {
queryCounter.increaseCount();
}
}
private boolean isPrepareStatement(Method method) {
return method.getName().equals(QUERY_PREPARE_STATEMENT);
}
private boolean isRequest() {
return RequestContextHolder.getRequestAttributes() != null;
}
}
invoke ๋ฉ์๋๊ฐ ์ค์ target์ ๋ฉ์๋ ํธ์ถ์ ๊ฐ๋ก์ฑ๊ธฐ ๋๋ฌธ์ ์ด ๋ถ๋ถ์ ์ถ๊ฐ์ ์ผ๋ก ์ ์ฉํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ฉด ๋๋ค. ์คํ๋๋ ๋ฉ์๋์ ๋ช ์ด "prepareStatement" ๋ผ๋ฉด countํด์ฃผ์๋ค.
ํ์ฌ QueryCounter๊ฐ RequestScope๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ isRequest ๋ถ๊ธฐ๊ฐ ์์ผ๋ฉด ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ผญ ์ถ๊ฐํด์ค์ผ ํ๋ค.
Scope ‘request’ is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;
AOP ์ ์ฉ
DataSource๊ฐ getConnection() ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ Connection์ ์ถ๊ฐ ๊ธฐ๋ฅ์ด ์ฅ์ฐฉ๋ ํ๋ก์ ๊ฐ์ฒด๋ก ๋ฎ์ด์์์ค์ผ ๋๋ค. ๋ฐฉ๊ธ ๋ง๋ Handler๋ฅผ ์ด์ฉํ์ฌ Connection ๋ค์ด๋๋ฏน ํ๋ก์๋ฅผ ๊ตฌํํด ๋ณด์.
์ฐ์ ์๋ฐ์์ ๋ค์ด๋๋ฏน ํ๋ก์๋ Java.lang.reflect.Proxy ํด๋์ค์ newProxyInstance() ๋ฉ์๋๋ฅผ ์ด์ฉํ์ฌ ์์ฑํ ์ ์๋ค.
Proxy.newProxyInstance(
ํ๋ก์ ํด๋์ค๋ฅผ ์ ์ํ๋ ํด๋์ค ๋ก๋,
๊ตฌํํ ํ๋ก์ ํด๋์ค์ ์ธํฐํ์ด์ค ๋ชฉ๋ก,
๋ฉ์๋ ํธ์ถ์ ์ ๋ฌํ๋ ํธ์ถ ํธ๋ค๋ฌ
);
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class QueryCountAspect {
private final QueryCounter queryCounter;
@Around("execution(* javax.sql.DataSource.getConnection(..))")
public Object getConnection(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object connection = proceedingJoinPoint.proceed();
return Proxy.newProxyInstance(
connection.getClass().getClassLoader(),
connection.getClass().getInterfaces(),
new ConnectionHandler(connection, queryCounter)
);
}
}
์ฟผ๋ฆฌ ์นด์ดํธ ๋ก๊น ์ธํฐ์ ํฐ ์์ฑ
@Slf4j
@Component
@RequiredArgsConstructor
public class LoggingInterceptor implements HandlerInterceptor {
private static final String QUERY_COUNT_LOG = "METHOD: {}, URL: {}, STATUS_CODE: {}, QUERY_COUNT: {}";
private static final String QUERY_COUNT_WARN_LOG = "์ฟผ๋ฆฌ๊ฐ {}๋ฒ ์ด์ ์คํ๋์์ต๋๋ค!!!";
private static final int WARN_QUERY_COUNT= 8;
private final QueryCounter queryCounter;
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
int queryCount = queryCounter.getCount();
log.info(QUERY_COUNT_LOG, request.getMethod(), request.getRequestURI(), response.getStatus(), queryCount);
if (queryCount >= WARN_QUERY_COUNT) {
log.warn(QUERY_COUNT_WARN_LOG, WARN_QUERY_COUNT);
}
}
}
AOP๋ฅผ ์ ์ฉํด๋ ์๊น์ ๋ง์ฐฌ๊ฐ์ง๋ก ์ฟผ๋ฆฌ ์นด์ดํ ์ด ์ ๋๋๊ฑธ ํ์ธํ ์ ์๋ค. ๐