JPA 사용 시 19가지 Tip

숑숑·2024년 2월 4일
197
post-thumbnail
post-custom-banner

여러분은 JPA를 잘 쓰시나요?
JPA는 개발자의 SQL 작업을 줄여주는 매우 편리한 ORM 라이브러리입니다.

그러나, 절대 '쉬운' 툴은 아니라고 생각합니다.
생각보다 깊게 공부가 되어있어야 하는데요, 함정이 상당히 많기 때문입니다.

함정이라 함은, JPA가 개발자의 의도와 다르게 동작하나 컴파일/런타임 에러 없이 넘어가지는 부분들을 말합니다.

제가 현업에서 여러가지 함정에 당해봤는데요(?)
이를 교훈 삼아서 주의사항에 가까운 tip을 19가지 모아봤습니다.

너무 known issue라고 생각되는 문항들도 있겠지만, 은근히 잊기 쉬운 부분들도 포함했습니다.

혹시 제가 틀린 부분이나 더 생각나는 함정이 있다면, 댓글로 알려주시면 더 많은 분들에게 도움이 될 것 같습니다 :)


읽기 전

  • JPA, SQL에 대한 기본적인 이해를 요합니다.
  • MySQL 벤더 위주로 작성되어 타 DBMS와 다른 내용이 있을 수 있습니다.

1. 모든 연관관계는 가능한 지연로딩으로

물론 즉시로딩이 개발하기 편한 측면도 있습니다.

그러나 당장 지연로딩이 필요한 비즈니스 로직이 없더라도, 앞으로의 요구사항에 유연하게 대응하려면 지연로딩으로 통일해두시는게 쿼리 컨트롤이 쉽습니다.

  • ManyToOne, OneToOne은 별도 설정이 없으면 기본이 즉시로딩이니 주의해야 합니다.

2. IDE에서 디버그 모드로 실행 시, 연관관계가 자동 지연로딩 될 수 있다.

참고: IDE마다 다를 수 있습니다. (저는 IntelliJ로 겪었던 이슈입니다.)

디버그 모드로 실행 후 Transaction 내에서 breakpoint 시, 엔티티가 연관 엔티티를 자동 로딩해서 필요 없는 쿼리가 나갈 수 있습니다.
때문에 쿼리 테스트 시 가급적 일반 모드로 실행하는게 정확합니다.

관련 Stackoverflow :: Trying to confirm lazy loading is working

3. 지연로딩 사용 시 Service 레이어에 '@Transactional' 어노테이션 사용

지연로딩 사용 시 Transaction 세션이 없으면 LazyInitializaitonException이 나게 됩니다.
조회만 하는 경우에는 readOnly=true 옵션을 붙여줍니다.

  • readOnly 설정 시 엔티티 Dirty Checking을 하지 않아 성능 상 낫습니다.
  • 주의! readOnly 설정할 경우 DML 쿼리가 무시될 수 있습니다.

4. 일대일 관계는 주의 필요 :: optional=false 이거나, 외래키 컬럼을 가지고 있는 경우만 지연로딩

일대일에서는 지연로딩 설정을 해도, 즉시로딩되는 경우가 있습니다.

  • null 값이 가능한 OneToOne 관계일 경우(optional=true 상황) 지연로딩이 동작하지 않습니다.
  • 예외: 단방향 관계이고, 연관관계의 키를 엔티티가 가지고 있는 경우 optional 여부와 무관하게 지연로딩 됩니다.

[개인적 Tip]
일대일 관계가 즉시로딩하는 기준을 이해하는데 꽤나 고전했어서 최대한 쉽게 설명해보겠습니다.
결론은 JPA 입장에서 연관관계가 null인지 아닌지를 확실히 알 수 있으면 지연로딩을 해줍니다. 이유는 만약 null이 오게 된다면 프록시로 감싸면 안 되기 때문입니다. (지연로딩은 연관관계를 직접 조회하기 전엔 프록시 객체를 넣습니다.)
연관 엔티티가 없으니 null인 것일텐데, 그 자리에 프록시 객체가 있다고 가정해봅시다.
그러면 연관 엔티티가 있는지 확인하기 위해 null 여부를 비교할 때, 프록시 객체 때문에 null이 아닌 상태가 되어버립니다.
그래서 즉시로딩을 하는 경우는, JPA가null 여부를 몰라서 프록시 처리를 할 수 없는 경우에 해당합니다.

  • optional=true: null이 올지 아닐지 알 수 없기에 즉시로딩
  • optional=false: 연관 엔티티가 있다고 약속되어있기에 지연로딩
  • DB 상 연관 엔티티의 pk 컬럼을 가지고 있는 경우: 조회할 때 얻어지는 컬럼 값으로 null인지 아닌지 바로 알 수 있으므로, optional 값에 상관없이 지연로딩

5. 엔티티 클래스를 final로 선언하지 않는다.

JPA는 프록시 메커니즘을 사용하여 지연 로딩을 구현하는데, final 클래스는 프록시 생성이 불가능하기 때문에 즉시로딩하게 됩니다.
아무 에러도 내지 않기 때문에, 지연로딩이 잘 동작하는 걸로 착각할 수 있으니 주의가 필요합니다.

6. Not null 컬럼 매핑 시 optional=false 조건 추가

연관관계 fetch join시 기본으로 outer join하게 되어있는데,
optional=false인 경우 inner join 합니다.

7. 엔티티 직렬화 시, 자동 지연로딩

엔티티의 직렬화 과정에서 자동으로 지연 로딩이 발생할 수 있습니다.
Redis와 같은 캐시 시스템에 엔티티를 캐싱하는 경우 등이 이에 해당합니다.

직렬화하기 전에 연관된 모든 엔티티를 미리 fetch join하거나,
@JsonIgnore 로 연관 엔티티를 직렬화에서 제외시켜주세요. (나중에 필요한 경우도 있을 것 같다면 추천하진 않습니다.)

가능하다면 엔티티를 직렬화하기보다, DTO로 직렬화하는걸 가장 추천합니다.

8. toString()에 연관관계 엔티티 제외 필요

toString()을 사용할 때, 연관된 엔티티를 포함하면 불필요한 즉시 로딩이 발생할 수 있습니다.

따라서 @ToString에서 연관관계가 있는 필드는 exclude 옵션을 사용하여 제외시켜야 합니다.

  • 예시: @ToString(exclude={”entity”})

toString()을 쓸 일이 없더라도, 라이브러리에서 내부적으로 사용하고 있을 가능성이 있기 때문에 가급적 처리해두시는걸 추천합니다.

  • 특히 Lombok@Data 어노테이션을 엔티티에 사용 중인 경우, toString()을 자동 생성합니다. 연관 엔티티 제외처리는 별도로 되어있지 않아 주의가 필요합니다.

9. Auto-increment PK :: Bulk Insert는 JPA가 아닌 JDBC로

JPA는 IDENTITY 전략 엔티티인 경우 bulk insert 지원이 잘 안 됩니다.
saveAll()이라는 메소드가 있지만, insert 쿼리가 엔티티마다 각각 나갑니다.

단일 쿼리로 여러개 insert 하고 싶을 경우, JdbcTemplate을 사용해주세요

10. ‘진짜’ 네이티브 쿼리를 로깅하도록 설정하기 (for MySQL)

쿼리 로그 확인 시 hibernate 로그 설정이나, p6spy 같은 라이브러리를 보통 많이 사용하시는데요
실제 동작하는 쿼리와 다르게 로깅하는 경우가 있습니다.

  • 특히 bulk insert의 경우, 실제론 bulk insert 쿼리로 나가고 있음에도 insert 쿼리가 각각 나가는 것처럼 출력되는 이슈가 있습니다. p6spy 사용 시에도 동일합니다.

MySQL이 남겨주는 네이티브 쿼리를 직접 보는게 여러모로 가장 정확합니다.
아래 설정을 해두시는걸 추천합니다.

spring:
  datasource:
    hikari:
      jdbc-url: jdbc:mysql://localhost:3306/hibernate_batch?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999

