스프링데이터JPA의 철학은 개발자가 작성한 리포지토리 인터페이스를 토대로 그 구현체를 자동생성하여 전달함에 있다. 그러나 스프링데이터JPA가 자동생성할 수 있는 구현체 로직은 JPA에 국한되어 있다. 동적쿼리를 위한 QueryDSL을 사용하거나 JPA가 아닌 MyBatis를 사용하거나 Spring JDBC Template을 사용하는 등의 스프링데이터JPA가 지원할 수 없는 로직을 담은 리포지토리 구현체는 자동생성 할 수 없다.
이런 경우, 개발자가 구현체를 직접 구현해야 한다. 스프링데이터JPA는 개발자가 구현한 구현체를 토대로 구현체 객체를 프록시로 자동생성하여 Bean으로 제공한다.
예를 들어보자.
개발자가 MemberRepository 인터페이스를 작성하고 JpaRepository를 상속하면 스프링데이터JPA는 JpaRepostiory 인터페이스의 구현체 SimpleJpaRepository를 토대로 Proxy객체를 생성하고 target을 SimpleJpaRepository로 설정한다. 그리고 프록시 객체를 Bean으로 등록하여 언제든 개발자가 주입하여 사용가능하도록 한다.
이로써 개발자는 스프링데이터JPA가 미리 만들어 놓은 리포지토리 로직을 사용할 수 있다. 그러나 QueryDSL, MyBatis, Spring JDBC Template 같은 스프링데이터JPA가 지원하지 않는 로직은 지원하지 않는다. 이는 개발자가 직접 작성해야 한다.
그래서 개발자는 MemberRepositoryCustom 인터페이스를 생성하고 MemberRepository로 하여금 상속하게 했다. 그리고 QueryDSL,MyBatis 등의 로직이 담길 MemberRepositoryImpl 구현체를 생성한다. 여기가 중요하다!
구현체 이름은 반드시 아래 두 가지 중 하나로 정해야 한다.
1) MemberRepositoryImpl ( 인터페이스명 + impl )
2) MemberRepositoryCustomImpl ( 인터페이스명 + impl )
이름에 규약이 있는 이유는 스프링데이터JPA가 구현체를 토대로 Proxy객체를 생성하기 때문이다. Proxy객체는 구현체를 토대로 생성됨과 동시에 target은 SimpleJpaRepository로 설정되기에, 스프링데이터JPA가 제공하는 로직과 더불어, 개발자가 직접 작성한 로직도 사용 가능해진다.
MemberRepository 인터페이스
public interface MemberRepository extends JpaRepository<Member,Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username); // 메소드 이름으로 쿼리 자동생성
}
MemberRepository 인터페이스는 3가지 기능을 사용할 수 있다.
1) JpaRepository가 제공하는 기능
2) 본인 인터페이스에 정의된 기능
3) MemberRepositoryCustom 기능
보통 공통적이고 정적인 쿼리를 실행하는 경우 1),2)로 충분히 커버가 가능하다. 그러나 동적인 쿼리를 수행해야 하거나 특수한 환경의 데이터 엑세스가 필요하면 3)과 같이 사용자가 정의한 인터페이스가 필요하다.
MemberRepositoryCustom 인터페이스
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
MemberRepositoryImpl 구현체 ( QueryDSL 로직 구현 )
public class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager entityManager){
this.queryFactory = new JPAQueryFactory(entityManager);
}
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition){
return queryFactory
.select(new QMemberTeamDto(
member.id.as("memberId"),
member.username,
member.age,
team.id.as("teamId"),
team.name.as("teamName")))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageBetween(condition.getAgeLoe(),condition.getAgeGoe())
)
.fetch();
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : null;
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression ageBetween(int ageLoe, int ageGoe){
return ageLoe(ageLoe).and(ageGoe(ageGoe));
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
}
search 기능이 QueryDSL로 구현되었다. 동적으로 조건을 달리하여 search가 가능한 메소드이다. 그럼 스프링에서 주입받은 구현체가 search 기능을 사용할 수 있는지 확인해보자.
Test 코드
@SpringBootTest
@Transactional
class MemberRepositoryTest {
@Autowired
EntityManager em;
@Autowired
MemberRepository memberRepository; // 스프링에서 구현체 주입받기
@Test
public void searchTest(){
//데이터 생성하기
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
Member member1 = new Member("member1", 10, teamA);
Member member2 = new Member("member2", 20, teamA);
Member member3 = new Member("member3", 30, teamB);
Member member4 = new Member("member4", 40, teamB);
em.persist(member1);
em.persist(member2);
em.persist(member3);
em.persist(member4);
// Repository 기능 호출
MemberSearchCondition condition = new MemberSearchCondition();
condition.setAgeGoe(35);
condition.setAgeLoe(40);
condition.setTeamName("teamB");
List<MemberTeamDto> result = memberRepository.search(condition); // 사용자정의 인터페이스 기능 호출
assertThat(result).extracting("username").containsExactly("member4"); // member4를 정확히 추출하는지 테스트
}
}
스프링에서 MemberRepository 구현체를 주입받은 후, 사용자정의 인터페이스에 정의된 기능을 테스트 해보았다.
정확히 조건에 맞는 member4를 추출하여 테스트에 성공하였다.
디버깅을 해보면 memberRepository 참조변수에는 Proxy 객체를 주입되었고 이는 MemberRepositoryImpl 구현체를 토대로 만들어졌다. 그리고 target은 SimpleJpaRepository 구현체로 설정하여 스프링데이터JPA가 제공하는 기능도 사용할 수 있다. 이렇듯, 사용자정의 인터페이스는 스프링데이터JPA가 제공하지 못하는 로직을 스프링데이터JPA 구현체가 제공할 수 있도록 하는데 필요한 요소이다.
참고자료
'JPA > Spring Data JPA' 카테고리의 다른 글
[SpringDataJPA] Auditing (0) | 2023.07.20 |
---|---|
[SpringDataJPA] @EntityGraph (0) | 2023.07.13 |
[SpringDataJPA] 벌크성 수정쿼리 ( @Modifying ) (0) | 2023.07.13 |
[SpringDataJPA] 페이징 ( Pageable, Page, Slice ) (0) | 2023.07.12 |
[SpringDataJPA] @Query (0) | 2023.07.11 |