[스프링/몽고DB] 스프링 데이터 몽고DB

객체 매핑

스프링 데이터 몽고DB의 객체 매핑 기능을 사용하기 위해서는 @Document 어노테이션을 사용하는 것이 좋다. @Document 어노테이션을 사용하여 데이터를 객체로 매핑하는 것을 메타데이터 기반 매핑(metadata-based mapping)이라고 한다. 매핑 대상 도메인 객체에 대해 이 어노테이션을 항상 사용해야 하는 것은 아니지만, 이를 사용하면 스프링은 클래스패스 스캐너를 사용하여 도메인 객체를 찾고 필요한 메타데이터를 추출한다. 이 어노테이션을 사용하지 않는다면 매핑 프레임워크는 도메인 객체의 프로퍼티와 이를 데이터베이스에 영속화(저장)하기 위한 메타데이터를 내부적으로 빌드해야 하기 때문에 애플리케이션이 도매인 객체를 저장할 때 약간의 성능 부담이 발생할 수 있다.


MongoTemplate

MongoTemplate은 몽고 데이터베이스와 상호 작용하기 위해 문서 생성, 갱신, 삭제, 쿼리와 관련된 다양한 기능을 제공하며 도메인 객체와 데이터베이스 문서 간의 매핑을 제공한다. MongoTemplate는 스레드 안전하며 여러 인스턴스에서 재사용할 수 있다.

MongoTemplateMongoOperations 인터페이스를 구현한다. MongoOperations의 메서드는 가능한한 드라이버 API에 익숙한 기존 개발자를 위해 드라이버 컬렉션 객체에서 사용할 수 있는 메서드의 이름을 따서 명명된다. 주요 차이점은 MongoOperations는 문서 대신 도메인 객체를 전달할 수 있으므로, 작업의 파라미터를 지정하기 위해 문서를 채우는 대신 Query, CriteriaUpdate 작업을 위해 제공되는 플루언트 API를 사용하면 된다. MongoTemplate 인스턴스에서 작업에 대한 객체를 참조하는 선호되는 방법은 인터페이스인 MongoOperations를 통해 사용하는 것이다.


지리 공간 쿼리

몽고DB는 평면이나 지구와 같은 구체의 기하학을 계산하는 지리 공간 쿼리(geospatial query) 기능을 제공한다. 이를 위해 위치 데이터를 경도와 위도의 레거시 좌표 쌍(legacy coordinate pairs) 또는 GeoJSON 객체로 저장한다. GeoJSON은 다양한 지리적 데이터 구조를 인코딩하기 위한 공개 표준 형식이다. GeoJSON의 예는 다음과 같다.

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [125.6, 10.1]
  },
  "properties": {
    "name": "Dinagat Islands"
  }
}


몽고DB는 WGS84 참조 시스템을 사용하여 GeoJSON 객체에 대한 지리 공간 쿼리를 구체에서 계산한다. GeoJson 데이터를 저장하려면 다음과 같은 필드를 갖는 내장 객체를 사용한다.

  • type 필드: GeoJSON 객체 타입을 지정
  • coordinates 필드: 객체의 좌표를 지정


<필드>: {
  type: <GeoJSON 타입>,
  coordinates: <좌표>
}
"location": {
  "type": "Point",
  "coordinates": [50, 2]
}


location 필드명은 변경해도 상관 없지만 내장 객체의 필드명인 typecoorinates 필드명은 GeoJSON를 위해 지정된 값이므로 변경할 수 없다.

몽고DB는 2차원 평면과 구면 기하학을 위한 특수한 인덱스인 지리 공간 인덱스를 제공한다. 2차원 평면의 포인트(point)를 위해서는 2d 인덱스를, WGS84 좌표계([경도, 위도])를 기반으로 지표면을 모델링하는 구면의 포인트을 위해서는 2dsphere 인덱스를 사용하면 된다. 2d 인덱스 또한 구면 기하학과 거리 계산을 모두 지원하지만 구체의 경우 극 주변에사 왜곡이 심하므로 2dsphere 인덱스에 비해 정확도가 떨어진다. 비구체인 평면을 대상으로는 2d 인덱스를, 구체를 대상으로는 2dsphere 인덱스를 사용하는 것이 좋다.

2dsphere 인덱스는 지구와 같은 구체에서 지리 공간 쿼리를 지원한다. 2dsphere 인덱스는 다음 작업을 가능하게 한다.

  • 지정된 영역 내의 포인트 결정
  • 지정된 포인트에 대한 근접성 계산
  • 좌표 쿼리로 정확한 일치 결과 반환


인덱싱된 필드의 값은 다음 중 하나여야 한다.

  • 레거시 좌표 쌍
  • GeoJSON 객체


두 값은 거리 계산에 큰 차이가 있다. 레거시 형식은 라디안을 기준으로 작동하는 반면, GeoJSON 형식은 미터를 사용한다.

