C#
Spatial 데이타

최근 모바일앱이나 웹사이트에서 GPS 위치를 이용한 응용프로그램들이 많이 사용되고 있다. 위치 기반 데이타를 저장하고 활용하기 위해서는 SQL Server의 Spatial 데이타 타입과 C#에서의 Spatial 관련 클래스들을 이해할 필요가 있다. (SQL Server 사용 가정)

Spatial 데이타는 크게 평면 데이타를 다루는 Geometry와 GPS 위치와 같은 3차원 데이타를 다루는 Geography로 구분할 수 있다. SQL Server는 SQL 2008부터 이 2가지 Spatial Data Type을 지원하고 있고 있는데, 이 Spatial Type들은 SQL 내부적으로 CLR 어셈블리로 구현되어 공간 데이타의 표현과 연산을 지원하고 있다.




SQL에서 Geography 데이타 사용

지구상의 위치는 보통 위도(Latitude), 경도(Longitude)로 표현되는데, 예를 들어 서울시청의 위치는 위도 37.566666 경도 126.978414 이다. 지구상의 위치 데이타 저장하고 각 위치들 간의 거리 같은 것을 계산하기 위해 SQL 에서 Geography 데이타 타입을 사용한다. (SQL 2008 이전 버전에서는 위치 정보를 위도컬럼, 경도컬럼으로 나누어 저장하였지만, SQL 2008부터는 Geography 컬럼 타입을 사용하여 하나의 컬럼에 저장하게 되었다)

SQL 상에서 위치정보를 저장하기 위해서 우선 테이블에 Geography 컬럼을 정의하고, INSERT에서 이 컬럼에 위도/경도를 넣으면 된다. Geography 컬럼에 위도/경도를 넣기 위해서는 몇 가지 다른 함수(메서드)가 제공되는데 가장 간단하게는 geography::Parse()를 사용할 수 있다. geography::Parse()의 파라미터는 하나의 문자열로서 위치포인트를 경도 위도 순으로 지정하게된다. 예를 들어 위의 서울시청 위치는 POINT(126.978414 37.566666) 와 같이 표현한다. POINT()내의 경도와 위도사이에 하나의 공백을 넣어 준다. 이렇게 특정 위치를 GEOGRAPHY 컬럼이나 변수에 넣은 후, GEOGRAPHY 타입의 데이타에 대한 거리 계산과 같은 연산을 할 수 있다. 예를 들어, STDistance() 메서드는 두 GEOGRAPHY 데이타 간의 거리를 계산하는데 사용된다. 거리의 단위는 미터이다.

아래 예제는 Store 테이블에 Location 컬럼을 정의하고 3개의 상점 위치를 추가하고 있다. 그리고 마지막 SELECT문에서 현재위치(시청이라고 가정)에서 반경 1km 내의 상점들을 찾는 쿼리를 표현하고 있다.


예제

-- 상점 테이블
CREATE TABLE Store ( 
    Id INT Primary Key,
    Name NVARCHAR(100),
    Location GEOGRAPHY
)

-- 상점 위치 저장
INSERT INTO Store (Id, Name, Location)
VALUES (1, N'KFC', geography::Parse('POINT(126.975970 37.571482)') )

INSERT INTO Store (Id, Name, Location)
VALUES (2, N'교보문고', geography::Parse('POINT(126.977831 37.571100)') )

INSERT INTO Store (Id, Name, Location)
VALUES (3, N'다니엘약국', geography::Parse('POINT(126.959521 37.523962)') )

-- 현재위치(서울시청)에서 반경 1km 상점 찾기
DECLARE @current GEOGRAPHY
SET @current = geography::Parse('POINT(126.978414 37.566666)') 

-- 결과 : KFC, 교보문고 만 출력
SELECT Id, Name, Location.STDistance(@current)
FROM Store
WHERE Location.STDistance(@current) < 1000



C#에서 Geography 데이타 사용

C#에서 Geography 데이타를 사용하기 위해 EntityFramework (EF6+)의 Spatial 관련 클래스들 (System.Data.Entity.Spatial.*)을 활용할 수 있다. 특히, DbGeography 클래스를 가장 많이 사용하는데, 이 클래스는 SQL에서의 CLR Type인 Microsoft.SqlServer.Types.SqlGeography을 EF에서 Wrapping한 클래스라고 볼 수 있다. DbGeography 클래스는 SQL 서버의 Geography 컬럼과 매핑되는 것으로, Entity Framework에서 Geography 컬럼을 정의할 때 DbGeography 타입으로 정하게 된다.

DbGeography는 기본적으로 위도/경도 정보를 갖는 것으로, 위치 정보를 갖는 여러 방식이 있으나, 간단한 메서드로 FromText() 메서드를 들 수 있다. 이 메서드는 'POINT(126.9784 37.5667)'와 같은 문자열을 받아들여 위치 데이타를 생성한다.
아래 예제는 서울과 부산의 위치 데이타로부터 2개의 DbGeography 객체를 생성하고 Distance() 메서드를 사용하여 두 위치간의 거리를 계산하는 코드이다.


예제

using System;
using System.Data.Entity.Spatial;

namespace DbGeo
{
    class Program
    {
        static void Main(string[] args)
        {
            // 서울 (경도: 126.9784, 위도: 37.5667)
            DbGeography seoul = DbGeography.FromText("POINT(126.9784 37.5667)");

            // 부산 (경도: 129.0403, 위도: 35.1028)
            DbGeography busan = DbGeography.FromText("POINT(129.0403 35.1028)");

            // 거리 (미터로 계산됨)
            double? meters = seoul.Distance(busan);

            // 출력
            int km = (int)(meters.Value / 1000);
            Console.WriteLine("서울-부산 거리 = {0} km", km);
        }
    }
}




Entity Framework에서 Geography 데이타 활용

이제 C#에서 Entity Framework에서 사용하여 SQL에 정의된 위치 데이타를 가져오는 과정을 살펴보자. SQL 서버에 (위의 [SQL에서 Geography 데이타 사용]에서 정의한 대로) Store 테이블이 정의되어 있다고 가정하고, 아래 예제와 같이 Store 클래스를 정의할 수 있다. SQL 테이블에서는 Location 컬럼을 GEOGRAPHY 타입으로 정의하였지만, C#에서는 이를 DbGeography으로 정의하여야 한다. 이렇게 Store 클래스가 정의된 후, 현재 위치에서 반경 5 km 내의 상점을 찾는 루틴을 구현하려면, StoreManager.FindStores()와 같이 작성할 수 있다. 즉, 입력파라미터로 현재위치의 위도/경도를 입력받고, 반경 5km의 거리를 산출하는데, 거리 산출은 현재위치와 Store.Location위치의 차이를 DbGeography의 Distance() 메서드를 사용하여 계산한다. (DbGeography.Distance()메서드는 SQL 서버로 보내질 때, (위의 SQL 예제에서 보이듯이) TSQL문에서는 .STDistance()로 변환된다). 마지막으로 select new {...} 문자에서 보이듯이, DbGeography 객체로부터 위도,경도를 가져오려면 .Latitude, .Longitude 속성을 사용하면 된다.

예제

using System;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Spatial;
using System.Linq;

namespace DbGeo
{
    #region EF 셋업 (Code-First)
    public class StoreDbContext : DbContext
    {
        public StoreDbContext() : base("DefaultConnection")
        {
            Database.SetInitializer<StoreDbContext>(null);
        }
        public DbSet<Store> Stores { get; set; }
    }

    [Table("Store")]
    public class Store
    {
        public int Id { get; set; }
        public string Name { get; set; }
        // 위치 속성 정의
        public DbGeography Location { get; set; }
    }
    #endregion


    public class StoreManager
    {
        // 현재위치에서 반경 x km 이내의 상점 찾기
        public static void FindStores(double currLat, double currLng, int withinKm = 5)
        {
            // 현재위치 DbGeography 객체
            string point = string.Format("POINT({0} {1})", currLng, currLat);
            DbGeography currentLocation = DbGeography.FromText(point);

            double distance = withinKm * 1000;

            var db = new StoreDbContext();

            // LINQ 쿼리 : 반경 5km 이내 상점
            var stores = from s in db.Stores
                         where s.Location.Distance(currentLocation) <= distance
                         select new
                         {
                             Id = s.Id,
                             Name = s.Name,
                             Latitude = s.Location.Latitude,  //위도 속성
                             Longitude = s.Location.Longitude //경도
                         };

            // 코드 단순화를 위해 그대로 출력함
            foreach (var s in stores)
            {
                Console.WriteLine("{0}: {1},{2}", s.Name, s.Latitude, s.Longitude);
            }
        }
    }
}




본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.