WordPress

Spring @Bean에 대해서..

Posted by wonwoo on 2018-06-06

오늘은 Spring @Bean 어노테이션과 관련해서 이야기를 하려한다. 깊게 볼 것은 아니고 특이한 거나 잘 몰랐던거? 알지만 해보지 않았던거? 그런것들을 알아볼 예정이니 그냥 이렇구나 정도만 알면 되겠다. 자주 사용될 일도 없으니 한번씩만 훑고 지나가면 될 것 같다.

요즘 회사를 이직준비 중이라 회사 다닐때 보다 더 바쁘고 스트레스 받는 것 같다.ㅠㅠ 아무튼 한번 알아보자.

Interface

버전은 언제인지 잘 모르겠는데 Interface에도 @Bean 어노테이션을 작성해도 동작한다. 아마 4.x부터 됐을 것으로 예상해보지만 그게 언제인지는 확인해보지 않았다. 예를들어 다음과 같은 설정을 해도 동작한다.


public class SomeBean { public void print() { System.out.println("hello world"); } } @Configuration public interface BeanInterface { @Bean default SomeBean someBean() { return new SomeBean(); } } @Configuration public class BeanClass implements BeanInterface { //... }

위와 같이 작성해도 SomeBean이라는 클래스가 Bean으로 등록된다. 여기서 주의할 것은 그에 맞기 구현체가 있어야 한다는 것이다. 만약 구현체가 없다면 Bean으로 등록되지 않는다.

public static void main(String[] args) {
  ConfigurableApplicationContext context = SpringApplication.run(SpringBeanApplication.class, args);
  SomeBean someBean = context.getBean(SomeBean.class);
  someBean.print();
}

위와 같이 실행시키면 hello world 가 print 된 것을 알 수 있다.

@Bean 명

Bean들은 자기만의 고유한 명이 있다. 뭐 다 알겠지만 그렇다. 기본적으로 아무설정 하지 않았을 경우에는 메서드명이 빈명이 된다.

@Bean
SomeBean someBean() {
  return new SomeBean();
}

위와 같이 사용할 경우 someBean이라는 이름으로 빈명이 등록이 된다.

public static void main(String[] args) {
  ConfigurableApplicationContext context = SpringApplication.run(SpringBeanApplication.class, args);
  String[] beanNames = context.getBeanNamesForType(SomeBean.class);
  System.out.println(beanNames[0]);
}

해당 빈이름을 가져왔을 때 someBean이라는 것이 프린트 될 것이다. 만약 빈명을 변경하고자 할 때는 @Bean 어노테이션 속성 중 value 또는 name을 사용해서 변경 할 수 있다.

@Bean("nothing")
SomeBean someBean() {
  return new SomeBean();
}

close

Spring Bean의 라이프사이클 중에 destroy라는 자원을 해제하거나 빈이 소멸될 때 마지막으로 호출해주는 메서드들이 존재한다. 예를들어 DisposableBean을 구현하거나 @PreDestroy 어노테이션 또는 destroyMethod 속성을 사용해서 자원 등을 해제 할 수 있다. 3가지 방법 말고도 spring 에서는 좀 더 많은 해제 법이 존재한다. 첫 번째 방법으로는 AutoCloseable 인터페이스를 구현하면 된다.

public class SomeBean implements AutoCloseable {

  @Override
  public void close() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성 후에 아무 설정 없이도 SomeBean 이라는 빈이 소멸될 때 close 메서드를 실행 시킨다. 한번씩 확인해 보자.

두 번째 방법으로는 굳이 AutoCloseable 필요 없이 close 메서드만 있어도 된다.

public class SomeBean  {

  public void close() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성해도 별탈 없이 close 메서드가 실행 된다. 더 정확하게는 그냥 close 메서드를 실행 시키는 것이다. 솔직히 위의 AutoCloseable 여부와 상관없이 그냥 close 메서드를 호출하는 것이다.

마지막 방법으로는 shutdown() 메서드 이다. 이 또한 그냥 shutdown() 메서드를 호출 하는 것이다.

public class SomeBean  {

