[Spring Boot] 입문 - Spring Data JPA 맛보기
스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 김영한 인프런 강의 참고
이전 포스팅
- [Spring Boot] 입문 - Spring Boot 프로젝트에 JPA 적용하기
- [Spring Boot] 입문 - JPA를 사용하는 이유
- [Spring Boot] 입문 - JDBC Template(feat. 스프링 통합 테스트)
드디어 우리의 Spring Boot 프로젝트에서 MemberRepository의 마지막 구현체다. 지금까지 메모리 기반 저장소부터 관계형 데이터베이스인 MySQl로 넘어오면서, 순수 JDBC 사용 → JDBC Template → JPA까지 구현체를 바꿔가며 어떻게 코드들이 마법처럼 간단해 지는지 확인했다.
근데 믿기지 않겠지만, 여기서 더 간단해 질 수 있다. 바로 Spring Data JPA를 사용하는 것이다. 이제는 실무에서 Spring Data JPA를 사용하는 것은 선택이 아닌 필수라고 한다. 어떻게 간단해지는지, 어떻게 사용하는지 우리의 프로젝트에 적용해 보면서 간단하게 맛보자.
Goals
- Spring Data JPA 소개
- Spring Data JPA로 repository 구현하기
- Spring Data가 제공하는 Class
Spring Data JPA 소개
Spring Data JPA는 Spring Data 프로젝트의 모듈 중 하나다. Spring Data는 Spring 애플리케이션에서 데이터 접근 시 보다 친숙하고 일관된 방식을 사용할 수 있도록 도와주며, 각 데이터 저장소의 고유한 특징은 그대로 유지한다.
그 중 Spring Data JPA는 JPA 기반의 repository를 쉽게 구현할 수 있게 도와주는 모듈이다. Spring Data JPA를 사용하면 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있다. 기본적인 CRUD 기능을 자동으로 모두 구현해 주고, 주입까지 해준다.
때문에 조금이라도 단순하고 반복적인 코드들이 확연하게 줄어 개발자가 핵심적인 비즈니스 로직을 개발하는데 집중할 수 있게 됐다.
과연 어떤 마법이 일어나는지 바로 프로젝트에 적용해 보면서 알아보겠다.
Spring Data JPA로 repository 구현하기
Spring Data JPA를 사용하기 위한 Gradle 설정
JPA를 사용했을 때와 동일하다.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
스프링 데이터 JPA 회원 리포지토리 인터페이스
우리는 단지 리포지토리 클래스가 아닌 인터페이스만 작성하면 된다.
/repository/SpringDataJpaMemberRepository.java
package com.example.practice.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.practice.domain.Member;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository{
Optional<Member> findByName(String name);
}
끝이다. 정말 끝이다. 단지 인터페이스를 작성했다. 단지 JpaRepository
라는 인터페이스와 MemberRepository
인터페이스를 상속 받았다. 심지어 메서드는 findByName
만 정의했을 뿐 다른 메서드는 정의하지도 않았다. 이게 돌아갈까? 스프링 설정을 변경해 주고 바로 테스트 해보자.
스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
SpringConfig.java
@Configuration
public class SpringConfig {
// private final DataSource dataSource;
// private final EntityManager em; // 추가
// public SpringConfig(DataSource dataSource, EntityManager em) {
// this.dataSource = dataSource;
// this.em = em;
// }
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
MemberService memberService() {
return new MemberService(memberRepository);
}
// @Bean
// MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
// return new JpaMemberRepository(em);
// }
}
스프링 빈을 주입 받는 방법도 훨씬 간단해졌다. 스프링 데이터 JPA가 SpringDataJpaMemberRepository
를 스프링 빈으로 자동 등록해주기 때문에 상위 인터페이스인 MemberRepository
역시 자동으로 DI가 가능하다.
이제 테스트 코드를 실행해 보자.
놀랍게도 테스트가 성공하며, 테스트 로그도 JPA를 사용했을 때와 동일하다. 어떻게 이런 일이 일어날 수 있는지 있을까?
Spring Data가 제공하는 Class
Spring Data는 기본적인 CRUD를 지원하는 인터페이스인 CrudRepository
와 페이징 기능을 지원하는 PagingAndSortingRepository
를 인터페이스로 가지고 있다. 그리고 이를 Spring Data JPA의 JpaRepository
인터페이스가 다시 상속을 받고, JPA에서 사용하는 특정 메서드들을 추가로 정의해 놓았다.
개발자는 이 인터페이스를 상속받기만 하면 된다. 이때 제네릭 타입으로 도메인 클래스 타입과 기본 키 속성 타입을 대입해 준다.
Interface JpaRepository<T,ID>
: 리포지토리의 JPA 스펙 확장 인터페이스
따라서 다음과 같이 리포지토리를 정의하기만 하면 된다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository
우리는 MemberRepository
에 1차적으로 사용할 메서드들을 정의해 놓았기 때문에 같이 상속 받아준다.
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
그럼 MemberRepository
에 정의되어 있는 메서드들을 스프링이 구현한 구현체를 자동으로 생성해 준다. 단, 메서드를 정의할 때, 상속 받은 메서드들과 같은 스펙으로 정의해야 스프링이 자동으로 생성해 준다.
(참고 : CrudRepository, ListCrudRepository, JpaRepository, SimpleJpaRepository)
만약 비즈니스 로직에 따라 사전에 정의되어 있지 않은 메서드를 사용해야 한다면 어떻게 할까?
그 예가 Optional<Member> findByName(String name);
와 같은 메서드다. 하지만 걱정하지 않아도 된다. Spring Data JPA는 메서드 이름을 지을 때 특정한 규칙을 준수한다면 메서드 이름만으로 적절한 JPQL 쿼리를 생성해 실행해주는 쿼리 메서드(Query Methods) 기능이 있다.
위 메서드의 경우는 다음과 같은 쿼리를 생성한다.
select m from Member m where m.name = :name
여기서 규칙은 findBy
로 이어지는 문자열이다. Name
과 파라미터로 선언된 name
이 일치해야 하는 것이다.
또한 2개의 파라미터를 사용할 수도 있다.
예시 : List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);
좀 더 다양한 예시들은 jpa.query-methods를 참고하자.
만약 더 복잡한 쿼리가 필요한 경우 역시 대안이 있다. Querydsl이라는 프레임워크를 사용하여 동적 쿼리를 생설할 수도 있다. 그래도 안되면 JPA와 JDBC Template을 같이 사용하여 쿼리를 직접 작성하는 방법도 열려있다.
Spring Data JPA의 기능을 간단히 정리해 보면 다음과 같다.
- 인터페이스를 통한 기본적인 CRUD
findByName()
,findByEmail()
처럼 메서드 이름 만으로 조회 기능 제공- 페이징 기능 자동 제공
- 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용
이 외에도 많은 내용들이 있으므로 아래 reference 링크를 참고해서 학습하고 사용해 보면 좋을 것 같다. 단, Spring Data JPA를 잘 사용하기 위해서는 JPA를 잘 알아야 한다는 사실을 잊어서는 안된다.