사용자 도구

사이트 도구


java:hibernate:gotchas

Hibernate/JPA Gotcha

하이버네이트/JPA에 관해 항상 기억해둬야 할 사항들을 정리해 둔다.

hashCode와 equals를 항상 구현한다

hashCode와 equals를 항상 구현해야 한다.

이때 DB Primary Key(ID)가 아닌 항상 변치 않고 해당 객체를 대표하는 의미를 가질 수 있는 비즈니스 키(business key)를 hashCode와 equals의 대상으로 삼도록 노력한다. 이유는 Set등에 신규 객체를 넣을 경우 신규 객체는 아직 프라이머리키가 지정되지 않은 상태이기 때문에 ID가 모두 null 혹은 0이며 이 경우 Set에 저장할 때 equals가 항상 true 여서 이전의 값을 뒤에 저장한 값이 계속 덮어써버리는 현상이 발생하기 때문이다.

하지만 실제로 해보면 인공키를 hashCode의 기준으로 삼을 수 밖에 없는 경우도 많다. Set/Map 과 함께 사용할 때 주의를 기울여야한다.

따라서 ID를 equals/hashCode의 기준으로 삼을 경우 매우 주의해야 한다. 관련 참조 - Hibernate equals and hashCode

to-many 관계에 대해서 fetch join은 안하는게 낫다

  • to-many 관계에 대해서 fetch join 을 하면 부모측이 자식측(many)의 갯수만큼 중복으로 결과 갯수가 늘어난다.
  • 이를 해결하려면 distinct 를 꼭 해야한다.
  • to-many 관계에 대해서 limit 을 걸게 되면 그 일반 SQL에서는 자식의 갯수에 대한 limit 이 돼 버린다. 이렇게 되면 실제 조회를 원하는 데이터(parent)의 갯수가 의도대로 나오지 않게 된다.
    • 때문에 하이버네이트는 SQL에 limit을 걸리 않고 limit 없이 전체 데이터를 조회한 뒤에 메모리에서 parent 데이터를 원하는 limit 갯수만큼 끊어서 반환한다.
    • 이는 엄청난 성능저하로 이어진다.
  • 결과적으로 특별한 이유가 없다면 to-many 관계에 대해서는 fetch join을 하지 말고 항상 Hibernate.initialize() 등으로 초기화를 하되 batch size 를 지정하는 식으로 한다.
    • Hibernate Performance Tuning hibernate.default_batch_fetch_size=30
    • default_batch_fetch_size를 지정하면 N+1 이 아니라 N 번 조회할 것은 in 쿼리로 batch_size 만큼의 인자를 넣어서 한 방에 조회한다.
  • Hibernate Configurations - hibernate.query.fail_on_pagination_over_collection_fetch=true 옵션을 켜서 미리 에러를 내고 확인해서 고쳐두는게 좋다.

One-To-One 과 Many-To-One 관계를 조심하라

One-To-One과 Many-To-One 관계에서 One 측은 not null이라고 명시하지 않는이상 Lazy Loading이 작동하지 않는다. 이에 관해서는 JPA One-To-One을 참조한다.

관계 필드에 존재하지 않는 엔티티에 대한 값을 넣지 말라

A → B 관계가 있을 때 A에 있는 B를 가리키는 컬럼에 존재하지 않는 B에 대한 값(특히 0)을 넣으면 A 를 조회할 때 A 자체가 조회가 안된다(Foreign Key가 안 걸려 있으면 존재하지 않는 값 지정이 가능함).

존재하지 않는 값은 null로 넣어야지 0이나 공백 같은 값을 사용하면 안된다.

toString 메소드를 조심하라

toString 메소드에서 레이지 로딩으로 지정된 필드를 출력하도록 해서 불필요하게 레이지 로딩 필드가 미리 읽혀지는 문제가 발생할 수 있다. toString 에서 출력값을 주의 깊게 선별해야 한다. 특히 lombok을 사용한다면 매우 주의하라.

OpenSessionInView 패턴 사용시 HTML 주석을 사용치 말라

무엇보다 가급적 OpenSessionInView 패턴을 사용하지 말라. 처음엔 편하지만 나중에는 불명확한 쿼리 실행으로 튜닝이 오히려 어렵고 복잡해진다.

toString 메소드에서와 같은 현상이 OpenSessionInView 패턴 사용시 뷰에서 발생할 수 있다. 더이상 필요없는 부분 특히 하이버네이트 도메인 객체를 호출하는 부분을 주석처리할 때 HTML 주석을 사용하지 말라. HTML주석은 서버사이드는 그대로 동작한다. 따라서 레이지 로딩으로 지정한 값을 HTML 주석 부분에서 호출하면 그대로 값이 로딩되어 출력된다.

항상 템플릿 엔진의 주석(JSP의 경우 <%– –%>)을 사용하라.

개발 환경에서는 쿼리 로그를 남겨라

hibernate.show_sql=true
hibernate.format_sql=true

