1. 기본 JPA Repository의 한계
Spring Data JPA는 JpaRepository 인터페이스를 통해 CRUD 기능 및 간단한 쿼리 메서드를 손쉽게 제공한다. 예컨대, 다음과 같이 사용자 이름을 기준으로 데이터를 조회할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
그러나 이러한 방식은 단일 조건 또는 간단한 범위 조건에만 적합하며, 다음과 같은 복잡한 조건을 포함하는 검색 기능은 지원하지 못한다.
- 팀 이름이 "teamB"
- 나이가 35세 이상 40세 이하
- 사용자 이름이 "member4"
이러한 복합 조건 처리를 위해서는 Querydsl을 기반으로 사용자 정의 리포지토리를 도입해야 한다.
2. 사용자 정의 리포지토리 설계 및 구현
사용자 정의 리포지토리 구성 모식도
연결 흐름
- JpaRepository는 JPA 기본 기능을 제공
- MemberRepository는 이를 상속하면서, MemberRepositoryCustom도 함께 상속
- MemberRepositoryImpl은 MemberRepositoryCustom을 구현하며 실제 search() 기능을 담당
- Spring Data JPA가 자동으로 MemberRepository와 MemberRepositoryImpl을 연결해 동작
왜 이렇게 나누는가?
이유 | 설명 |
관심사 분리 | 기본 CRUD와 커스텀 Querydsl 로직 분리 |
유지보수 | 커스텀 쿼리가 많아질 경우, 별도 구현체에 집중 |
확장성 | 다양한 검색 조건 추가 시 BooleanExpression 조합 등 유연한 확장 가능 |
스프링 구조 호환 | Spring Data JPA의 자동 빈 등록/합성과 호환됨 |
요약
구성 요소 | 역할 | 주의사항 |
JpaRepository | 기본 CRUD 제공 | - |
MemberRepository | JPA + 사용자 정의 조합 | MemberRepositoryImpl과 이름 일치 필수 |
MemberRepositoryCustom | 사용자 정의 기능 선언 | 쿼리 메서드 정의만 |
MemberRepositoryImpl | 사용자 정의 기능 구현 | Impl 접미어 필수, Querydsl 사용 |
예제 코드
먼저, 사용자가 정의한 검색 기능을 선언할 인터페이스를 작성한다.
public interface MemberRepositoryCustom {
List<MemberTeamDto> search(MemberSearchCondition condition);
}
이후, 실제 Querydsl 쿼리를 구현할 클래스를 작성하는데, 반드시 MemberRepositoryImpl이라는 명명 규칙을 따라야 한다. 그렇지 않으면 Spring Data JPA가 해당 구현체를 인식하지 못한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
private final JPAQueryFactory queryFactory;
public MemberRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public List<MemberTeamDto> search(MemberSearchCondition condition) {
return queryFactory
.select(new QMemberTeamDto(
member.id, member.username, member.age,
team.id, team.name
))
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
)
.fetch();
}
// Querydsl 동적 조건 메서드
private BooleanExpression usernameEq(String username) {
return isEmpty(username) ? null : member.username.eq(username);
}
private BooleanExpression teamNameEq(String teamName) {
return isEmpty(teamName) ? null : team.name.eq(teamName);
}
private BooleanExpression ageGoe(Integer ageGoe) {
return ageGoe == null ? null : member.age.goe(ageGoe);
}
private BooleanExpression ageLoe(Integer ageLoe) {
return ageLoe == null ? null : member.age.loe(ageLoe);
}
}
MemberRepository에서 JpaRepository와 커스텀 리포지토리를 상속 받음.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
List<Member> findByUsername(String username);
}
3. Querydsl 기반 동적 검색 로직 구현
Querydsl은 BooleanExpression을 통해 null-safe한 where 조건을 생성할 수 있다. null인 조건은 무시되므로 사용자가 일부 조건만 설정해도 문제없이 동작한다.
DTO 매핑에는 QMemberTeamDto와 같이 @QueryProjection 기반 생성자가 있는 DTO가 필요하다.
* @QueryProjection? @QueryProjection은 Querydsl에서 제공하는 애너테이션으로, 쿼리 결과를 DTO에 직접 매핑할 때 사용하는 컴파일 타임 기반의 타입 안전한 방식입니다. 이 애너테이션을 활용하면 Querydsl이 자동으로 해당 DTO의 Q타입 클래스를 생성해주며, 생성자 기반으로 select 절에서 DTO를 직접 생성할 수 있게 도와줍니다. |
@Data
public class MemberTeamDto {
private Long memberId;
private String username;
private int age;
private Long teamId;
private String teamName;
@QueryProjection
public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) {
this.memberId = memberId;
this.username = username;
this.age = age;
this.teamId = teamId;
this.teamName = teamName;
}
}
4. 테스트 코드를 통한 검증
@Test
public void searchTest() {
// 팀과 멤버 생성
Team teamA = new Team("teamA");
Team teamB = new Team("teamB");
em.persist(teamA);
em.persist(teamB);
em.persist(new Member("member1", 10, teamA));
em.persist(new Member("member2", 20, teamA));
em.persist(new Member("member3", 30, teamB));
em.persist(new Member("member4", 40, teamB));
// 검색 조건
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");
}
위 코드에서 확인되는 바와 같이, memberRepository로 커스텀 repository의 search메서드를 사용할 수 있는 것을 볼 수 있다.
5. 전체 구조 요약
역할 | 클래스 및 인터페이스 | 설명 |
기본 Repository | MemberRepository | JPA 기본 기능 + 커스텀 상속 |
사용자 정의 인터페이스 | MemberRepositoryCustom | search 메서드 선언 |
구현체 | MemberRepositoryImpl | Querydsl로 로직 구현 |
검색 조건 DTO | MemberSearchCondition | 사용자 입력 조건 캡슐화 |
결과 DTO | MemberTeamDto, QMemberTeamDto | 결과값 매핑 |
6. 추가 팁 및 주의사항
- @QueryProjection을 DTO 생성자에 붙이면 컴파일 시 자동으로 Q타입이 생성된다.
- @Repository 애너테이션은 생략 가능하다. Spring Data JPA가 자동으로 인식한다.
- 구현 클래스의 이름은 반드시 Repository + Impl 형태여야 한다.
- Querydsl을 사용하면 JPQL보다 타입 안정성이 뛰어나며, 유지보수가 용이하다.
'Backend > JPA' 카테고리의 다른 글
테스트를 위한 H2 Database + Spring Data JPA 구성 방법 (0) | 2025.04.18 |
---|---|
Spring Data JPA와 Querydsl을 활용한 페이징 처리 및 성능 최적화 (0) | 2025.04.11 |
QueryDSL - 한 번의 SQL 쿼리로 여러 건의 데이터를 일괄 수정 또는 삭제하는 방법 (벌크 연산) (0) | 2025.03.25 |
QueryDSL 동적 쿼리 - (BooleanBuilder, Where 다중 파라미터 방식) (0) | 2025.03.25 |
QueryDsl - 프로젝션을 DTO 결과로 반환하는 방법과 개념 (0) | 2025.03.17 |