SpringData 란?
JPA로 개발을 하다보면 각 엔티티마다 공통으로 사용되는 쿼리들이 있습니다. 예를 들면, 기본적인 CRUD는 어떤 엔티티든 CRUD가 되는 대상만 다르지 방식이 모두 동일하다고 봐도 무방합니다. 그런데, 순수 JPA만 사용해서 개발하게 되면, 모든 엔티티마다 따로따로 CRUD 코드를 작성해야 되는데, 겹치는 부분들이 많이 발생합니다.
이렇게 겹치는 코드들을 제거하게 해줄 수 있는 것이 바로 SpringData 입니다.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public Member find(Long id) {
return em.find(Member.class, id);
}
public void delete(Member member) {
em.remove(member);
}
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);
}
public long count() {
return em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
}
}
JPA민 사용 해서 CRUD를 구현한 코드 입니다. 해당 코드들을 보면, Member가 엔티티가 아닌 Team 엔티티로 바꿀 경우에는, Member를 Team으로 변경만 하면 Team 엔티티에 대한 CRUD가 됩니다. 그렇기 때문에, 많은 코드가 겹치게 되지만, 각 엔티티에 대해서 각각 구현해야 합니다.
public interface MemberDataRepository extends JpaRepository<Member, Long> {
// JpaRepository<Entity, Entity PK Type>
// @Repository 애노테이션이 없어도 자동으로 JpaRepository를 상속 받으면 Component 스캔이 됨
}
SpringData를 사용 했을때의 Member CRUD 입니다. 보다 시피, JpaRepository<T,K>만 상속해도 위에서 구현한 코드들을 모두 생략할 수 있습니다. 이게 가능한 이유는 JpaRepositoryJpaRepository<T,K>에서 구현되어 있는 메서드들을 확인 해보면 됩니다.
@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberDataRepositoryTest {
/**
* memberRepository.getClass() 출력 시 -> class com.sun.proxy.$ProxyXXX
* 아무것도 구현하지 않은 인터페이스 임에도 불구하고 save, findById() 등 이 작동하는 이유는
* 해당 인터페이스를 보고 스프링이 자동으로 프록시로 구현 클래스를 만들어서 관계 주입 시켜줌
*/
@Autowired
private MemberDataRepository memberDataRepository;
@Test
void basicCRUDTest() {
Member member1 = new Member("member1");
Member member2 = new Member("member2");
memberDataRepository.save(member1);
memberDataRepository.save(member2);
Member findMember1 = memberDataRepository.findById(member1.getId()).get();
Member findMember2 = memberDataRepository.findById(member2.getId()).get();
//단건 조회 검증
Assertions.assertThat(findMember1).isEqualTo(member1);
Assertions.assertThat(findMember2).isEqualTo(member2);
//리스트 조회 검증
List<Member> members = memberDataRepository.findAll();
Assertions.assertThat(members.size()).isEqualTo(2);
//카운트 검증
long count = memberDataRepository.count();
Assertions.assertThat(count).isEqualTo(2);
//삭제 검증
memberDataRepository.delete(member1);
org.junit.jupiter.api.Assertions.assertThrows(NoSuchElementException.class, () -> memberDataRepository.findById(member1.getId()).get());
}
}
JUnit을 이용해서 테스트를 진행해 보면, 위의 테스트는 Pass하는 것을 확인 할 수 있습니다. 보다시피, MemberDataRepository에서는 단 하나의 CRUD 관한 메서드를 구현하지 않았음에도 CRUD가 제대로 작동하는 것을 확인 할 수 있습니다.
MemberDataRepository가 상속 받고 있는 JpaRepository<T, ID>를 보면, find(), findAll, findById, delete()등 많은 메서드들이 구현되어 있는 것을 볼 수 있습니다.
이때, MemberDataRepository는 @Repository 애노테이션이 없어도 JpaRepository<T, ID>를 상속 받고 있기 때문에 Spring에서는 Component 스캔을 자동으로 해줍니다. 또한, MemberDataRepository에서 메서드들을 구현하지 않았음에도 CRUD가 작동하는 이유는 MemberDataRepository를 출력해보면 알 수 있습니다.
출력 시, class com.sun.proxy.$ProxyXXX가 출력 되는 데, 이 뜻은 Spring에서 자동으로 프록시로 해당 메서드들을 구현한 클래스를 만들고, 해당 클래스를 관계 주입까지 알아서 진행해 줍니다.
JpaRepository<T, ID>를 보면, JpaRepository<T, ID>또한 PagingAndSortingRepository<T, ID>을 상속 받고 있으며, PagingAndSortingRepository<T, ID>는 CrudRepository<T, ID>를 상속하고, CrudRepository<T, ID>는 Repository<T, ID>를 상속합니다. 이렇기 때문에, JpaRepository<T, ID>는 단순 CRUD 외에 더 많은 기능들을 제공하며, 해당 기능들을 앞으로 더 살펴 보겠습니다.