오늘은 Spring Data의 몇가지 기능들을 살펴 볼 예정이다. 예전에 작성했던 Spring data common 기타 기능과는 별개로 유용하게 사용할 수 있는 것들을 정리해 보도록 하자. 물론 어떤 특정한 버전, 프로젝트들은 따로 명시를 해놓도록 하겠다.

query method

Spring data 프로젝트의 특징인 query method는 아주 유용한 기능이다. 물론 복잡한 쿼리에는 사용할 수 없지만 간단한 쿼리를 작성하는데는 더할 나위 없이 유용한 기능이다.
필자도 간단한 쿼리를 작성할 때는 자주 이용하고 있다. 잘 모르고 있을 수도 있는 기능들을 좀 더 살펴보자.

public interface PersonRepository extends CrudRepository<Person, String> {

    List<Person> findByName(String name);

}

만약 이름으로 select을 하고 있다면 대부분 위와 같이 작성을 할 것이다. find란 키워드와 by라는 키워드 spring data에서 정의한 키워드이다. 만약 두개의 키워드가 존재하지 않다면 쿼리메서드는 제대로 동작하지 않을 수 있다.

하지만 select 할때는 find 라는 키워드 말고도 여러 키워드들이 존재한다. 굳이 find 키워드를 사용할 필요 없이 다른 키워드를 사용해도 된다.

public interface PersonRepository extends CrudRepository<Person, String> {

    List<Person> findByName(String name);

    List<Person> queryByName(String name);

    List<Person> streamByName(String name);

    List<Person> getByName(String name);

    List<Person> readByName(String name);

}

find 라는 키워드 말고도 query, stream, get, read 등으로 대체 할 수 있다.
이외에도 카운터를 세는 count 존재 여부를 알려주는 exists 등이 있다.

또한 삭제쿼리 메서드도 존재하는데 remove, delete 두가지 키워드를 사용해서 삭제할 수 있다.

public interface PersonRepository extends CrudRepositor<Person, String> {

    void deleteByName(String name);

    void removeByName(String name);

}

위와 같이 사용해도 동일한 결과를 얻을 수 있다.

find 라는 키워드 말고도 여러 키워드가 있으니 다른 키워드도 사용해도 좋다.

사실 위와 같이 delete method 에는 @Modifying 어노테이션이 필요 없다. @Modifying 어노테이션은 @Query(update or delete) 어노테이션을 사용할 때만 작성하면 된다.

또한 한글, 중국어, 일어 등도 가능하다. 사실 그럴일은 거의거의 없지만 그냥 가능하다고 만 알고 있자.

//@Entity, //Document ...
public class Person {

  // ...

  private String 이름;

  //... getter setter

}

public interface PersonRepository extends CrudRepositor<Person, String> {

  List<Book> findBy이름(String name);

}

entityName (Data-JPA)

이것은 Jpa에 특화된 기능이다. 다른 data 하위 프로젝트엔 동작하지 않는다. @Query 어노테이션을 사용할 때 유용하게 사용될 수 있는 기능이다.

entityName 이라는 키워드를 통해 해당 엔티티를 조회, 저장, 삭제등을 할 수 있다.

@MappedSuperclass
public class Product {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    ....
}

// book, disc 

@NoRepositoryBean
public interface ProductRepository<T extends Product> extends JpaRepository<T, Long> {

    @Query("select p from #{#entityName} p where p.name = :name")
    List<T> findName(String name);
}


public interface BookRepository extends ProductRepository<Book> {

}

public interface DiscRepository extends ProductRepository<Disc> {

}

주로 사용하고 있는 곳으로는 위와 같이 공통적인 상위 인터페이스를 정의 한 후 공통된 쿼리들을 entityName을 이용하여 처리 할 수 있다.
@Query 어노테이션을 자주 사용하는 분들은 아주 유용한 기능인 듯 싶다.

Mongo (Data-Mongo)

Spring data mongo를 사용할 때 몇가지 유용한 기능이다.

Document(model) 를 작성할 때 굳이 @Document 어노테이션과 @Id를 선언하지 않아도 된다.

@Value
public class Person {

    ObjectId id;

    String name;

    int age;

}

public interface PersonRepository extends MongoRepository<Person, ObjectId> {

}