# 출처: https://techblog.woowahan.com/2695/
  • rewriteBatchedStatements=true : bulk insert 쿼리를 허용하는 설정
  • profileSQL=true: 드라이버에서 전송하는 쿼리 출력 허용 설정
  • logger=SLF4J: 기본값이 System.err 로 출력하도록 설정되어 있기 때문에 필수로 지정 필요 (MariaDB 드라이버일 경우 불필요)
  • maxQuerySizeToLog=999999 : 출력할 쿼리 길이 (기본값은 0이기에 필수 지정)

11. JPA로 bulk 쿼리 시 필수 어노테이션 :: @Modifying(clearAutomatically = true, flushAutomatically = true)

해당 어노테이션 사용 시, 쿼리 실행 시 flush와 함께 영속성 컨텍스트를 비워줍니다.
@Query처럼 쿼리를 직접 지정한 벌크연산의 경우 JPA가 영속성 컨텍스트 업데이트를 안 해주기 때문에, DML 벌크 쿼리 시 반드시 필요한 어노테이션입니다.

미사용 시, 예를 들면 삭제 쿼리가 나갔음에도, 엔티티 영속은 되어있기에 조회가 되는 버그가 생길 수 있습니다.

12. flush는 commit이 아니다

flush는 DB에 쿼리가 나가긴 하지만, 트랜잭션을 commit해주는건 아니기에
DB 격리수준이 READ COMMITTED 이상일 경우, 다른 작업에서 쿼리 결과를 조회할 순 없습니다.

13. saveAndFlush() 보다는 변경 감지를 사용한다.

쿼리 횟수와 결과물만 본다면 둘이 같습니다.
그러나 변경감지가 훨씬 Hibernate 최적화가 잘 되어있습니다.

또한 변경감지는 한번의 flush 작업으로 여러 엔티티를 반영할 수 있는데 반해,
saveAndFlush()는 호출될 때마다 모든 엔티티의 변경 여부를 확인해야 합니다.

아래는 hibernate에서 flush를 수행하는 로그 비교입니다.
자세한 비교는 이 링크에서 참고하실 수 있습니다.

# 변경 감지를 사용한 경우
48408600 nanoseconds spent executing 1 flushes (flushing a total of 4 entities and 12 collections);

# 매 엔티티마다 saveAndFlush 한 경우
74224500 nanoseconds spent executing 5 flushes (flushing a total of 20 entities and 60 collections);

14. Auto-increment PK를 가지는 엔티티 생성 시, saveAndFlush() 는 불필요하다.

save만 해도 flush까지 됩니다. (auto_increment 키 값을 할당하기 위해)

15. Auto-increment PK 키 매핑 시 GenerationType.AUTO 쓰지 않는다.

AUTO 전략일 때 sequence를 지원하지 않는 DB를 사용하는 경우,
Hibernate는 TABLE 전략을 사용합니다.

  • TABLE은 별도의 Key 생성 전용 Table을 생성하고, 키 생성 때마다 해당 테이블에 비관적 락을 걸며 유니크한 키를 만들어주는 전략입니다. (DB 벤더에 중립적이라는 장점이 있습니다.)

확장성, 성능 면에서 손해볼 수 있으므로, GenerationType.IDENTITY로 정확히 지정해줍시다.

16. 조회만 할 시, DTO projection으로 필요한 데이터만 select하는 것이 성능이 좋다.

DTO를 만드는게 귀찮을 순 있지만, 엔티티 생명주기를 관리하지 않으므로 성능적으로 좋습니다.
조회 후 엔티티 수정이 필요하지 않다면 DTO projection을 추천합니다

17. DTO projection 하더라도 엔티티가 fetch 되는 경우가 있다.

아래 조건 중 하나라도 해당할 시 엔티티 전체가 fetch 됩니다.

  • List 등 다른 객체와 연관관계를 가지는 필드가 있을 경우
  • SPEL 사용하는 필드가 있을 경우

위 경우 에러가 나진 않지만, 성능 개선 효과도 못 보고 코드만 번거로워지는 결과를 낳게 됩니다.

