[JPA] QueryDSL 사용자 정의 레포지토리 적용
0. 관련 포스팅
- QueryDSL 설정하기
- QueryDSL DTO 반환 방법
- QueryDSL 동적 쿼리 적용
- QueryDSL 사용자 정의 레포지토리 적용 <현재 포스팅>
- QueryDSL 페이징 적용
1. 개요
기존 JPA Repository에 연결하여 별도의 클래스에 QueryDSL 구현체 클래스를 구성하고
JPA Repository 인터페이스에서 호출하여 사용하는 방식에 대한 포스팅이다.
(꼭 QueryDSL이 아니더라도 다른 라이브러리 사용에도 적용가능한 기능이다.)
하나의 Repository 를 호출해서 사용하기에 여러 클래스를 호출하지 않아서 좋긴 하지만
별도로 연결에 필요한 인터페이스를 구성해야하고 구현체의 클래스 명 규칙을 지켜야한다는 점이 있다.
'필수로 구성해야한다기 보다는 하나의 방식으로 봐주면 좋을 듯 하다.'
2. 구성
사용자 정의 레포지토리를 적용하는 방법은 위의 이미지와 같다.
(Basic Java 구현 방식은 아니고 Spring과 Jpa 사용 시에 가능한 방식이다. 오해하지 말자!)
MemberRepositoryCustom 인터페이스를 만들고
기존 사용하는 JPA Repository에는 extends,
새로 만든 구현체인 MemberRepositoryImpl 클래스에는 Implements 하여 사용하게 된다.
이때 지켜야할 규칙이 있는데,
구현체의 클래스 이름은 ~Repository[Impl] 라는 부분을 명시해야한다.
해당 부분 설명을 보면 PersomRepository 의 사용자 정의 레포지토리 구현을 할때는 Postfix 값을 붙여 PersonRepositoryImpl 로 만들어야 한다고 정의되어있다.
Default 값이 'Impl' 일 뿐이지 원하는 문자열 값으로 수정도 가능하다.
이번 포스팅에서는 커스텀 설정이 아닌 Default 설정인 Impl 로 사용할 예정이다.
3. 구현
Repository 에서 만들어야할 부분과 SearchCondition Class 대해서만 정의하였다.
(Entity 및 Dto 부분은 이전 포스팅 참고!)
a. MemberRepository (JpaRepository Interface)
@Repository
public interface MemberRepository extends JpaRepository<Member,Long>, MemberRepositoryCustom {
@Query("select m from Member m join fetch m.boards")
List<Member> findAllFetchBoard();
}
기존 사용하던 JPA Repository 인터페이스이다.
인터페이스이기 때문에 다중 상속으로 MemberRepositoryCustom 을 추가해준다.
b. MemberRepositoryCustom (Custom Interface)
public interface MemberRepositoryCustom {
List<PreMemberDto> search(MemberSearchCondition condition);
}
구현체와 연결을 해줄 MemberRepositoryCustom 인터페이스.
이 위치에는 구현체에서 구현할 메소드에 대한 반환값과 Arguments를 작성해주게 된다.
c. MemberRepositoryImpl (구현체 Class)
ublic class MemberRepositoryImpl implements MemberRepositoryCustom{
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<PreMemberDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QPreMemberDto(member.id, member.name, member.age))
.from(member)
.where(
usernameLike(condition.getName())
// .and(ageGoe(condition.getAgeGoe()))
// .and(ageLoe(condition.getAgeLoe()))
.and(ageBetween(condition.getAgeGoe(), condition.getAgeLoe()))
)
.fetch();
}
private BooleanExpression usernameLike(String usernameCond) {
// username 컨디션 값이
// null이 아닌 경우 like
// null인 경우 name 이 필수 값이라는 가정하에 is not null로 반환값이 null이 아니도록 세팅
// BooleanExpression이 null 인 경우 .and() .or() 연결이 불가능 하므로 설계상 고려점
return usernameCond != null ? member.name.like(usernameCond) : member.name.isNotNull();
}
private BooleanExpression usernameEq(String usernameCond) {
if (usernameCond == null) {
return null; // null 을 반환 시에 where에서 해당 조건을 무시하고 넘어간다.
}
return member.name.eq(usernameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe != null ? member.age.goe(ageGoe) : null;
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe != null ? member.age.loe(ageLoe) : null;
}
// 기존 메소드 연결로 만들어낼 수 있다.
private BooleanExpression ageBetween(int ageGoeCond, int ageLoeCond) {
return ageGoe(ageGoeCond).and(ageLoe(ageLoeCond));
}
}
위에서 만든 MemberRepositoryCustom 인터페이스의 구현체 클래스.
이전 포스팅에서 사용하던 QueryDSL 조회 로직을 일부 가져와 메소드에 적용해준다.
별도의 MemberSearchCondition DTO 클래스를 만들어주고 WhereParam 동적 조회를 만들어주었다.
(MemberSearchConditon 클래스는 아래 코드를 확인!)
D. MemberSearchCondition (조회용 DTO Class)
@Getter
public class MemberSearchCondition {
private String name;
private Integer age;
private Integer ageGoe;
private Integer ageLoe;
@Builder
public MemberSearchCondition(String name, Integer age, Integer ageGoe, Integer ageLoe) {
this.name = name;
this.age = age;
this.ageGoe = ageGoe;
this.ageLoe = ageLoe;
}
}
4. Run Test
테스트에 사용되는 구현 코드는 아래 이미지와 같다.
구현체로 만든 MemberRepositoryImpl 클래스가 아닌
기존 JPA Repository를 주입해주고 사용한다.
실행 로그를 보면
search 메소드에서 우리가 구현한 QueryDSL 코드가 정상적으로 적용되는 모습을 볼 수 있다.
5. 마무리
사용자 정의 레포지토리를 적용하므로써,
구현체에 대한 부분을 쉽게 교체하여 사용이 용이하다 생각된다.
(QueryDSL에서 Jooq로 변경을 한다던가 같은 것들)
'구현체가 바뀐다고 주입 받은 클래스마다 수정을 하지 않아도 된다는 점이 최대 장점이지 않을까?' 생각한다.
물론 JPA와 QueryDSL 클래스 구분은 되어있으나 사용에 있어 어떤걸로 되어있는지 구분하는게 어렵겠지만
객체지향의 캡슐화 은닉화 관점에서 보면 맞는 방법이기도 하다.
'어떤 방법이 맞다' 라고 보기보다는 하나의 구현 방법이라 보면 좋을 듯 하다.
JPA 관련 포스팅으로 읽어보면 좋을것같은게 있어서 아래 링크 첨부해본다.
사용자 정의 레포지토리가 만들어지는 방식과 연관하여 생각해볼만해서 한번 읽어보길 추천한다.
Spring Data JPA 는 어떻게 interface 만으로도 동작할까? (feat. reflection, proxy)
Spring Data JPA를 공부하면서 궁금한 것이 있었습니다. public interface MemberRepository extends JpaRepository { List findAllByName(String name); } 위와 같이 MemberRepository는 인터페이스고, @Repository 애노테이션을 붙여
pingpongdev.tistory.com
6. GitHub
https://github.com/mk1-p/jpa-sample-code
GitHub - mk1-p/jpa-sample-code: Test for Spring JPA
Test for Spring JPA. Contribute to mk1-p/jpa-sample-code development by creating an account on GitHub.
github.com
7. 참고문서
[Spring Data JPA] 사용자 정의 Repository
일반적으로 Spring Data JPA를 활용하면 인터페이스만 정의하고 그에 대한 구현체는 정의하지 않는다 왜냐하면 Spring의 ProxyFactory에 의해서 정의한 인터페이스에 대한 "프록시"를 생성하고 이 프록시
cs-ssupport.tistory.com