Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] Feat/#548 공간 인덱스 구현 #584

Merged
merged 12 commits into from
Oct 17, 2023
Merged

Conversation

cpot5620
Copy link
Collaborator

@cpot5620 cpot5620 commented Oct 14, 2023

작업 대상

  • LocationRepository
  • 전체 테스트 환경(TestContainer)

📄 작업 내용

핀을 저장할 때, 데이터를 효율적으로(?) 저장하기 위해, 특정 반경내에 좌표가 존재하는지 확인합니다.
기존에는 함수를 활용했기 때문에, 인덱스를 사용할 수 없었습니다.
MySql에서 제공하는 함수를 통해, 공간 인덱스를 적용하였습니다.

어느정도의 성능 이점을 가져오는지는 블로그 글 작성시에 남겨두도록 하겠습니다.

JPA상에서는 적용해두었으나, 실제 개발 및 운영 환경의 DB에는 공간 인덱스가 적용되지 않은 상태입니다.
이에따라, 추가적으로 필요한 작업은 아래와 같습니다.

  1. 공간 인덱스 생성
create spatial index location_idx01 on location(coordinate);
  1. 테이블 구조 변경
    longitude(double), lattitude(double) -> coordinate(geometry)로 변경
    테이블 자체에 SRID 값이 0으로 자동 설정될 수 있는데, 이는 명시적으로 4326 값을 사용하도록 수정해야함.
  • 참고
Hibernate: 
    create table location (
        created_at datetime(6),
        id bigint not null auto_increment,
        updated_at datetime(6),
        legal_dong_code varchar(255) not null,
        parcel_base_address varchar(255) not null,
        road_base_address varchar(255) not null,
        **coordinate geometry SRID 4326 not null, // 이 부분**
        primary key (id)
    ) engine=InnoDB

🙋🏻 주의 사항

LocationRepository에서 JPQL을 mysql에서 지원해주는 함수로 변경하였는데요.
이에따라, 기존 H2 DB에서 mode=MySQL로 설정하더라도 동작하지 않는 이슈가 있었습니다.
이에따라, 테스트를 수행할 때 Dokcer를 실행시킨 뒤에 TestContainer를 띄워야합니다.

스크린샷

📎 관련 이슈

closed #548

레퍼런스

@cpot5620 cpot5620 added BE 백엔드 관련 이슈 우선순위 : 중 refactor 리팩토링 관련 이슈 labels Oct 14, 2023
@github-actions
Copy link

github-actions bot commented Oct 14, 2023

Unit Test Results

  66 files    66 suites   39s ⏱️
304 tests 304 ✔️ 0 💤 0
313 runs  313 ✔️ 0 💤 0

Results for commit aef78bb.

♻️ This comment has been updated with latest results.

Copy link
Collaborator Author

@cpot5620 cpot5620 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설명이 필요한 부분 코멘트 남겨두었습니다.

+ " * cos( radians( l.coordinate.longitude ) - radians(:#{#current_coordinate.longitude}) ) "
+ " + sin( radians(:#{#current_coordinate.latitude}) ) "
+ " * sin( radians( l.coordinate.latitude ) ) ) ) <= :distance"
+ "WHERE ST_Contains(ST_Buffer(:coordinate, :distance), l.coordinate.coordinate)"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ST_Contains(ST_Buffer(:coordinate, :distance), l.coordinate.coordinate)
이 부분 설명드릴게요.
ST_Buffer(A,B) 함수는 다음과 같습니다.
A는 특정 좌표를 나타내고, B는 거리를 나타냅니다. 이때 단위는 Meter이빈다.
A의 좌표로부터 B 거리 내에 존재하는 MBR을 반환해주는데요.
A의 좌표로부터 B 거리 내를 나타내는 것은 원이겠죠 ?
MBR은 이를 감싸는 사각형이라고 생각하시면 됩니다.

ST_Contains(A, B) 함수는 A라는 MBR(Minimum Bounding Rectangle)안에 B가 존재하는지 여부를 판단합니다.
즉, 위에서 이야기한 MBR 내에 존재하는 좌표 값들을 조회한다고 생각하시면 됩니다.

두 함수 모두 MySQL에서 지원하는 함수입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한게 있어용 쥬니짱