SPEL, 연관관계 없이 기본 자료형 필드로만 구성되어야 원하는 컬럼만 가져오도록 쿼리할 수 있습니다.

18. DTO projection 방법 중, Interface projection이 가장 느리다.

projection 별로 성능을 비교해주신 분이 있습니다.
그 결과 엔티티 조회와 Interface projection의 차이가 미미하게 나왔습니다.

class의 경우 바로 class로 로딩하는데 비해, interface의 경우 Proxy를 사용해 몇가지 절차를 더 거치기 때문으로 보입니다.

즉 DTO projection을 하는 주 이유가 성능 개선이라면, interface 보다는 다른 형식을 사용하는게 더 효과가 좋을겁니다.

관련 Stackoverflow :: Why are interface projections much slower than constructor projections and entity projections in Spring Data JPA with Hibernate?

19. nativeQuery로 Pageable 사용 시 COUNT 쿼리 주의

이건 일종의 JPA 버그인데, 해당 경우 COUNT 쿼리가 올바르게 동작하지 않는 경우가 있습니다.
(아직 정확한 원인이 밝혀지진 않은 것 같습니다.)

이 문제를 자세히 다뤄주신 블로그가 있는데,
여기서는 countQuery 옵션으로 직접 쿼리를 지정해주면 된다고 합니다.
그러나, 또 JPA의 github 이슈를 보면 countQuery를 지정해도 문제가 있는 것으로 보입니다.

즉 nativeQuery + Pageable 조합이 필요할 시, 쿼리가 어떻게 나가는지 반드시 확인해야 합니다.

profile
툴 만들기 좋아하는 삽질 전문(...) 주니어 백엔드 개발자입니다.
post-custom-banner

7개의 댓글

comment-user-thumbnail
2024년 2월 5일

감사합니다! 덕분에 잘 읽고 갑니당

답글 달기
comment-user-thumbnail
2024년 2월 8일

좋은글 잘 읽고 갑니다

답글 달기
comment-user-thumbnail
2024년 2월 8일

좋은 글 감사히 읽고 갑니다! JPA를 쓰고 있는데 이런 문제도 발생할 수 있군요,,

답글 달기
comment-user-thumbnail
2024년 2월 9일

와우,, 꿀팁 감사합니다.,

답글 달기
comment-user-thumbnail
2024년 2월 14일

이런 이슈들이 있군요..! 정리 감사합니다. 잘 읽었어요!

답글 달기
comment-user-thumbnail
2024년 2월 15일

Drift Boss: Addictive drifting game with high-speed action.

Drift Boss is a free drift racing game for mobile devices where you will control powerful cars and perform breathtaking drifts on challenging tracks. The game offers a realistic and adrenaline-filled drifting experience, along with beautiful 3D graphics and dynamic sounds.

Highlights of Drift Boss:

Simple but challenging gameplay: With just one control button, you can easily control the car left or right to perform perfect drifts. However, to become a real Drift Boss, you need to practice and hone your skills to conquer the most difficult races.

Many types of cars to choose from: Drift Boss has a diverse car collection with many different types of vehicles, from powerful sports cars to rugged trucks. Each car has its own characteristics, giving you a unique driving experience.

Upgrade and customize your vehicle: You can upgrade your vehicle's engine, tires, and other parts to improve performance and increase drifting ability. Additionally, you can also customize the vehicle's exterior to express your personal style.

Beautiful 3D environment: Drift Boss possesses high quality 3D graphics with meticulously designed and detailed racing tracks. You will experience drifting in different environments such as cities, highways, or even on winding mountain roads.

Diverse game modes: Drift Boss has many different game modes for you to choose from, including Career mode, Time Attack mode and Free Roam mode. You can play in your own way and explore all that the game has to offer.

Drift Boss is a great game for racing and drifting lovers. The game provides an interesting and challenging entertainment experience, helping you relax after stressful hours of studying and working.

답글 달기
comment-user-thumbnail
2024년 2월 16일

현재 JPA공부하면서 플젝중인데 많은 도움이 되었습니다 감사합니다!

답글 달기