// MemberRepository
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
// TeamRepository
@Repository
@RequiredArgsConstructor
public class TeamJpaRepository {
private final EntityManager em;
public Team save(Team team) {
em.persist(team);
return team;
}
Repository에 기본적으로 작성해놓는 CRUD기능들은 코드 형태가 대부분 동일하다. 위의 예시는 save만 보였지만 delete나 findById, findAll과 같은 CRUD관련 메서드들 또한 엔티티에 따라 달라지는 점이 크지 않다.
Spring Data JPA의 유용한 기능중 하나는 이러한 중복 문제를 해결해준다는 것이다. 그리고 이 적용법은 매우 간단하다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
인터페이스로 Repository를 만들어주고, JpaRepository<Entity type, PK type> 만 상속받아주면 기본적인 CRUD는 물론 개발자가 생각할 수 있는 간단한 기능들은 모두 사용이 가능하다.
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
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) {
return Optional.ofNullable(em.find(Member.class, id));
}
public long count() {
return em.createQuery("select count(m) from Member m", Long.class).getSingleResult();
}
public Member find(Long id) {
return em.find(Member.class, id);
}
위와 같은 노동이 사라진다.(매우 매우 매우 강력하다.)
public interface MemberRepository extends JpaRepository<Member, Long> {
}
//////////
@Autowired MemberRepository memberRepository;
@Test
public void basicCRUD() {
Member memberA = new Member("memberA");
Member memberB = new Member("memberB");
memberRepository.save(memberA);
memberRepository.save(memberB);
Member findMemberA = memberRepository.findById(memberA.getId()).get();
Member findMemberB = memberRepository.findById(memberB.getId()).get();
assertThat(findMemberA).isEqualTo(memberA);
assertThat(findMemberB).isEqualTo(memberB);
List<Member> all = memberRepository.findAll();
assertThat(all.size()).isEqualTo(2);
long count = memberRepository.count();
assertThat(count).isEqualTo(2);
memberRepository.delete(memberA);
memberRepository.delete(memberB);
long deleteCount = memberRepository.count();
assertThat(deleteCount).isEqualTo(0);
}
해당 인터페이스로 곧바로 이전처럼 기능을 사용할 수 있다. 이질감이 약간 느껴지는데.
자바 문법을 배운 입장에서는 인터페이스만을 설계했는데 이를 통해 구현체도 없이 어떻게 사용이 가능한가 싶을 수 있다.
Spring Data Jpa는 JpaRepository 상속이 적용될 경우 구현 클래스를 대신 생성해준다.(@Repository 생략 가능)

간단한 기능은 JpaRepository 상속을 통해 곧바로 CRUD기능들을 이용할 수 있었다. 만약 a라는 이름을 가지고 age가 15인 Member 엔티티만을 조회라는 기능을 수행하기 위해 우리는 보통 jpql을 직접 작성했다.
엔티티에 선언하는 @NamedQuery로 하여금 key:value 느낌으로 query 문자열에 이름을 지정해서 사용할 수 있지만 이 기능은 엔티티 클래스 자체에 선언해야한다는 문제도 존재하고 spring data jpa가 제공하는 Query Method가 너무 강력하기에 잘 사용하지 않는다.
public interface MemberRepository
extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
쿼리 메서드는 위와 같이 인터페이스에 메서드명만 규칙대로 잘 작성하면 해당하는 jpql을 수행해준다.
@Modifying(clearAutomatically = true) // update 수행
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
이전에 배운 jpql로 직접 짜는 벌크성 쿼리의 방식과 비슷하다. 단지 벌크성 쿼리를 다룰 때 쿼리 직후 영속성 컨텍스트와 DB간의 동기화를 위해 영속성 컨텍스트에 대한 clear()작업이 필요했다.
@Modifying은 executeUpdate()를 실행시켜주고, 이에 인자로 clearAutomatically = true를 주면 EntityManager clear까지 수행해준다.
현재 엔티티 member -> team은 지연로딩 관계이기 때문에 member를 조회하고 team을 조회하게 되면 추가 쿼리가 발생한다.
1개의 쿼리로 100개의 member 엔티티를 가져왔을 때 100개의 member 엔티티의 team을 조회한다면 100개의 쿼리가 발생하므로 1+N문제가 발생한다.
우리는 이를 fetch join이나 @BatchSize등으로 해결했는데 spring data jpa는 이를 간단하게 최적화할 수 있도록 @EntityGraph를 지원한다.
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();
JpaRepository의 findAll을 오버라이드 하여 작성하는 대신 추가로 @EntityGraph에 위와 같이 team을 인자로 주면 Member 조회시 Team에 대한 fetch join(left outer join)을 수행한다.