오늘 이야기 하려는 것은 Spring data common 의 몇가지 기능을 알아보려고 한다. Spring data는 대부분 알고 있듯이 Query methods의 유용함을 다 알고 있을 듯하다. 그래서 따로 이부분은 설명하지 않겠다. 그래서 제목도 기타 기능이라고..

JPA를 사용할 떄 사용하는 JpaRepository나 기타 다른 스토어를 사용할때 사용하는 {store}Repository는 Spring data common 에 있는 것이 아니라 그에 따른 구현체별로 존재한다. 뭐 이미 다 알고 있겠지만 혹시나..

spring data common에 존재하는 RepositoryCrudRepositoryPagingAndSortingRepository 인터페이스만 존재하고 나머지는 그에 따른 구현체를 통해 사용해야 한다.

이 포스팅 또한 JPA기준이며 다른 스토어들에서는 동작을 하지 않을 수도 있다.

@RepositoryDefinition

spring data common에 존재하는 @RepositoryDefinition 어노테이션은 일반적으로 사용하는 그런 어노테이션은 아닌 것같다. 사용하고 싶다면 사용해야 되지만 글쎄다. 마치 그냥 Repository 인터페이스를 사용하는 것과 동일하다.

속성에는 idClassdomainClass 을 작성해주는 속성이 있다. 이 두 속성은 필수이다. Repository<T, ID> 이와 동일하다. T는 domainClass를 의미하고 ID 는 idClass 와 동일하다고 생각하면 될 듯 싶다.

예제를 통해 간단히 살펴보자.

@RepositoryDefinition(idClass = Long.class, domainClass = Account.class)
public interface AccountRepository {

    List<Account> findAll();

    Optional<Account> findById(Long id);

    Account save(Account account);
}

위와 같이 RepositoryDefinition 어노테이션을 사용했을 경우 위에서 말했듯이 Repository 인터페이스를 사용하는 것과 동일해 보인다. 위와 같이 정의 했을 경우 spring data가 적당하게 빈으로 등록을 시킨다. spring data 설정외의 따로 설정할 것은 없다.
한번 테스트를 해보자.

@RunWith(SpringRunner.class)
@DataJpaTest
public class AccountRepositoryTests {


    @Autowired
    private AccountRepository accountRepository;

    @Test
    public void save() {
        Account account = accountRepository.save(new Account("wonwoo", "test@test.com"));
        assertThat(account.getName()).isEqualTo("wonwoo");
        assertThat(account.getEmail()).isEqualTo("test@test.com");
    }

    @Test
    public void findById() {
        Account account = accountRepository.save(new Account("wonwoo", "test@test.com"));
        Optional<Account> newAccount = accountRepository.findById(account.getId());
        assertThat(newAccount.get().getName()).isEqualTo("wonwoo");
        assertThat(newAccount.get().getEmail()).isEqualTo("test@test.com");
    }

    @Test
    public void findAll() {
        accountRepository.save(new Account("wonwoo", "test@test.com"));
        accountRepository.save(new Account("kevin", "kevin@test.com"));
        List<Account> accounts = accountRepository.findAll();
        assertThat(accounts).hasSize(2);
    }
}

아주 잘 동작한다. 하지만 특별한 경우가 아니라면 그냥 Repository 인터페이스 하위의 있는 인터페이스를 사용하는 것이 나아보인다.

projections

Spring data 에서는 아주 간단하게 projections을 지원한다. 예를들어 특정한 도메인 오브젝트가 아닌 다른 오브젝트로 반환을 쉽게 할 수 있다. 몇가지 방법을 지원하니 사용하고 싶은 것으로 사용하면 되겠다.

interface

인터페이스를 사용해서 모델을 반환받을 수 있다. 사용법은 아주 간단하다.

public interface Named {

    String getName();
}

위와 같이 getter를 정의해서 사용하면 끝난다.

public interface PersonRepository extends CrudRepository<Person, Long> {

    List<Named> findByName(String name);

}

그리고 나서 위와 같이 리턴값에 해당 인터페이스를 작성하면 spring data가 자동으로 getName()을 호출할 때 해당 값을 불러오게 된다.

class

interface와 마찬가지다. 아주 쉽다. 흔히 이야기하는 dto를 만들어서 리턴값에 작성만 해주면 된다.


@Value public class PersonDto { String name; }

하지만 여기서 주의할 점은 위와 같이 lombok을 사용하다면 @Value 어노테이션을 사용해야 한다. 위 어노테이션은 불변의 클래스를 만들어주는 역할을 한다. 위의 코드를 바닐라 코드로 본다면 name이 있는 생성자와 getter가 생성된다. setter는 존재하지 않는다. 만약 lombok을 사용하지 못한다면 해당하는 필드의 생성자가 있어야 한다. 더 자세한건 테스트를 해보도록..

public interface PersonRepository extends CrudRepository<Person, Long> {

    List<PersonDto> findByName(String name);

}

