Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다.
공통 인터페이스 설정
원래는 스프링에서는 JavaConfig로 설정해야한다.
@SpringBootApplication
@EnableJpaRepositories(basePackages = "study.datajpa.repository")
public class DataJpaApplication {
public static void main(String[] args) {
SpringApplication.run(DataJpaApplication.class, args);
}
}
스프링 부트에서는 다행히도 생략이 가능하며 @SpringBootApplication가 적용된 파일의 패키지와 하위 패키지를 인식할 수 있다. 만약 위치가 달라지면 @EnableJpaRepositories를 사용하긴 해야한다.
MemberRepository.interface
public interface MemberRepository extends JpaRepository<Member, Long> {
}
- JpaRepository<엔티티, 엔티티의 PK 타입>
저번에 잠깐 생성하고 맛만 봤었던 interface를 다시 한 번 살펴보자. 느낌이 오겠지만 이게 바로 스프링에서 제공하는 스프링 데이터 JPA이다. 이 인터페이스는 JpaRepository를 상속받고 있다. JpaRepository 안에 소스를 들여다보면 역시 interface라 구현체가 없다. 구현체가 없는데 어떻게 기능이 동작할까?
MemberRepositoryTest.class
전에 생성했었던 테스트 파일에 memberRepository의 정체를 출력해보자.
@SpringBootTest
@Transactional
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void testMember() {
// memberRepository의 정체
System.out.println("memberRepository = " + memberRepository.getClass());
//... 생략
}
}
// 출력 로그 중 일부
memberRepository = class com.sun.proxy.$Proxy117
스프링이 인터페이스를 보고 스프링 데이터 JPA에서 구현 클래스를 만들어 주입을 해준 것이다.
Spring Data JPA 구현 클래스 생성
- 'org.springframework.data.repository.Repository'를 구현한 클래스는 스캔 대상
- MemberRepository가 동작한 이유
- 실제 출력해보기(Proxy)
- memberRepository.getClass() -> class com.sun.proxy.$ProxyXXX
- @Repository 애노테이션 생략 가능
- 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
- JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리함
예제
- 전 포스팅에서 만들었던 TeamRepository.class 이름을 TeamJpaRepository.class로 변경
- TeamRepository.interface 생성
public interface TeamRepository extends JpaRepository<Team, Long> {
}
공통 인터페이스 적용(테스트)
MemberRepositoryTest.class
@SpringBootTest
@Transactional
@Rollback(false)
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
// ... 생략
@Test
public void basicCRUD() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberRepository.save(member1);
memberRepository.save(member2);
// 단건 조회 검증
Member findMember1 = memberRepository.findById(member1.getId()).get();
Member findMember2 = memberRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
findMember1.setUsername("member!!!!!!!!!!");
// 리스트 조회 검증
List<Member> all = memberRepository.findAll();
assertThat(all.size()).isEqualTo(2);
// 카운트 검증
long count = memberRepository.count();
assertThat(count).isEqualTo(2);
// 삭제 검증
memberRepository.delete(member1);
memberRepository.delete(member2);
long deleteCount = memberRepository.count();
assertThat(count).isEqualTo(2);
}
}
MemberJpaRepository.class에서 만들었던 테스트를 가지고 와서 memberJpaRepository를 memberRepository로 수정해주고 돌려주면 테스트가 통과하는 걸 볼 수 있다. repository만 수정한 건데 왜 통과되냐면 애초에 MemberJpaRepository.class에서의 테스트 코드(findById, findAll 등)를 스프링 데이터 JPA에서 제공하는 기능의 이름과 맞췄기 때문이다.
공통 인터페이스 분석
이렇게 쉽게 코드를 작성할 수 있게 해주는 JpaRepository 인터페이스의 정체는 무엇일까? 한 번 속을 까보자.
- JpaRepository 인터페이스 : 공통 CRUD 제공
- 제네릭은<엔티티 타입, 식별자 타입>이다.
JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID>
extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T>
{
...
}
JpaRepository를 사용하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {
}
공통 인터페이스 구성
- 주의
- T findOne(ID) → Optional<T> findById(ID) 변경
- 제네릭 타입
- T : 엔티티
- ID : 엔티티의 식별자 타입
- S : 엔티티와 그 자식 타입
- 주요 메서드
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제. 내부에서 EntityManager.remove()를 호출한다.
- findById(ID) : 엔티티 하나 조회. 내부에서 EntityManager.find()를 호출한다.
- getOne(ID) : 엔티티를 프록시로 조회. 내부에서 EntityManager.getReference() 호출
- findAll(...) : 모든 엔티티 조회. 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.
- 참고!
- JpaRepository는 대부분의 공통 메서드를 제공한다.