MyBatis 기반으로 작성된 DAO 코드를 JPA Repository로 변경하는 과정에서, @Repository 어노테이션 사용(stereotype) / JpaRepository / CrudRepository / Repository 인터페이스 상속 (Spring Data JPA) 중 어느 것을 선택해야 하는지 고민하게 되었습니다.
이전까지는 기계적으로 JpaRepository 인터페이스를 상속받아 사용했기 때문에, 이번 기회에 정확한 차이점을 알아보고자 합니다.
이 글은 개인적으로 공부하며 이해한 내용을 정리한 것으로, 부정확한 정보가 있는 경우 댓글로 피드백 주시면 감사하겠습니다.
Persistence Layer 역할을 하는 class에 사용하는 어노테이션으로, 해당 class를 루트 컨테이너에 Bean으로 생성&등록하고 핸들러가 스캔할 수 있도록 합니다.
@Repository
public class MemberRepository {
private final EntityManager em;
public void register(Member member) {
em.persist(member);
em.merge(member);
}
}
내부 코드는 다음과 같습니다.
@Component 어노테이션을 포함하므로, Classpath Scanning 과정에서 자동으로 루트 컨테이너에 등록될 수 있습니다.
JPA를 사용할 때, Repository에서 일반적으로 상속받는 인터페이스에는 Repository, CrudRepository, PagingAndSortingRepository, JpaRepository 등이 있습니다.
Repository, CrudRepository, PagingAndSortingRepository는 Spring Data Project에서 공통적으로 사용할 수 있는 인터페이스이고, JpaRepository는 Spring Data JPA에서 제공하는 인터페이스입니다.
Repository가 최상위 인터페이스이고, 아래쪽으로 갈수록 상위 인터페이스를 상속하여 정의한 구현체이며 더 많은 기능을 제공합니다.
각 인터페이스에서 제공하는 기능을 간단하게 정리하면 다음과 같습니다.
CrudRepository | CRUD 관련 기능 (select, insert, update, delete)
PagingAndSortingRepository | CRUD 관련 기능 + Paging 및 Sorting 관련 기능
JpaRepository | CRUD 관련 기능 + Paging 및 Sorting 관련 기능 + JPA 관련 특화기능 (Flushing 및 Batch 작업 등)
그렇다면 각 인터페이스에는 구체적으로 어떤 차이가 있는지, 코드를 통해 알아보겠습니다.
Repository는 최상위 인터페이스로, Domain (or Entity)의 타입과 ID(PK) 타입을 받는 역할을 합니다. Repository의 특징은 다음과 같습니다.
기본적으로 구현되어 있는 메서드가 없으므로, 인터페이스를 상속받은 곳(ex. UserRepository)에서 사용할 메서드를 직접 선언해야 한다.
선언된 메서드만 외부에 개방되므로, 각 레포지토리의 역할과 책임을 명확하게 표현할 수 있다.
CrudRepository는 Repository를 상속받은 인터페이스로, Repository의 역할(Domain & ID 타입 저장)과 함께 CRUD 기능을 제공합니다. CrudRepository의 특징은 다음과 같습니다.
@NoRepositoryBean => CrudRepository는 Repository를 상속받는 구현체이지, 그 자체로 Repository Layer의 역할을 하지는 않는다. 따라서, Spring에서 CrudRepository를 Bean으로 등록하는 것을 방지하기 위해 해당 어노테이션을 사용한다. (@Repository 어노테이션을 사용해도 무시됨)
select, insert, update, delete, count에 대응되는 작업을 수행하는 메서드들이 선언되어 있다.
(CrudRepository를 상속받은) 레포지토리에서 별도의 메서드 선언을 하지 않아도, CrudRepository 내부에 선언된 메서드를 외부(Service Layer 등)에서 호출할 수 있다.
PagingAndSortingRepository는 Repository를 상속받은 인터페이스로, Repository의 역할과 함께 Paging과 Sorting 관련 기능을 제공합니다. PagingAndSortingRepository의 특징은 다음과 같습니다.
@NoRepositoryBean
select * from domain 쿼리의 결과를 주어진 sort 조건에 따라 정렬하여 리턴한다.
select * from domain 쿼리의 결과를 주어진 paging 조건에 따라 pagination 된 상태로 리턴한다.
레포지토리에서 별도의 메서드 선언을 하지 않아도, PagingAndSortingRepository 내부에 선언된 메서드를 외부에서 호출할 수 있다.
JpaRepository는 CrudRepository와 PagingAndSortingRepository를 상속받은 인터페이스로, CrudRepository, PagingAndSortingRepository의 역할과 함께 Flush, Batch 관련 기능을 제공합니다. JpaRepository의 특징은 다음과 같습니다.
@NoRepositoryBean
Flush - Persistence Context(영속성 컨텍스트)의 변경 내용을 DB 테이블에 실제로 반영하는 기능을 제공한다.
Batch - 여러 개의 SQL Statement를 하나의 그룹으로 묶어서 한 번에 처리하는 기능을 제공한다.
레포지토리에서 별도의 메서드 선언을 하지 않아도, JpaRepository 내부에 선언된 메서드를 외부에서 호출할 수 있다.
Flush와 Batch에 관한 자세한 설명은 별도의 포스팅에서 다루겠습니다.
선택을 위해 고려한 프로젝트의 특징은 다음과 같습니다.
서비스의 비즈니스 로직 상, 쿼리는 대부분 기본적인 CRUD 기능을 수행하고 있습니다. Join, Subquery 등 커스텀이 필요한 쿼리가 많이 존재하지 않으므로, 이미 JpaRepository에서 제공되는 메서드들을 하나하나 직접 선언하는 것은 불필요하다고 생각했습니다.
키워드 기준 검색, 제목 기준 검색, 장소 기준 검색 등의 구현을 위해 select 쿼리가 많이 사용되는데, Repository를 사용하는 경우 select a, b from table where c = "qwerty" / select b, c from table where a = "oiuy" 등 유사한 포맷의 쿼리를 반복해서 작성해야 하는 번거로움이 있을 것이라고 생각했습니다.
위와 같은 이유로, JpaRepository를 상속받아 사용하기로 결정했습니다.
Spring Data Project에서 제공하는 Reposiory 인터페이스를 사용하면, @Repository 어노테이션을 추가적으로 선언해주지 않아도 해당 레포지토리가 Bean으로 등록됩니다.
이러한 작업이 가능한 것은, Spring Boot에서 기본적으로 @EnableJpaRepositories가 설정되어 있기 때문입니다.
위 코드에서 실질적으로 레포지토리를 Bean으로 등록하는 역할을 하는 부분은 @Import의 JpaRepositoriesRegistrar.class 입니다.
JpaRepositoriesRegistrar는 다시 ImportBeanDefinitionRegistrar을 상속받습니다.
Annotation metadata를 받아 JpaRepository를 Bean으로 등록합니다.