  public void shutdown() throws Exception {
    System.out.println("close");
  }
}

위와 같이 작성해도 close를 출력하는 것을 확인 할 수 있다. 만약 좀 더 확인하고 싶다면 DisposableBeanAdapter 클래스를 좀 더 확인하면 알 수 있다.

class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {

  private static final String CLOSE_METHOD_NAME = "close";

  private static final String SHUTDOWN_METHOD_NAME = "shutdown";

  //...
}

참고로 DisposableBean 인터페이스와 위의 세가지 방법을 같이 쓰면 DisposableBean 의 destroy 메서드가 우선순위가 높다.

Lite Mode

Spring Bean에는 lite 모드라는 것이 있다. 그게 뭐냐고 물어보면 조금 뭐라고 해야할지.. lite니까 조금 가벼운거?
아무튼 lite 모드는 해당 설정에 @Configuration 어노테이션이 아닌 @Component어노테이션을 작성하면 그게 lite mode가 된다. lite 모드로 하면 설정이 Configuration으로 하는 것보다는 빠르지만 그렇게 확 티가 날 정도는 아닌 걸로 기억하고 있다. 물론 많으면 말이 달라지겠지만.. 아무튼 그럼 차이가 뭘까? 차이는 Cglib을 사용하냐 하지 않느냐 차이가 가장 큰 차이 같다.

@Configuration 같은 경우에는 cglib을 사용하지만 @Component 경우에는 cglib을 사용하지 않는다.

@Configuration
public class Config {

  @Bean
  SomeBean someBean() {
    return new SomeBean();
  }

  @Bean
  NothingBean nothingBean() {
    return new NothingBean(someBean());
  }
}

예를 들어 위와 같은 설정이 있다고 가정하자. NothingBean은 SomeBean을 디펜더시 받고 있다. 그래서 위와 같이 설정을 했다고 하자. 그럼 someBean 의 메서드는 몇 번 호출 이 될까? 일반적으로 볼 때에는 두번 호출 되는 게 맞다고 생각한다. SomeBean을 빈으로 등록할 때와 NothingBean을 빈으로 등록할 때 someBean() 메서드를 호출하니 두번이 맞다. 하지만 @Configuration를 사용하면 아까 말했다시피 cglib을 이용해서 위의 someBean을 한번만 호출 하도록 한다.

@Component
public class Config {

  @Bean
  SomeBean someBean() {
    return new SomeBean();
  }

