로그백과 p6Spy 활용하여 JPA로깅 하기 + 로그백 필터링

들어가며

  • 여러번 세팅을 하지만 항상 애를 먹었던 것 중 하나가 로그이다. 로그를 하나 하나 디테일하게 설정하는 것이 어렵다.
  • 특히 특정 로그는 발생시키고 다른 로그는 제거하는 로직을 구현하는 것 또한 힘들었다.
  • 로깅이 원하는 방향대로 동작하지 않아 힘들었는데 ㅠ 이 글이 많은 분들께 도움이 되기를 바란다!

요구사항

  • JPA의 쿼리를 예쁘게 만들고 싶다. 그러니까 JPA의 쿼리가 단 한 번만 발생하고, 파라미터는 ?가 아니라 바로 주입되었으면 좋겠다.
  • 쿼리를 발생시킨 메서드를 확인하고 싶다. mybatis의 경우 쿼리가 발생한 메서드의 이름이 로거에 바로 남는다. 하지만 jpa는 언제나 org.hibernate.SQL 로 나온다. 예를 들면 아래와 같다.
마이바티스 : 
2022-04-14 15:45:19.195 [main] DEBUG a.b.c.d.e.f.findById - ==>  Preparing: SELECT * FROM TABLE WHERE ID = ? 

JPA:
2022-07-23 04:53:22.887 [main] DEBUG org.hibernate.SQL - 
    /*  select
        max(s.no) 
    from
        Send s */ select
            max(send0_.no) as col_0_0_ 
        from
            send send0_
  • 반복되는 쿼리는 필터링하고 싶다. 스케줄러를 통해 특정 상태를 가지는 레코드에 대한 이벤트 처리를 하고 싶을 수 있다. 이 때 쿼리가 계속 발생하는 것을 방지하고 싶을 수 있다.
@Scheduled(fixedDelay = 100) 
public void run() {
    // 0.1초마다 아래의 쿼리가 로그에 남는다.
    List<Send> sendRequests = sendRunnerService.findByStatus(REQUEST);
    // 이하 생략
}

로깅을 사용하는 가장 간단한 방법

  • 앞서의 요구사항을 수행하기 전에, 프로퍼티스를 활용한 최대한 간단한 로깅을 정리하였다.
  • 스프링과 jpa를 활용한 가장 쉬운 로깅 방법은 application.yml 을 아래와 같이 설정하는 것이다. 로깅의 레벨과 파일의 저장, 롤링까지 세세하게 설정 가능하다.
  • 이하 테스트는 # 설정 부분만을 건든다.
spring:
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
       show_sql: true # 설정
       format_sql: true # 설정
       use_sql_comments: true # 설정

logging:
  level:
    root: INFO
    study.querydsl: DEBUG # 프로젝트 위치
    org.hibernate.SQL: DEBUG  # 설정
    org.hibernate.type.descriptor.sql: TRACE # 설정
  pattern:
    rolling-file-name: ${LOG_FILE}.%d{yyyy-MM-dd}-%i.log
  file:
    path: C://logs//study//querydsl
    max-size: 10MB
  • 더 나아가 테스트를 진행할 로직은 아래와 같다. 동일한 로직을 계속 반복할 예정이다.
package study.querydsl.entity;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import java.util.List;

@SpringBootTest
@Transactional
class TeamTest {
    @Autowired
    EntityManager em;

    @Test
    void test(){
        Team team = new Team("teamA"); 

        // insert into team (team_id, name) values (?, ?);
        em.persist(team); 

        em.flush();
        em.clear();

        // select * from team t where t.team_id =?; 
        em.find(Team.class, team.getId()); 
    }
}

SQL 로깅 생략

  • 위의 내용을 확인하기에 앞서, 다음과 같은 형태로 로깅을 해보자.
  • 아래와 같이 진행하면 콘솔과 파일은 생성되고 로그가 정상 동작한다. 하지만 SQL로그는 발생하지 않음을 확인할 수 있다.
spring:
  jpa:
    properties:
      hibernate:
       show_sql: false
       format_sql: false
       use_sql_comments: false