말씀해주신 것처럼 MBR 은 A의 좌표로부터 B 거리 내를 나타내는 원을 감싸는 사각형이라고 하셨는데, 그렇다는 것은 MBR 은 원을 제외한 잉여공간이 있는 것인가요?? 사각형의 꼭짓점 부분들이요!

그렇다면 실제로 A의 좌표로부터 B 거리 이상 떨어진 좌표임에도 불구하고 조회해 올 수도 있는 건가요?

image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞아요. 그래서 명확한 계산을 하기 위해서는 ST_DISTANCE 함수를 활용해야하는데, MySQL에서는 해당 함수를 사용할 때, 인덱싱을 처리해주지 않아요.

우리 서비스가 엄청 정확한 거리계산을 필요로하지 않는다는 점과, 실제 테스트해보지 않았지만 찾아본 결과 20배 가량의 성능 향상 결과가 나타난다는 점에서 위와 같은 결정을 내리게 되었어요.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멋집니다~!

mySQLContainer.start();
}

@DynamicPropertySource
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동적으로 설정파일을 수정해줍니다 ~

컨테이너가 뜰때마다 포트번호가 다르다보니, url이 달라지잖아요 ?
그래서 사용합니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지렸네용

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +14 to +21
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(JpaConfig.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public @interface RepositoryTest {

}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 컨테이너를 사용하는데, @DataJpaTest를 사용하면 내부적으로 InMemory H2 데이터베이스를 사용합니다.
이로 인해, mysql 에서 지원해주는 함수를 사용함으로 인해 예외가 발생합니다.
그래서 데이터베이스를 바꾸지 않는다는 어노테이션(@AutoConfigureTestDatabase(replace = Replace.NONE)을 사용해야 하는데요.

모든 테스트에서 이러한 작업을 해주기보단, Repository 계층 테스트를 위한 어노테이션을 구현했습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 그렇군요ㅎ 고생하셨네용

Comment on lines 33 to 35
private Coordinate(double latitude, double longitude) {
this.latitude = latitude;
this.longitude = longitude;
this.coordinate = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ클래스명이랑 겹쳐서 org.xxxx 나오는데.. 이거 어떻게하죠 ㅠ_ㅠ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

제가 생각한 방법은 총 3가지의 방법이 있는 것 같아요!

  1. geom.Coordintate 를 반환하는 클래스를 따로 만든다
  2. 위 사진처럼 import alias 를 실행해준다.
  3. 그냥... 놔둔다

다른 분들도 의견이 있으시다면 내주시죵~~

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 그냥 놔두는 거 한 표요!
패키지명이 나와서 지저분해보이긴 하지만 .. 오히려 저게 없으면, 코드를 읽을 때 어떤 Coordinate의 생성자인지 헷갈릴 것 같아서요!

Copy link
Collaborator

@kpeel5839 kpeel5839 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쥬니짱 너어어무우우 고생하셨어요~!

단 코멘트 외에 질문이 하나 있어요!

모든 테스트에 TestDatabaseContainer 를 붙이신 이유가 있을까용??

제가 AtlasCommandService 에서 상속을 지워보니까 테스트가 안터져서용!

@@ -29,6 +29,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation group: 'com.github.maricn', name: 'logback-slack-appender', version: '1.6.1'
implementation group: 'org.hibernate', name: 'hibernate-spatial', version:'6.2.5.Final'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

허허허허 logback 도 적용이 안되어 있긴 하지만 org.hibernate:hibernate-spatial:6.2.5.Final 이런 식으로 명시하여 다른 implementation 과 형식을 동일하게 가져가면 어떨까용?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했어요~!

@@ -44,7 +44,7 @@ public List<TopicResponse> findNearbyTopicsSortedByPinCount(
) {
Coordinate coordinate = Coordinate.of(latitude, longitude);
List<Location> nearLocation = locationRepository.findAllByCoordinateAndDistanceInMeters(
coordinate,
coordinate.getCoordinate(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

간지나네용

Comment on lines +25 to +27
/*
* 4326은 데이터베이스에서 사용하는 여러 SRID 값 중, 일반적인 GPS기반의 위/경도 좌표를 저장할 때 쓰이는 값입니다.
* */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 그래서 SRID 값이 4326 이었던 거군용

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일종의 식별코드 같은 것이로군요

@@ -14,7 +14,7 @@
public class DatabaseCleanup implements InitializingBean {

private static final String TRUNCATE_SQL_MESSAGE = "TRUNCATE TABLE %s";
private static final String SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE = "SET REFERENTIAL_INTEGRITY %s";
private static final String SET_REFERENTIAL_INTEGRITY_SQL_MESSAGE = "SET FOREIGN_KEY_CHECKS = %s";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 MYSQL 로 변경되어서 그런건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넹 H2랑 문법이 다르니까여

mySQLContainer.start();
}

@DynamicPropertySource
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지렸네용

Copy link
Collaborator

@yoondgu yoondgu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쥬니 공간 인덱스 멋지게 잘 구현해주셨네요! 수고하셨습니당
저도 궁금했던 점들을 매튜가 질문 잘 달아주셔서,
저는 간단한 변경 제안 몇 개 달았는데요. (P1 1개, P2 1개)
반영해주실 걸 알고 미리 Approve 드립니다 ^.^

@Column(columnDefinition = "Decimal(18,15)")
private double longitude;
@Column(columnDefinition = "geometry SRID 4326", nullable = false)
private Point coordinate;

private Coordinate(double latitude, double longitude) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private Coordinate(double latitude, double longitude) {
private Coordinate(final Point point) {
this.coordinate = point;
}
public static Coordinate of(double latitude, double longitude) {
validateRange(latitude, longitude);
final Point point = geometryFactory.createPoint(new org.locationtech.jts.geom.Coordinate(longitude, latitude));
return new Coordinate(point);
}

P2. 이렇게 해서 생성 로직을 정적 팩터리 메서드에서 확인할 수 있는 것은 어떨까요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(file change 부분이 아닌 라인이 있어서 코멘트 위치가 애매하게 달아졌어요)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞네여.. 뇌 빼고 했어요 죄송합니다

);
@Param("coordinate") Point coordinate,
@Param("distance") double distance);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1. 개행!!

mySQLContainer.start();
}

@DynamicPropertySource
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -44,6 +45,8 @@ dependencies {
testImplementation 'io.rest-assured:rest-assured'
testImplementation 'io.rest-assured:spring-mock-mvc'
testImplementation 'org.assertj:assertj-core:3.19.0'
testImplementation 'org.testcontainers:mysql:1.17.2'
testImplementation 'org.testcontainers:junit-jupiter:1.17.2'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홍 testContainer 전용으로 junit 의존성도 추가로 필요한가보군요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예쓰 !! junit에서 제공해주는 것 같더라구요

Comment on lines +25 to +27
/*
* 4326은 데이터베이스에서 사용하는 여러 SRID 값 중, 일반적인 GPS기반의 위/경도 좌표를 저장할 때 쓰이는 값입니다.
* */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일종의 식별코드 같은 것이로군요

Comment on lines -54 to -60
public double calculateDistanceInMeters(Coordinate otherCoordinate) {
double earthRadius = 6_371_000;
public double getLatitude() {
return coordinate.getY();
}

return acos(sin(toRadians(otherCoordinate.latitude)) * sin(toRadians(this.latitude)) + (
cos(toRadians(otherCoordinate.latitude)) * cos(toRadians(this.latitude))
* cos(toRadians(otherCoordinate.longitude - this.longitude))
)) * earthRadius;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제로 쓰이지 않고 있는 메서드를 삭제하고 테스트도 함께 지워주셨군요!
확인했습니다 👍

Copy link
Collaborator

@junpakPark junpakPark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다 👍
오늘 8시 전까지 개발서버 DB 설정 변경하겠습니다.

관련 자료

스크린샷 2023-10-16 오후 2 44 24

@cpot5620
Copy link
Collaborator Author

@kpeel5839
아마, AtlasServiceTest 부분에서 예외가 발생하지 않았던 이유는 LocationRepository을 사용하지 않았기 때문인 것 같아요.
사실, MySQL에서 지원하는 함수로 작성한 JPQL 메서드를 호출하지 않으면, 테스트 컨테이너 상속을 해주지 않아도 되는게 맞아요.

근데, 일종의 통일성을 맞추기 위함과 실제 우리 서비스가 MySQL 환경에서 돌아간다는 점에서 일괄적으로 적용했어요.

@junpakPark junpakPark merged commit 11320bb into develop-BE Oct 17, 2023
3 checks passed
@junpakPark junpakPark deleted the feat/#548-spatial branch October 17, 2023 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
BE 백엔드 관련 이슈 refactor 리팩토링 관련 이슈 우선순위 : 중
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants