여러분은 JPA를 잘 쓰시나요?
JPA는 개발자의 SQL 작업을 줄여주는 매우 편리한 ORM 라이브러리입니다.
그러나, 절대 '쉬운' 툴은 아니라고 생각합니다.
생각보다 깊게 공부가 되어있어야 하는데요, 함정이 상당히 많기 때문입니다.
함정이라 함은, JPA가 개발자의 의도와 다르게 동작하나 컴파일/런타임 에러 없이 넘어가지는 부분들을 말합니다.
제가 현업에서 여러가지 함정에 당해봤는데요(?)
이를 교훈 삼아서 주의사항에 가까운 tip을 19가지 모아봤습니다.
너무 known issue라고 생각되는 문항들도 있겠지만, 은근히 잊기 쉬운 부분들도 포함했습니다.
혹시 제가 틀린 부분이나 더 생각나는 함정이 있다면, 댓글로 알려주시면 더 많은 분들에게 도움이 될 것 같습니다 :)
읽기 전
- JPA, SQL에 대한 기본적인 이해를 요합니다.
MySQL
벤더 위주로 작성되어 타 DBMS와 다른 내용이 있을 수 있습니다.
물론 즉시로딩이 개발하기 편한 측면도 있습니다.
그러나 당장 지연로딩이 필요한 비즈니스 로직이 없더라도, 앞으로의 요구사항에 유연하게 대응하려면 지연로딩으로 통일해두시는게 쿼리 컨트롤이 쉽습니다.
ManyToOne
, OneToOne
은 별도 설정이 없으면 기본이 즉시로딩이니 주의해야 합니다.참고: IDE마다 다를 수 있습니다. (저는
IntelliJ
로 겪었던 이슈입니다.)
디버그 모드로 실행 후 Transaction 내에서 breakpoint 시, 엔티티가 연관 엔티티를 자동 로딩해서 필요 없는 쿼리가 나갈 수 있습니다.
때문에 쿼리 테스트 시 가급적 일반 모드로 실행하는게 정확합니다.
관련 Stackoverflow :: Trying to confirm lazy loading is working
지연로딩 사용 시 Transaction 세션이 없으면 LazyInitializaitonException
이 나게 됩니다.
조회만 하는 경우에는 readOnly=true
옵션을 붙여줍니다.
일대일에서는 지연로딩 설정을 해도, 즉시로딩되는 경우가 있습니다.
optional=true
상황) 지연로딩이 동작하지 않습니다.[개인적 Tip]
일대일 관계가 즉시로딩하는 기준을 이해하는데 꽤나 고전했어서 최대한 쉽게 설명해보겠습니다.
결론은 JPA 입장에서 연관관계가null
인지 아닌지를 확실히 알 수 있으면 지연로딩을 해줍니다. 이유는 만약null
이 오게 된다면 프록시로 감싸면 안 되기 때문입니다. (지연로딩은 연관관계를 직접 조회하기 전엔 프록시 객체를 넣습니다.)
연관 엔티티가 없으니null
인 것일텐데, 그 자리에 프록시 객체가 있다고 가정해봅시다.
그러면 연관 엔티티가 있는지 확인하기 위해null
여부를 비교할 때, 프록시 객체 때문에null
이 아닌 상태가 되어버립니다.
그래서 즉시로딩을 하는 경우는, JPA가null
여부를 몰라서 프록시 처리를 할 수 없는 경우에 해당합니다.
optional=true
: null이 올지 아닐지 알 수 없기에 즉시로딩optional=false
: 연관 엔티티가 있다고 약속되어있기에 지연로딩- DB 상 연관 엔티티의 pk 컬럼을 가지고 있는 경우: 조회할 때 얻어지는 컬럼 값으로
null
인지 아닌지 바로 알 수 있으므로,optional
값에 상관없이 지연로딩
JPA는 프록시 메커니즘을 사용하여 지연 로딩을 구현하는데, final 클래스는 프록시 생성이 불가능하기 때문에 즉시로딩하게 됩니다.
아무 에러도 내지 않기 때문에, 지연로딩이 잘 동작하는 걸로 착각할 수 있으니 주의가 필요합니다.
연관관계 fetch join시 기본으로 outer join하게 되어있는데,
optional=false
인 경우 inner join 합니다.
엔티티의 직렬화 과정에서 자동으로 지연 로딩이 발생할 수 있습니다.
Redis와 같은 캐시 시스템에 엔티티를 캐싱하는 경우 등이 이에 해당합니다.
직렬화하기 전에 연관된 모든 엔티티를 미리 fetch join하거나,
@JsonIgnore
로 연관 엔티티를 직렬화에서 제외시켜주세요. (나중에 필요한 경우도 있을 것 같다면 추천하진 않습니다.)
가능하다면 엔티티를 직렬화하기보다, DTO로 직렬화하는걸 가장 추천합니다.
toString()
을 사용할 때, 연관된 엔티티를 포함하면 불필요한 즉시 로딩이 발생할 수 있습니다.
따라서 @ToString
에서 연관관계가 있는 필드는 exclude
옵션을 사용하여 제외시켜야 합니다.
@ToString(exclude={”entity”})
toString()
을 쓸 일이 없더라도, 라이브러리에서 내부적으로 사용하고 있을 가능성이 있기 때문에 가급적 처리해두시는걸 추천합니다.
Lombok
의 @Data
어노테이션을 엔티티에 사용 중인 경우, toString()
을 자동 생성합니다. 연관 엔티티 제외처리는 별도로 되어있지 않아 주의가 필요합니다.JPA는 IDENTITY
전략 엔티티인 경우 bulk insert 지원이 잘 안 됩니다.
saveAll()
이라는 메소드가 있지만, insert 쿼리가 엔티티마다 각각 나갑니다.
단일 쿼리로 여러개 insert 하고 싶을 경우, JdbcTemplate
을 사용해주세요
쿼리 로그 확인 시 hibernate 로그 설정이나, p6spy
같은 라이브러리를 보통 많이 사용하시는데요
실제 동작하는 쿼리와 다르게 로깅하는 경우가 있습니다.
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이기에 필수 지정)해당 어노테이션 사용 시, 쿼리 실행 시 flush와 함께 영속성 컨텍스트를 비워줍니다.
@Query
처럼 쿼리를 직접 지정한 벌크연산의 경우 JPA가 영속성 컨텍스트 업데이트를 안 해주기 때문에, DML 벌크 쿼리 시 반드시 필요한 어노테이션입니다.
미사용 시, 예를 들면 삭제 쿼리가 나갔음에도, 엔티티 영속은 되어있기에 조회가 되는 버그가 생길 수 있습니다.
flush는 DB에 쿼리가 나가긴 하지만, 트랜잭션을 commit해주는건 아니기에
DB 격리수준이 READ COMMITTED
이상일 경우, 다른 작업에서 쿼리 결과를 조회할 순 없습니다.
쿼리 횟수와 결과물만 본다면 둘이 같습니다.
그러나 변경감지가 훨씬 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);
save만 해도 flush까지 됩니다. (auto_increment 키 값을 할당하기 위해)
AUTO
전략일 때 sequence를 지원하지 않는 DB를 사용하는 경우,
Hibernate는 TABLE
전략을 사용합니다.
확장성, 성능 면에서 손해볼 수 있으므로, GenerationType.IDENTITY
로 정확히 지정해줍시다.
DTO를 만드는게 귀찮을 순 있지만, 엔티티 생명주기를 관리하지 않으므로 성능적으로 좋습니다.
조회 후 엔티티 수정이 필요하지 않다면 DTO projection을 추천합니다
아래 조건 중 하나라도 해당할 시 엔티티 전체가 fetch 됩니다.
List
등 다른 객체와 연관관계를 가지는 필드가 있을 경우SPEL
사용하는 필드가 있을 경우위 경우 에러가 나진 않지만, 성능 개선 효과도 못 보고 코드만 번거로워지는 결과를 낳게 됩니다.
즉 SPEL, 연관관계 없이 기본 자료형 필드로만 구성되어야 원하는 컬럼만 가져오도록 쿼리할 수 있습니다.
projection 별로 성능을 비교해주신 분이 있습니다.
그 결과 엔티티 조회와 Interface projection의 차이가 미미하게 나왔습니다.
class의 경우 바로 class로 로딩하는데 비해, interface의 경우 Proxy를 사용해 몇가지 절차를 더 거치기 때문으로 보입니다.
즉 DTO projection을 하는 주 이유가 성능 개선이라면, interface 보다는 다른 형식을 사용하는게 더 효과가 좋을겁니다.
이건 일종의 JPA 버그인데, 해당 경우 COUNT 쿼리가 올바르게 동작하지 않는 경우가 있습니다.
(아직 정확한 원인이 밝혀지진 않은 것 같습니다.)
이 문제를 자세히 다뤄주신 블로그가 있는데,
여기서는 countQuery
옵션으로 직접 쿼리를 지정해주면 된다고 합니다.
그러나, 또 JPA의 github 이슈를 보면 countQuery
를 지정해도 문제가 있는 것으로 보입니다.
즉 nativeQuery + Pageable 조합이 필요할 시, 쿼리가 어떻게 나가는지 반드시 확인해야 합니다.
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.
감사합니다! 덕분에 잘 읽고 갑니당