logging:
  level:
    org.hibernate.SQL: INFO
    org.hibernate.type.descriptor.sql: INFO

SQL의 콘솔과 로그에서의 출력

  • 아래와 같이 레벨을 DEBUG와 TRACE로 각각 설정하면 아래와 같이 SQL로그가 발생한다.
logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql: TRACE
2022-07-23 08:52:52.404 DEBUG 13188 --- [Test worker] org.hibernate.SQL                        : insert into team (name, team_id) values (?, ?)
2022-07-23 08:52:52.409 TRACE 13188 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [teamA]
2022-07-23 08:52:52.410 TRACE 13188 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-07-23 08:52:52.437 DEBUG 13188 --- [Test worker] org.hibernate.SQL                        : select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=?
2022-07-23 08:52:52.439 TRACE 13188 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-23 08:52:52.449 TRACE 13188 --- [Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name2_2_0_] : [VARCHAR]) - [teamA]
  • org.hibernate.SQL 은 PreparedStatement 를 출력한다.
  • org.hibernate.type.descriptor.sql은 바인딩되는 파라미터를 출력한다.
  • 로깅과 콘솔을 위한 가장 간단한 세팅이다.

콘솔에서만 출력한다면?

  • 이번에는 다음과 같이 설정하자.
spring:
  jpa:
    properties:
      hibernate:
       show_sql: true
       format_sql: false
       use_sql_comments: false

logging:
  level:
    org.hibernate.SQL: INFO
    org.hibernate.type.descriptor.sql: INFO
Hibernate: call next value for hibernate_sequence
Hibernate: insert into team (name, team_id) values (?, ?)
Hibernate: select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=?
  • 위는 콘솔에 찍힌 로그이다. 아까와 달리 로그가 다소 달라짐을 확인할 수 있다.
  • 그리고 path에 저장된 로그 파일을 열면, 어떤 sql로 없는 것을 확인할 수 있다.
  • 내가 가장 헤맸던 부분이 바로 이 부분이다! 로그백의 필터링을 먹이고 해당 필터가 동작함을 분명하게 확인하였는데도 불구하고 위의 로그가 찍혔다.
  • 사실 이러한 이유는 단순했다. show_sql은 콘솔에 출력하는 System.out이었고, org.hibernate.SQL은 로거를 사용한 로깅이었기 때문이다. 이 개념이 없었던 나는 많은 시간을 헤맸다🥶🥶🥶.
  • 개인적으로 로그파일과 콘솔에 출력되는 내용은 동일한게 보기 좋다고 생각했다. 특히 로그백 필터 등을 콘솔에 적용하고 싶을 경우 그러하다. 개인적으로 show_sql은 false가 낫다고 생각한다.

format_sql

spring:
  jpa:
    properties:
      hibernate:
       show_sql: false
       format_sql: true
       use_sql_comments: false

logging:
  level:
    org.hibernate.SQL: DEBUG
    org.hibernate.type.descriptor.sql: TRACE
2022-07-23 09:03:13.434 DEBUG 13852 --- [Test worker] org.hibernate.SQL                        : 
    insert 
    into
        team
        (name, team_id) 
    values
        (?, ?)
2022-07-23 09:03:13.439 TRACE 13852 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [teamA]
2022-07-23 09:03:13.440 TRACE 13852 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
2022-07-23 09:03:13.467 DEBUG 13852 --- [Test worker] org.hibernate.SQL                        : 
    select
        team0_.team_id as team_id1_2_0_,
        team0_.name as name2_2_0_ 
    from
        team team0_ 
    where
        team0_.team_id=?
2022-07-23 09:03:13.471 TRACE 13852 --- [Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2022-07-23 09:03:13.480 TRACE 13852 --- [Test worker] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([name2_2_0_] : [VARCHAR]) - [teamA]
  • preparedStatement가 예쁘게 찍힌다!

use_sql_comments

  • use_sql_comments : true 로 변경한다.
  • Spring-data-jpa를 사용할 경우 아래와 같은 형태로 사용하면, 작성한 주석이 발생한다.
    @Test
    void test2() {
        String query = "select t from Team t";
        final List<Team> result = em
                .createQuery(query, Team.class)
                .setHint(QueryHints.HINT_COMMENT, "This is my comment")
                .getResultList();
    }
2022-07-23 10:23:59.699 DEBUG 17752 --- [    Test worker] org.hibernate.SQL                        : 
    /* This is my comment */ select
        team0_.team_id as team_id1_2_,
        team0_.name as name2_2_ 
    from
        team team0_
  • spring-data-jpa에서는 아래와 같이 설정한다.
public interface TeamRepository extends JpaRepository<Team, Long> {
    @QueryHints({
            @QueryHint(name = org.hibernate.annotations.QueryHints.COMMENT, value = "add comment!")
    })
    @Query("select t from Team t")
    List<Team> findAll();
}
  • 원하지 않는 쿼리에 대해서 누락시켰을 때, 원래 이러한 방식으로 진행하였다.
  • p6spy를 통해 처리하는 것이 좋았다.

logback-spring.xml

  • 로그백은 앞서의 프로퍼티스에서 설정할 수 있지만 xml을 통해 설정할 수 있다.
  • 마이바티스와 달리 logback-spring.xml 하나로 간편하게 설정할 수 있다.

  • 먼저 아래의 의존성을 받는다. 필터를 사용하는 방식 중 하나인 EvaluatorFilter 을 사용하려면 아래의 의존성이 필요하다.
// build.gradle
implementation 'org.codehaus.janino:janino'
  • xml 설정을 통해 properties에서 로거와 관련한 내용을 뺄 수 있다.
  • 다만, hibernate의 포맷 등 기능을 사용하려면 아래와 같이 설정해야 한다.
spring:
  datasource:
    url: jdbc:h2:mem:test
    username: sa
    password:
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: false
        format_sql: false
        use_sql_comments: false

#logging:
#  level:
#    root: INFO
#    study.querydsl: DEBUG # 프로젝트 위치
#    org.hibernate.SQL: DEBUG
#    org.hibernate.type.descriptor.sql: TRACE
#  pattern:
#    rolling-file-name: ${LOG_FILE}.%d{yyyy-MM-dd}-%i.log
#  file:
#    path: C://logs//study//querydsl
#    max-size: 10MB
  • 실제 xml 파일은 아래와 같다.
  • resources/logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="LOG_PREFIX" value="study-querydsl"></property>
    <property name="LOG_PATH" value="c:/logs/study/querydsl"/>
  
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator>
                <expression>if (message.contains("insert")){ return true; } return false;</expression>
            </evaluator>
            <OnMismatch>ACCEPT</OnMismatch>
            <OnMatch>DENY</OnMatch>
        </filter>

        <file>${LOG_PATH}/${LOG_PREFIX}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/${LOG_PREFIX}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator>
                <expression>if (message.contains("insert")){ return true; } return false;</expression>
            </evaluator>
            <OnMismatch>ACCEPT</OnMismatch>
            <OnMatch>DENY</OnMatch>
        </filter>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>


    <root level="INFO">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="FILE" />
    </root>

    <logger name="study.querydsl" level="DEBUG" additivity="true"/> <!--프로젝트 로그-->
    <logger name="org.hibernate.SQL" level="DEBUG" additivity="true" /> <!--sql 로그-->
    <logger name="org.hibernate.type.descriptor.sql" level="TRACE" additivity="true" /> <!--파라미터 로그-->
</configuration>
  • 위의 내용 중 특이한 부분은 필터 부분이다. 자바 문법을 사용하여 <expression> 태그에 boolean을 리턴하는 필터를 구현한다.
  • true일 경우 <OnMatch>의 동작을 수행하며 그렇지 않을 경우 <OnMismatch>을 수행한다.
  • ACCEPT은 해당 로그를 실제로 작성한다는 의미이며 DENY일 경우 로그를 제거한다.
  • 위의 필터는 insert가 들어간 로그를 제거한다는 의미이며 가장 먼저 예로 든 테스트를 동작하면 아래와 같은 로그가 발생한다.
2022-07-23 10:44:37.661 [Test worker] DEBUG org.hibernate.SQL - call next value for hibernate_sequence
2022-07-23 10:44:37.767 [Test worker] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [VARCHAR] - [teamA]
2022-07-23 10:44:37.768 [Test worker] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [2] as [BIGINT] - [1]
2022-07-23 10:44:37.793 [Test worker] DEBUG org.hibernate.SQL - select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=?
2022-07-23 10:44:37.795 [Test worker] TRACE o.h.type.descriptor.sql.BasicBinder - binding parameter [1] as [BIGINT] - [1]
2022-07-23 10:44:37.804 [Test worker] TRACE o.h.t.descriptor.sql.BasicExtractor - extracted value ([name2_2_0_] : [VARCHAR]) - [teamA]
  • 두 번째와 세 번째 로그에는 insert에 사용하는 파라미터가 있음을 확인할 수 있다. 그러니까 두 번재 로그에 원래는 insert into ... 의 형태의 로그가 발생해야 하는데 발생하지 않는다.
  • 이하 select 쿼리는 로그가 잘 찍힘을 확인할 수 있다.
  • 단순한 preparedStatement만 로그를 작성할 경우 이 기능으로 충분하다. 하지만 좀 더 복잡한 로직을 수행하려면 logback으로만 필터링을 하기에는 다소 어렵다.
  • 좀 더 쿼리를 세세하게 짤 수 있는 p6spy를 활용하여 요구사항에 따라 로그를 작성할 수 있었다.

추가… ILoggingEvent 활용하기

  • 필터를 자바 파일로 추출할 수 있다. 컴파일 시점에서 문법오류를 잡기 때문에 xml보다 나은 방식이다.
  • xml 파일이 장황해지는 것을 방지한다.
  • 의존성 janino가 필요 없다.
<filter class="study.querydsl.log.IgnoreLogFilter"/>
<!-- 이하 주석 처리한다.
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
  <evaluator>
      <expression>if (message.contains("insert")){ return true; } return false;</expression>
  </evaluator>
  <OnMismatch>ACCEPT</OnMismatch>
  <OnMatch>DENY</OnMatch>
</filter>
-->
package study.querydsl.log;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

public class IgnoreLogFilter extends Filter<ILoggingEvent> {
    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (event.getMessage().contains("insert") ) {
            return FilterReply.DENY;
        } else {
            return FilterReply.ACCEPT;
        }
    }
}

p6spy의 활용

  • 다음의 의존성을 추가한다.
  • implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'

  • logback-spring.xml에서 아래의 로깅 레벨을 INFO로 변경한다. p6spy는 데이타소스를 데코레이션 패턴으로 감싸기 때문에, 로깅 레벨이 필요 없다. (이로 인하여 성능 문제가 있다. 운영에서 쓰면 안된다.)
<logger name="org.hibernate.SQL" level="INFO" additivity="true" /> <!--sql 로그-->
<logger name="org.hibernate.type.descriptor.sql" level="INFO" additivity="true" /> <!--파라미터 로그-->
  • 로그백의 필터를 주석처리한다. insert 쿼리가 발생하도록 한다.
<!--<filter class="study.querydsl.log.IgnoreLogFilter"/>-->
  • 프로퍼티스에서 아래와 같이 p6spy의 사용 여부를 true로 한다. default가 true 이므로 생략해도 된다.
decorator:
  datasource:
    p6spy:
      enable-logging: true
  • 이렇게 설정하면 아래와 같이 로그가 발생한다. 좀 더 구체적인 정보가 노출된다. 파라미터가 입력된 상태로 노출되어 보기 더 편해졌다.
2022-07-23 11:02:52.015 [Test worker] INFO  p6spy - #1658541772015 | took 1ms | statement | connection 3| url jdbc:h2:mem:test
insert into team (name, team_id) values (?, ?)
insert into team (name, team_id) values ('teamA', 1);
2022-07-23 11:02:52.040 [Test worker] INFO  p6spy - #1658541772040 | took 0ms | statement | connection 3| url jdbc:h2:mem:test
select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=?
select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=1;
2022-07-23 11:02:52.070 [Test worker] INFO  p6spy - #1658541772070 | took 0ms | rollback | connection 3| url jdbc:h2:mem:test
  • 만약 필터를 로그백에서 추가하면 어떨까? 그럼 아래와 같이 로그가 발생한다. insert 로그가 발생하지 않음을 확인할 수 있다.