위와 같이 Person 모델에 @Document 를 붙이지 않아도 자동으로 Spring data가 만들어서 넣어 준다. 만약 @Document 없다면 클래스명(첫글자는 소문자)으로 collection이 만들어 진다.

@Id도 마찬가지로 id, _id 라는 필드가 존재하면 그것을 키로 잡아 저장을 한다.

만약 어노테이션이 많아 보기가 힘들다면 작성하지 않아도 되며 명시적인 어노테이션을 선호한다면 작성해도 무방하다.

단, 아래코드는 동작하지 않는다.


@Value public class Person { ObjectId id; String name; int age; } public interface PersonRepository extends CrudRepository<Person, String> { }

그 이유는 해당 엔티티가 어느 스토어를 사용하지는 알수 없어 빈으로 등록하지 못한다. 위의 경우엔 명시적으로 @Document 어노테이션을 사용하거나 직접적인 MongoRepository를 사용해야 한다.

다른 spring data 하위 프로젝트(JPA는 해당사항 없다.) 들은 어떻게 되어 있는지 확인해보지 않았다. 만약 다른 프로젝트도 이런 기능이 있을 수도, 없을 수도 있으니 잘 보고 적용해야 된다.

Spring data 쪽에는 엔티티를 위와 같이 불변으로 만들어도 된다. (사실.. 확인은 mongo, redis 만 했다. 이것 또한 JPA는 해당사항 없다.)

JPA는 명시된 스펙으로 작성을 해야 되기 때문에 @Entity, @Id, 기본생성자, 정의된 스펙에 맞게 작성해야 한다.

public class Person {

    private final ObjectId id;

    private final String name;

    private final int age;

    public Person(ObjectId id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
   // getter
}

or

@Value
public class Person {

    ObjectId id;

    String name;

    int age;

}

or

@Value(staticConstructor = "of")
public class Person {

    ObjectId id;

    String name;

    int age;

}

@Value 는 롬복에 있는 어노테이션이며 여기를 참고하며 되겠다. 또한 팩토리 메서드를 만들어 사용해도 된다. 다른 더 많은 기능이 있으나 주로 사용될만한 기능은 이정도 일듯 싶다.

Streamable

Spring data 2.0 에서 추가된 Streamable 인터페이스를 query method 를 사용할 때 리턴타입으로 사용할 수 있다.

public interface PersonRepository extends MongoRepository<Person, String> {

    Streamable<Person> findByName(String name);

}

해당 인터페이스를 사용하면 바로 map, filter를 사용할 수 있다.

 List<String> names = people.map(Person::getName)
                    .stream()
                    .collect(Collectors.toList());

사실 사용 Api를 보면 List를 사용하는 것과 동일하다. 어차피 Stream으로 다시 만들어 하기에 굳이 사용할 필요가 있나 싶기도 하다.

하지만 spring data 2.2 부터는 (현재기준으로 아직 릴리즈는 되지 않았다) 조금 더 간편하게 사용할 수 있다.

List<String> names = people.map(Person::getName)
                    .toList()

Set<String> names = people.map(Person::getName)
                    .toSet()

내부적인 코드는 동일하지만 사용하는 Api는 간단하게 사용할 수 있다.

또한 spring data 2.2 부터는 좀 더 커스텀한 Wrapper Type의 Streamable 만들어 사용할 수 있다.

//@RequiredArgsConstructor(staticName = "of")
@RequiredArgsConstructor
public class Persons implements Streamable<Person> {

    private final Streamable<Person> people;

    public int getAge() {
        return people.stream()
                .mapToInt(Person::getAge)
                .sum();
    }

    @Override
    public Iterator<Person> iterator() {
        return people.iterator();
    }
}


public interface PersonRepository extends MongoRepository<Person, String> {

    Persons readByName(String name);

}

Persons people = personRepository.readByName("wonwoo");
int totalAge = people.getTotalAge();

이렇게 Wrapper Type을 만들어 내부적으로 기능들을 추가할 수 있어 유용한 기능인 것 같다.

오늘은 이렇게 Spring data 쪽에 사용되는 유용한 기능들을 살펴봤다. 조금이라도 도움이 되었으면 좋겠다.

오랜만에 포스팅을 했다. 이런저런 일도 있고 사실 귀찮았던게 더 컷다. 일주일에 한번(적어도 이주일엔)은 꼭 쓰도록 노력해야겠다.