전체 목록
SpringMedium#080

Spring Data JPA에서 Repository 인터페이스의 종류와 쿼리 작성 방법을 설명해주세요.

#Spring#JPA#Repository#SpringData

답변 포인트

CrudRepository, JpaRepository, 메서드 이름 쿼리, @Query를 생각해보세요.

정답 및 해설

빠른 요약

Repository 계층: Repository → CrudRepository → PagingAndSortingRepository → JpaRepository JpaRepository: 가장 많이 사용. findAll, save, delete, flush, saveAll 등 JPA 특화 메서드 제공.

Spring Data JPA는 데이터 접근 계층의 보일러플레이트 코드를 크게 줄여주는 프레임워크입니다. Repository 인터페이스 계층 구조를 제공하며, 메서드 이름, JPQL, 네이티브 SQL, QueryDSL 등 다양한 방법으로 쿼리를 작성할 수 있습니다. 이를 통해 개발자는 반복적인 CRUD 코드 없이 비즈니스 로직에 집중할 수 있습니다.

Repository 인터페이스 계층 구조

Java
Repository ( )
    
     CrudRepository<T, ID>
             - save(), findById(), findAll(), delete(), count()...
            
             PagingAndSortingRepository<T, ID>
                     - findAll(Pageable), findAll(Sort)
                    
                     JpaRepository<T, ID>
                             - flush(), saveAndFlush(), deleteInBatch()
                             - findAll(Example), getReferenceById()
                              JPA  

CrudRepository

기본 CRUD 연산을 제공합니다.

Java
public interface CrudRepository<T, ID> extends Repository<T, ID> {
    <S extends T> S save(S entity);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> ids);
    long count();
    void deleteById(ID id);
    void delete(T entity);
    void deleteAll();
}

PagingAndSortingRepository

페이징과 정렬 기능을 추가합니다.

Java
public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
}

JpaRepository (가장 많이 사용)

JPA 특화 기능을 포함하며, 일반적으로 이 인터페이스를 상속하여 사용합니다.

Java
// 사용 예시
public interface UserRepository extends JpaRepository<User, Long> {
    // 기본 제공 메서드 외에 커스텀 메서드 추가
}

// 기본 제공 기능 활용
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    public UserDto findById(Long id) {
        return userRepository.findById(id)  // Optional<User> 반환
                .map(UserDto::from)
                .orElseThrow(() -> new UserNotFoundException(id));
    }

    public Page<UserDto> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
        return userRepository.findAll(pageable).map(UserDto::from);
    }

    public List<UserDto> findAllSortedByName() {
        return userRepository.findAll(Sort.by("name").ascending())
                .stream().map(UserDto::from).collect(Collectors.toList());
    }
}

쿼리 작성 방법 1: 메서드 이름으로 쿼리 생성

Spring Data JPA가 메서드 이름을 분석하여 자동으로 JPQL을 생성합니다.

기본 문법

Java
public interface UserRepository extends JpaRepository<User, Long> {

    // 단일 조건
    Optional<User> findByEmail(String email);
    List<User> findByActive(boolean active);
    long countByActive(boolean active);
    boolean existsByEmail(String email);

    // 복합 조건 (And/Or)
    Optional<User> findByEmailAndActive(String email, boolean active);
    List<User> findByNameOrEmail(String name, String email);

    // 비교 연산
    List<User> findByAgeGreaterThan(int age);
    List<User> findByAgeBetween(int minAge, int maxAge);
    List<User> findByCreatedAtAfter(LocalDateTime dateTime);

    // Like/Contains
    List<User> findByNameContaining(String keyword);    // %keyword%
    List<User> findByNameStartingWith(String prefix);  // prefix%
    List<User> findByNameEndingWith(String suffix);    // %suffix

    // In
    List<User> findByIdIn(List<Long> ids);
    List<User> findByRoleNotIn(List<String> roles);

    // Null 체크
    List<User> findByDeletedAtIsNull();
    List<User> findByDeletedAtIsNotNull();

    // 정렬
    List<User> findByActiveOrderByNameAsc(boolean active);
    List<User> findByActiveOrderByCreatedAtDesc(boolean active);

    // 페이징
    Page<User> findByActive(boolean active, Pageable pageable);
    Slice<User> findByRole(String role, Pageable pageable);

    // Top/First (결과 개수 제한)
    Optional<User> findFirstByOrderByCreatedAtDesc(); // 최신 1명
    List<User> findTop5ByActiveOrderByCreatedAtDesc(boolean active); // 최신 5명
}