<!-- 필터를 사용한다 -->
<filter class="study.querydsl.log.IgnoreLogFilter"/> 
call next value for hibernate_sequence;
2022-07-23 11:05:28.892 [Test worker] INFO  p6spy - #1658541928892 | took 0ms | statement | connection 3| url jdbc:h2:mem:test
select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=?
select team0_.team_id as team_id1_2_0_, team0_.name as name2_2_0_ from team team0_ where team0_.team_id=1;
2022-07-23 11:05:28.936 [Test worker] INFO  p6spy - #1658541928936 | took 0ms | rollback | connection 3| url jdbc:h2:mem:test

p6spy의 포매터

  • p6spy은 데이터를 로그로 변환할 때 MessageFormattingStrategy을 기준으로 한다. 위의 로깅 방식을 수정하려면 MessageFormattingStrategy 을 상속한 객체를 구현한 후, 해당 포매터를 바꿔야 한다. 구체적인 내용은 다음의 링크를 참고 바란다. 이번 p6spy를 설정하는데 절대적인 도움을 받았다!! https://github.com/shirohoo/p6spy-custom-formatter
  • 위의 블로그를 참고하여 작성한, 특정 쿼리의 로그를 누락하기 위한 코드는 아래와 같다.
package study.querydsl.log;

import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import org.hibernate.engine.jdbc.internal.FormatStyle;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.Objects;
import java.util.Stack;
import java.util.function.Predicate;

import static java.util.Arrays.stream;

// 출처 :
// https://github.com/shirohoo/p6spy-custom-formatter
public class MyP6spyFormattingStrategy implements MessageFormattingStrategy {
    private static final String NEW_LINE = System.lineSeparator();
    private static final String P6SPY_FORMATTER = "MyP6spyFormattingStrategy";
    private static final String PACKAGE = "study.querydsl";  // 패키지를 설정해야 메서드 스택을 확인할 수 있다. 
    private static final String CREATE = "create";
    private static final String ALTER = "alter";
    private static final String COMMENT = "comment";


    @Override
    public String formatMessage(final int connectionId, final String now, final long elapsed, final String category, final String prepared, final String sql, final String url) {
        return sqlFormatToUpper(sql, category, getMessage(connectionId, elapsed, getStackBuilder()));
    }

    private String sqlFormatToUpper(final String sql, final String category, final String message) {
        if (Objects.isNull(sql.trim()) || sql.trim().isEmpty()) {
            return "";
        }

        return new StringBuilder()
                // .append(NEW_LINE)
                .append(sqlFormatToUpper(sql, category))
                .append(message)
                .toString();
    }

    private String sqlFormatToUpper(final String sql, final String category) {
        if (isStatementDDL(sql, category)) {
            return FormatStyle.DDL
                    .getFormatter()
                    .format(sql)
                    .toUpperCase(Locale.ROOT)
                    .replace("+0900", "");
        }
        return FormatStyle.BASIC
                .getFormatter()
                .format(sql)
                .toUpperCase(Locale.ROOT)
                .replace("+0900", "");
    }

    private boolean isStatementDDL(final String sql, final String category) {
        return isStatement(category) && isDDL(sql.trim().toLowerCase(Locale.ROOT));
    }

    private boolean isStatement(final String category) {
        return Category.STATEMENT.getName().equals(category);
    }

    private boolean isDDL(final String lowerSql) {
        return lowerSql.startsWith(CREATE) || lowerSql.startsWith(ALTER) || lowerSql.startsWith(COMMENT);
    }

    private String getMessage(final int connectionId, final long elapsed, final StringBuilder callStackBuilder) {
        return new StringBuilder()
                .append(NEW_LINE)
                .append(NEW_LINE)
                .append("\t").append(String.format("Connection ID: %s", connectionId))
                .append(NEW_LINE)
                .append("\t").append(String.format("Execution Time: %s ms", elapsed))
                .append(NEW_LINE)
                .append(NEW_LINE)
                .append("\t").append(String.format("Call Stack (number 1 is entry point): %s", callStackBuilder))
                .append(NEW_LINE)
                .append(NEW_LINE)
                .append("----------------------------------------------------------------------------------------------------")
                .toString();
    }

