본문 바로가기
데이터 접근 기술

Hibernate Best Practices

by oncerun 2023. 2. 1.
반응형

Thorben Janssen

 

Home - Thorben Janssen

Thrive Leads Shortcode could not be rendered, please check it in Thrive Leads Section! As seen at: Bring your Java persistence skills to the next…

thorben-janssen.com

 

JPA와 Hibernate에 대해서 조금 더 알아야 할 것 같아서 best practices의 글을 읽으면서 정리하고 공부하는 시간을 가졌다.

 

Use a projection that fits your use case

 

Projection은 쿼리의 검색 결과에서 반환되는 속성을 제한하는 기능을 말합니다.

 

즉 하나의 엔티티를 조회할 때 모든 속성을 가져오는 것이 아닌 필요한 속성만을 선택적으로 반환할 수 있도록 하는 기능입니다.

 

우리가 SELECT문을 작성할 때 우리의 use case에 맞춰 칼럼들을 가져와야 합니다.

 

JPA와 Hibernate는 더 많은 projections을 지원합니다. 최근에는 Spring-data-jpa에서도 projections에 대한 기능이 개선이 되었던 걸로 기억합니다.

 

Entity

em.find(Author.class, 1L);

가장 일반적인 projection으로 모든 특성이 필요할 때와 적은 수의 엔티티에만 영향을 주는 업데이트 또는 삭제 작업에 사용되어야 합니다.

 

 

POJO

POJO projection은 Entity projection과 유사하지만 우리가 필요한 사례에 따라서 특정 데이터 베이스 레코드를 생성할 수 있습니다.

우리가 엔티티에 대한 아주 작은 속성들만 필요한 경우나 몇몇의 연관관계가 있는 엔티티의 속성이 필요한 경우 아주 유용할 수 있습니다.

List <BookPublisherValue> bookPublisherValues = em.createQuery(
"SELECT new org.thoughts.on.java.model.BookPublisherValue(b.title, b.publisher.name) FROM Book b",
BookPublisherValue.class). getResultList();

 

Scalar Values

 

상당히 특이한 구조여서 테스트를 좀 진행해 보았다.

속성에 명시된 칼럼들을 하나의 배열로 묶고 그 타입을 Object로 한 List를 반환한다.

List<Object[]> resultList = entityManager
        .createQuery("select c.id, c.categoryStatus from Categories c", Object[].class)
        .getResultList();

이는 자주 사용되지 않는 projections의 종류인데 그 이유는 Object [] 타입으로 반환을 하기 때문이다.

이를 사용하는 경우는 즉시 비즈니스 로직에서 사용되거나 적은 속성들을 원할 때 사용한다고 한다.

추가적으로 특정 값에 대해서만 추출하려고 할 때 POJO projections이 더 좋은 선택지라고 한다.

이는 큰 속성을 조회하는 것과 더불어 쿼리 결과를 다른 시스템으로 보내기에도 적절한 옵션이라고 한다.

Use the kind of query that fits your use case

JPA와 Hibernate는 쿼리를 정의하기 위해 명시적이거나 암묵적인 여러 옵션을 제공합니다.

이러한 옵션들 중 어느 것 하나도 모든 사용 사례에 적합한 것은 없기 때문에 이러한 옵션은 개발자가

가장 적합하다고 생각하는 옵션을 선택해야 함을 의미합니다.

 

EntityManager.find()

entityManager.find(Item.class, 3L);

엔티티 매니저의 find() method는 기본키를 사용하여 엔티티를 얻는 가장 쉬운 방법일 뿐만 아니라

성능 및 보안에 대한 이점도 제공합니다.

잘 아시다시피 Hibernate는 쿼리를 실행하기 전에 1차, 2차 캐시를 확인하여 cache hit이면 조회쿼리를 보내지 않고 영속성 콘텍스트에 보관되어 있는 엔티티를 반환하기 때문에 성능상 이점이 존재합니다.

또한 Hibernate는 쿼리를 생성함에 있어서 SQL injection을 방어하기 위해 기본 키값을 파라미터로 설정합니다. 이는 단순 문자열을 이어 붙여서 쿼리를 만드는 구조가 아니라 준비된 템플릿 쿼리에 파라미터를 매개변수로 전달받는 구조이기 때문에 좀 더 보안적이라고 말하는 것 같습니다.

 

JPQL

 

이는 Java Persistence Query Language의 줄임말로 JPA의 표준이며 SQL과 매우 비슷합니다.

SQL과 다르게 테이블 대신 엔티티 및 엔티티 관계에서 작동합니다.

JPQL을 사용하면 적거나 보통정도인 복잡도를 가진 쿼리를 만들 수 있습니다.

 

