[스프링] 스프링 Data Jpa(7) 사용자 정의 리포지토리 구현

2025. 8. 26. 13:30·스프링 데이터 JPA

스프링 데이터 JPA는 정말 편리하다. 인터페이스만 선언해도 기본적인 CRUD는 물론이고, 메서드 이름만으로 쿼리를 자동 생성해주는 기능까지 제공한다.


하지만 모든 상황이 이렇게만 해결되는 건 아니다.

프로젝트를 진행하다 보면, 복잡한 동적 쿼리라든가 비즈니스에 특화된 로직이 필요할 때가 생긴다. 이런 경우 단순히 JpaRepository 인터페이스만으로는 한계가 있다. 그렇다고 리포지토리 인터페이스에 선언된 수많은 메서드를 직접 전부 구현하기에는 너무 복잡하고 사실상 불가능에 가깝다.

 

그래서 필요한 것이 바로 사용자 정의 리포지토리(Custom Repository) 기능이다.

 

1. 문제 상황: 직접 구현한다면?

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);

    @Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);

    @Query("select m from Member m where m.username=:username and m.age=:age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);

    @Query("select m.username from Member m")
    List<String> findUsernameList();

    @Query("select new study.data_jpa.dto.MemberDto(m.id, m.username, t.name) from Member m join m.team t")
    List<MemberDto> findMemberDto();

    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") List<String> names);

    Page<Member> findByAge(int age, Pageable pageable);

    @Modifying
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
}

만약 여기 정의된 모든 메서드를 직접 구현 클래스에서 다 작성해야 한다면?
→ EntityManager를 직접 써서 JPQL을 하나하나 만들어야 하고, CRUD부터 페이징까지 전부 손으로 구현해야 한다.
→ 생산성은 급격히 떨어지고, 유지보수도 사실상 불가능하다.

이런 복잡함을 해결하기 위해 스프링 데이터 JPA가 사용자 정의 리포지토리 방식을 지원한다.

 

2. 사용자 정의 리포지토리 기본 구조

사용자 정의 인터페이스 생성

public interface MemberRepositoryCustom {
    List<Member> findMemberCustom();
}

 

사용자 정의 구현 클래스 작성

@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m", Member.class)
                 .getResultList();
    }
}

 

기존 리포지토리와 결합

public interface MemberRepository 
        extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}

이렇게 하면 JpaRepository가 제공하는 기본 기능은 그대로 쓰면서, 필요한 메서드만 직접 구현해서 확장할 수 있다.

 

3. 구현 클래스 이름 규칙

스프링 데이터 JPA 2.x부터는 구현 클래스 네이밍 규칙이 확장되었다.

  • 기존 방식:
    • MemberRepository + Impl → MemberRepositoryImpl
  • 새로운 방식(권장):
    • MemberRepositoryCustom + Impl → MemberRepositoryCustomImpl

즉, 사용자 정의 인터페이스 이름에 Impl을 붙이는 방식도 인식한다.
이 방식은 인터페이스명과 구현체명이 매칭되므로 직관적이고, 여러 개의 커스텀 인터페이스를 분리해서 각각 구현할 때도 깔끔하다.

 

4. 실무에서의 활용

실무에서는 리포지토리를 운영할 때 복잡도에 따라 접근 방식을 다르게 가져가는 게 좋다. 보통 이렇게 세 단계로 나눌 수 있다.

 

1) 간단한 CRUD

회원 가입, 단일 엔티티 조회, 삭제 같은 단순 기능들은 JpaRepository가 기본 제공하는 메서드만으로 충분하다.

memberRepository.save(member);
memberRepository.findById(id);
memberRepository.delete(member);

 

2) 복잡한 조회 쿼리 → 사용자 정의 리포지토리 + QueryDSL

조건이 많아지고 조인, 정렬, 페이징이 복잡하게 얽히면 @Query로 처리하기 힘들어진다.
이때는 사용자 정의 리포지토리를 만들어 확장하고, 내부 구현을 QueryDSL로 작성하는 방식을 많이 쓴다.

public interface MemberRepositoryCustom {
    List<Member> searchMembers(String username, Integer age);
}

//구현체
@RequiredArgsConstructor
public class MemberRepositoryImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    @Override
    public List<Member> searchMembers(String username, Integer age) {
        return queryFactory
            .selectFrom(member)
            .where(
                username != null ? member.username.eq(username) : null,
                age != null ? member.age.goe(age) : null
            )
            .fetch();
    }
}

