지도 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

 

 

 

 

JPA는 스펙상 엔티티, 임베디드 값 타입에 리플렉션(Reflection), 프록시(Proxy) 기술을 적용하기 위해 public 또는 protected의 기본 생성자를 두는 것을 정의하고 있다.

 

 

리플렉션이란?

더보기

Java Reflection API

 

자바는 컴파일 시점에서 타입이 결정되는데 다음과 같이 구체적인 클래스로 생성하지 않을 경우 컴파일 시점에서 Object 타입으로 결정이 되기 때문에 Car 클래스의 메소드 등에 접근할 수가 없다.

 

Object car = new Car(); 

 

자바에서는 JVM이 실행되면 자바 코드가 컴파일러를 거쳐 바이트코드로 변환되어 static 영역에 저장되는데 리플렉션은 이 정보를 활용하여 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해준다. 하지만 런타임 시점에 동적으로 타입을 분석하기 때문에 JVM을 최적화할 수 없는 등 여러 문제점이 있어서 일반적으로는 사용하지 않는 것이 좋다.

리플렉션은 애플리케이션 개발보다 프레임워크나 라이브러리에서 많이 사용하는데 프레임워크나 라이브러리는 사용자가 어떤 클래스를 만들지 예측할 수가 없기 때문에 리플렉션을 활용한다.

JPA도 동적으로 객체 생성시 리플렉션을 활용하는데 리플렉션으로 가져올 수 없는 정보 중 하나가 생성자의 인자 정보이다. 그래서 기본 생성자가 반드시 있어야 객체를 생성할 수 있다. 기본 생성자로 객체를 생성만 하면 필드 값 등을 리플렉션으로 넣어줄 수 있다.

 

 

Reflection API 간단히 알아보자.

Spring Framework를 학습하다 보면 Java Reflection API를 자주 접하게 된다. 하지만 Reflection API…

tecoble.techcourse.co.kr

 

 

하이버네이트 같은 구현체들은 기본 생성자가 없어도 라이브러리들을 통해 이러한 문제를 어느정도 회피하지만 완벽한 해결책이 아니기 때문에 기본 생성자를 필수로 두는 것이 좋다. 기본 생성자를 public으로 열어두면 의도치 않은 무분별한 생성이 될 수 있기 때문에  protected로 선언하여 다른 패키지에서 기본 생성자를 생성할 수 없도록 제한한다.

 

그리고 엔티티는 생성자나 Setter를 사용하는 것보다는 빌더(Builder) 패턴 또는 정적 팩토리 메서드(static factory method)를 사용하는 것이 좋은데 빌더 패턴은 필요한 데이터만 설정할 수 있고 유연성, 가독성이 뛰어나며 변경 가능성을 최소화 할 수 있다.

 

[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유

객체를 생성하기 위해서는 생성자 패턴, 정적 메소드 패턴, 수정자 패턴, 빌더 패턴 등을 사용할 수 있습니다. 개인적으로 객체를 생성할 때에는 반드시 빌더 패턴을 사용해야 한다고 생각하는

mangkyu.tistory.com

 

정적 팩토리 메서드(static factory method)는 객체 생성을 캡슐화하는 기법으로 객체를 생성하는 메소드를 만들고 static으로 선언하여 사용한다. 메서드명을 의미 있게 지을 수 있어 생성자에 비해 가독성이 좋아지고 호출할 때마다 새로운 객체를 생성할 필요가 없다. 그리고 하위 타입 객체를 반환할 수 있으며 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. 

 

public static User createUser(String name, int age) {
    OrderItem orderItem = new OrderItem();
    this.name = name;
    this.age = age;
    
    return user;
}

 

파라미터가 많거나 변경 가능성이 많은 경우에는 빌더 패턴을, 그렇지 않은 경우에는 정적 팩토리 메소드를 사용하는 것이 좋다.

+ Recent posts