Spring JpaRepository

Spring Data JPA는 “JpaRepository"라는 기능을 사용하면 매우 간단히 데이터를 검색 할 수 있도록 된다. 그 기본적인 사용법을 설명한다.

JpaRepository 인터페이스 생성

이전에 기본적인 데이터베이스 액세스 대해 대충 설명을 했었다. 이 때, 데이터의 검색에 대해서는 “JPQL"라는 쿼리 문을 사용 할 수 있다는 것을 설명했었는데 기억하는가?

이 설명을 읽고 본인도 모르게 한숨을 쉰 사람도 분명 많았을 것이다. SQL이라는 것을 보고 싶지 않아서 프레임워크를 사용하려고 했는데, 설마 SQL 같은 것이 있을 거라고는 생각하지 못했을 것이다.

그래도 아직 안심해도 된다. Spring Framework에는 더 똑똑하게 데이터 검색을 할 수 있는 구조가 제대로 준비되어 있다. 그것이 “JpaRepository"라는 것이다.

JpaRepository는 인터페이스이다. 인터페이스에 미리 검색 메소드를 정의 해 두는 것으로, 메소드를 호출하는 것만으로 스마트한 데이터 검색을 할 수 있게 되는 것이다. “결국, 그 메소드 중에는 JPQL를 사용하여 검색 처리를 쓰는 거지 ……“라고 생각했다면 조금 더 참아주기 바란다. 결코 손해가되지 않을 것이다.

우선 JpsRepository 인터페이스를 만들어 보자. com.devkuma.spring.db 패키지에 “SampleEntityRepository.java"라는 파일명으로 소스 코드 파일을 작성하자. 그리고 아래와 같이 인터페이스를 작성하자.

package com.devkuma.spring.db;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface SampleEntityRepository
    extends JpaRepository <SampleEntity, Long> {
}

JpaRepository 인터페이스는 org.springframework.data.jpa.repository 패키지의 “JpaRepository"라는 인터페이스를 상속하여 만든다. 이 인터페이스는 범용적으로 사용한다. 코드를 보면 이런 형태로 쓰여져 있는 것을 알 수 있을 것이다.

public interface 이름 extends JpaRepository <엔티티 ID 유형>

<>안에는 엔티티 클래스 이름과 ID 필드 타입이 지정된다. 주의할 점은 “기본형의 경우, 래퍼 클래스를 지정한다는 점이다. 샘플 SampleEntity 클래스는 long 형을 ID를 지정하고 있기 때문에, 여기에서는 <SampleEntity, Long>라고 작성을 한다.

또 하나 주의해야 할 것은 어노테이션이다. 클래스의 선언 앞에 “@Repository"라는 어노테이션이 붙여져 있다. 그러면 이 인터페이스가 JpaRepository임을 나타낸다. 이는 반드시 붙여 두어야 하는 것이다.

Bean설정 파일 준비

그럼 Bean 설정 파일을 준비해 보자. 먼저, dbbean.xml라는 파일을 준비하고 있다. 그걸 그대로 다시 사용하도로 하겠다.

아래와 같이 dbbean.xml 소스 코드 내용을 다시 작성하자.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/jdbc 
        http://www.springframework.org/schema/jdbc/spring-jdbc.xsd  
        http://www.springframework.org/schema/data/jpa 
        http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
 
    <jdbc:embedded-database id="dataSource" type="H2" />
 
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <property name="generateDdl" value="true" />
            </bean>
        </property>
    </bean>
         
    <jpa:repositories base-package="com.devkuma.spring.db" />
     
    <bean id="transactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
</beans>

이번에는 Bean 관련 태그가 2개가 추가되어 있다. 먼저 JpaRepository에 대한 태그가 하나 포함되어 있다.

<jpa:repositories base-package="com.devkuma.spring.db"/>

이 <jpa:repositories> 태그에는 base-package라는 속성이 포함되어 있다. 이제 검색할 위치(패키지)를 지정한다. 이렇게 하면 지정된 패키지 내에서 JpaRepository을 찾아서 Bean 화한다.

다른 하나는 “트랜잭션 관리자"라는 Bean이다. 이것은 트랜잭션을 관리하기 위한 것으로, 다음과 같이 작성되어 있다.

<bean id="transactionManager"
    class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

여기에 정의되어 있는 것은 “transactionManager"라는 이름의 Bean으로써, class에는 JpaTransactionManager 클래스를 지정한다. 다른 “entityManagerFactory"라는 속성이 있는데, 이것은 작성하는 Bean에서 사용하는 EntityManagerFactory를 지정한다 (위에 있는 id=“entityManagerFactory"를 ref로 지정되어 있다).

이것으로 JpaRepository를 이용하는데 필요한 Bean의 준비가 되었다. “어? 아까 준비한 SampleEntityRepository은 Bean으로 준비해야 하지 않나?“라고 생각할 수 있는데, 이것은 필요하지 않다. <jpa:repositories>에 의해 지정된 패키지를 검색하고 @repository가 지정된 JpaRepository를 Bean으로 자동 등록하게 되는 것이다. 따라서 별도로 <bean>를 준비 할 필요는 없다.

어플리케이션에서 JpaReposiory을 이용

그럼 JpaRepository를 이용해 보기로 하자.

com.devkuma.spring.db 패키지에 있는 App 클래스를 아래와 같이 고쳐 보자.

package com.devkuma.spring.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
    private static EntityManager manager;
     
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
         
        SampleEntityRepository repository = app.getBean(SampleEntityRepository.class);
 
        // make dummy
        makeDummyData();
         
        // get list
        List list = repository.findAll();
        printList(list);
         
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData() {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower"));
        manager.persist(new SampleEntity("taro", "taro@yamada"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy"));
        manager.flush();
        transaction.commit();
    }
 
    // print all entity.
    public static void printList(List list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
}

그리고 실행하자. 그러면 더미로 만들고 있는 모든 엔티티가 표시된다.

여기에서는 먼저 EntityManager로 부터 SampleEntityRepository 인스턴스를 얻는다.

SampleEntityRepository repository =
    app.getBean (SampleEntityRepository.class);

이것으로 Bean을 가져옵니다. dbbean.xml에는 Bean의 정의는 포함되어 있지 않지만, 이렇게 제대로 Bean을 꺼낼 수 있다. 이것도 @Repository 덕분이다.

List list = repository.findAll ();

그리고 “findAll"메소드를 호출해서, 엔티티 목록을 검색한다. “이런 메소드 도대체 어디에 있는거야?“라고 생각한 사람이 있을 것이다. 이는 SampleEntityRepository에서 상속하는 JpaRepository에 준비되어 있기 때문이다.

클래스는 도대체 어디에?

그런데, 여기에 코드를 보고 좀 이상하게 생각한 사람도 있을 것이다. SampleEntityRepository 인터페이스이다. 클래스가 아니다. 그런데 당연한 듯이 Bean을 얻어 오는 것이 가능하다.

JpaRepository는 인터페이스를 준비하기만 하면, 자동으로 클래스를 만들고 Bean을 생성하는 것이다. 필요한 것은 인터페이스 뿐이다.

Bean 설정 클래스를 이용

Bean은 설정 파일을 사용하지 않고, 클래스로 정의 할 수 있다. 그럼 이것도 해보자.

com.devkuma.spring.db 패키지의 “SampleEntityConfig"클래스 소스 코드를 아래와 같이 고쳐 보자.

package com.devkuma.spring.db;
 
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import javax.transaction.TransactionManager;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
 
@Configuration
@EnableJpaRepositories("com.tuyano.libro.db")
@EnableTransactionManagement
class SampleEntityConfig {
 
    @Bean
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
        return builder.setType(EmbeddedDatabaseType.H2).build();
    }
 
    @Bean
    public EntityManagerFactory entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(true);
 
        LocalContainerEntityManagerFactoryBean factory = new
            LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.tuyano.libro.db");
        factory.setDataSource(dataSource());
        factory.afterPropertiesSet();
 
        return factory.getObject();
    }
 
    @Bean
    protected JpaTransactionManager transactionManager
            (EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

그리고 App에서 다음의 문장을 수정하여 해당 Bean 설정 파일에서 이 SampleEntityConfig 클래스를 이용하도록 변경하자.

ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");

    ↓

ApplicationContext app = new  AnnotationConfigApplicationContext(SampleEntityConfig.class);

수정하면 App을 실행하여 보자. 앞서 살펴본 바와 같이 더미 데이터가 나열된다. 여기에서는 우선 눈길을 끄는 것은 클래스 선언 앞에 있는 어노테이션이다.

@EnableJpaRepositories("com.devkuma.spring.db")

이것은 JpaRepository를 ON으로 하기 위한 것이다. 이 어노테이션을 작성하는 것으로, 지정된 패키지를 검색하고 @Repository를 붙인 클래스를 Bean으로 등록한다. 괄호 () 내에는 검색할 패키지를 지정해야 한다. <jpa:repositories>에 해당하는 것으로 생각해도 된다.

@EnableTransactionManagement

이것은 TransactionManagement를 사용하기 위한 것이다. 이것을 준비해 두는 것으로 TransactionManager가 ON된다.

@Bean 어노테이션을 붙인 Bean 생성을 위한 메소드으로는 “JpaTransactionManager"라는 메소드가 추가되어 있다. 이것으로 JpaTransactionManager 인스턴스를 만들고 return한다. 보면 알 수 있는 거처럼, new JpaTransactionManager 한 것을 돌려주고있을 뿐이다. 인수는 EntityManagerFactory 인스턴스를 전달하도록 하고 있다.

보면 알 수 있듯이, 준비되어있는 Bean 생성 메소드에는 레파지토리 생성을 위한 것은 아니다. 레파지토리는 자동으로 Bean 화되는 것을 잘 알것이다.

JpaRepository 표준 방법

여기에서는 SampleEntityRepository 인터페이스를 준비했지만, 이것의 내용은 텅 빈 채였다. 그래도 findAll라는 메소드를 호출하여 전체 엔터티를 검색 할 수 있었다.

상속 받을 JpaRepository에는 표준에서 몇 개의 메소드가 정의되어 있다. 우선 그들을 사용하여 데이터베이스의 기본적인 조작이 가능하다. 여기서 주요 방법에 대해 간단하게 정리하겠다.

findAll()

이미 등장했었다. 전체 엔터티를 정리 한 List를 돌려준다.

getOne("ID")

ID를 지정하여 엔터티를 하나를 얻어온다. 인수에는 그 엔터티의 ID에 지정된 형식의 값이 들어간다.

saveAndFlush(엔티티)

인수에 지정된 엔티티를 데이터베이스에 저장한다.

delete("ID")

인수에 지정된 ID의 엔티티를 데이터베이스에서 삭제한다.

count()

엔티티의 수를 int 값으로 반환한다.

검색 대해서는 findAll 밖에 없지만, 엔티티의 저장과 삭제 등의 메소드도 갖추어져 있다. 이 메소드들은 트랜잭션 처리를 시작할 필요도 없다. 단지 메소드를 호출하는 것만으로 간단히 저장하거나 제거 할 수 있다.

JpaRepository에 메소드 추가

데이터베이스의 핵심이 되는 것은 뭐니 뭐니해도 “검색"이다. 필요에 따라 데이터를 범위를 지정하여 검색이 가능할 수 있어야 데이터베이스의 위력을 발휘할 수 있다.

그런데 JpaRepository에는 검색 관련 메소드는 “findAll"밖에 없다. 이걸로는 사용을 할 수 없다라고 생각했을지도 모르겠다. 하지만 실은 JpaRepository의 진정한 위력은 검색에 있는 것이다. 인터페이스에 검색을 위한 메소드를 추가하는 것으로, 필요한 검색 메소드를 늘릴 수 있다.

실제로 해보자. 아래를 참고하여 SampleEntityRepository 인터페이스를 수정하자.

package com.devkuma.spring.db;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface SampleEntityRepository
        extends JpaRepository<SampleEntity, Long> {
 
    public List<SampleEntity> findByNameLike(String name);
    public List<SampleEntity> findByMailEndingWith(String mail);
}

여기에서는 다음의 두 문장을 추가되었다.

public List <SampleEntity> findByNameLike(String name);
public List <SampleEntity> findByMailEndingWith(String mail);

첫 번째 “findByNameLike"는 name의 값을 사용하여 like 검색하기 위한 것이다. 두 번째 “findByMailendingWith"는 mail 값이 지정된 텍스트로 끝나는 엔티티를 검색한다.

그저 메소드 선언문을 작성했을 뿐이지만, 주의해야 하는 것은 메소드 이름입니다. 이것은 마음대로 변경하거나 하지 말아야 한다. 반드시 여기에 올린 이름대로 메소드 선언을 작성하자. 이런 메소드 이름에는 의미가 있다. 변경하면 잘 작동하지 않는다.

JpaRepository의 메소드 이용

그럼 아래를 참고하여 App.java을 변경하도록 하자.

package com.tuyano.libro.db;
 
import java.util.List;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
    private static EntityManager manager;
 
    public static void main(String[] args) {
        //ApplicationContext app = new 
            AnnotationConfigApplicationContext(SampleEntityConfig.class);
        ApplicationContext app = new ClassPathXmlApplicationContext("dbbean.xml");
 
        EntityManagerFactory factory = app.getBean(EntityManagerFactory.class);
        manager = factory.createEntityManager();
        makeDummyData();
         
        // get repository
        SampleEntityRepository repository = app.getBean(SampleEntityRepository.class);
         
        // get list
        List list1 = repository.findByNameLike("%ko");
        System.out.println("*Find By Name*");
        printList(list1);
        List list2 = repository.findByMailEndingWith(".com");
        System.out.println("*Find By Mail*");
        printList(list2);
         
        System.out.println("...ok.");
    }
 
    // create dummy entity data.
    public static void makeDummyData() {
        EntityTransaction transaction = manager.getTransaction();
        transaction.begin();
        manager.persist(new SampleEntity("tuyano", "syoda@tuyano.com"));
        manager.persist(new SampleEntity("hanako", "hanako@flower.uk"));
        manager.persist(new SampleEntity("taro", "taro@yamada.jp"));
        manager.persist(new SampleEntity("sachiko", "sachico@happy.com"));
        manager.flush();
        transaction.commit();
    }
 
    // print all entity.
    public static void printList(List list) { ...생략... }
}

코드를 보면 간단히 필요한 엔티티를 검색 할 수 있다는 것을 알 수 있다.

name으로 검색

List list1 = repository.findByNameLike("%ko");

검색 결과 :

SampleEntity [id=2, name=hanako, mail=hanako@flower.uk]
SampleEntity [id=4, name=sachiko, mail=sachico@happy.com]

mail로 검색

List list2 = repository.findByMailEndingWith(".com");

검색 결과 :

SampleEntity [id=1, name=tuyano, mail=syoda@tuyano.com]
SampleEntity [id=4, name=sachiko, mail=sachico@happy.com]

findByNameLike("%ko")는 name 값이 “ko"로 끝나는 엔티티가 검색된다. 또한 findByMailEndingWith(“com”)에서는 mail 값이 “.com"으로 끝나는 엔티티가 검색되는 것을 알 수 있다. 제대로 필요한 것만 검색이 가능하다.

여기서 주의 깊게 봤으면 하는 것은 “SampleEntityRepository에는 메소드의 선언을 했을 뿐이다"라는 점이다. 즉, 이러한 메소드에서 실제로 수행하는 처리는 전혀 쓰여 있지 않다는 것이다. 단지 메소드의 선언만으로 제대로 움직이는 메소드가 자동 생성된다.

이것이 JpaRepository의 위력이다. JpaRepository는 미리 정해진 형식의 메소드 이름을 작성하면, 그 이름을 바탕으로 실제로 움직이는 메소드가 자동 생성된다.

JpaRepository의 메소드 명명 규칙

JpaRepository는 메소드 이름 작성 방법만 알다면, 필요한 메소드를 빠르게 쓰고 추가할 수 있다. 처리는 코드 일체 필요 없다. 단지, 메소드 선언문만 있으면 된다. 단지, 미리 정해진 룰에 따라 제대로 메소드 이름을 붙이기만 하면 된다. 적당히 이름을 붙이는 것만으로 메소드가 자동 생성이 되는 것이다.

그럼, 어떤 형태로 메소드 이름을 붙이면 되는지 대해, 여기에 그 이름 지정 규칙에 대해 간략하게 정리하면 아래와 같다.

  • findByXX 기본은 이것이다. “findBy” 이후에 엔티티의 속성 이름을 붙이다. 이 속성 이름은 첫 글자는 대문자로 한다. 예를 들어, name 검색한다면 “findByName"이며, mail에서 찾는다면 “findByMail"가 된다.

이 다음에는 기본형인 ‘findByXX” 이후에 계속 이어서 쓰면 된다.

  • Like / NotLike
    “퍼지 검색"에 관한 것이다. Like를 붙이면, 인수에 지정된 텍스트를 포함하는 엔티티를 검색한다. 또한 NotLike을 쓰면 인수의 텍스트를 포함하지 않는 것을 검색한다. “findByNameLike"이라면, name에서 인수의 텍스트를 퍼지 검색한다.
  • StartingWith / EndingWith
    텍스트 값에서 인수에 지정된 텍스트로 시작하거나 끝나는 것을 검색하기 위한 것이다. findByNameStartingWith(“A”)이라면, name의 값이 “A"로 시작하는 항목을 검색한다.
  • IsNull / IsNotNull
    값이 null이 거나, 혹은 null이 아닌 것을 검색한다. 인수는 필요없다. “findByNameIsNull()“이라면, name의 값이 null의 것만 검색한다.
  • True / False
    부울 값으로 true 인 것, 혹은 false 인 것을 검색한다. 인수는 필요없다. “findByCheckTrue()“이라면, check라는 항목이 true 인 것만을 검색한다.
  • Before / After
    시간 값으로 사용한다. 인수에 지정한 값보다 이전의 것, 혹은 이후 것을 검색한다. “findByCreateBefore(new Date())“라고 하면, create라는 항목의 값이 현재보다 이전의 것만을 찾는다 (create가 Date 인 경우).
  • LessThan / GreaterThan 숫자 값으로 사용한다. 그 항목의 값이 인수보다 작거나 큰 것을 검색한다. “findByAgeLessThan(20)“이라면, age의 값이 20보다 작은 것을 찾는다.
  • Between 두 값을 인수로 가지고 그 두 값 사이의 것을 검색한다. 예를 들어, “findByAgeBetween(10, 20)“라고 한다면 age 값이 10 이상 20 이하인 것을 검색한다. 수치뿐만 아니라 시간의 항목 등에도 사용할 수 있다.

이 외에도 아직 더 있지만, 일단 이것들을 사용할 수 있다면 기본적인 검색은 대체로 가능하게 될 것이다. JpaRepository를 잘 다룰 수 있게 되면 코딩이 많이 없어도 다양한 데이터 검색을 할 수 있다. 매우 간단하므로 샘플을 기반으로 여러가지 시도해 보길 바란다.




최종 수정 : 2024-01-18