티스토리 뷰

주어진 상황

JavaScript로 유저의 위치를 구하였고 유저와 헬퍼의 최단거리까지 구하였다.

하지만 우리는 NestJs로 개발을 하여야 했고 또한 해당거리내에 있는 유저들을 찾아야 했다.

 

해야할 목록

1. JavaScript로 테스트 해보았던 기능들 NestJs로 구현

    → javascript에서는 서버단과 연결하는 부분을 주로 하고 로직적인 부분을 최대한 서비스 로직으로 가져옴.

    이유 : 두 지점간의 거리를 구하는 공식같은 부분은 여러 서비스에서 계속 사용하기 때문에

              서비스 단에서 유지 보수및 재사용 하기 위해 서비스로직으로 분리하였다.

2. 유저 위치기반 주변 유저 찾기

    → Spatial database 활용(MySQL, 벡터데이터 모델)

 

공간데이터를 사용하는 이유

지금 사용하려는 데이터 베이스에서 공간 쿼리를 제공한다.

공간쿼리는 공간데이터를 사용하여 지리적 위치를 기반으로 데이터를 필터링 할수가 있다.

그렇기 때문에 특정 지역에 속해있는 유저들을 선택할수도 있고

공간데이터를 사용하여 유저의 위치를 준심으로 주변의 다른 유저들을 찾을수 있다.

또한 경로를 최적화 하여 효율적인 서비스를 제공할수도 있기 때문에 공간 데이터를 사용하였습니다.

 

 

맞이한 문제 1

error: Error: ER_SP_DOES_NOT_EXIST: FUNCTION distance.AsText does not exist
ERROR [ExceptionsHandler] ER_SP_DOES_NOT_EXIST: FUNCTION distance.AsText does not exist

위 에러는 지금 까지 봤던 에러들중에 처음 보는 에러였다.

AsText가 존재 하지 않는다는데 AsText는 나에게 없는 속성이였다.

어디에도 AsText는 없었다. 처음에는 자력으로 해결해 보고자 TypeORM에서도 실수로 잘못적었는지 확인해보았다.

    await this.distanceRepository.save({
      id: 2,
      latitude,
      longitude,
      location: { type: 'Point', coordinates: [longitude, latitude] },
    });

그 어디에서 AsText는 없다.

AsText : 공간데이터를 텍스트 형식으로 변환하는 함수나 메서드

원인은 TypeORM은 기본적으로 지오메트리 형식을 지원하지 않는다고 한다.

그렇기 때문에 Point같은 타입으로 location에 위도 경도를 넣으려고하니 AsText타입이 없다고 나오는 것이다.

그렇게 되면 할수 있는 방식이 로우 쿼리 밖에 없다.

일단 현재 mysql에서 지오메트리 타입을 지원하는지 확인해 보았다.

-- "SPATIAL"이라는 키워드를 가진 플러그인 정보를 찾기
SELECT * FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE 'SPATIAL';

데이터가 나오진 않았지만 공간데이터를 사용 할수 있는것 같았다.

"SPATIAL"이라는 부분이 포함된 플러그인은 공간 데이터와 관련된 기능을 제공할 수 있는 플러그인을 가리기 때문이다.

 

그리고 현재 데이터 베이스에서 테스트를 해보았다.

INSERT INTO distance (user_id,latitude,longitude, location)
VALUES 
    (1, 37.566205021936,126.97770627907,ST_GeomFromText('POINT(126.97770627907 37.566205021936)'));

유저와 위도, 경도를 입력해주고 ST_GeomFromText메서드를 사용하여 포인트 지점을 입력하여 테스트 하였다.

더보기

ST_GeomFromText

    • 텍스트형식(WKT - Well Known Text) 또는 GeoJSON형식의 GeoJSON형식을 취하며,

      선택적으로 공간 참조 시스템 식별자를 입력 매개변수로 사용하여 해당 지오메트리를 리턴한다.

    • 즉, Geometry객체를 표현하기 위한 WKT형식을 기본으로 한다.

데이터가 들어가긴 했는데 BLOB이라면서 location에 들어간 정보는 보이지 않았다.

확인해보니 text로는 문자열로 보낸 정보가 wkt형식으로 볼수가 있었고

진법으로 표현하면 위와 같이 생긴걸 볼수가 있었다.

이제 들어간 그대로 옮기기만 하면된다.

  async saveMyLocation(location: any) {
    const { latitude, longitude } = location;

    try {
      await this.distanceRepository.query(`
              insert into distance (user_id, latitude, longitude, location)
              VALUES (100, ${latitude}, ${longitude}, ST_GeomFromText('POINT(${longitude} ${latitude})'))
          `);

      return '위치 정보 저장 성공';
    } catch (error) {
      console.log(error);

      return '위치 정보 저장 실패';
    }
  }

동적으로 유저의 데이터를 받아올수 있도록 설정한뒤 테스트 해봤다.

 

위치 정보가 잘 저장되는걸 확인하였다.

 

 

맞이한 문제 2

const userLocation = await this.distanceRepository.findOne({
   where: { userId },
});
/* 에러 발생 
[Nest]ERROR [ExceptionsHandler] ER_SP_DOES_NOT_EXIST: FUNCTION distance.AsText does not exist
QueryFailedError: ER_SP_DOES_NOT_EXIST: FUNCTION distance.AsText does not exist
*/

이번엔 데이터 저장이 아닌 조회를 하는것에서 에러가 나왔다.

이부분도 TypeORM에서  Geometry객체를 표현하지 못하여 생기는 문제라 생각하여 로우 쿼리로 바꿔주었다.

    const userLocation = await this.distanceRepository.query(`
      SELECT * 
      FROM distance 
      WHERE user_id = ${userId}
    `);

이렇게 바꿔서 조회를 해보니

  [
    RowDataPacket {
      id: 1,
      user_id: 14,
      latitude: 37.4013952,
      longitude: 127.~~~,
      location: { x: 127.~~~, y: 37.4013952 },
      time_stamp: 2024-04-06T11:10:23.615Z
    }
  ]

문제가 해결되었다.

 

맞이한 문제 3

     const helper = await this.distanceRepository.query(`
      SELECT * FROM distance
      WHERE
      ST_Distance_Sphere(location, ST_GeomFromText('POINT(${userLocation[0].longitude} ${userLocation[0].latitude})')) <= 1000;
      `);
      /* 에러발생
      error: Error: UNKNOWN_CODE_PLEASE_REPORT: Latitude 127.~~~ is out of range in function st_distance_sphere. It must be within [-90.000000, 90.000000].
      QueryFailedError: UNKNOWN_CODE_PLEASE_REPORT: Latitude 127.~~ is out of range in function st_distance_sphere. It must be within [-90.000000, 90.000000].
        query: '\n' +
    '     SELECT * FROM distance \n' +
    '     WHERE \n' +
    "     ST_Distance_Sphere(location, ST_GeomFromText('POINT(127.~~ 37.4013952)')) <= 1000;\n" +
    '     ',
  		parameters: undefined,
      */

에러문자만 해석해보면 위도는 -90 부터 90 사이의 존재하여야 하는데 그 이상을 넘어갔다고 한다.

여기서 조회하는 쿼리문에는 일단 문제가 없었다.

그럼 어디가 문제인지 확인해보는데 

위도와 경도가 다르게 저장된 데이터를 찾았다.

즉, 문제가 발생한건 여기서 잘못 저장된 데이터를 

ST_Distance_Sphere를 통해 거리반경을 구하려고 하니 생기는 문제였다.

이 부분을 지우고 테스트를 해보니 

나의 거리와 내 주변의 유저의 거리가 잘뜨는걸 확인할 수가 있었다.

 

 

기능 구현

import { Injectable } from '@nestjs/common';
import { CreateDistanceDto } from './dto/create-distance.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Distance } from './entities/distance.entity';
import { Repository, getConnection } from 'typeorm';

@Injectable()
export class DistanceService {
  constructor(
    @InjectRepository(Distance)
    private readonly distanceRepository: Repository<Distance>,
  ) {}
  
  // 나의 위치 찾기
  async saveMyLocation(location: any) {
    const { latitude, longitude } = location;
    try {
      await this.distanceRepository.query(`
              insert into distance (user_id, latitude, longitude, location)
              VALUES (100, ${latitude}, ${longitude}, ST_GeomFromText('POINT(${longitude} ${latitude})'))
          `);

      return 'Location saved successfully.';
    } catch (error) {
      console.log(error);

      return 'Failed to save location:';
    }
  }

// 하버사인 공식으로 유저와의 최단 거리 찾기
  async haverSign(id: number, location: any) {
    const { latitude, longitude } = location;
    const userLocation = await this.distanceRepository.query(`
      SELECT * 
      FROM distance 
      WHERE user_id = ${id}
    `);

    // 사람이 한명 나왔다고 가정.
    const shortestDistance = await distance(
      latitude,
      longitude,
      userLocation[0].latitude,
      userLocation[0].longitude,
    );

    return shortestDistance;
  }

// 헬퍼 찾기
  async findHelper(userId: number) {
    const userLocation = await this.distanceRepository.query(`
      SELECT * 
      FROM distance 
      WHERE user_id = ${userId}
    `);
    try {
      const helpers = await this.distanceRepository.query(`
      SELECT *,ST_Distance_Sphere(POINT(${userLocation[0].longitude}, ${userLocation[0].latitude}),location)AS distance 
      FROM distance 
      WHERE 
      ST_Distance_Sphere(POINT(${userLocation[0].longitude}, ${userLocation[0].latitude}),location) <= 1000  
      ORDER BY distance;
     `);
      const helper = helpers.map((helper) => helper.user_id);

      return helper;
    } catch (err) {
      console.log(err);

      return 'Failed';
    }
  }
}

async function distance(lat1, lon1, lat2, lon2) {
  const R = 6371; // 지구 반지름 (단위: km)
  const dLat = await deg2rad(lat2 - lat1);
  const dLon = await deg2rad(lon2 - lon1);
  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(await deg2rad(lat1)) *
      Math.cos(await deg2rad(lat2)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distance = R * c; // 두 지점 간의 거리 (단위: km)
  const finalDistance = Number(distance.toFixed(1));
  return finalDistance;
}

async function deg2rad(deg) {
  return deg * (Math.PI / 180);
}

 

 

개선해야 할 점

지금은 다 로우 쿼리로 작성을 하여 공간데이터를 활용하였다.

하지만 이렇게 되면 sql inject 의 공격을 받을수 있지 않나? 라는 생각이 들었다.

더 안전하고 보안적인 면에서 강하게 작용할수있는 방법을 찾아봐야겠다.

 

또한 하버사인 공식을 함수로 하는게 과연 좋은 방법인가에 대해서 고민을 더 해봐야 겠다.

어느 것이 성능적인 면에서 더 나은지, 만약 유저가 1000명 10000명, 100000명이 사용한다고 하면 그 모든사람의 거리 계산을 함수를 끌어와서 해야 한다는건데 데이터 베이스에서 처리할수 있지 않을까?

좀더 공부가 필요한것같다.

공간데이터자체도 기하학을 사용한 공간데이터 활용이라고 하는데 더 이해하려면 더 많이 사용해보고 이론적으로 파고 들어야 할것 같다.

 

 

 

 

참고자료

공간 데이터

 

Geographic data models

Two common data models used to represent geographic data are the vector data model and the raster data model. Vector data model The vector d...

gis4uo.blogspot.com

 

지리 공간 데이터가 무엇인가요? - 지리 공간 데이터 - AWS

지리 공간 인텔리전스는 정보에 입각한 의사 결정을 위해 지리 공간 데이터의 수집, 분석 및 해석을 설명하는 용어입니다. 지리 데이터를 이미지, 신호 인텔리전스 및 대인 정보 활용을 포함한

aws.amazon.com

 

 

공간 데이터 함수

 

제4장 Spatial 관련 함수

본 장에서는 Tibero Spatial에서 제공되는 함수에 대해 설명한다. ST_CONTAINS는 GEOMETRY 객체 2가 GEOMETRY 객체 1의 외부에 존재하지 않고, 객체 1의 내부에 하나 이상의 포인트가 존재할 때만 1을 반환하는

technet.tmaxsoft.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/02   »
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
글 보관함