2024. 2. 1. 15:58ㆍJPA/스프링 데이터 JPA
[ 작성 이유 ]
이전에 코드를 작성하다가 JpaRepository로 쿼리를 작성하는데 일반적인 코드는 어렵지 않았지만 추가적인동적쿼리나 복잡한 쿼리를 만드는 건 굉장히 어려웠던 기억이 난다. 실전! 스프링 데이터 JPA 강의를 보다가 그 방법을 배워서 그 내용에 관해서 쭉 정리를 해보고자 한다.
[ JpaRepository ]
스프링 데이터 JPA에서는 JpaRepository라는 인터페이스를 제공한다. 사용하는 방법은 다음과 같다. 우선 Member라는 객체 Entity를 만들어보자. 아래의 코드를 작성해주면 된다.
@Entity
@Getter @Setter
@NoArgsContructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
Member(String username, int age){
this.username = username;
this.age = age;
}
}
이제 해당 코드로 생성된 테이블에 쿼리를 날릴 수 있게 도와주는 JpaRepository를 상속 받은 MemberRepository를 만들어주자.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
}
정말 간단하다. 이렇게만 코드를 작성해주면 웬만한 CRUD는 다 가능하다. 그 이유는 스프링 데이터 JPA 내부에서 JpaRepository를 상속받은 인터페이스를 작성하면 delete, save, findById 같은 메서드를 자동으로 만들어서 주입을 해주기 때문이다.
이걸 사용하고 싶으면 다음과 같이 Test 코드를 작성해볼 수 있다. 사용방법도 간단하고 기본적으로 필요한 것들이 들어가 있어서 굉장히 유용하다. em.flush(), em.clear()는 SQL 쿼리를 보기 위해서 저렇게 작성을 했다.
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@PersistenceContext
EntityManager em;
@Test
public void 기본데이터CRUD(){
Member member = new Member("m1", 10);
memberRepository.save(member2);
em.flush();
em.clear();
Member findMember = memberRepository.findById(member.getId()).get();
assertThat(member.getUsername()).isEqualTo(findMember.getUsername());
findMember.setAge(20);
em.flush();
em.clear();
Member findMember = memberRepository.findById(member.getId()).get();
assertThat(findMember.getAge()).isEqualTo(20);
memberRepository.delete(member);
em.flush();
em.clear();
List<Member> members = memberRepository.findAll();
assertThat(members.size()).isEqualTo(0);
}
}
[ 기능을 확장하는 방법, JPA 기본 제공 ]
JpaRepository가 기본적으로 제공해주는 메서드를 사용하면 간단하게 사용할 수 있지만 문제점이 있다. 항상 findById 같이 간단한 조회 쿼리를 날리는게 아니고 복잡한 경우의 수가 있다. 그 경우에는 Username이나 Age로 찾고 싶은 경우가 있을 수 있는데 이것 또한 JpaRepository를 상속 받기만 했다면 간단하게 해주는 방법이 있다.
아래처럼 아무런 코드 작성없이 메서드 이름을 find...ByUsername 같이 작성만 해주면 된다. ...이라고 적어놓은 이유는 저 부분은 어떤 말이 들어가도 상관없기 때문이다. Hello 이런 말이 들어가 있어도 find와 ByUsername만 가지고서 Member의 Username으로 Member를 찾는 쿼리를 날리는 거구나 하고 알아서 만들어준다. 그리고 And, Or과 같이 조건을 추가하는 것도 가능하고 GreaterThan 같이 숫자가 있을 때에 <= 이런 조건을 넣는 것도 할 수 있게 해준다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
public Member findByUsername(String username);
public Member findByUsernameAndAgeGreaterThan(String username, int age);
}
Spring 3.2.2 공식문서 부분이고 해당 부분에서 Spring Data JPA에 들어가 JPA Query Methods를 검색해보면 어떤 걸 지원하고 마지막 where 절에 들어가는 부분을 어떻게 처리해주는 지를 알려준다. 이 부분을 참고하고 공부하길 바란다.
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
JPA Query Methods :: Spring Data JPA
As of Spring Data JPA release 1.4, we support the usage of restricted SpEL template expressions in manually defined queries that are defined with @Query. Upon the query being run, these expressions are evaluated against a predefined set of variables. Sprin
docs.spring.io
[ 기능을 확장하는 방법, JPQL을 통한 정적 쿼리 작성 ]
앞의 방법과 다르게 JPQL을 작성해서 넣는 방법도 있다. 다음과 같이 작성하고 where 절에서 조건으로 넣은 age 같은 부분에 값을 넣어주기 위해 메서드 내부의 파라미터에 @Param으로 해당 값을 표기해주면 된다. 이렇게 작성했을 때 내가 느꼈던 장점은 세 가지가 있었던 것 같다.
1. 간단하게 작성할 수 있고 동적 쿼리는 불가능하지만 정적쿼리 중에서 복잡한 쿼리는 JPARepository 내부에서 작성할 수 있다.
2. 지연 로딩으로 연관관계 값을 가져오는 것으로 설정을 했는데 N+1 문제가 발생할 수 밖에 없는 경우, 한번에 땡겨오고 싶을 때 보통 fetch 조인을 사용한다. 이 경우에 사용하면 좋다.
3. Or, And에서 우선 순위가 필요할 때 사용하면 좋다. 이전에 프로젝트에서 분명히 검색되면 안되는 조건의 음식점이 검색돼 나왔던 경험이 있는데 And 조건와 Or 조건이 여러 개라 우선 순위 문제 때문에 생긴 일이었다. 이런 경우인데 코드가 복잡하지 않은 경우에 사용하면 좋다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.age = :age")
public List<Member> findByAge(@Param("age") int age);
}
해당 코드가 정상적으로 작동하는지 확인하는지 확인하는 방법은 아래의 코드를 기존의 MemberRepositoryTest 부분에 넣어보고 확인하면 된다.
@Test
public void 멤버나이조회테스트(){
Member member1 = new Member("m1", 10);
Member member2 = new Member("m2", 10);
Member member3 = new Member("m3", 20);
Member member4 = new Member("m4", 20);
Member member5 = new Member("m5", 20);
memberRepository.save(member1);
memberRepository.save(member2);
memberRepository.save(member3);
memberRepository.save(member4);
memberRepository.save(member5);
List<Member> members = memberRepository.findByAge(10);
assertThat(members.size()).isEqualTo(2);
}
[ 기능을 확장하는 방법, 동적 쿼리 혹은 메서드를 직접 정의하고 싶은 경우 ]
예전에 프로젝트를 할 때 이걸 몰라서 임의로 정의한 Repository에 findAll이나 그런 메서드들을 내가 직접 다 JPQL로 작성해서 귀찮게 작성했던 기억이 있다. 아무튼 해당 방법을 사용하기 위해서는 인터페이스 클래스를 하나 작성을 해줘야 한다.
public interface MemberRepositoryCustom {
List<Member> findMemberCustom();
}
그리고 해당 클래스를 구현한 클래스를 만들되 주의할 것은 Impl 부분이다. 기본적으로 스프링 데이터 JPA에서는 "Impl"이라는 키워드를 통해 추가적으로 구현된 부분을 찾아 MemberRepository에 추가해주는데 이름 형태를 지켜줘야 한다. 안 그러면 찾을 수 없다고 에러가 뜨면서 SpringBoot가 로딩 시점에 꺼진다.
아래처럼 코드를 일단 작성을 해준다.
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final EntityManager em;
@Override
public List<Member> findMemberCustom() {
return em.createQuery("select m from Member m")
.getResultList();
}
}
이제 MemberRepository에 다시 돌아와서 다음과 같이 인터페이스로 정의했던 MemberRepositoryCustom 부분을 추가해주면 사용할 수 있다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
정말로 MemberRepository에 아무런 코드를 작성하지 않고 MemberRepositoryCustom을 상속받기만 해도 된다. Test를 다음과 같이 작성을 해보면 알 수 있다.
@Test
public void 커스텀메서드사용확인(){
Member m1 = new Member("m1", 10);
memberRepository.save(m1);
List<Member> memberCustom = memberRepository.findMemberCustom();
assertThat(memberCustom.get(0).getUsername()).isEqualTo("m1");
}
[ 참고 자료 ]
실전! 스프링 데이터 JPA - 김영한 강의
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html
'JPA > 스프링 데이터 JPA' 카테고리의 다른 글
스프링 데이터 JPA - Hint(조회용임을 알림) (0) | 2024.02.02 |
---|---|
스프링 데이터 JPA - Entity 그래프 (0) | 2024.02.02 |
스프링 데이터 JPA - 벌크성 수정 쿼리 (0) | 2024.02.02 |
스프링 데이터 JPA - 페이징, 슬라이스 (0) | 2024.02.01 |
스프링 데이터 JPA - Dto로 객체 가져오기 (0) | 2024.02.01 |