서론

토이 프로젝트를 진행 중, 장소의 위치의 위도, 경도 좌표를 저장해야 했는데, float type의 ‘latitude’, ‘longitude’라는 2개의 컬럼을 만들어서 각각 따로 저장하려고 했는데, 우연찮은 기회에 공간 데이터 타입이라는 것이 따로 있다는 것을 발견하여 적용해본 경험을 글로 작성하여 보려고 한다.

본론

공간(空間, 영어: space)은 어떤 물질 또는 물체가 존재할 수 있거나 어떤 일이 일어날 수 있는 장소이다.


공간 데이터란, 위 같은 점, 선, 면의 공간을 데이터화 한 것을 의미하는데, 필자는 지도에서 위치를 표시하기 위해 2D 공간 데이터인 점(Point) 데이터를 저장할 필요가 있었다.
필자는 Spring boot + JPA(Hibernate) + H2 환경을 사용하고 있어서, 공간 데이터를 다루기 위해 hibernate-spatial 의존성을 추가해주었다.


1
2
3
4
5
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>${hibernate.version}</version>
</dependency>

그 후, hibernate-spatial 라이브러리에 포함된 The JTS Topology Suite를 사용해 좌표 데이터를 다루면 되는데, 위에서 말한 것처럼 필자는 2D 데이터를 다루기 위해 Point를 사용했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import javax.persistence.Column;
import javax.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.locationtech.jts.geom.Point;

@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Address {

@Column(name = "road")
private String road;

@Column(name = "jibun")
private String jibun;

@Column(name = "zip", nullable = false)
private String zip;

@Column(name = "location", nullable = false)
private Point location;

@Builder
public Address(String road, String jibun, String zip, Point location) {
this.road = road;
this.jibun = jibun;
this.zip = zip;
this.location = location;
}
}
  • 만약 필자처럼 Point를 사용할 경우 org.springframework.data.geo.Point를 import하면 정상적으로 동작하지 않으므로, 꼭 import문을 확인하길 바란다.

위처럼 당차게 Entity class를 작성하고, 아래 같은 테스트 데이터를 넣고, 호기롭게 테스트를 돌려본 결과…

입력 데이터

1
2
3
4
5
6
7
8
9
10

2022-11-30 22:48:44.230 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [TIMESTAMP] - [null]
2022-11-30 22:48:44.231 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [2] as [TIMESTAMP] - [null]
2022-11-30 22:48:44.231 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [3] as [VARCHAR] - [지번 서울시]
2022-11-30 22:48:44.232 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [4] as [VARBINARY] - [POINT (19 2)]
2022-11-30 22:48:44.233 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [5] as [VARCHAR] - [도로명 서울시]
2022-11-30 22:48:44.234 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [6] as [VARCHAR] - [01006]
2022-11-30 22:48:44.235 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [7] as [BIGINT] - [null]
2022-11-30 22:48:44.235 TRACE 21788 --- [ main] o.h.type.descriptor.sql.BasicBinder : binding parameter [8] as [VARCHAR] - [우리집]

  • 위 로그처럼 sql에 입력된 파라미터들이 보고 싶은 경우 아래처럼 설정을 추가해주면 된다.
1
2
3
4
5
6
7
logging:
level:
org:
hibernate:
type:
descriptor:
sql: trace

발생한 에러 로그

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Caused by: org.h2.jdbc.JdbcSQLDataException: Value too long for column "LOCATION BINARY VARYING(255)": "X'aced00057372001f6f72672e6c6f636174696f6e746563682e6a74732e67656f6d2e506f696e74... (1151)"; SQL statement:
insert into place (id, create_date, update_date, jibun, location, road, zip, category_id, name) values (default, ?, ?, ?, ?, ?, ?, ?, ?) [22001-214]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:506)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:477)
at org.h2.message.DbException.get(DbException.java:223)
at org.h2.message.DbException.getValueTooLongException(DbException.java:322)
at org.h2.value.Value.getValueTooLongException(Value.java:2573)
at org.h2.value.Value.convertToVarbinary(Value.java:1371)
at org.h2.value.Value.convertTo(Value.java:1125)
at org.h2.value.Value.convertForAssignTo(Value.java:1092)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:369)
at org.h2.table.Table.convertInsertRow(Table.java:926)
at org.h2.command.dml.Insert.insertRows(Insert.java:167)
at org.h2.command.dml.Insert.update(Insert.java:135)
at org.h2.command.CommandContainer.executeUpdateWithGeneratedKeys(CommandContainer.java:242)
at org.h2.command.CommandContainer.update(CommandContainer.java:163)
at org.h2.command.Command.executeUpdate(Command.java:252)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:209)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:169)
at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
... 136 more

테이블 생성 SQL

1
2
3
4
5
6
7
8
9
10
11
12
create table place (
id bigint generated by default as identity,
create_date timestamp,
update_date timestamp,
jibun varchar(255),
location varbinary(255) not null,
road varchar(255),
zip varchar(255) not null,
category_id bigint,
name varchar(255) not null,
primary key (id)
)

이처럼 예상하지 못한 결과가 발생하였는데… 로그를 보니, table create 시 좌표를 저장하는 컬럼의 타입이 기대했던 GEOMETRY이 아닌, varbinary 타입으로 생성되어서 발생한 에러인가 싶어서 컬럼 타입을 “GEOMETRY”로 지정하고 생성해봤지만, 결과는 마찬가지였다.