그럼 위와 같이 작성하면 해당하는 dto로 반환시켜 준다. 잘되나 테스트를 해보자.

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

    @Autowired
    private PersonRepository personRepository;

    @Test
    public void personsInterfaceProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<PersonDto> persons = personRepository.findByName("wonwoo");
//        List<Named> persons = personRepository.findByName("wonwoo");
        assertThat(persons).hasSize(2);
        assertThat(persons.iterator().next().getName()).isEqualTo("wonwoo");
    }
}

DynamicProjections

spring data는 DynamicProjections 도 지원한다. 이 것 또한 클래스와 인터페이스 모두 지원한다. 자신이 좋아하는 것로 개발하면 되겠다.

public interface PersonRepository extends CrudRepository<Person, Long> {

    <T> List<T> findByName(String name, Class<T> clazz);
}

위와 같이 리턴받을 타입을 제네릭으로 작성하면 spring data가 알아서 잘 변환해준다. 이 것 역시 위의 설명한 제약이 포함되어 있다. 동일하다고 생각하면 되겠다. 테스트를 해보자.

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

    @Autowired
    private PersonRepository personRepository;

    @Test
    public void personsClassDynamicProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<PersonDto> persons = personRepository.findByName("wonwoo", PersonDto.class);
        assertThat(persons).hasSize(2);
    }

    @Test
    public void personsInterfaceDynamicProjectionsTest() {
        personRepository.save(new Person("wonwoo"));
        personRepository.save(new Person("wonwoo"));
        List<Named> persons = personRepository.findByName("wonwoo", Named.class);
        assertThat(persons.get(0).getName()).isEqualTo("wonwoo");
        assertThat(persons.get(1).getName()).isEqualTo("wonwoo");
    }
}

조금 괜찮은 기능이다. 필요하다면 자주 이용 해야겠다.

Custom Repository

필자도 자주 사용하는 기능이다. 대부분 querydsl을 사용할 때 이 기능을 사용한다. Spring data의 주요 장점인 Query methods를 사용할 수 없는 쿼리가 있을 경우 커스텀하게 Repository 를 만들 수 있다. 다음과 같이 몇몇 가지 코드를 작성해야 한다.

public interface CustomizedPersonRepository {

  Person selectPersonName(String name);

  void insert(Long id, String name);
}

public class PersonRepositoryImpl implements CustomizedPersonRepository {

  private final JdbcTemplate jdbcTemplate;

  public PersonRepositoryImpl(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
  }

  @Override
  public Person selectPersonName(String name) {
    return jdbcTemplate.queryForObject("select * from person where name = ?",
        new BeanPropertyRowMapper<>(Person.class), name);
  }

  @Override
  public void insert(Long id, String name) {
    jdbcTemplate.update("insert into person (id, name) values (?,?)", id, name);
  }
}


public interface PersonRepository extends CrudRepository<Person, Long>, CustomizedPersonRepository {

   //etc
}

위와 같이 custom한 인터페이스를 만들고 그에 따른 구현체도 구현을 해야 한다. 당연히 커스텀하게 JdbcTemplate, querydsl등 기타 여러 가지 도구를 이용해서 구현하면 된다.
구현을 다 완료 하였다면 위와 같이 원래의 인터페이스에 자신이 만든 커스텀한 인터페이스를 상속하면 된다. 그럼 spring data가 해당 커스텀한 인터페이스를 따로 저장하여 관리한다.

여기서 몇가지 주의사항이 있다. 일단 첫 번째로 커스텀하게 만든 구현체의 네이밍이다. 기존의 PersonRepository 인터페이스를 만들어 Repository를 사용했다면 우리가 만든 커스텀클래스는 꼭 PersonRepositoryImpl 이어야 한다. 이건 규칙이다. 하지만 이 규칙은 변경할 수 있다. @EnableJpaRepositories 어노테이션에 repositoryImplementationPostfix 속성을 이용해서 변경할 수는 있다. 근데 특별한일이 아니라면 변경할 필요는 굳이 없어 보인다. 하지만 커스텀한 인터페이스의 명은 제약사항이 없다. 마음껏 네이밍해도 좋다.

필자도 이번에 하면서 알았다. 왜 이때까지 몰랐나 싶다. 생각해보면 그럴일이 없었던거 같다. PersonRepository 와 커스텀한 클래스인 PersonRepositoryImpl는 같은 패키지에 있어야 한다. 필자도 맨날 같은 패키지에 넣어서 몰랐던 것이다. 만약 다른 패키지에 넣는다면 에러가 발생한다. 대략 이정도만 주의하면 될 듯 싶다.

오늘은 이렇게 spring data common의 몇가지 기능을 대해서 알아봤다. 이보다 많은 기능이 존재한다. 따라서 각자가 문서를 보면서 공부하는 것이 좋겠다.

기회가 된다면 좀 더 많은 기능을 살펴보도록 하자.
해당 테스트 코드를 여기에 있으니 참고하면 되겠다.