Spring Data JPA에서 Repository 인터페이스의 종류와 쿼리 작성 방법을 설명해주세요.
답변 포인트
CrudRepository, JpaRepository, 메서드 이름 쿼리, @Query를 생각해보세요.
정답 및 해설
빠른 요약
Repository 계층: Repository → CrudRepository → PagingAndSortingRepository → JpaRepository JpaRepository: 가장 많이 사용. findAll, save, delete, flush, saveAll 등 JPA 특화 메서드 제공.
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 개발 (권장) |