2dsphere 인덱스는 레거시 좌표 형식의 데이터를 GeoJSON 형식으로 변환한다. 2dsphere 인덱스를 만들려면, 문자열 2dsphere를 필드명의 값으로 하여 인덱스 유형으로 지정한다.

db.<collection>.createIndex( { <필드명> : "2dsphere" } )


몽고DB가 제공하는 지리 공간 쿼리 연산자는 다음과 같다.

  • $geoIntersects: GeoJSON 기하 도형과 교차하는 도형을 선택한다. $geometry 연산자를 사용하여 GeoJSON 객체를 지정한다. 2dsphere 인덱스가 지원한다.
  • $geoWithin: 경계가 있는 GeoJSON 도형(Polygon 또는 MultiPolygon 타입) 내에서 도형을 선택한다. 2dsphere2d 인덱스가 지원한다.
  • $near: 포인트에 근접한 지리 공간 객체를 반환한다. 2dsphere2d 인덱스가 지원한다.
  • $nearSphere: 구의 한 포인트에 근접한 위치의 지리 공간 객체를 반환한다. 2dsphere2d 인덱스가 지원한다.
  • $geometry: 위 지리 공간 쿼리 연산자와 함께 사용하여 GeoJSON 도형을 지정한다.
  • $maxDistance: $near 또는 $nearSphere 쿼리 결과를 지정된 거리로 제한한다. 최대 거리의 측정 단위는 사용 중인 좌표계에 따라 결정된다. GeoJSON의 포인트 타입의 경우 라디안이 아닌 미터 단위로 거리를 계산한다. 레거시 좌표 쌍은 라디안으로 계산한다. 2dsphere2d 인덱스가 지원한다.
  • $minDistance: $near 또는 $nearSphere 쿼리 결과를 중심점으로부터 지정된 거리 이상 떨어진 도큐먼트로 필터링한다. 중심점을 GeoJSON의 포인트 타입으로 지정하는 경우 거리를 미터 단위의 음수가 아닌 숫자로 지정하고 레거시 좌표 쌍으로 지정하는 경우 라디안 단위의 음수가 아닌 숫자로 지정한다. 쿼리에서 중심점을 GeoJSON 포인트 타입으로 지정하는 경우 2dsphere 인덱스만 사용할 수 있다.


몽고DB는 또한 지리 공간 집계 연산자를 제공한다. 이를 사용하여 지리 공간 집계 파이프라인(aggregation pipeline)을 구성할 수 있다.

  • $geoNear: 지리 공간 포인트과의 근접성에 기반한 정렬된 도큐먼트 스트림을 반환한다. 지리 공간 데이터에 대한 $match, $sort, $limit의 기능을 통합한다. 출력 도큐먼트에는 추가적인 거리 필드와 위치 식별자 필드가 포함될 수 있다.


$geoNear 연산자를 사용 시 레거시 좌표 쌍과 GeoJSON 객체를 사용할 수 있다. 쿼리 예는 다음과 같다.

// 레거시 좌표 쌍
{
  "$geoNear": {
    "near": [-73.99171, 40.738868]
  }
}

// GeoJSON 객체
{
  "$geoNear": {
    "near": { "type": "Point", "coordinates": [-73.99171, 40.738868] }
  }
}


레거시 좌표 쌍과 GeoJSON 객체는 $geoNear 쿼리 사용 시 거리 계산에 큰 차이가 있다. 레거시 형식은 라디안을 기준으로 작동하는 반면, GeoJSON 형식은 미터를 사용한다. 따라서 maxDistance 필드를 지정하여 최대 거리를 지정하는 경우 계산에 유의해야 한다.

지리 공간 쿼리와 관련된 메서드는 org.springframework.data.mongodb.core.queryCriteria 클래스에 정의되어 있으며, 이러한 메서드들과 함께 사용되는 기하 도형과 관련된 클래스(Box, Circle, Point)는 org.springframework.data.geo에 포함되어 있다.


스프링에서 도큐먼트의 필드가 좌표 데이터인 경우 해당 필드 타입을 double[]로 지정하여 레거시 좌표 쌍을 사용할 수 있다.

@Document(collection="newyork")
public class Store {
  @Id
  private String id;
  private String name;
  private double[] location;
}


GeoJSON 형식을 사용하려면 org.springframework.data.mongodb.core.geo 패키지가 제공하는 GeoJsonPoint, GeoJsonPolygon 등의 클래스를 사용하면 된다.

@Document(collection="newyork")
public class Store {
  @Id
  private String id;
  private String name;
  private GeoJsonPoint location;
}


벌크 연산