  @Bean
  NothingBean nothingBean() {
    return new NothingBean(someBean());
  }
}

하지만 이 경우에는 어떨까? 이 경우에는 우리가 일반적으로 생각하는게 맞다. 이 때에는 someBean() 메서드가 두번 호출된다.
@Configuration 은 cglib을 사용해서 일반적으로 @Component 를 사용할 때보다 조금 느린것 같다. 하지만 그 차이는 눈에 보이지 않는 다는 거..
만약 config을 lite mode로 사용할 경우에는 좀 더 신중하게 사용해야 될 듯 싶다. 그냥 필자는 @Configuration을 사용하련다..

오늘은 Spring의 Bean에 대해서 몇 가지 알아봤다. Spring을 사용하는데에는 아주 쓸모있는 기능은 아니지만 알아둬서 나쁠껀 없으니 한번씩 해보도록 하자.

Spring jsr305

Posted by wonwoo on 2018-05-07

오늘은 Spring5 부터 지원하는 jsr305 어노테이션에 대해서 알아보자. 많은 이야기는 아니지만 Spring 에서 이 어노테이션을 몇가지 기능을 지원해 주고 있다.

Spring에서 사용하는 Nullable, NonNull, NonNullApi 어노테이션은 jsr305의 메타 어노테이션을 사용한다. 실제 간단히 코드를 보자면 다음과 같다.

//...
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierDefault;

//...
@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}

jsr305 어노테이션은 그냥 메타 어노테이션으로만 사용하고 있다. 하지만 Spring 에서는 몇가지 기능을 지원해주고 있으니 알아보도록 하자.

Controller

Spring web에서 흔히 파라미터로 받을 경우 사용할 수 있다. @RequestParam 어노테이션을 사용할 경우 required 속성의 기본값은 true이다. 그래서 name이라는 파라미터를 보내지 않을 경우 에러가 발생한다.

@GetMapping("/")
public String hello(@RequestParam String name) {
  return "hello " + name + "!";
}

만약 위와 같이 @RequestParam의 required 속성을 false로 하지 않을 경우 아래와 같이 에러가 발생한다.

http :8080

HTTP/1.1 400
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 12:38:23 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Required String parameter 'name' is not present",
    "path": "/",
    "status": 400,
    "timestamp": "2018-05-07T12:38:23.120+0000"
}

물론 required 속성을 false로 해도 되지만 Spring5 부터는 @Nullable 어노테이션을 사용해서 null을 허용해도 된다.

@GetMapping("/")
public String hello(@RequestParam @Nullable String name) {
  return "hello " + name + "!";
}

위와 같이 @Nullable 어노테이션을 사용했을 경우에는 아래와 같이 에러가 발생하지 않는다.

http :8080

HTTP/1.1 200
Content-Length: 11
Content-Type: text/plain;charset=UTF-8
Date: Mon, 07 May 2018 12:41:14 GMT

hello null!

Endpoint

위와 비슷한 동일한 맥략이다. 커스텀한 Endpoint를 만들 경우에 @Nullable 어노테이션을 사용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(String name) {
    return "hello " + name + "!";
  }
}

만약 위와 같은 코드를 작성했을 경우 name이라는 파라미터를 보내지 않으면 위와 동일하게 에러가 발생한다.

http :8080/actuator/hello

HTTP/1.1 400
Connection: close
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:44:03 GMT
Transfer-Encoding: chunked

{
    "error": "Bad Request",
    "message": "Missing parameters: name",
    "path": "/actuator/hello",
    "status": 400,
    "timestamp": "2018-05-07T12:44:03.471+0000"
}

하지만 여기에서도 @Nullable 어노테이션을 작성하여 null을 허용할 수 있다.

@Endpoint(id = "hello")
@Component
public class HelloEndpoint {

  @ReadOperation
  public String hello(@Nullable String name) {
    return "hello " + name + "!";
  }
}

다음과 같이 작성할 경우에는 파라미터를 보내지 않아도 에러가 발생하지 않는다.

http :8080/actuator/hello
HTTP/1.1 200
Content-Length: 11
Content-Type: application/vnd.spring-boot.actuator.v2+json;charset=UTF-8
Date: Mon, 07 May 2018 12:45:17 GMT

hello null!

Null injection

Spring5 에서는 null을 허용하는 주입을 @Nullable 어노테이션을 사용하여 주입하면 된다. 예전에는 @Autowired 어노테이션의 required 속성을 false로 하면 주입하는 Object가 null 이어도 에러가 발생하지 않고 null 그대로 주입한다. 또한 필자는 요즘에 주입받는 Object에 @Autowired를 잘 작성하지 않는다. Spring 4.3 부터는 생성자 혹은 빈의 디펜더시 받는 Object에 @Autowired 가 존재 하지 않아도 자동으로 Spring이 주입을 해주고 있어서 좋은 기능 같다.

public class PersonService {
  //nothing
}

@Bean
ApplicationRunner applicationRunner(@Nullable PersonService personService) {
  return args -> {
  };
}

//생성자
public Some(@Nullable PersonService personService) {
  this.personService = personService;
}

