오늘은 Spring boot에서 지원해주는 spring data nosql의 마지막시간이다. 저번시간까지 Redis, MongoDB, Neo4j 에 대해서 예제를 살펴봤는데 오늘은 Solr, Elasticsearch, Cassandra, Couchbase 예제들을 살펴보도록 하자. 거의 비슷한 맥략이라 손쉽게 따라 할 수 있을 듯하다.

Solr

Solr의 경우에도 SolrCrudRepository가 존재한다. 별다른 설정 없이 해당 인터페이스를 이용해서 repositories를 사용하면 된다.

public interface PersonRepository extends SolrCrudRepository<Person, String> {

  Person findByName(String name);
}

다들 알기 때문에 설명할 내용이 없다. Solr 역시도 메서드 이름 기반의 쿼리를 지원한다. 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTests {

  @Autowired
  private PersonRepository personRepository;

  @Test
  public void repository() {
    personRepository.deleteAll();
    personRepository.save(new Person("wonwoo"));
    personRepository.save(new Person("kevin"));
    assertThat(personRepository.findByName("wonwoo").getName()).isEqualTo("wonwoo");
    assertThat(personRepository.findAll()).hasSize(2);
  }
}

테스트도 잘 통과 하였다. Solr도 SolrTemplate이라는 클래스가 존재하는데 이는 자동 설정에 포함안되어 있는 것 같다. 그래서 따로 설정을 해줘야 하는데 다음과 같이 설정해주면 된다.

@Bean
public SolrClient solrClient(SolrProperties solrProperties) {
  return new HttpSolrClient(solrProperties.getHost());
}

@Bean
public SolrTemplate solrTemplate() {
  return new SolrTemplate(solrClient(null));
}

이렇게 위와 같이 SolrTemplate은 SolrClient solr프로젝트의 클라이언트를 디펜더시 받고 있다. SolrTemplate의 사용법은 간단히 살펴보자.

@Component
public class PersonTemplate {

  private final SolrTemplate solrTemplate;
  private final String collectionName = "collection1";

  public PersonTemplate(SolrTemplate solrTemplate) {
    this.solrTemplate = solrTemplate;
  }

  protected long count(Query query) {
    return this.solrTemplate.count(collectionName, SimpleQuery.fromQuery(query));
  }

  public long count() {
    return count(new SimpleQuery(new Criteria(Criteria.WILDCARD).expression(Criteria.WILDCARD)));
  }


  public void deleteAll() {
    this.solrTemplate.delete(collectionName, new SimpleFilterQuery(new Criteria(Criteria.WILDCARD).expression(Criteria.WILDCARD)));
    this.solrTemplate.commit(this.collectionName);
  }

  public void save(Person person) {
    this.solrTemplate.saveBean(person);
    this.solrTemplate.commit(this.collectionName);
  }

  public Page<Person> findAll() {
    Pageable pageable = new SolrPageRequest(0, (int) count());
    return this.solrTemplate.queryForPage(collectionName,
        new SimpleQuery(new Criteria(Criteria.WILDCARD).expression(Criteria.WILDCARD))
            .setPageRequest(pageable),
        Person.class);
  }
}

페이징 처리를 해서 조금 복잡해 보이는데 페이징 처리를 뺀다면 그나마 덜 복잡하다. 일반적으로 read는 빼고 거의 다른 template과 동일하다. 하지만 solr 역시 검색 스토어이기에 쿼리가 조금 복잡해 보일 수 있다. 한번 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonTemplateTests {

  @Autowired
  private PersonTemplate personTemplate;

  @Test
  public void template() {
    personTemplate.deleteAll();
    personTemplate.save(new Person(UUID.randomUUID().toString(), "wonwoo"));
    personTemplate.save(new Person(UUID.randomUUID().toString(),"kevin"));
    assertThat(personTemplate.findAll()).hasSize(2);
  }
}

Elasticsearch

Elasticsearch 역시 검색 스토어이다. solr와 마찬가지로 아파치 루씬을 검색엔진으로 삼고 있다. 어떤게 더 좋은지는 모르겠다. 각각이 장단점이 있으니 살펴보고 결정하면 되겠다.

public interface PersonRepository extends ElasticsearchRepository<Person, String> {

  Person findByName(String name);
}