이렇게 하면 JpaRepository의 CRUD 기능은 그대로 두고, 복잡한 조회 로직만 별도로 구현할 수 있다.
특히 동적 쿼리 작성에 강력한 QueryDSL을 조합하는 게 일반적인 실무 패턴이다.

 

3) 더더욱 복잡하다면 → 별도 클래스 분리(권장, 강요x)

사용자 정의 리포지토리로 감당하기 힘들 정도로 쿼리 로직이 커지면, 아예 별도의 쿼리 전용 리포지토리를 만들어 관리하는 것도 좋은 방법이다.

 

예를 들어, 회원 관련 조회 로직이 지나치게 복잡해진다면 MemberQueryRepository라는 클래스를 따로 두고 스프링 빈으로 등록하는 식이다.

@Repository
@RequiredArgsConstructor
public class MemberQueryRepository {

    private final EntityManager em;

    public List<Member> findComplexQuery(String teamName, int minAge) {
        return em.createQuery(
                "select m from Member m join m.team t " +
                "where t.name = :teamName and m.age >= :minAge", Member.class)
            .setParameter("teamName", teamName)
            .setParameter("minAge", minAge)
            .getResultList();
    }
}

이 방식은 스프링 데이터 JPA와는 별개로 독립적인 DAO처럼 동작한다.
즉, 기본 리포지토리는 단순 CRUD, 사용자 정의 리포지토리는 적당히 복잡한 조회, 그리고 정말 큰 덩어리의 쿼리는 별도 클래스로 분리해서 관리하는 구조가 유지보수에 가장 유리하다.

 

마무리하며

스프링 데이터 JPA는 기본적으로 인터페이스만 선언하면 되는 편리한 도구지만, 모든 문제를 자동으로 해결해주지는 않는다.
복잡한 쿼리나 특별한 로직이 필요할 때는 사용자 정의 리포지토리를 통해 깔끔하게 확장할 수 있다.

 

기본 제공 기능은 최대한 활용하되, 한계에 부딪힐 때는 커스텀 리포지토리를 통해 필요한 부분만 직접 구현하는 것이 가장 실용적인 접근이다.


특히 QueryDSL과 함께라면 유지보수성과 생산성 모두를 잡을 수 있을 것이다.

 

감사합니다.

'스프링 데이터 JPA' 카테고리의 다른 글

[스프링] 스프링 Data JPA(마지막) Auditing  (2) 2025.08.26
[스프링] 스프링 Data JPA(6) @EntityGraph  (0) 2025.08.21
[스프링] 스프링 Data JPA(5) 벌크성 수정쿼리  (0) 2025.08.21
[스프링] 스프링 Data JPA(4) JPA 페이징과 정렬  (0) 2025.08.21
[스프링] 스프링 Data JPA(3) @Query, 리포지토리 메소드에 쿼리 정의하기  (0) 2025.08.21
'스프링 데이터 JPA' 카테고리의 다른 글
  • [스프링] 스프링 Data JPA(마지막) Auditing
  • [스프링] 스프링 Data JPA(6) @EntityGraph
  • [스프링] 스프링 Data JPA(5) 벌크성 수정쿼리
  • [스프링] 스프링 Data JPA(4) JPA 페이징과 정렬
0kingki_
0kingki_
자바 + 스프링 웹 개발
  • 0kingki_
    0kingki_
    0kingki_
  • 전체
    오늘
    어제
    • 분류 전체보기 (134)
      • 코딩 테스트 (54)
      • 자바 (21)
      • 스프링 (27)
      • 타임리프 (16)
      • 스프링 데이터 JPA (8)
      • 최적화 (2)
      • QueryDSL (4)
      • AWS (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    spring
    쿼리
    최적화
    JPA
    LocalDateTime
    thymeleaf
    스프링 컨테이너
    예외 처리
    mvc
    SOLID
    스프링 데이터 JPA
    쿼리dsl
    QueryDSL
    객체지향
    코딩테스트
    코딩 테스트
    불변객체
    예외처리
    SpringDataJpa
    Java
    다형성
    자바
    BFS
    백준
    fetch join
    컬렉션
    스프링
    dfs
    재귀
    타임리프
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
0kingki_
[스프링] 스프링 Data Jpa(7) 사용자 정의 리포지토리 구현
상단으로

티스토리툴바