    private StringBuilder getStackBuilder() {
        final Stack<String> callStack = new Stack<>();
        stream(new Throwable().getStackTrace())
                .map(StackTraceElement::toString)
                .filter(isExcludeWords())
                .forEach(callStack::push);

        int order = 1;
        final StringBuilder callStackBuilder = new StringBuilder();
        while (!callStack.empty()) {
            callStackBuilder.append(MessageFormat.format("{0}\t\t{1}. {2}", NEW_LINE, order++, callStack.pop()));
        }
        return callStackBuilder;
    }

    private Predicate<String> isExcludeWords() {
        return charSequence -> charSequence.startsWith(PACKAGE) && !charSequence.contains(P6SPY_FORMATTER);
    }
}
  • 위의 코드를 설정하기 위한 설정 코드는 아래와 같다.
package study.querydsl.log;

import com.p6spy.engine.spy.P6SpyOptions;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;

@Configuration
public class LogConfiguration {
    @PostConstruct
    public void setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(MyP6spyFormattingStrategy.class.getName());
    }
}
  • 로그백 필터는 아래와 같이 수정한다. SQL을 대문자로 변경하여, 이에 대응하도록 수정하였다.
package study.querydsl.log;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;

public class IgnoreLogFilter extends Filter<ILoggingEvent> {
    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (event.getMessage().contains("INSERT") || event.getMessage().length()==0 ) {
            return FilterReply.DENY;
        } else {
            return FilterReply.ACCEPT;
        }
    }
}
  • 그럼 아래와 같이 로그가 발생하며, insert 쿼리가 깔끔하게 제거됨을 확인할 수 있다.
2022-07-23 11:21:51.246 [Test worker] INFO  p6spy - 
    CALL NEXT VALUE FOR HIBERNATE_SEQUENCE

	Connection ID: 3
	Execution Time: 17 ms

	Call Stack (number 1 is entry point): 
		1. study.querydsl.entity.TeamTest.test(TeamTest.java:26)

----------------------------------------------------------------------------------------------------
2022-07-23 11:21:51.406 [Test worker] INFO  p6spy - 
    SELECT
        TEAM0_.TEAM_ID AS TEAM_ID1_2_0_,
        TEAM0_.NAME AS NAME2_2_0_ 
    FROM
        TEAM TEAM0_ 
    WHERE
        TEAM0_.TEAM_ID=1

	Connection ID: 3
	Execution Time: 0 ms

	Call Stack (number 1 is entry point): 
		1. study.querydsl.entity.TeamTest.test(TeamTest.java:32)

----------------------------------------------------------------------------------------------------
  • 위 로그의 특징은, 파라미터가 붙은 쿼리를 생성하기 때문에, 필터링에 유리하다.
  • study.querydsl.entity.TeamTest.test(TeamTest.java:32) 을 통해 마이바티스에서 한 것처럼 어떤 메서드가 sql을 호출했는지 알 수 있다. Throwable을 통해 스택으로 쌓인 메서드 목록을 추출함을 확인할 수 있다.
  • 이러한 조건에서 필터링의 기준을 다양하게 잡을 수 있다. Queryhints를 사용하여 주석을 달아서 필터링 여부를 결정할 수 있다. 패키지와 매서드명을 기준으로 할 수 있다. 여러 응용이 가능하다.

나아가며

  • JPA, 로그백, p6spy까지 다양한 라이브러리의 로깅에 대하여 정리하였다.
  • System.out, 로거, 데코레이션 패턴 등 매우 다양한 방식으로 로깅을 할 수 있다. 처음 내가 로그를 세팅할 때, 각각 분리하여 생각하지 않아 많은 어려움을 느꼈다. 나같은 삽질을 안하기를 바라며 글을 남긴다.
  • 기타 이 문제를 해소하는데 도움을 준 글은 아래와 같다.
  • https://github.com/shirohoo/p6spy-custom-formatter
  • https://logback.qos.ch/