Elasticsearch 도 ElasticsearchRepository 라는 인터페이스를 제공해주고 있다. 일반적으로 spring data들의 Repository 구현체명들은 Simple* 로 시작한다. 예를들어 ElasticsearchRepository의 구현체는 SimpleElasticsearchRepository이다. 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTests {

  @Autowired
  private PersonRepository personRepository;

  @Test
  public void repository(){
    personRepository.deleteAll();
    personRepository.save(new Person("wonwoo"));
    personRepository.save(new Person("kevin"));
    assertThat(personRepository.findByName("wonwoo").getName()).isEqualTo("wonwoo");
    assertThat(personRepository.findAll()).hasSize(2);
  }
}

ElasticsearchTemplate도 spring boot에서 자동설정에 포함되어있다. 그래서 아무 설정하지 않아도 자동으로 ElasticsearchTemplate이 빈으로 등록된다. 그래서 우리는 사용만 하면 된다.

@Component
public class PersonTemplate {
  private final ElasticsearchTemplate elasticsearchTemplate;

  public PersonTemplate(ElasticsearchTemplate elasticsearchTemplate) {
    this.elasticsearchTemplate = elasticsearchTemplate;
  }

  public void deleteAll() {
    DeleteQuery deleteQuery = new DeleteQuery();
    deleteQuery.setQuery(matchAllQuery());
    this.elasticsearchTemplate.delete(deleteQuery, Person.class);
    this.elasticsearchTemplate.refresh(Person.class);
  }

  public void save(Person person) {
    IndexQuery indexQuery = new IndexQuery();
    indexQuery.setObject(person);
    this.elasticsearchTemplate.index(indexQuery);
    this.elasticsearchTemplate.refresh(Person.class);
  }

  public List<Person> findAll() {
    SearchQuery query = new NativeSearchQueryBuilder().withQuery(matchAllQuery()).build();
    return this.elasticsearchTemplate.queryForList(query, Person.class);
  }

}

Elasticsearch도 검색 스토어이기에 검색 관련 API가 많다. 그래서 사용하려면 해당 문서를 잘보면서 테스트를 해봐야 될 듯 싶다. 잘 동작하나 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonTemplateTests {

  @Autowired
  private PersonTemplate personTemplate;

  @Test
  public void template() {
    personTemplate.deleteAll();
    personTemplate.save(new Person("wonwoo"));
    personTemplate.save(new Person("kevin"));
        assertThat(personTemplate.findAll()).hasSize(2);
  }
}

Cassandra

Cassandra도 CassandraRepository를 지원한다. 별다른 설정 없이도 CassandraRepository 또는 TypedIdCassandraRepository를 이용해도 된다.

public interface PersonRepository extends CassandraRepository<Person> {

  Person findByName(String name);

}

Cassandra도 다른 Repository와 동일하게 메서드 이름 기반의 쿼리를 지원한다. 한번 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTests {

  @Autowired
  private PersonRepository personRepository;

  @Test
  public void repository() {
    personRepository.deleteAll();
    personRepository.save(new Person(UUIDs.timeBased(),"wonwoo"));
    personRepository.save(new Person(UUIDs.timeBased(),"kevin"));
    assertThat(personRepository.findByName("wonwoo").getName()).isEqualTo("wonwoo");
    assertThat(personRepository.findAll()).hasSize(2);
  }

}

잘 동작하는 것을 볼 수 있다. 마찬가지로 CassandraTemplate도 자동 설정에 포함 되어있어 별다른 설정 없이도 우리는 그냥 사용해도 된다.

@Component
public class PersonTemplate {

  private final CassandraTemplate cassandraTemplate;

  public PersonTemplate(CassandraTemplate cassandraTemplate) {
    this.cassandraTemplate = cassandraTemplate;
  }

  public void deleteAll() {
    cassandraTemplate.deleteAll(Person.class);
  }

  public void save(Person person) {
    cassandraTemplate.insert(person);
  }

  public List<Person> findAll() {
    return cassandraTemplate.selectAll(Person.class);
  }
}