수정한 코드 및 실행된 SQL

1
2
3
4
5
6
7
8
9

...

// columnDefinition 속성으로 컬럼 타입 지정
@Column(name = "location", nullable = false, columnDefinition = "GEOMETRY")
private Point location;

...


1
2
3
4
5
6
7
8
9
10
11
12
create table place (
id bigint generated by default as identity,
create_date timestamp,
update_date timestamp,
jibun varchar(255),
location GEOMETRY not null, -- 정상적으로 컬럼의 데이터 타입이 GEOMETRY로 변경
road varchar(255),
zip varchar(255) not null,
category_id bigint,
name varchar(255) not null,
primary key (id)
)

다시 원인을 검색해본 결과… hibernate-spatial에서는 H2가 아닌 H2의 확장 데이터베이스인 GeoDB를 지원한다고 되어있는 것을 확인할 수 있었다.

The GeoDBDialect supports the GeoDB a spatial extension of the H2 in-memory database.

그 후 GeoDB를 적용하기 위해 properties 혹은 yaml에 아래처럼 dialect 설정에 org.hibernate.spatial.dialect.h2geodb.GeoDBDialect를 추가해 주면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
datasource:
url: jdbc:h2:mem:test
username: sa
password:
driver-class-name: org.h2.Driver

jpa:
database-platform: org.hibernate.dialect.H2Dialect
defer-datasource-initialization: true
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
dialect: org.hibernate.spatial.dialect.h2geodb.GeoDBDialect # 추가 혹은 변경

dialect 설정 전 dialect

1
2022-11-30 23:37:47.975  INFO 15720 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect

dialect 설정 후 dialect

1
2022-11-30 23:35:42.124  INFO 18400 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.spatial.dialect.h2geodb.GeoDBDialect


이처럼 테스트가 성공하는 모습을 확인할 수 있었다.


참고 사이트

댓글 공유

상황

프로덕션 환경에서 Mariadb를 활용하고 있었는데, 프로덕션 환경의 DB로 Repository class를 테스트를 진행하니 테스트 데이터는 롤백되어 DB 남지 않았지만, Auto increment로 지정한 ID가 증가돼서 실제 서비스에서 데이터 저장 시 증가된 ID로 데이터가 저장되는 문제가 발생했다.

해서 Test할 때는 사용 할 데이터베이스 H2를 이용하기로 했다.
H2 데이터베이스는 인메모리 관계형 데이터베이스로 메모리 안에서 실행되기 때문에 어플리케이션을 시작할 때마다 초기화되어 테스트 용도로 많이 사용된다.
하지만 테스트 환경도 프로덕션 환경과 비슷하게 만들어서 테스트 하는 경우에는 테스트환경에도 프로덕션 DB를 생성해서 사용하는 경우도 있다고 한다.

필자가 okky에 올린 질문글
Repository Test시 ID 자동 증가


Test 환경에서 H2 적용

우선 H2 DB를 POM.xml에 추가하여 의존성을 등록한다.

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

그 후 Test 환경에서 사용하는 appication.properties에서 데이터베이스 설정을 H2로 설정해준다.

application-test.properties 위치

application-test.properties 내용

1
2
3
4
5
6
7
8
9
spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true

spring.jpa.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
  • 프로덕션 환경에서 Mariadb를 사용하기 때문에 dialect를 Mariadb로 설정하고 MODE=Mysql로 설정했다.
  • spring.jpa.hibernate.ddl-auto=none으로 설정하면 시작 시 마다 초기화되지 않기 떄문에 테스트 환경에선 꼭 create-drop으로 설정해준다.

Repository Test class

1
2
3
4
5
6
@ExtendWith(SpringExtension.class)
@DataJpaTest
@TestPropertySource("classpath:application-test.properties")
class WordRepositoryTest {
...
}
  • JPA Test를 위해 JPA 관련된 설정을 불러오는 @DataJpaTest
  • Test환경에선 프로덕션 환경과 다르게 H2 DB를 사용하므로 H2 DB설정을 지정한 application-test.properties를 호출하기 위한 @TestPropertySource

이 정도로 설정하고 Test하면 정상적으로 H2를 이용한 테스트가 성공할 것이다.


여담

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@BeforeEach
void setUp() {
user = new User();
user.setUsername("jgji");
user.setPassword("qwe123");

userRepository.save(this.user);

user1 = new User();
user.setUsername("haha");
user.setPassword("qwe123");

userRepository.save(this.user1);

List<Word> givenWordList = getWordGiven();
this.word = givenWordList.get(0);
this.word1 = givenWordList.get(1);
}

위 처럼 @Before 메서드를 지정해놓았는데, 각각 Test 메서드를 실행하였을 땐 Auto increment로 지정한 user의 id가 정상적으로 1, 2 이런식으로 생성되었지만 test class 전체로 test를 실행하니 DB가 메서드 마다 각각 실행하고 초기화 되는 것이 아닌지 User Id가 계속 증가해서 테스트가 실패하는 문제가 있었다.

테스트 시 꼭 메서드 각각으로하고 성공한다고 넘어가는게 아니라 클래스 전체로 테스트를 해봐야 할 것 같다.


프로젝트 전체 코드


참고 사이트

댓글 공유

  • page 1 of 1

Junggu Ji

author.bio


author.job