순수 JPA 기반 리포지토리 만들기
- 우선 순수한 JPA기반 리포지토리를 만들어 보자
- 기본 CRUD
1. Create(저장)
public Member save(Member member) {
em.persist(member);
return member;
}
2. Update(변경 → 변경감지 사용)
JPA에서 수정은 변경감지 기능을 사용하면 된다.
트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경감지 기능이 작동 해서 변경된 엔티티를 감지하고 UPDATE SQL을 실행한다.
3. Delete(삭제)
public void delete(Member member) {
em.remove(member);
}
4. Read(조회)
public Member find(Long id) {
return em.find(Member.class, id);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
5. Count(카운트)
public long count() {
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
테스트
@Test
public void basicCRUD () {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberJpaRepository.save(member1);
memberJpaRepository.save(member2);
// 단건 조회 검증
Member findMember1 = memberJpaRepository.findById(member1.getId()).get();
Member findMember2 = memberJpaRepository.findById(member2.getId()).get();
assertThat(findMember1).isEqualTo(member1);
assertThat(findMember2).isEqualTo(member2);
// 리스트 조회 검증
List<Member> all = memberJpaRepository.findAll();
assertThat(all.size()).isEqualTo(2);
// 카운트 검증
long count = memberJpaRepository.count();
assertThat(count).isEqualTo(2);
// 삭제 검증
memberJpaRepository.delete(member1);
memberJpaRepository.delete(member2);
long deleteCount = memberJpaRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
공통 인터페이스 설정
JavaConfig 설정
프로젝트명Application.java 파일에 basePackages 설정
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
- 스프링 부트 사용 시 @EnableJpaRepositories(basePackages="jpabook.jpashop.repository") 의 역할은 @SpringBootApplication 이 대신 하게 된다.
- 그래서 따로 작성해줄 필요가 없고 만약 위치가 달라지면 그때 @EnableJpaRepositories을 작성해주면 된다.
스프링 데이터 JPA가 구현 클래스 대신 생성
그렇다면 여기서 의문점이 생길 수 있다. 우리는 분명 아래와 같이 Interface만 만들어줬을 뿐인데 구현체는 대체 어디있길래 save(), findById() 와 같은 메소드가 동작하는 것일까??
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;
public interface MemberRepository extends JpaRepository<Member, Long> {}
- 이는 개발자가 Interface만 선언해주면 Spring Data JPA 가 자동으로 구현클래스를 만들어 주입해주기 때문이다.
- Ex: xxxRepository.save() , find(), findAll() ...
- 즉, org.springframework.data.repository.Repository 를 상속한 인터페이스를 스캔 대상으로 인식하고 이에 대한 구현체로 Proxy 객체를 만들어 주입해주는 것이다.
- memberRepository.getClass() => class com.sun.proxy.$ProxyXXX
그리고 위의 코드를 보면 @Repository 애노테이션이 생략되어 있는 것을 볼 수 있다. 이는 애노테이션이 생략되어도 Spring Data JPA 가 인터페이스를 보고 자동으로 Proxy객체를 만들어 주입해주기 때문에 생략이 가능한 것이다.
그리고 사실 @Repository 애노테이션은 다음과 같은 2가지 기능을 보장한다.
- 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리
- JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리
공통 인터페이스 적용
이번엔 순수 JPA로 구현한 MemberJpaRepository 대신에 스프링 데이터 JPA가 제공하는 공통 인터페이스 사용하여 테스트를 진행해보자.
스프링 데이터 JPA 기반 MemberRepository
public interface MemberRepository extends JpaRepository<Member, Long> {}
MemberRepository 테스트 코드
@SpringBootTest
@Transactional
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);
// 리스트 조회 검증
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(deleteCount).isEqualTo(0);
}
}
테스트 코드를 실행해보면 기존 순수 JPA 기반 테스트에서 사용했던 코드를 그대로 스프링 데이터 JPA 리포지토리 기반 테스트로 변경해도 동일한 방식으로 정상 동작하는 것을 확인할 수 있다.
공통 인터페이스 분석
JpaRepository 인터페이스: 공통 CRUD 제공
- ex: find, findAll, save, delete ...
제네릭은 <엔티티 타입, 식별자 타입> 설정
public interface ExampleRepository extends JpaRepository<Entity, PrimityType>{}
공통 인터페이스 구성
주의사항
- T findOne(ID) => Optional<T> findById(ID) 변경
- boolean exists(ID) => boolean existsById(ID) 변경
제네릭 타입
- T :엔티티
- ID : 엔티티의 식별자 타입
- S : 엔티티와 그 자식 타입
주요 메서드
- save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
- delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
- findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
- getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
- findAll(...) : 모든 엔티티를 조회한다. 정렬(Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다.