IntelliJ 디버깅을 할 때 JPA의 지연 로딩 옵션이 실행 되면서 쿼리가 나가는 경우가 있다. 아래 예시에서 장소(Spot)는 여러개의 방명록(Post)을 가지고 있고 @OneToMany 매핑이 되어 있다. 먼저 findAll()로 조회를 한 상태에서는 select 쿼리만 나간 상태이다.

 

 

이제 Spot 리스트에서 Spot의 내부를 열어보는순간 JPA의 BatchSize로 설정해놓은 size만큼 in 쿼리가 실행이 된다.

 

 

IntelliJ 디버깅에서 지연 로딩 쿼리가 발생하는 것으로 인해서 혼동이 올 수 있다. IntelliJ에서는 이를 옵션으로 설정할 수 있다.

Debugger 창의 variables 창 부분을 우클릭하면 Mute Renderers, Customize Data View 옵션이 있다. Mute Renderers에서 전체 체크를 하거나 Customize Data View에서 개별적으로 설정을 할 수 있다.

 

 

Mute Renderers를 체크하게 되면 디버거에서 모든 값들을 불러와서 보여주지 않고 제한적으로 보여준다. 이제 posts의 size()를 클릭하면 size()를 불러오면서 지연 로딩 쿼리가 나가게 된다.

 

 

위에 posts를 보면 인스턴스가 PersistentBag으로 되어 있는데 이는 JPA에서 Collection을 감싼 프록시 클래스다.

 

PersistentBag의 size()를 호출하면 내부적으로 실제 컬렉션이 초기화가 된다.

 

readSize() 내부를 보면 initialize()에서 initializeCollection()을 호출하고 EventListner로 초기화를 한다.

 

생각해보면 @OneToMany 관계인 Colleciton이 지연 로딩 상태에서 size() 또는 get()으로 List의 내부 요소에 대해 알려면 쿼리가 나가는 것이 당연하다. 

 

[참고]

https://intellij-support.jetbrains.com/hc/en-us/community/posts/4411482324114-How-could-intellij-changed-Hibernate-lazyload-rules-with-breakpoint-

https://kimcoder.tistory.com/503?category=964983

 

https://www.baeldung.com/java-debug-interface

 

지도 API에 사용할 좌표 데이터를 DB에 관리하려고 하는 중 지원하는 기능이 없나 찾아보니 MySQL에서 Point 자료형과 JPA에서 Hibernate Spatial을 사용하여 좌표 데이터를 편리하게 다룰 수 있다.

 

 

Hibernate ORM 5.6.14.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

위에 문서를 보면 Hibernate Spatial은 지리 데이터 저장 및 쿼리 기능에 대한 표준화된 교차 데이터베이스 인터페이스를 제공하며 Oracle 10g/11g, PostgreSQL/PostGIS, MySQL 등의 데이터베이스에서 지원 가능하다고 한다. Hibernate Spatial은 JTS와 geolatte-geom이라는 두 라이브러리를 지원하는데 JTS는 사실상의 표준, Geolatte-geom은 JTS에서는 사용할 수 없는 많은 기능을 지원하는 최신 라이브러리다.

 

Jts, GeoLatte

 

JTS Topology Suite

The JTS Topology Suite is an API of spatial predicates and functions for processing geometry. It has the following design goals: JTS conforms to the Simple Features Specification for SQL published by the Open Geospatial Consortium JTS provides a complete,

www.tsusiatsoftware.net

 

GitHub - GeoLatte/geolatte-geom: A geometry model that conforms to the OGC Simple Features for SQL specification.

A geometry model that conforms to the OGC Simple Features for SQL specification. - GitHub - GeoLatte/geolatte-geom: A geometry model that conforms to the OGC Simple Features for SQL specification.

github.com

 

 

build.gradle

build.gradle 설정은 간단하다. (원래는 build.gradle에 hibernate 버전을 맞춰서 적어줘야 되는데 Hibernate 5.2.x 이후부터는 org.hibernate:hibernate-spatial만 추가하면 된다고 한다)

 

implementation group: 'org.hibernate', name: 'hibernate-spatial'

 

Hibernate Spatial은 데이터베이스의 공간 기능을 HQL 및 JPQL 내에서 사용할 수 있도록 Hibernate ORM 방언을 확장할 수 있는데 5.6.1 이전의 MySQL 버전은 공간 연산자에 대한 지원이 제한적이었지만 5.6.1 버전 이후 공간 연산자가 도입되면서 방언MySQLSpatial56Dialect를 통해 새롭고 정확한 연산자를 지원한다.

(MySQLSpatialDialect -> MySQL5SpatialDialect -> MySQLSpatial56Dialect)

그래서  applicaiton 설정에 MySQL56InnoDBSpatialDialect를 추가해주었다.

 

apllication yml

jpa:
    database-platform: org.hibernate.spatial.dialect.mysql.MySQL56InnoDBSpatialDialect

 

 

 

지도의 좌표 데이터 정도만 사용을 하려고 하기 때문에 사용하는 부분은 간단하다.

