ohjongsung's Dev Story

Ohjongsung's Dev Story

NoSQL에서 스키마 변경과 마이그레이션

2019년 03월 01일

NoSQL에서 스키마를 변경한 경우, 모든 문서를 건 바이 건으로 변경된 스키마를 반영한다고 생각해보자. 저장된 데이터의 양이 작다면 순식간에 끝날 것이다. 그러나 데이터의 양이 많아서 며칠이 걸린다면? 중간에 에러가 발생해서 중단되면? 스키마 마이그레이션 작업의 부하가 크다면 어떻게 해야 할까? 이 글을 통해서 NoSQL의 스키마 변경을 어떻게 반영하는지 알아보자.

Schemaless

NoSQL의 특성 중 하나인 이 말은 스키마가 없다는 것이 아니라, RDB의 스키마보다 유연하고 탄력적인 스키마를 뜻한다. RDB에서는 ALTER 쿼리를 사용해서 스키마를 변경한다. 그리고 쿼리를 실행하면 Row가 아무리 많아도 동시에 변경된다. ACID 트랜잭션 덕분에 쿼리 실행이 실패하면 그 어떤 행에도 스키마가 변경되지 않고, 어설프게 반영되는 일은 없다. 반대로 NoSQL에서는 따로 스키마를 관리하는 포인트가 없다. 그저 Application에 정의한 Entity가 스키마고, 여기에 필드를 추가하거나 변경하면 새로 입력된 데이터는 변경된 스키마가 반영되어 저장된다.

RDB에서 스키마 변경

ALTER TABLE Products ADD description VARCHAR(250) NOT NULL DEFAULT("소개글");

NoSQL에서 스키마 변경

public class Product{
	private long productId;
	private String name;
	private long price;
	// description field를 추가하자.
	private String description;
}

스키마가 RDB처럼 Database에 정의하는 것이 아니라, Application에 정의한다고 생각하면 편하다. 그런데 RDB를 다루던 습관때문인지 NoSQL에서 스키마를 변경하게 되면, 변경 사항을 이미 저장된 문서에 반영하고 싶어 진다. NoSQL은 스키마가 다른 여러 문서도 수용하는데 말이다.

다양한 스키마 지원

NoSQL은 스키마가 다른 여러 문서도 수용할 수 있다. 그렇다면, 굳이 스키마를 RDB처럼 하나의 스키마로 유지하려고 애쓰지 말자. 어떤 스키마로 저장되어 있든, Application단에서 메모리에 올라갈때, 최신 버전의 스키마로 데이터를 변환해주면 된다.

기존 데이터는 Entity version 1로 다음과 같이 저장되어 있다고 하자.

public class Product{
	private int version = 1;
	private String name;
	private long productId;
	private long price;
}
// json data
{
    _productId: 123,
   name: "CouchBase"
    version: 1,
    price: 1000
},
{
    _productId: 125,
    name: "Hbase"
    version: 1,
    price: 1200
}

그리고 comments 필드를 추가하면서 Entity version을 2로 변경해서 저장하기 시작했다.

public class Product{
	private int version = 2;
	private long productId;
	private long price;
	private String description;
}
// json data
{
    _productId: 127,
    name: "MongoDB"
    version: 2,
    price: 1500,
    description: "Document Database"
}

그리고 version이 0 에서 1로 올라갈때, 데이터 마이그레이션 함수를 만들어 두자. 이 함수는 문서를 Application에서 읽을 때 저장된 버전이 최신 버전과 다를때 실행된다.

public Product getProduct(long productId){
	JsonDocument doc = nosqlRepository.get(productId);
	Product product = defaultConvertDocument(doc);
	if (doc['version'] != product.getVersion()){
		return migrateOneToTwo(product);
	}
}

public void migrateOneToTwo(Product product) {
	product.setDescription("NoSQL Database");
	// 필요할 경우 저장도 하자.
	// nosqlRepository.save(product);
	return product;
}

이렇게 작업을 하면, 데이터가 호출될때는 무조건 최신 스키마가 반영된 데이터를 확인 할 수 있게 된다. 그리고 마이그레이션 작업의 부하가 분산되는 효과도 얻을 수 있다. 따지고 보면, 우리가 데이터 포멧 변경하기 위한 코드와 크게 다르지 않다. 그리고 이런 방법을 on-demand migration이라고 한다.

마무리

설명한 방식이 제대로 작동하려면 데이터의 접근이 꼭 앞서 보여준 코드의 getProduct 메서드를 통해야 한다는 것이다. 누군가 새로운 클라이언트를 만들어 데이터에 접근한다면, 문제가 발생할 수 있다. 그리고 NoSQL에서 지원하는 쿼리 기능을 사용할 경우, 변경된 스키마 내용이 쿼리 조건에 들어가거나 인덱싱되어야 한다면 의미가 없어진다. 건 바이 건으로 전체 마이그레이션을 해줘야 한다. 그런데 이런 경우가 많다면 NoSQL을 사용할게 아니라 RDB로 가야하는 거 아닐까? NoSQL 사용이 서비스 목적에 맞는지 생각해 볼 일이다.

Reference