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 |
| Like | LIKE | findByNameLike |
| Containing | %keyword% | findByNameContaining |
| Starting/EndingWith | prefix/suffix | findByNameStartingWith |
| In | IN 절 | findByIdIn |
| Not | 부정 | findByNameNot |
| IsNull | NULL | findByDeletedAtIsNull |
| 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 Native | DB 특화 기능 사용 가능 | DB 종속성, 객체 매핑 어려움 | DB 특화 쿼리 |
| @Modifying | 벌크 연산 가능 | 영속성 컨텍스트 수동 초기화 필요 | 대량 UPDATE/DELETE |
| QueryDSL | 타입 안전, 동적 쿼리, 컴파일 오류 감지 | 설정 복잡, Q 클래스 생성 필요 | 복잡한 동적 검색 |
| Repository 종류 | 주요 기능 | 사용 권장 상황 |
|---|---|---|
| Repository | 마커 인터페이스 | 기능 최소화 필요 시 |
| CrudRepository | 기본 CRUD | 단순 CRUD만 필요 |
| PagingAndSortingRepository | CRUD + 페이징/정렬 | 목록 조회 필요 |
| JpaRepository | 모든 JPA 기능 | 일반적인 JPA 개발 (권장) |