
지난 글에서는 Repository를 JpaRepository를 구현하지 않고 직접 만들어 Querydsl을 사용해봤는데요, 이번 글에서는 JpaRepository를 구현한 Repository를 만들어 Querydsl을 사용해보겠습니다.
이 과정을 처음 보면 관계가 복잡하다고 느낄 수 있는데요, 모든 과정이 끝나고 그림으로 그려보면 바로 이해가 될 겁니다. 우리가 통상 사용하는 MemberRepository 인터페이스는 JpaRepository<Member, Long>을 상속합니다. 인터페이스이기 때문에 이 곳에서 매서드의 내부 로직을 일일이 다 적어줄 수는 없는 거죠.
그렇기 때문에 Querydsl을 사용하려면 쉽게 말해 다른 구현체와 관계를 맺어야 합니다. 여기에는 JpaRepository, MemberRepository, MemberRepositoryCustom, MemberRepositoryImpl까지 총 4개의 Repository가 등장합니다.
먼저 기본적으로 JpaRepository를 상속한 MemberRepository입니다. 위에서 말했듯 우리가 흔히 사용하는 형식의 것이죠.
public interface MemberRepository extends JpaRepository<Member, Long> {
...
}
그리고 MemberRepositoryCustom 인터페이스를 만들어야 합니다. 이 인터페이스는 다형성의 원리를 이용하기 위한 것으로, 단순한 상속과 구현의 중간다리 역할입니다. 인터페이스이기 때문에 매서드를 갖고는 있어도 내부를 구현하지는 못합니다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> searchMember(MemberSearchCondition condition);
}
말했듯 이 곳에서는 매서드의 내부 로직을 적을 수 없으니, 이게 가능한 MemberRepositoryImpl을 만들어야 합니다. 그리고 이 Impl은 MemberRepositoryCustom 인터페이스를 구현합니다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> searchMember(MemberSearchCondition memberSearchCondition) {
return queryFactory
.select(new QMemberTeamDto(
member.id,
member.username,
member.age,
team.id,
team.name
))
.from(member)
.leftJoin(member.team, team)
.where(memberTeamDtoEq2(memberSearchCondition))
.fetch();
}
private BooleanBuilder memberTeamDtoEq2(MemberSearchCondition memberSearchCondition) {
BooleanBuilder builder = new BooleanBuilder();
return builder.and(usernameEq(memberSearchCondition.getUsername()))
.and(teamNameEq(memberSearchCondition.getTeamName()))
.and(ageGoe(memberSearchCondition.getAgeGoe()))
.and(ageLoe(memberSearchCondition.getAgeLoe()));
}
private BooleanExpression usernameEq(String username) {
return hasText(username) ? member.username.eq(username) : null;
}
private BooleanExpression teamNameEq(String teamName) {
return hasText(teamName) ? team.name.eq(teamName) : 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;
}
}
그리고 이제 MemberRepository 인터페이스가 MemberRepositoryCustom 인터페이스를 상속하면 됩니다.
public interface MemberRepository extends JpaRepository<Member, Long>,
MemberRepositoryCustom {
}
그리고 이제 그림을 보겠습니다.

기본적으로 MemberRepository 인터페이스는 Spring-Data-Jpa 라이브러리가 제공하는 JpaRepository와 우리가 만든 MemberRepositoryCustom을 상속합니다. 이로 인해 findAll()이나 Save()와 같이 JpaRepository가 제공하는 수 많은 매서드들을 공짜로 편하게 사용할 수 있는 것이죠. 이와 같은 방법으로, MemberRepositoryCustom이 가지고 있는 searchMember() 매서드를 사용할 수 있게 됩니다.
다시 설명하자면, MemberRepositoryCustom은 인터페이스이기 때문에 매서드를 선언만 할 뿐입니다. 내부 로직을 직접 구현하진 못하죠. 그래서 MemberRepositoryImpl이 MemberRepositoryCustom을 구현해 매서드를 오버라이딩하게 되면, 다형성에 의해 MemberRepository는 구현된 searchMember() 매서드를 사용할 수 있게 되는 것이죠.
이 같은 원리로 만약 MemberRepositoryCustom을 구현하는 Repository가 10개이고, 각각 모두 다른 방식으로 searchMember() 매서드를 오버라이딩 한다면 MemberRepository는 각각 다른 10개의 searchMember() 매서드를 사용할 수 있게 되는 겁니다.
이번 글에서는 JpaRepository를 구현하면서 Querydsl을 이용하는 방법을 알아보았습니다. 개념적인 내용이라 꽤나 복잡하게 느껴집니다만, 다형성이라는 키워드로만 보면 이해가 조금 쉬울 것 같습니다. 그럼 다음 글에서는 Querydsl을 이용한 페이징 처리 방법에 대해 알아보겠습니다.
항상 좋은 글 감사합니다.