키워드 목록

키워드설명예시
And조건 결합findByNameAndEmail
Or조건 선택findByNameOrEmail
Is, Equals동등 비교findByName, findByNameIs
Between범위findByAgeBetween
LessThan미만findByAgeLessThan
GreaterThan초과findByAgeGreaterThan
LikeLIKEfindByNameLike
Containing%keyword%findByNameContaining
Starting/EndingWithprefix/suffixfindByNameStartingWith
InIN 절findByIdIn
Not부정findByNameNot
IsNullNULLfindByDeletedAtIsNull
OrderBy정렬findByActiveOrderByName
Top/First개수 제한findTop5By

쿼리 작성 방법 2: @Query (JPQL)

복잡한 쿼리는 JPQL을 직접 작성합니다.

Java
public interface UserRepository extends JpaRepository<User, Long> {

    // 기본 JPQL (파라미터: 위치 기반)
    @Query("SELECT u FROM User u WHERE u.email = ?1 AND u.active = ?2")
    Optional<User> findByEmailAndActive(String email, boolean active);

    // 명명된 파라미터 (권장)
    @Query("SELECT u FROM User u WHERE u.email = :email AND u.role = :role")
    List<User> findByEmailAndRole(@Param("email") String email,
                                  @Param("role") String role);

    // JOIN 쿼리
    @Query("SELECT u FROM User u JOIN u.orders o WHERE o.status = :status")
    List<User> findUsersWithOrderStatus(@Param("status") OrderStatus status);

    // FETCH JOIN (N+1 문제 해결)
    @Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.orders WHERE u.active = true")
    List<User> findActiveUsersWithOrders();

    // 집계 함수
    @Query("SELECT COUNT(u) FROM User u WHERE u.active = true")
    long countActiveUsers();

    // DTO 프로젝션
    @Query("SELECT new com.example.dto.UserSummaryDto(u.id, u.name, u.email) " +
           "FROM User u WHERE u.active = true")
    List<UserSummaryDto> findActiveUserSummaries();

    // 페이징과 함께 사용
    @Query(value = "SELECT u FROM User u WHERE u.active = :active",
           countQuery = "SELECT COUNT(u) FROM User u WHERE u.active = :active")
    Page<User> findByActive(@Param("active") boolean active, Pageable pageable);
}

쿼리 작성 방법 3: @Query (Native SQL)

JPQL로 표현하기 어려운 DB 특화 쿼리를 작성할 때 사용합니다.

Java
public interface UserRepository extends JpaRepository<User, Long> {

    // 네이티브 SQL
    @Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
    Optional<User> findByEmailNative(@Param("email") String email);

    // 복잡한 집계 쿼리
    @Query(value = """
            SELECT u.*, COUNT(o.id) as order_count
            FROM users u
            LEFT JOIN orders o ON u.id = o.user_id
            WHERE u.active = true
            GROUP BY u.id
            HAVING COUNT(o.id) > :minOrders
            ORDER BY order_count DESC
            """,
           nativeQuery = true)
    List<Object[]> findActiveUsersWithOrderCount(@Param("minOrders") int minOrders);

    // 네이티브 쿼리 + 페이징
    @Query(value = "SELECT * FROM users WHERE role = :role ORDER BY created_at DESC",
           countQuery = "SELECT count(*) FROM users WHERE role = :role",
           nativeQuery = true)
    Page<User> findByRoleNative(@Param("role") String role, Pageable pageable);
}

쿼리 작성 방법 4: @Modifying (UPDATE/DELETE)

데이터 변경 쿼리는 @Modifying을 함께 사용해야 합니다.

Java
public interface UserRepository extends JpaRepository<User, Long> {

    // UPDATE 쿼리
    @Modifying
    @Query("UPDATE User u SET u.active = :active WHERE u.id = :id")
    int updateActiveStatus(@Param("id") Long id, @Param("active") boolean active);

    // 대량 UPDATE (벌크 연산)
    @Modifying
    @Query("UPDATE User u SET u.lastLoginAt = :now WHERE u.id IN :ids")
    int updateLastLoginAt(@Param("ids") List<Long> ids,
                          @Param("now") LocalDateTime now);

    // DELETE 쿼리
    @Modifying
    @Query("DELETE FROM User u WHERE u.active = false AND u.createdAt < :cutoffDate")
    int deleteInactiveUsers(@Param("cutoffDate") LocalDateTime cutoffDate);
}