위 프라퍼티 옵션으로 쿼리 로그를 남길 수 있다. 그러나 hibernate.show_sql=false로 두고 되도록 Log4j 옵션을 사용하는 것이 좋다. Hibernate Log 남기기를 참조한다.

단, 실 운영환경에서는 로그를 남기는 순간 매우 느려지므로 로그 레벨을 높여야 한다.

쿼리에 진짜로 ":"로 들어간다면?

Native SQL에 :를 넣어야 할 필요가 있다면 Escape을 해줘야 한다. java - How can I use MySQL assign operator(:=) in hibernate native query? \\: 형태로 Escape 한다.

SELECT k.`news_master_id` AS id, @ROW \\:= @ROW + 1 AS rownum 
    FROM keyword_news_list k 
    JOIN (SELECT @ROW \\:= 0) r 
    WHERE k.`keyword_news_id` = :kid
ORDER BY k.`news_master_id` ASC

Lazy 자식 컬렉션 2중 insert 문제(children collection insert twice)

  • Lazy @OneToMany 컬렉션에 add(자식Entity)를 하면서 entityManger.merge(부모Entity)를 호출 할 경우 두번 insert 되는 문제가 있다.
  • 이 현상은 @OneToMany 컬렉션이 Lazy Loading일 경우 컬렉션의 내용이 로딩이 안된 상태에서 add를 하면 발생하는 것 같다.
  • 재현
    • 부모 엔티티를 읽어온다. (A a = em.find(A.class, id))
    • 부모측 컬렉션에 자식을 추가한다.(a.getBs.add(new B()))
    • 명시적으로 merge를 호출하여 저장한다. (em.merge(a))
  • 따라서 자식 엔티티를 부모에 추가할 때는
    • 미리 Lazy 컬렉션을 로딩하거나, (Hibernate.initialize(a.getBs())
    • 아니면 부모 엔티티를 수정하지 말고, 자식 엔티티에 부모 엔티티의 관계를 맺고, 따로 insert 하거나
    • 아니면 entityManager.merge(entity)를 수행하지 말고 트랜잭션 종료시에 자동 저장되도록 처리한다.

JPQL Positional Parameter Bug

@Where 에서 true, false 등에 대해 잘못된 반응

  • @OneToMany@org.hibernate.annotations.Where(clause = “deleted = false”)와 같은 조건을 주었을 때, SQL이 잘못 생성된다.
    SELECT ... FROM address address0_ 
    WHERE  ( address0_.deleted=address0_.FALSE) -- 이 부분
      AND address0_.contact_id=?
  • true, false 등이 Dialect에 keyword로 등록이 되어있지 않아서 생기는 현상이다.
  • 조건을 delete = 0 형태로 주거나 DB 종류에 따라 문자열 'true/false'를 true/false로 인식하면 문자열로 사용하는 방식으로 회피 가능하다.
  • 혹은 사용중인 Dialect 에 true, false를 추가한다.
    public class ImprovedHSQLDialect extends HSQLDialect {
     
        public ImprovedHSQLDialect() {
            super();
            registerKeyword("true");
            registerKeyword("false");
            // registerKeyword("unknown");
        }
    }

SQL / JPQL / HQL Keyword "in", "by" 같은 Keyword 가 JPA Entity package 에 들어가면 파싱 오류 발생

null id in entry

HHH000099: an assertion failure occurred (this may indicate a bug in Hibernate, but is more likely due to unsafe use of the session): org.hibernate.AssertionFailure: null id in com.mycompany.XXX entry (don't flush the Session after an exception occurs)
  • 핵심은 “동일 트랜잭션 안에 저장이 실패한, 혹은 나도 모르게 직접 쿼리를 통해 DB에서 삭제된 Entity가 EntityManager 1차 캐시에 남아있는 상황”은 존재해서는 안된다.
  • 하나의 트랜잭션에서
    • 쓰기 작업시 에러가 발생해서 XXX 엔티티가 저장이 안됐는데, 이를 try/catch 블록에서 잡아서 그 후속 작업을 진행했거나,
    • 혹은, entity 삭제가 아니 직접 쿼리로 엔티티를 삭제하거나 한 상황에서
    • Session/EntityManager 입장에서는 잘못된 Entity 가 여전히 1차 캐시에 존재하는 상태이기 때문에
    • 나중에 flush 가 일어날 때 잘못된 Entity 혹은 DB상에 존재하지도 않지만 기존에 존재했던 것처럼 표시된 Entity 때문에 오류가 발생하게 된다.
  • 쓰기 작업과 그 후속 읽기 작업 트랜잭션을 분리해서 Session/EntityManager가 쓰레기 엔티티를 갖지 않게 하거나
  • 혹은 직접 쿼리를 통한 삭제 작업의 경우에는 session.clear() 혹은 entityManager.clear()를 통해 쓰레기 Entity 를 1차 캐시에서 모두 제거해야 한다.

참조

java/hibernate/gotchas.txt · 마지막으로 수정됨: 2023/07/03 14:54 저자 kwon37xi