CassandraTemplate API 역시도 사용하기 쉽고 직관적이다. 무엇을 하는지 메서드명만 봐도 어떤 일을 하는지 알 수 있다. 한번 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonTemplateTests {

  @Autowired
  private PersonTemplate personTemplate;

  @Test
  public void template() {
    personTemplate.deleteAll();
    personTemplate.save(new Person(UUIDs.timeBased(), "wonwoo"));
    personTemplate.save(new Person(UUIDs.timeBased(),"kevin"));
    assertThat(personTemplate.findAll()).hasSize(2);
  }
}

Couchbase

마지막으로 알아볼 Couchbase도 CouchbaseRepository를 지원한다. 별다른 설정 없이도 CouchbaseRepository를 이용하면 된다.

@ViewIndexed(designDoc = "person")
public interface PersonRepository extends CouchbaseRepository<Person, String> {

  @View(viewName = "byName")
  List<Person> findByName(String name);
}

Couchbase 는 좀 까탈스럽다. View도 생성해야 하며 자바스크립트로 코딩도 해야 된다. View code에 필자는 다음과 같이 넣어 두었다.

function (doc, meta) {
  emit(doc.name, null);
}

byName라는 뷰를 생성한 후에 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonRepositoryTests {

  @Autowired
  private PersonRepository personRepository;

  @Test
  public void repository() {
    personRepository.deleteAll();
    personRepository.save(new Person(UUID.randomUUID().toString(),"wonwoo"));
    personRepository.save(new Person(UUID.randomUUID().toString(),"kevin"));
    assertThat(personRepository.findByName("wonwoo").get(0).getName()).isEqualTo("wonwoo");
    assertThat(personRepository.findAll()).hasSize(2);
  }
}

필자가 잘 몰라서 List 타입으로 반환하였다. 단일 정보를 반환하려면 어떻게 하지? 아직 깊이 있게 보는 중은 아니라 프로덕션이나 필자가 개인적으로 사용한다면 다시 살펴 볼 예정이다.

마찬가지로 CouchbaseTemplate도 별다른 설정을 하지 않아도 자동으로 빈으로 등록 된다.

@Component
public class PersonTemplate {

  private final CouchbaseTemplate couchbaseTemplate;

  public PersonTemplate(CouchbaseTemplate couchbaseTemplate) {
    this.couchbaseTemplate = couchbaseTemplate;
  }

  public void deleteAll() {
    ViewQuery query = ViewQuery.from("person", "all");
    query.reduce(false);
    query.stale(couchbaseTemplate.getDefaultConsistency().viewConsistency());

    ViewResult response = couchbaseTemplate.queryView(query);
    for (ViewRow row : response) {
      try {
        couchbaseTemplate.remove(row.id());
      } catch (DataRetrievalFailureException e) {
        //ignore
      }
    }
  }

  public void save(Person person) {
    couchbaseTemplate.save(person);
  }

  public List<Person> findAll() {
    ViewQuery query = ViewQuery.from("person", "all");
    query.reduce(false);
    query.stale(couchbaseTemplate.getDefaultConsistency().viewConsistency());
    return couchbaseTemplate.findByView(query, Person.class);
  }

}

별다른 설정 없이도 CouchbaseTemplate를 사용하였다. 테스트를 해보자.

@RunWith(SpringRunner.class)
@SpringBootTest
public class PersonTemplateTests {

  @Autowired
  private PersonTemplate personTemplate;

  @Test
  public void template() {
    personTemplate.deleteAll();
    personTemplate.save(new Person(UUID.randomUUID().toString(), "wonwoo"));
    personTemplate.save(new Person(UUID.randomUUID().toString(),"kevin"));
    assertThat(personTemplate.findAll()).hasSize(2);
  }
}

뷰를 잘 설정 했다면 정상적으로 동작할 것으로 보인다.

이렇게 간단하게나마 Spring boot에서 지원해주는 nosql들을 살펴봤다. 물론 필자도 다 사용하지는 않는다. 현재는 개인적으로 레디스와 엘라스틱 서치만 사용하고 있다.

여기 필자가 만든 블로그 중에 여기 있는 검색 시스템이 엘라스틱 서치로 개발되어 있다. 물론 아주 잘 사용한 건 아니지만 한번 해봤다는 것에 의미를 두고 있다. 만약 프로덕션에 사용한다면 좀 더 공부를 하고 사용해야 될 듯 싶다.

그럼 Spring data nosql은 여기서 마치겠다. 지금까지의 소스는 여기에 있다.