티스토리 뷰

JAVA

JPA로 생성된 쿼리에 주석 붙이기

Voyager Woo 2019. 12. 13. 07:43
반응형

최근에는 명령(Command)과 조회(Query) 모델을 분리해서 개발하고 있다. 그래서 JPA로 명령 모델에서 쓰이는 엔티티와 조회 모델에서 쓰는 엔티티를 따로 만든다. 그런데 생성, 삭제, 변경 류의 명령(command) 모델에서 생성된 쿼리들은 간단하고 식별하기 쉽지만, 조회 모델에서 Hibernate가 만든 복잡한 쿼리들은 이게 어떻게 실행된 쿼리인지 식별하기 어렵다. 현재 회사에서는 DBA가 가끔 슬로쿼리를 뽑아서 전달해 주는데, 해당 쿼리가 어디서 실행된 쿼리인지 찾는데 좀 오래걸린 경험이 있다. 그래서 조회용 쿼리에 주석을 붙여서 찾기 쉽게 해보려고 한다.

관련 코드는 아래 깃헙 링크에 있다. 코드는 코틀린으로 작성되어 있다.
https://github.com/voyagerwoo/springboot-hello-world

application.properties 설정

application.properties

spring.jpa.properties.hibernate.use_sql_comments 이 부분이 중요하다.

(참고로, schema.sqldata.sql은 임베디드 데이터베이스 일때만 실행된다. 참고 링크: https://stackoverflow.com/questions/49335967/spring-boot-not-running-schema-sql/52056124)

spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

spring.datasource.schema=classpath:schema-book.sql
spring.datasource.data=classpath:data-book.sql
spring.jpa.hibernate.ddl-auto=none

application-mariadb.properties

실제 DB 로그에 주석이 추가가 되었는지 확인하기 위한 mariadb 접속 설정이다. 간단하게 도커로 띄워서 확인해볼 예정이다.

spring.datasource.url=jdbc:mariadb://127.0.0.1:3307/book
spring.datasource.username=root
spring.datasource.password=password
spring.datasource.driver-class-name=org.mariadb.jdbc.Driverv
spring.jpa.database-platform=org.hibernate.dialect.MariaDB102Dialect

코드 작성

엔티티

package vw.helloworld.book

import java.time.LocalDateTime
import javax.persistence.*

@Entity
data class Book (
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        var id: Long? = null,
        val isbn: String,
        var name: String,
        var author: String,
        val publishedDate: LocalDateTime,
        val publisher: String
)

레포지토리

JPA에서 쿼리 힌트를 이용하여 주석을 설정한다.

package vw.helloworld.book

import org.springframework.data.jpa.domain.Specification
import org.springframework.data.jpa.repository.QueryHints
import org.springframework.data.repository.Repository
import javax.persistence.QueryHint

interface BookRepository : Repository<Book, Long> {
    @QueryHints(value = [QueryHint(name="org.hibernate.comment", 
            value="BookRepository.findAll")])
    fun findAll(): List<Book>

    @QueryHints(value = [QueryHint(name="org.hibernate.comment", 
            value="BookRepository.findAllBySpecs")])
    fun findAll(specs: Specification<Book>): List<Book>
}

참고로 스프링 데이터의 스팩(Specification)으로 생성된 쿼리도 잘 나오는 지 확인하기 위해서 스팩도 추가했다.

(참고링크: https://javacan.tistory.com/entry/SpringDataJPA-Specifcation-Usage)

Api(Controller) 작성

package vw.helloworld.book

import org.springframework.data.jpa.domain.Specification
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
class BookApi(private val bookRepository: BookRepository) {

    @GetMapping("/all-books")
    fun findAll(): List<Book> {
        return bookRepository.findAll()
    }

    @GetMapping("/books")
    fun findAllBySpecs(@RequestParam nameContains: String,
                       @RequestParam(required = false) author: String?): List<Book> {
        var specs = Specification
                .where(BookSpecs.nameContains(nameContains))!!
        if (author != null && author.isNotBlank())
            specs = specs.and(BookSpecs.author(author))!!

        return bookRepository.findAll(specs)
    }
}

테스트

mariadb 컨테이너 실행

docker run -d --name mariadb \
        -e MYSQL_ROOT_PASSWORD=password \
        -p 3307:3306 \
        mariadb:10.4.10-bionic

dababase 및 테이블 생성

컨테이너 내부로 들어간다.

docker exec -it mariadb bash

컨테이너 내부에서 mysql 클라이언트로 접속한다.

mysql -p -u root

데이터베이스와 테이블을 생성한다.

create database book;
create table if not exists book (
    id bigint NOT NULL AUTO_INCREMENT,
    author varchar(255),
    isbn varchar(255),
    name varchar(255),
    published_date timestamp,
    publisher varchar(255),
    primary key (id)
);

테스트용 데이터를 하나 추가한다.

insert into book (id, author, isbn, name, published_date, publisher) values (null, 'Joel', '919-11-1241-123-5', 'Joel Recipe', '2019-11-25 05:33:33', 'Wikibooks');

현재 mysql 클라이언트가 실행중인 터미널은 종료하지 않고 다른 작업할 때 계속 사용한다.

(참고로, mariadb는 한글 설정이 필요하다. 이번 실습에서는 중요하지 않기 때문에 하지 않았고 데이터를 영문으로 추가했다.)

스프링 애플리케이션 실행

자기 편한대로 실행하면 된다. mariadb 프로파일로 실행한다.

./mvnw spring-boot:run -Dspring-boot.run.profiles=mariadb

curl 명령어로 테스트 해본다.

curl http://localhost:8080/all-books

(참고로, JSON을 예쁘게 보고 싶다면 curl http://localhost:8080/all-books | jq . 이렇게 명령어를 실행한다.)

mariadb 로그 설정

mariadb의 쿼리 로그를 보기 위해서 mariadb general log 설정을 한다.
(참고 링크: https://mariadb.com/kb/en/library/general-query-log/)

general log는 클라이언트에서 요청받은 모든 SQL 쿼리와 각 클라이언트 연결 및 연결 해제에 대한 로그로 굉장히 상세한 로그이다. 런타임에 설정을 활성화/비활성화 할 수 있다. 로그는 파일로도 저장할 수 있고, 테이블에도 저장할 수 있는데 이번 실습은 로그에 주석이 남는지에 대해서 확인하는 것이므로 테이블에 저장하도록 한다.

mysql 클라이언트에서 아래 쿼리를 실행한다.

SET GLOBAL general_log = 1;
SET GLOBAL log_output = 'TABLE';

다음의 쿼리로 로그를 확인할 수 있다.

select * from mysql.general_log;

최종 확인

스프링 서버에 요청을 날려본다.

curl http://localhost:8080/all-books
curl http://localhost:8080/books?nameContains=Joel

mysql 클라이언트에서 로그를 확인한다.

select * from mysql.general_log;

결론

JPA 조회용 모델에서 생성된 쿼리를 식별하는데 편리하기 위해서 주석을 추가하고 잘 동작하는 것을 확인했다.

실제 운영 프로젝트의 코드에 추가할까 고민했지만, 아직은 하지 않았다. 어노테이션으로 Repository에 추가되는 형태가 마음에 들지 않았고, 자동화된 주석이 아닌 직접 작성하는 주석이기 때문이었다. 아직 좀 더 나은 해결책을 고민해보고 있다.

참고 링크

반응형
댓글
댓글쓰기 폼