기존 sql문 로깅 방식으로는 SQL 문과 인자를 따로 보여주어 SQL 문을 한 번에 보기 힘들다는 단점이 있습니다.
p6spy
를 이용하면 SQL 문과 인자를 조합하여 한 번에 보여주어 보기 편리해 보였습니다.
이 글을 통해 p6spy
설정을 익히고 다음번에도 손쉽게 사용할 수 있도록 글을 정리해 보겠습니다.
기존 로깅 환경에서 SQL 문을 확인한 후 p6spy
설정을 추가하며 변경해 보겠습니다.
아래는 기존 로깅 코드입니다.
// build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// h2
runtimeOnly 'com.h2database:h2'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
# application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
logging.level:
# SQL 문을 기록합니다.
org.hibernate.SQL: debug
# Hibernate6 JDBC 매개변수에 바인딩된 값을 기록합니다.
org.hibernate.orm.jdbc.bind: trace
위와 같이 Spring Boot 환경을 구성하면 로깅으로 SQL 문이 기록됩니다.
아래는 테스트에서 사용할 엔티티와 레포지토리 및 테스트 코드입니다.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Entity
public class Member {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
private String name;
}
@Transactional(readOnly = true)
@Repository
public class MemberRepository {
@PersistenceContext
EntityManager entityManager;
@Transactional
public Long save(Member member) {
entityManager.persist(member);
return member.getId();
}
public Member find(Long id) {
return entityManager.find(Member.class, id);
}
}
@Transactional
@SpringBootTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@DisplayName("멤버를 저장하면 아이디 값을 부여합니다.")
@Test
void test() {
// given
Member member = Member.builder()
.name("새로운 멤버")
.build();
// when
Long savedId = memberRepository.save(member);
// then
assertThat(savedId).isNotNull();
}
}
SQL 문과 바인딩된 값을 잘 출력합니다. 하지만 로그가 2개로 나누어져 보는데 불편함이 있습니다.
P6spy
를 이용하게 되면 기존 애플리케이션 코드 변경 없이 Connection
, Statement
, ResultSet
를 가로채어 기록해줍니다.
Spring Boot
에 설정하는 방법도 간단합니다. 깃허브 문서에 따라 설정해 보겠습니다.
dependencies {
// p6spy
testImplementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.1'
}
P6spy
의존을 추가해줍니다. application.yml
에 추가 커스텀할 수 있습니다. 하지만 기본 설정으로도 충분합니다.
또한, testImplementation
로 테스트 환경의 의존만 추가해주었습니다.
로그를 찍는 것이 성능 저하를 일으킬 수 있고, SQL 문을 포매팅해준다면 더 큰 성능 저하로 이루어질 수 있기 때문에 테스트 환경에서만 사용할 예정입니다.
여기서 2가지를 수정해야 합니다.
p6spy
를 둘 다 기록합니다. 기존 로깅을 제거해야 합니다.p6spy
이 포매팅 되어 있지 않습니다. 추가 설정을 통해 포매팅 해주어야 합니다.# application.yml
spring:
jpa:
properties:
hibernate:
format_sql: true
logging.level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
application.yml
에서 위에 보이는 로깅 관련 속성들을 제거해줍니다.
p6spy
관련 로그들만 남은 것을 볼 수 있습니다.
p6spy 동작 방식과 코드
p6spy JdbcEventListener를 만들어야 하는 이유
포매팅하는 코드는 위 2개의 글을 참조하여 만들었습니다. 자세한 동작 방식과 이유들을 알려줍니다.
public class P6spyPrettySqlFormatter implements MessageFormattingStrategy {
private static final String NEW_LINE = System.lineSeparator();
private static final String P6SPY_FORMATTER = "P6spyPrettySqlFormatter";
private static final String PACKAGE = "io.p6spy";
private static final String CREATE = "create";
private static final String ALTER = "alter";
private static final String COMMENT = "comment";
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared,
String sql, String url) {
return sqlFormatToUpper(sql, category, getMessage(connectionId, elapsed, getStackBuilder()));
}
private String sqlFormatToUpper(String sql, String category, String message) {
if (sql.trim().isEmpty()) {
return "";
}
return NEW_LINE
+ sqlFormatToUpper(sql, category)
+ message;
}
private String sqlFormatToUpper(String sql, 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(String sql, String category) {
return isStatement(category) && isDDL(sql.trim().toLowerCase(Locale.ROOT));
}
private boolean isStatement(String category) {
return Category.STATEMENT.getName().equals(category);
}
private boolean isDDL(String lowerSql) {
return lowerSql.startsWith(CREATE) || lowerSql.startsWith(ALTER) || lowerSql.startsWith(COMMENT);
}
private String getMessage(int connectionId, long elapsed, StringBuilder callStackBuilder) {
return NEW_LINE
+ NEW_LINE
+ "\t" + String.format("Connection ID: %s", connectionId)
+ NEW_LINE
+ "\t" + String.format("Execution Time: %s ms", elapsed)
+ NEW_LINE
+ NEW_LINE
+ "\t" + String.format("Call Stack (number 1 is entry point): %s", callStackBuilder)
+ NEW_LINE
+ NEW_LINE
+ "----------------------------------------------------------------------------------------------------";
}
private StringBuilder getStackBuilder() {
Stack<String> callStack = new Stack<>();
stream(new Throwable().getStackTrace())
.map(StackTraceElement::toString)
.filter(isExcludeWords())
.forEach(callStack::push);
int order = 1;
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<? super String> isExcludeWords() {
return charSequence -> charSequence.startsWith(PACKAGE) && !charSequence.contains(P6SPY_FORMATTER);
}
}
public class P6SpyEventListener extends JdbcEventListener {
@Override
public void onAfterGetConnection(ConnectionInformation connectionInformation, SQLException e) {
P6SpyOptions.getActiveInstance().setLogMessageFormat(P6spyPrettySqlFormatter.class.getName());
}
}
@Configuration
public class P6SpyConfig {
@Bean
public P6SpyEventListener p6SpyCustomEventListener() {
return new P6SpyEventListener();
}
@Bean
public P6spyPrettySqlFormatter p6SpyCustomFormatter() {
return new P6spyPrettySqlFormatter();
}
}
로그가 Data Definition Language
에서 Data Manipulation Language
까지 읽기 쉬운 형식으로 기록되는 것을 볼 수 있습니다.