import org.locationtech.jts.geom.Point;

 

Point를 그대로 반환하면 Json이 변환을 못해서 추가 처리를 해야 되는데 간단하게 X, Y값을 빼내서 Dto로 만들었다.

 

마지막으로 MySQL에서 공간 자료형인 POINT 컬럼을 추가하면 깔끔하게 해결 된다.

 

 

java:hibernate:spatial [권남]

 

kwonnam.pe.kr

 

Hibernate Spatial을 이용한 좌표간 거리계산 (feat. PostGIS, H2GIS)

💽 Hibernate Spatial Hibernate Spatial은 지리 데이터를 계산하기 위해 만들어 졌고, Hibernate 5.0 버전 부터 Hibernate 라이브러리에 공식적으로 마이그레이션이 됐다. 현재 지원하는 데이터베이스는 , , , ,

seongsu.me

 

Spring JPA MySql 위치 정보(위도, 경도) 저장 및 Json으로 응답

의존성을 추가할 때, 반드시 hibernate 버전을 확인하고 그에 맞게 추가해야한다.build.gradle의 dependencies에 다음 한 줄을 추가한다.application.yml파일의 jpa database-platform을 다음과 같이 변경한다.위치정

velog.io

 

프로젝트를 하면서 fetch join을 다시 JPA 책을 보면서 복습했다.

 

fetch join은 JPQL 기능으로 연관된 엔티티나 컬렉션을 한번에 같이 조회하는 것이다.

 

"select m from Member m join fetch m.team"

 

이렇게 페치 조인을 하면 Member만 조회를 했지만 실제 쿼리는 Member와 연관된 Team도 함께 조회하게 된다.

페치 조인을 사용하지 않을 경우 Member만 조회하고 조인을 한 Team은 조회하지 않는다.

연관 관계가 지연 로딩으로 설정 되어 있으면 프록시나 아직 초기화 되지 않은 컬렉션 래퍼를 반환한다. 그리고 나중에 접근할때 추가로 쿼리가 나가기 때문에 페치 조인을 잘 활용하는 것이 좋다.

 

fetch join을 사용할때 주의할 점

 

1. 페치 조인 대상에 별칭을 잘못 사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있으므로 조심해서 사용해야 한다. (연관된 데이터 수가 달라진 상태에서 2차 캐시에 저장되면 위험)

 

2. 둘 이상의 컬렉션을 페치할 수 없다. (카테시안 곱) -> @BatchSize 활용

 

3. 컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다. (경고 로그를 남기면서 메모리에서 페이징 처리를 해서 위험)

 

 

회원 정보를 수정하는 경우 해당 회원은 새로 가입하는 것이 아닌 기존 정보를 수정하는 것이기 때문에 DB에 한번 저장되어서 식별자가 존재하는 상태이다. 이런 경우도 준영속 엔티티라고 하는데 준영속 엔티티는 영속성 컨텍스트가 더는 관리하지 않는 엔티티를 말한다.

다음과 같이 회원 수정 폼으로 변경 데이터를 받아와서 임의의 엔티티 객체를 생성하고 값을 전달하면 해당 엔티티는 준영속 상태이기 때문에 변경 감지가 이루어지지 않는다.

 

@Transactional
void update(UpdateForm updateForm) {
    Member member = new Member();
    member.setId(updateForm.getId());
    member.setPhoneNumber(updateForm.getPhoneNumber());
}

 

그래서 준영속 상태의 엔티티를 영속 상태로 변경하는 방법으로 병합(merge) 기능이 있는데 준영속 엔티티를 파라미터로 전달하면 식별자 값으로 1차 캐시에서 엔티티를 조회하고, 1차 캐시에 엔티티가 없을 경우 DB에서 엔티티를 조회하여 1차 캐시에 저장한다. 그리고 조회한 영속 엔티티에 파라미터 준영속 엔티티의 값을 채워 넣은 뒤에 영속 상태의 엔티티를 반환한다. 이렇게 되면 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 DB에 UPDATE SQL이 실행되어 실제 변경이 이루어진다. 

 

@Transactional
void update(UpdateForm updateForm) {
    Member member = new Member();
    member.setId(updateForm.getId());
    member.setPhoneNumber(updateForm.getPhoneNumber());
    
    Member mergeMember = em.merge(member);
}

 

하지만 병합 기능은 모든 필드를 교체해버리기 때문에 파라미터로 전달된 준영속 엔티티의 특정 필드가 비어있는 경우 null로 변경이 되는 위험이 있다. 

 

수정 시 모든 값을 변경해버리는 상황은 거의 없기 때문에 엔티티를 변경할 때는 병합은 되도록 하지 않고 다음과 같이 식별자를 통해 엔티티를 조회해와서 변경 감지를 사용하는 것이 좋다. 

@Transactional
void update(Member memberForm) {
    Member findMember = em.find(Member.class, memberForm.getId());
    findMember.setPhoneNumber(memberForm.getPhoneNumber());
}

 

 

 

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강

www.inflearn.com

 

 

 

+ Recent posts