코틀린 코루틴 드라이버

  • 순차(ordered) 연산
    val mongoClient = MongoClient.create()
    mongoClient.getDatabase("database")
      .getCollection<DomainObject>("collection")
      .insertMany(list)
    mongoClient.close()
    
  • 비순차(unordered) 연산
    val mongoClient = MongoClient.create()
    mongoClient.getDatabase("database")
      .getCollection<DomainObject>("collection")
      .insertMany(list, InsertManyOptions().ordered(false))
    mongoClient.close()
    


MongoTemplate

  • 순차 연산
    val bulkInsertion: BulkOperations = mongoTemplate.bulkOps(BulkOperations.BulkMode.ORDERED, DomainObject::class.java)
    domainObjectList.forEach { e -> bulkInsertion.insert(e) }
    val bulkWriteResult = bulkInsertion.execute()
    
  • 비순차 연산
    val bulkInsertion: BulkOperations = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, DomainObject::class.java)
    domainObjectList.forEach { e -> bulkInsertion.insert(e) }
    val bulkWriteResult = bulkInsertion.execute()
    

페이징

MongoTemplate

몽고DB의 쿼리(query) 객체인 org.springframework.data.mongodb.core.query.Querylimit(), skip() 메서드를 사용하여 limitskip 속성을 설정함으로써 데이터 조회 시 페이징 처리를 할 수 있다. 이는 단순히 데이터 조회 시 반환할 도큐먼트의 수를 limit 속성으로 제한하고, 결과를 반환하기 전에 건너뛸 도큐먼트 수를 skip 속성으로 설정하는 것이다. 하지만 이 경우 전체 페이지 번호에 대한 현재 페이지 번호를 구할 수는 없다. 무한 페이징 처리 시 사용할 수 있다. 페이지 번호는 0부터 시작한다고 가정한다.

var query = Query().limit(pageSize).skip(pageNumber * pageSize);
var result = mongoTemplate.find(query, DomainObject.class);


다른 방법으로 Querywith() 메서드를 사용할 수 있다. 메서드의 인자로 org.springframework.data.domain.Pageable 인터페이스 구현체를 전달하면 내부적으로 Querylimitskip 속성이 설정되므로 앞의 방법과 동일하게 페이징 처리를 할 수 있다. 이 경우 limit 속성은 PageabletoLimit() 메서드의 반환값으로, skip 속성은 PageablegetOffset() 메서드의 반환값으로 설정된다. toLimit() 메서드는 Pageable 인터페이스에 기본 메서드로 구현이 정의되어 있으며 페이지 사이즈에 의해 결정된다. getOffset() 메서드는 정의만 되어 있어 구현체에 의해 정의된다. PageablePageable의 기본 자바빈 구현인 org.springframework.data.domain.PageRequest.PageRequestof() 메서드를 통해 생성할 수 있으며 PageRequest가 상속하는 추상 클래스인 org.springframework.data.domain.AbstractPageRequestgetOffset() 메서드는 다음 구현을 제공한다.

public long getOffset() {
  return (long)this.pageNumber * (long)this.pageSize;
}


즉, PageRequestof() 메서드 호출 시 페이지 번호와 페이지 사이즈를 전달하고 반환되는 PageableQuerywith() 메서드의 인자로 전달하면 첫 번째 방법과 동일한 방식으로 limitskip 속성을 설정함으로써 페이징 처리를 할 수 있다.

MongoTemplate의 경우 Pageable를 인자로 받고 Page를 반환하는 메서드가 정의되어 있지 않다. 애플리케이션 내에서 PagePageable 인터페이스를 통한 페이징 처리를 하려면 별도의 방법이 필요하다. 이 경우 Pageable을 사용한 쿼리 실행 및 최적화를 지원하는 org.springframework.data.support.PageableExecutionUtilsgetPage() 메서드를 사용할 수 있다. getPage() 메서드는 데이터 조회 결과, 페이징 정보를 담고 있는 Pageable, 최적화를 적용하는 LongSupplier를 기반으로 Page를 구성한다. LongSupplier는 카운트(count) 쿼리를 실행하는 함수형 인터페이스이다. 데이터 조회 결과 크기와 Pageable을 기반으로 총 합계를 결정할 수 있는 경우 카운트 쿼리를 생략한다. 단, 데이터 조회 쿼리가 카운트 쿼리보다 빠른 경우 이러한 최적화를 활용할 수 있다고 가정한다. 데이터 조회 쿼리가 너무 많은 데이터 조회하는 경우 적합하지 않을 수 있다.

var pageable: Pageable = PageRequest.of(0, 10);
var query: Query = Query().with(pageable);
var result: List<DomainObject> = mongoTemplate.find(query, DomainObject.class);
var page: Page<DomainObject> = PageableExecutionUtils.getPage(
  result,
  pageable,
  () -> mongoTemplate.count(Query.of(query).limit(-1).skip(-1), DomainObject.class)
);


참고

Comments