전체 목록
SpringMedium#80

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

#Spring#JPA#Repository#SpringData
힌트

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

정답 및 해설

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

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

Repository 인터페이스 계층 구조

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 연산을 제공합니다.

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

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

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

JpaRepository (가장 많이 사용)

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

// 사용 예시
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을 생성합니다.

기본 문법

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을 직접 작성합니다.

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 특화 쿼리를 작성할 때 사용합니다.

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을 함께 사용해야 합니다.

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 (타입 안전 동적 쿼리)

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

// 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 (인터페이스 기반)

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

// 인터페이스 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 개발 (권장)