Criteria API

우선 Criteria API는 런타임에 동적 쿼리를 작성하기 쉽습니다.

하지만 코드의 가독성을 개선하려면 많은 리팩토링 작업이 필요하고 그리 직관적이지 못해 주목받지는 못하고 있습니다.

이 대신 우리는 Querydsl을 사용하려고 합니다. 저도 Querydsl을 공부하고 있습니다.

 

Native Queries

 

Native 쿼리는 말 그대로 일반 SQL문을 작성하는 것을 말합니다. 이는 매우 복잡한 쿼리를 작성해야 할 때 가장 적합한 방법입니다.

혹은 특정 데이터베이스 스펙에 맞는 쿼리를 작성해야 할 때 이용할 수 있습니다.

MyEntity e = (MyEntity) em.createNativeQuery(

“SELECT * FROM myentity e WHERE e.jsonproperty->’longProp’ = ‘456’“,

MyEntity.**class**). getSingleResult();

I explain native queries in more detail in Native Queries – How to call native SQL queries with JPA  and How to use native queries to perform bulk updates. (참고하세요)

 

Use static Strings for named queries and parameter names

 

@NamedQuery(name = Author.QUERY_FIND_BY_LAST_NAME,
query = “SELECT a FROM Author a WHERE a.lastName = :” + Author.PARAM_LAST_NAME)
@Entity
public class Author {
 
  public static final String QUERY_FIND_BY_LAST_NAME = “Author.findByLastName”;
  public static final String PARAM_LAST_NAME = “lastName”;
 
  …
 
}

Query q = em.createNamedQuery(Author.QUERY_FIND_BY_LAST_NAME);
q.setParameter(Author.PARAM_LAST_NAME, “Tolkien”);
List<Author> authors = q.getResultList();

NamedQuery와 해당 매개 변수의 이름을 정적 문자열로 정의하면 더 쉽게 작업할 수 있다곤 한다

Specify natural identifier

고유 키 대신 대리키를 사용하도록 결정된 경우에 우리는 natural identifiers를 사용할 수 있다.

어떠한 정책에 의해서 기본 키를 사용하지 못하고 대리 키를 사용해야 하는 경우가 있을지는 모르겠지만 대리 키를 기본 키로 사용하기로 결정했다면 자연 식별자를 지정해주어야 합니다.

예를 들어 ISBN과 같이 정말 unique key를 사용하게 되었다면 다음과 같이 설정해 주면 됩니다.

Hibernate를 사용하면 엔티티의 자연 식별자로 모델링할 수 있고 검색을 할 수 있습니다.

단순히 @NaturalId 어노테이션을 unique key에 추가하면 됩니다.

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = id, updatable = false, nullable = false)
    private Long id;

    @NaturalId
    private String isbn;

}
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);

Book b = session.byNaturalId(Book.class).using(Book_.isbn.getName(), “978-0321356680”).load();

하지만 성능상 문제가 되는 부분이 있는데 1차 2차 캐시를 확인할 때 영속성 콘텍스트에는 기본 키로 저장되어 있기 때문에 쿼리가 2번 발생한다.

Log and analyze all queries during development

 

의도치 않게 실행된 쿼리가 너무 많은지 로깅을 해야 합니다. 대부분의 성능 저하 원인이 의도치 않은 쿼리작동 방식으로 부터 발생합니다.

그 이유는 JPA, Hibernate는 데이터 베이스와의 상호작용을 API 뒤에 모두 감추기 때문에 아주 특별한 경우에 대해 수행할 쿼리 수를 예측하기 어려운 경우가 많습니다.

이 문제를 해결하는 가장 좋은 방법은 개발 중에 모든 SQL문을 기록하고 구현 작업을 완료하기 전에 이를 분석하는 것이라고 합니다.

org.hibernate의 로그 수준을 설정하여 SQL문을 디버깅할 수 있습니다.

org.hibernate 이 범주에는 Hibernate가 작성한 모든 메시지가 포함됩니다. 이를 사용하여 특정하지 않은 문제를 분석하거나 Hibernate에서 사용하는 범주를 찾을 수 있습니다. 이 범주를 정밀한 로그 수준으로 설정하면 많은 로그 출력이 생성될 수 있습니다.