위와 같이 PersonService는 빈으로 등록되지 않은 Object이다. 그래서 만약 이 상태로 주입받으려 하면 PersonService 라는 빈이 존재하지 않아 에러가 발생한다. 하지만 이제부터는 @Nullable 어노테이션을 사용해서 null을 허용하면 null이 주입된다.

만약 위와 같이 사용한다면 null check 는 꼭 해줘야 할 것 같다.

Spring data

Spring data 프로젝트에서도 jsr305 어노테이션을 지원한다. Spring data에서 query method를 사용할 경우 파라미터와 리턴값에 위와 같은 어노테이션을 작성할 수 있다.

여기서 주의할 점은 해당 패키지안에 package-info.java 를 작성해줘야 한다. 이때 사용하는 어노테이션은 @NonNullApi 어노테이션이다.

@NonNullApi
package ml.wonwoo.springjsr305.domain;

import org.springframework.lang.NonNullApi;

위와 같이 작성한다면 기본적으로 파라미터 와 리턴값은 null이 아니어야 한다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByName(String name);
}

만약 NonNullApi 사용하고 위와 같이 사용한다면 name에는 null이 될수 없고 반환값도 null이 될 수 없다. 만약 null을 넣거나 null을 리턴한다면 IllegalArgumentException exception이 발생한다. 한번 controller로 테스트를 해보자.

@GetMapping("/hello")
public Person helloName(String name) {
  return personRepository.findByName(name);
}
http :8080/hello

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:09:16 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Parameter of type String at index 0 in PersonRepository.findByName must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:09:16.024+0000"
}

위와 같이 호출할 경우 파라미터가 null이라고 에러가 발생한다. 이번에는 리턴값이 null인 것으로 호출해보자.

http :8080/hello name==foo

HTTP/1.1 500
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:11:29 GMT
Transfer-Encoding: chunked

{
    "error": "Internal Server Error",
    "message": "Result must not be null!",
    "path": "/hello",
    "status": 500,
    "timestamp": "2018-05-07T13:11:29.367+0000"
}

이번에는 리턴값이 null이라고 에러가 발생한다. 한번 제대로 된 값으로 호출해보자.

http :8080/hello name==wonwoo

HTTP/1.1 200
Content-Type: application/json;charset=UTF-8
Date: Mon, 07 May 2018 13:12:59 GMT
Transfer-Encoding: chunked

{
    "id": 1,
    "name": "wonwoo"
}

정상적으로 호출이된다. 만약 위와 다르게 null을 허용하고 싶다면 이때까지 알아본 @Nullable 어노테이션을 사용하면 된다. 이것은 파라미터와 메서드위에 작성할 수 있다.

public interface PersonRepository extends JpaRepository<Person, Long> {

  Person findByNameIsLike(@Nullable String name);

  @Nullable
  Person findByName(String name);
}

위와 같이 한다면 findByNameIsLike 파라미터는 null을 허용하고 즉 findByNameIsLike(null) 같이 메소드를 호출해도 된다. (하지만 해당 메서드는 null을 넣으면 에러가 발생한다. jsr305 때문이 아니라 쿼리 자체가 null을 허용하지 않는 듯 하다.)
또한 findByName 메서드는 파라미터에는 null을 작성하면 안되고 리턴값 자체를 null값 이어도 상관 없다.

오늘 이렇게 Spring 에서 지원해주는 jsr305 어노테이션에 대해서 알아봤다. 물론 더 많은 기능이 있을지는 모른다. 필자가 알아본 부분은 여기까지이다. 더 많은 정보가 있다면 댓글로..

이상!

Spring data common 기타 기능

Posted by wonwoo on 2018-04-29

오늘 이야기 하려는 것은 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의 몇가지 기능을 대해서 알아봤다. 이보다 많은 기능이 존재한다. 따라서 각자가 문서를 보면서 공부하는 것이 좋겠다.

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