// 주의: 벌크 연산 후 영속성 컨텍스트 초기화 필요
@Service
@Transactional
public class UserService {

    @Modifying
    @Transactional
    public int deactivateUsers(List<Long> ids) {
        int count = userRepository.updateActiveStatus(false, ids);
        // 벌크 연산은 영속성 컨텍스트를 거치지 않으므로 캐시와 DB가 불일치할 수 있음
        entityManager.clear(); // 영속성 컨텍스트 초기화
        return count;
    }
}

쿼리 작성 방법 5: QueryDSL (타입 안전 동적 쿼리)

조건이 동적으로 변하는 복잡한 검색 쿼리에 적합합니다.

Java
// build.gradle 의존성
// implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'

// QueryDSL Repository 구현
@Repository
@RequiredArgsConstructor
public class UserQueryRepository {

    private final JPAQueryFactory queryFactory;

    public List<User> searchUsers(UserSearchCondition condition) {
        return queryFactory
                .selectFrom(user)
                .where(
                    nameContains(condition.getName()),
                    emailEquals(condition.getEmail()),
                    activeEquals(condition.getActive()),
                    ageGoe(condition.getMinAge())
                )
                .orderBy(user.createdAt.desc())
                .fetch();
    }

    public Page<User> searchUsersPageable(UserSearchCondition condition, Pageable pageable) {
        List<User> content = queryFactory
                .selectFrom(user)
                .where(buildPredicate(condition))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(user.createdAt.desc())
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(user.count())
                .from(user)
                .where(buildPredicate(condition));

        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
    }

    // BooleanExpression: null 반환 시 자동으로 조건에서 제외
    private BooleanExpression nameContains(String name) {
        return StringUtils.hasText(name) ? user.name.contains(name) : null;
    }

    private BooleanExpression emailEquals(String email) {
        return StringUtils.hasText(email) ? user.email.eq(email) : null;
    }

    private BooleanExpression activeEquals(Boolean active) {
        return active != null ? user.active.eq(active) : null;
    }

    private BooleanExpression ageGoe(Integer minAge) {
        return minAge != null ? user.age.goe(minAge) : null;
    }

    private BooleanExpression buildPredicate(UserSearchCondition condition) {
        return ExpressionUtils.allOf(
            nameContains(condition.getName()),
            emailEquals(condition.getEmail()),
            activeEquals(condition.getActive())
        );
    }
}

Projection (인터페이스 기반)

엔티티 전체가 아닌 일부 필드만 조회할 때 사용합니다.

Java
// 인터페이스 Projection
public interface UserSummary {
    Long getId();
    String getName();
    String getEmail();

    // SpEL 사용 가능
    @Value("#{target.name + ' (' + target.email + ')'}")
    String getNameWithEmail();
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserSummary> findByActive(boolean active);
    <T> List<T> findByActive(boolean active, Class<T> type); // 동적 Projection
}

// 클래스 기반 Projection (DTO)
public record UserSummaryRecord(Long id, String name, String email) {}

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT new com.example.dto.UserSummaryRecord(u.id, u.name, u.email) " +
           "FROM User u WHERE u.active = true")
    List<UserSummaryRecord> findActiveSummaries();
}

요약 표

쿼리 방법장점단점적합한 상황
메서드 이름간단, 타입 안전, 별도 코드 없음복잡한 조건 시 이름이 길어짐단순 조건 조회
@Query JPQL복잡한 쿼리 가능, 객체 지향문자열 쿼리, 컴파일 시 오류 미감지중간 복잡도 쿼리
@Query NativeDB 특화 기능 사용 가능DB 종속성, 객체 매핑 어려움DB 특화 쿼리
@Modifying벌크 연산 가능영속성 컨텍스트 수동 초기화 필요대량 UPDATE/DELETE
QueryDSL타입 안전, 동적 쿼리, 컴파일 오류 감지설정 복잡, Q 클래스 생성 필요복잡한 동적 검색
Repository 종류주요 기능사용 권장 상황
Repository마커 인터페이스기능 최소화 필요 시
CrudRepository기본 CRUD단순 CRUD만 필요
PagingAndSortingRepositoryCRUD + 페이징/정렬목록 조회 필요
JpaRepository모든 JPA 기능일반적인 JPA 개발 (권장)

관련 질문

같은 카테고리/태그 기준