org.hibernate.SQL JDBC를 통해 실행되는 모든 SQL 문은 이 범주에 기록됩니다 . JDBC 매개 변수 및 결과에 대한 자세한 정보를 얻기 위해 org.hibernate.type.descriptor.sql 또는 org.hibernate.orm.jdbc.bind 와 함께 사용할 수 있습니다 .
org.hibernate.type.descriptor.sql Hibernate 4 및 5Hibernate는 JDBC 매개변수에 바인딩되고 JDBC 결과에서 추출된 값을 이 범주에 기록합니다. 이 범주는 org.hibernate.SQL과 함께 사용되어 SQL 문도 기록해야 합니다.
org.hibernate.orm.jdbc.bind Hibernate 6Hibernate는 JDBC 매개변수에 바인딩된 값을 이 범주에 기록합니다. 이 범주는 org.hibernate.SQL과 함께 사용되어 SQL 문도 기록해야 합니다.
org.hibernate.SQL_SLOW Hibernate >= 5.4.5Hibernate는 SQL 문 실행이 구성된 임계값보다 오래 걸리는 경우 슬로우 쿼리 로그에 메시지를 기록합니다( https://thorben-janssen.com/hibernate-slow-query-log/ 참조 ).
org.hibernate.pretty Hibernate는 최대 플러시 시간에 상태를 기록합니다. 이 범주에 20개의 엔터티가 있습니다.
org.hibernate.cache 두 번째 수준 캐시 작업에 대한 정보가 이 범주에 기록됩니다.
org.hibernate.stat Hibernate는 각 쿼리에 대한 일부 통계를 이 범주에 기록합니다. 통계는 별도로 활성화해야 합니다( https://thorben-janssen.com/how-to-activate-hibernate-statistics-to-analyze-performance-issues/ 참조 ).
org.hibernate.hql.internal.ast.AST Hibernate 4 및 5이 범주는 쿼리 구문 분석 중에 HQL 및 SQL AST를 그룹화합니다.
org.hibernate.tool.hbm2ddl Hibernate는 이 로그 범주에 대한 스키마 마이그레이션 중에 실행된 DDL SQL 쿼리를 작성합니다.

show_sql을 사용하여 SQL을 기록하지 말라고 한다.

show_sql을 사용하면 표준 출력을 통해 SQL문을 기록하기 때문에 성능이 저하되고 로그 파일을 정의하기가 어려워진다.

이보다 더 좋은 로깅을 활성화하는 방법은 org.hibernate.SQL 로그 수준을 DEBUG로 설정하는 것입니다.

logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

dev 환경 수준에서는 다음과 같은 수준의 로그를 활성화하는 것을 추천드립니다.

 

1. Hibernate statistics은 쿼리의 통계를 로그로 보여줍니다.

 

스프링 부트에 저는 다음과 같이 dev 환경에 설정되어 있습니다

logging.level.org.hibernate.stat=DEBUG spring.jpa.properties.hibernate.generate_statistics=true

stat 로그에는 실행된 jpql 쿼리와 실행 시간 및 총 rows 수가 로그로 남으며 statistics을 활성화시킨 경우 사용된 JDBC 연결 및 명령문 수, 캐시 사용량, 수행된 플러쉬 정보를 제공합니다.

 

여기서 우리는 쉽게 N +1문제를 확인할 수 있고 이 사용됐는지도 확인할 수 있어 다양한 방법으로 개발단계에서 여러 문제를 확인하고 고칠 수 있습니다.

 

2023-02-01 21:41:44.213  INFO 19944 --- [           main] i.StatisticalLoggingSessionEventListener : Session Metrics {
    651900 nanoseconds spent acquiring 1 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    4958900 nanoseconds spent preparing 15 JDBC statements;
    6825800 nanoseconds spent executing 15 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    17065100 nanoseconds spent executing 2 flushes (flushing a total of 12 entities and 4 collections);
    23500 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

 

이 로그를 보면 1개의 JDBC 커넥션을 얻는 시간과, 

커넥션을 반환하는데 걸린 시간, 

15개의 SQL문을 준비하는데 걸린시간, 실행하는데 걸린 시간,

2번의 flushes를 하는데 소모된 시간 ( 플러시는 영속성 컨텍스트의 모든 변경사항을 데이터베이스와 동기화하는 시간)

 

partial-flushes는 데이터베이스와의 통신을 최적화하기 위한 기술로 영속성 컨텍스트의 변경된 부분만 데이터베이스에 반영하는 것을 의미합니다. 전체 플러쉬 보다는 더욱 빠르게 디비에 반영할 수 있고 트랜잭션의 성능을 향상시킬 수 있습니다. 

 

 

2. Hibernate 5.4.5 이상이라면 Slow query log를 활성화시키는 것을 추천드립니다

spring.jpa.properties.hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS=20

 

org.hibernate.SQL_SLOW에 대한 로그가 찍히면서 20ms를 초과하는 쿼리에 대해 로그가 발생합니다.

 

여기서 주의할 점은 설정된 임계값은 Hibernate의 준비 또는 결과를 처리하는 단계를 포함하지 않는 쿼리의 순수한 실행 시간이라는 점입니다.

 

또한 SQL_SLOW 가 5.2 기준으로 deprecated 되면서 SQL_SLOW는 OFF 해버렸기 때문에 저는 다음과 같이 SQL_SLOW를 off 시켰습니다.

logging.level.org.hibernate.SQL_SLOW=off

슬로 쿼리 발생 시 com.zaxxer.hikari.pool.HikariPool에서 로그를 찍어준다.

 

Production 환경에서는 로그는 다음을 추천합니다.

 

문제를 분석할 필요가 없는 한 가능한 적은 정보를 기록해야 합니다.

이는 간단하게 Hibernate 관련 로그를 ERROR로 설정하면 됩니다. 그리고 통계와 느린 쿼리로그는 비활성화해야 합니다.

 

운영 환경에서는 정말 필요한 로그만을 남기도록 구성하는 것이 좋습니다.

 

이는 운영 환경마다 다르겠지만 저는 대부분 로그 기준을 ERROR로 설정하기도 했고 최상위 로그 레벨을 ERROR로 변경했습니다.  그리고 통계나 슬로쿼리 기능을 끄고 최대한 가볍게 배포하였습니다.

 

Don’t use FetchType.EAGER

 

필요 없는 오버헤드를 방지하기 위해 즉시로딩보다는 지연 로딩으로 설정하라고 조언합니다.

만약 데이터가 즉시 필요한 경우 fetch join이나 batchsize를 이용하여 최적화를 진행하고 설정은 fetchType.LAZY를 유지하는 것을 추천드립니다.

 

Initialize required lazy relationships with the initial query

FetchType.LAZY 옵션은 필요할 때 관련 엔티티를 조회하는 기능을 말합니다.

이렇게 하면 특정 성능 문제를 방지할 수 있지만 이는 LazyInitializationException을 발생시키는 원인이거나 n + 1의 발생 원인이 될 수 있습니다.

두 가지 문제를 모두 방지하는 방법은 use case 마다 필요한 엔티티를 동시에 가져오는 것이며 이에 대한 옵션으로는 JPQL의 fetch join을 사용하는 방법입니다.

5 ways to initialize lazy associations and when to use them

 

5 ways to initialize lazy associations and when to use them

Initializing an association the right ways makes the difference between horrible and great performance. Here are 5 different ways and when to use the.

thorben-janssen.com

Avoid cascade remove for huge relationships

많은 개발자들은 CascadeType.REMOVE, CascadeType.ALL를 사용합니다. 이는 엔티티를 삭제할 때 연관된 엔티티도 같이 삭제한다는 것을 의미합니다.

저자의 경험으로는 삭제 시 의도하지 않게 다른 레코드가 삭제될 수도 있는 경험은 하지 않았지만 이 Cascade.REMOVE는 엔티티를 삭제하면 정확히 어떤 일이 발생하는지 이해하기 매우 어렵습니다.

Hibernate가 관련된 엔티티를 삭제하는 방법을 살펴보면 우리는 Cascade.REMOVE를 피해야 하는 또다른 이유를 찾을 수 있습니다.

Hibernate는 2개의 SQL 문을 실행합니다.

첫 번째는 SELECT문으로 연관된 엔티티를 조회합니다.

두 번째로는 DELETE문으로 엔티티를 제거합니다.

이는 연관된 엔티티의 수에 비례하여 발생하는 SQL문이 증가됨을 이야기하며 이는 성능 저하를 발생시킬 수 있습니다.

Use @Immutable when possible

Hibernate는 데이터베이스 업데이트 요청시 PersistenceContext에 있는 모든 엔티티에 대해 dirty checking을 수행합니다.

이는 mutable한 엔티티에게는 필요하지만 모든 객체가 가변적이어야 하는 것은 아닙니다.

어떤 엔티티는 데이터베이스의 view를 매핑한 엔티티일 수도 있기 때문입니다.

이러한 엔티티에 대해 dirty check 검사를 수행하는 것은 피해야할 오버헤드 중 하나입니다.

@Immutable 어노테이션을 달면 Hibernate는 모든 dirty check에서 이를 무시하고 데이터베이스에 변경사항을 기록하지 않습니다.

반응형

'데이터 접근 기술' 카테고리의 다른 글

Querydsl 기본(3)  (0) 2023.02.07
Querydsl 기본(2)  (0) 2023.02.06
QueryDSL 적용 방법 알아보기.  (0) 2023.02.06
[Querydsl] 기본  (0) 2023.02.01
[Querydsl] 시작  (0) 2023.02.01

댓글